diff --git a/.flake8 b/.flake8 index 5735456ae7d..4ff88403244 100644 --- a/.flake8 +++ b/.flake8 @@ -29,6 +29,8 @@ ignore = B950, W191, 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 max-line-length = 200 exclude=.github/helper/semgrep_rules diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b5e46fb8107..9b5ea2f58ab 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -17,3 +17,6 @@ f0bcb753fb7ebbb64bb0d6906d431d002f0f7d8f # imports cleanup 4b2be2999f2203493b49bf74c5b440d49e38b5e3 + +# formatting with black +c07713b860505211db2af685e2e950bf5dd7dd3a diff --git a/.github/helper/install.sh b/.github/helper/install.sh index e7f46410e6c..f9a7a024aea 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -2,6 +2,13 @@ set -e +# Check for merge conflicts before proceeding +python -m compileall -f "${GITHUB_WORKSPACE}" +if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 +fi + cd ~ || exit sudo apt-get install redis-server libcups2-dev diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 4b1147e79f9..5b607a99406 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -11,4 +11,4 @@ jobs: - name: curl run: | apk add curl bash - curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests + curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}' diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 54b381d7f89..30ca22aedc5 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -5,7 +5,6 @@ on: paths-ignore: - '**.js' - '**.md' - types: [opened, unlabeled, synchronize, reopened] workflow_dispatch: @@ -30,11 +29,6 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - - name: Check for merge conficts label - if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }} - run: | - echo "Remove merge conflicts and remove conflict label to run CI" - exit 1 - name: Clone uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..5a46002820c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Generate Semantic Release +on: + push: + branches: + - version-13 +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Node.js v14 + uses: actions/setup-node@v2 + with: + node-version: 14 + - name: Setup dependencies + run: | + npm install @semantic-release/git @semantic-release/exec --no-save + - name: Create Release + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GIT_AUTHOR_NAME: "Frappe PR Bot" + GIT_AUTHOR_EMAIL: "developers@frappe.io" + GIT_COMMITTER_NAME: "Frappe PR Bot" + GIT_COMMITTER_EMAIL: "developers@frappe.io" + run: npx semantic-release \ No newline at end of file diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 1c9743c5700..c62622eecec 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -5,7 +5,6 @@ on: paths-ignore: - '**.js' - '**.md' - types: [opened, unlabeled, synchronize, reopened] workflow_dispatch: push: branches: [ develop ] @@ -40,12 +39,6 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - - name: Check for merge conficts label - if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }} - run: | - echo "Remove merge conflicts and remove conflict label to run CI" - exit 1 - - name: Clone uses: actions/checkout@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b74d9a640da..dc3011f050f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,12 +26,19 @@ repos: args: ['--config', '.github/helper/.flake8_strict'] exclude: ".*setup.py$" + - repo: https://github.com/adityahase/black + rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 + hooks: + - id: black + additional_dependencies: ['click==8.0.4'] + - repo: https://github.com/timothycrosley/isort rev: 5.9.1 hooks: - id: isort exclude: ".*setup.py$" + ci: autoupdate_schedule: weekly skip: [] diff --git a/.releaserc b/.releaserc new file mode 100644 index 00000000000..8a758ed30a6 --- /dev/null +++ b/.releaserc @@ -0,0 +1,24 @@ +{ + "branches": ["version-13"], + "plugins": [ + "@semantic-release/commit-analyzer", { + "preset": "angular", + "releaseRules": [ + {"breaking": true, "release": false} + ] + }, + "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", { + "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py' + } + ], + [ + "@semantic-release/git", { + "assets": ["erpnext/__init__.py"], + "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github" + ] +} \ No newline at end of file diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 5f87d7b5538..c77d8bca032 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -1,53 +1,60 @@ - import inspect import frappe from erpnext.hooks import regional_overrides -__version__ = '13.2.0' +__version__ = "13.27.0" + def get_default_company(user=None): - '''Get default company for user''' + """Get default company for user""" from frappe.defaults import get_user_default_as_list if not user: user = frappe.session.user - companies = get_user_default_as_list(user, 'company') + companies = get_user_default_as_list(user, "company") if companies: default_company = companies[0] else: - default_company = frappe.db.get_single_value('Global Defaults', 'default_company') + default_company = frappe.db.get_single_value("Global Defaults", "default_company") return default_company def get_default_currency(): - '''Returns the currency of the default company''' + """Returns the currency of the default company""" company = get_default_company() if company: - return frappe.get_cached_value('Company', company, 'default_currency') + return frappe.get_cached_value("Company", company, "default_currency") + def get_default_cost_center(company): - '''Returns the default cost center of the company''' + """Returns the default cost center of the company""" if not company: return None if not frappe.flags.company_cost_center: frappe.flags.company_cost_center = {} if not company in frappe.flags.company_cost_center: - frappe.flags.company_cost_center[company] = frappe.get_cached_value('Company', company, 'cost_center') + frappe.flags.company_cost_center[company] = frappe.get_cached_value( + "Company", company, "cost_center" + ) return frappe.flags.company_cost_center[company] + def get_company_currency(company): - '''Returns the default company currency''' + """Returns the default company currency""" if not frappe.flags.company_currency: frappe.flags.company_currency = {} if not company in frappe.flags.company_currency: - frappe.flags.company_currency[company] = frappe.db.get_value('Company', company, 'default_currency', cache=True) + frappe.flags.company_currency[company] = frappe.db.get_value( + "Company", company, "default_currency", cache=True + ) return frappe.flags.company_currency[company] + def set_perpetual_inventory(enable=1, company=None): if not company: company = "_Test Company" if frappe.flags.in_test else get_default_company() @@ -56,9 +63,10 @@ def set_perpetual_inventory(enable=1, company=None): company.enable_perpetual_inventory = enable company.save() + def encode_company_abbr(name, company=None, abbr=None): - '''Returns name encoded with company abbreviation''' - company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr") + """Returns name encoded with company abbreviation""" + company_abbr = abbr or frappe.get_cached_value("Company", company, "abbr") parts = name.rsplit(" - ", 1) if parts[-1].lower() != company_abbr.lower(): @@ -66,65 +74,73 @@ def encode_company_abbr(name, company=None, abbr=None): return " - ".join(parts) + def is_perpetual_inventory_enabled(company): if not company: company = "_Test Company" if frappe.flags.in_test else get_default_company() - if not hasattr(frappe.local, 'enable_perpetual_inventory'): + if not hasattr(frappe.local, "enable_perpetual_inventory"): frappe.local.enable_perpetual_inventory = {} if not company in frappe.local.enable_perpetual_inventory: - frappe.local.enable_perpetual_inventory[company] = frappe.get_cached_value('Company', - company, "enable_perpetual_inventory") or 0 + frappe.local.enable_perpetual_inventory[company] = ( + frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0 + ) return frappe.local.enable_perpetual_inventory[company] + def get_default_finance_book(company=None): if not company: company = get_default_company() - if not hasattr(frappe.local, 'default_finance_book'): + if not hasattr(frappe.local, "default_finance_book"): frappe.local.default_finance_book = {} if not company in frappe.local.default_finance_book: - frappe.local.default_finance_book[company] = frappe.get_cached_value('Company', - company, "default_finance_book") + frappe.local.default_finance_book[company] = frappe.get_cached_value( + "Company", company, "default_finance_book" + ) return frappe.local.default_finance_book[company] + def get_party_account_type(party_type): - if not hasattr(frappe.local, 'party_account_types'): + if not hasattr(frappe.local, "party_account_types"): frappe.local.party_account_types = {} if not party_type in frappe.local.party_account_types: - frappe.local.party_account_types[party_type] = frappe.db.get_value("Party Type", - party_type, "account_type") or '' + frappe.local.party_account_types[party_type] = ( + frappe.db.get_value("Party Type", party_type, "account_type") or "" + ) return frappe.local.party_account_types[party_type] + def get_region(company=None): - '''Return the default country based on flag, company or global settings + """Return the default country based on flag, company or global settings You can also set global company flag in `frappe.flags.company` - ''' + """ if company or frappe.flags.company: - return frappe.get_cached_value('Company', - company or frappe.flags.company, 'country') + return frappe.get_cached_value("Company", company or frappe.flags.company, "country") elif frappe.flags.country: return frappe.flags.country else: - return frappe.get_system_settings('country') + return frappe.get_system_settings("country") + def allow_regional(fn): - '''Decorator to make a function regionally overridable + """Decorator to make a function regionally overridable Example: @erpnext.allow_regional def myfunction(): - pass''' + pass""" + def caller(*args, **kwargs): region = get_region() - fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__ + fn_name = inspect.getmodule(fn).__name__ + "." + fn.__name__ if region in regional_overrides and fn_name in regional_overrides[region]: return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs) else: @@ -132,10 +148,16 @@ def allow_regional(fn): return caller + def get_last_membership(member): - '''Returns last membership if exists''' - last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', - dict(member=member, paid=1), order_by='to_date desc', limit=1) + """Returns last membership if exists""" + last_membership = frappe.get_all( + "Membership", + "name,to_date,membership_type", + dict(member=member, paid=1), + order_by="to_date desc", + limit=1, + ) if last_membership: return last_membership[0] diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index a6d08d8ff61..d844766e899 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -23,29 +23,31 @@ class ERPNextAddress(Address): if self.is_your_company_address and not [ row for row in self.links if row.link_doctype == "Company" ]: - frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."), - title=_("Company Not Linked")) + frappe.throw( + _("Address needs to be linked to a Company. Please add a row for Company in the Links table."), + title=_("Company Not Linked"), + ) def on_update(self): """ After Address is updated, update the related 'Primary Address' on Customer. """ address_display = get_address_display(self.as_dict()) - filters = { "customer_primary_address": self.name } + filters = {"customer_primary_address": self.name} customers = frappe.db.get_all("Customer", filters=filters, as_list=True) for customer_name in customers: frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display) + @frappe.whitelist() -def get_shipping_address(company, address = None): +def get_shipping_address(company, address=None): filters = [ ["Dynamic Link", "link_doctype", "=", "Company"], ["Dynamic Link", "link_name", "=", company], - ["Address", "is_your_company_address", "=", 1] + ["Address", "is_your_company_address", "=", 1], ] fields = ["*"] - if address and frappe.db.get_value('Dynamic Link', - {'parent': address, 'link_name': company}): + if address and frappe.db.get_value("Dynamic Link", {"parent": address, "link_name": company}): filters.append(["Address", "name", "=", address]) if not address: filters.append(["Address", "is_shipping_address", "=", 1]) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index 1c1364ed111..fefec0ee7b4 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -12,15 +12,24 @@ from frappe.utils.nestedset import get_descendants_of @frappe.whitelist() @cache_source -def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None, heatmap_year = None): +def get( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, +): if chart_name: - chart = frappe.get_doc('Dashboard Chart', chart_name) + chart = frappe.get_doc("Dashboard Chart", chart_name) else: chart = frappe._dict(frappe.parse_json(chart)) timespan = chart.timespan - if chart.timespan == 'Select Date Range': + if chart.timespan == "Select Date Range": from_date = chart.from_date to_date = chart.to_date @@ -31,17 +40,23 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d company = filters.get("company") if not account and chart_name: - frappe.throw(_("Account is not set for the dashboard chart {0}") - .format(get_link_to_form("Dashboard Chart", chart_name))) + frappe.throw( + _("Account is not set for the dashboard chart {0}").format( + get_link_to_form("Dashboard Chart", chart_name) + ) + ) if not frappe.db.exists("Account", account) and chart_name: - frappe.throw(_("Account {0} does not exists in the dashboard chart {1}") - .format(account, get_link_to_form("Dashboard Chart", chart_name))) + frappe.throw( + _("Account {0} does not exists in the dashboard chart {1}").format( + account, get_link_to_form("Dashboard Chart", chart_name) + ) + ) if not to_date: to_date = nowdate() if not from_date: - if timegrain in ('Monthly', 'Quarterly'): + if timegrain in ("Monthly", "Quarterly"): from_date = get_from_date_from_timespan(to_date, timespan) # fetch dates to plot @@ -54,16 +69,14 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d result = build_result(account, dates, gl_entries) return { - "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result], - "datasets": [{ - "name": account, - "values": [r[1] for r in result] - }] + "labels": [formatdate(r[0].strftime("%Y-%m-%d")) for r in result], + "datasets": [{"name": account, "values": [r[1] for r in result]}], } + def build_result(account, dates, gl_entries): result = [[getdate(date), 0.0] for date in dates] - root_type = frappe.db.get_value('Account', account, 'root_type') + root_type = frappe.db.get_value("Account", account, "root_type") # start with the first date date_index = 0 @@ -78,30 +91,34 @@ def build_result(account, dates, gl_entries): result[date_index][1] += entry.debit - entry.credit # if account type is credit, switch balances - if root_type not in ('Asset', 'Expense'): + if root_type not in ("Asset", "Expense"): for r in result: r[1] = -1 * r[1] # for balance sheet accounts, the totals are cumulative - if root_type in ('Asset', 'Liability', 'Equity'): + if root_type in ("Asset", "Liability", "Equity"): for i, r in enumerate(result): if i > 0: - r[1] = r[1] + result[i-1][1] + r[1] = r[1] + result[i - 1][1] return result + def get_gl_entries(account, to_date): - child_accounts = get_descendants_of('Account', account, ignore_permissions=True) + child_accounts = get_descendants_of("Account", account, ignore_permissions=True) child_accounts.append(account) - return frappe.db.get_all('GL Entry', - fields = ['posting_date', 'debit', 'credit'], - filters = [ - dict(posting_date = ('<', to_date)), - dict(account = ('in', child_accounts)), - dict(voucher_type = ('!=', 'Period Closing Voucher')) + return frappe.db.get_all( + "GL Entry", + fields=["posting_date", "debit", "credit"], + filters=[ + dict(posting_date=("<", to_date)), + dict(account=("in", child_accounts)), + dict(voucher_type=("!=", "Period Closing Voucher")), ], - order_by = 'posting_date asc') + order_by="posting_date asc", + ) + def get_dates_from_timegrain(from_date, to_date, timegrain): days = months = years = 0 @@ -116,6 +133,8 @@ def get_dates_from_timegrain(from_date, to_date, timegrain): dates = [get_period_ending(from_date, timegrain)] while getdate(dates[-1]) < getdate(to_date): - date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain) + date = get_period_ending( + add_to_date(dates[-1], years=years, months=months, days=days), timegrain + ) dates.append(date) return dates diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 46b7dc6a2a6..a8776fa3448 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ from frappe.email import sendmail_to_system_managers @@ -23,20 +22,23 @@ from erpnext.accounts.utils import get_account_currency def validate_service_stop_date(doc): - ''' Validates service_stop_date for Purchase Invoice and Sales Invoice ''' + """Validates service_stop_date for Purchase Invoice and Sales Invoice""" - enable_check = "enable_deferred_revenue" \ - if doc.doctype=="Sales Invoice" else "enable_deferred_expense" + enable_check = ( + "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" + ) old_stop_dates = {} - old_doc = frappe.db.get_all("{0} Item".format(doc.doctype), - {"parent": doc.name}, ["name", "service_stop_date"]) + old_doc = frappe.db.get_all( + "{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"] + ) for d in old_doc: old_stop_dates[d.name] = d.service_stop_date or "" for item in doc.items: - if not item.get(enable_check): continue + if not item.get(enable_check): + continue if item.service_stop_date: if date_diff(item.service_stop_date, item.service_start_date) < 0: @@ -45,21 +47,31 @@ def validate_service_stop_date(doc): if date_diff(item.service_stop_date, item.service_end_date) > 0: frappe.throw(_("Service Stop Date cannot be after Service End Date")) - if old_stop_dates and old_stop_dates.get(item.name) and item.service_stop_date!=old_stop_dates.get(item.name): + if ( + old_stop_dates + and old_stop_dates.get(item.name) + and item.service_stop_date != old_stop_dates.get(item.name) + ): frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx)) + def build_conditions(process_type, account, company): - conditions='' - deferred_account = "item.deferred_revenue_account" if process_type=="Income" else "item.deferred_expense_account" + conditions = "" + deferred_account = ( + "item.deferred_revenue_account" if process_type == "Income" else "item.deferred_expense_account" + ) if account: - conditions += "AND %s='%s'"%(deferred_account, account) + conditions += "AND %s='%s'" % (deferred_account, account) elif company: conditions += f"AND p.company = {frappe.db.escape(company)}" return conditions -def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=''): + +def convert_deferred_expense_to_expense( + deferred_process, start_date=None, end_date=None, conditions="" +): # book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM if not start_date: @@ -68,14 +80,19 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d end_date = add_days(today(), -1) # check for the purchase invoice for which GL entries has to be done - invoices = frappe.db.sql_list(''' + invoices = frappe.db.sql_list( + """ select distinct item.parent from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p where item.service_start_date<=%s and item.service_end_date>=%s and item.enable_deferred_expense = 1 and item.parent=p.name and item.docstatus = 1 and ifnull(item.amount, 0) > 0 {0} - '''.format(conditions), (end_date, start_date)) #nosec + """.format( + conditions + ), + (end_date, start_date), + ) # nosec # For each invoice, book deferred expense for invoice in invoices: @@ -85,7 +102,10 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d if frappe.flags.deferred_accounting_error: send_mail(deferred_process) -def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=''): + +def convert_deferred_revenue_to_income( + deferred_process, start_date=None, end_date=None, conditions="" +): # book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM if not start_date: @@ -94,14 +114,19 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da end_date = add_days(today(), -1) # check for the sales invoice for which GL entries has to be done - invoices = frappe.db.sql_list(''' + invoices = frappe.db.sql_list( + """ select distinct item.parent from `tabSales Invoice Item` item, `tabSales Invoice` p where item.service_start_date<=%s and item.service_end_date>=%s and item.enable_deferred_revenue = 1 and item.parent=p.name and item.docstatus = 1 and ifnull(item.amount, 0) > 0 {0} - '''.format(conditions), (end_date, start_date)) #nosec + """.format( + conditions + ), + (end_date, start_date), + ) # nosec for invoice in invoices: doc = frappe.get_doc("Sales Invoice", invoice) @@ -110,31 +135,43 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da if frappe.flags.deferred_accounting_error: send_mail(deferred_process) + def get_booking_dates(doc, item, posting_date=None): if not posting_date: posting_date = add_days(today(), -1) last_gl_entry = False - deferred_account = "deferred_revenue_account" if doc.doctype=="Sales Invoice" else "deferred_expense_account" + deferred_account = ( + "deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account" + ) - prev_gl_entry = frappe.db.sql(''' + prev_gl_entry = frappe.db.sql( + """ select name, posting_date from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s and is_cancelled = 0 order by posting_date desc limit 1 - ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) + """, + (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), + as_dict=True, + ) - prev_gl_via_je = frappe.db.sql(''' + prev_gl_via_je = frappe.db.sql( + """ SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c WHERE p.name = c.parent and p.company=%s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1 - ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) + """, + (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), + as_dict=True, + ) if prev_gl_via_je: - if (not prev_gl_entry) or (prev_gl_entry and - prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date): + if (not prev_gl_entry) or ( + prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date + ): prev_gl_entry = prev_gl_via_je if prev_gl_entry: @@ -158,66 +195,94 @@ def get_booking_dates(doc, item, posting_date=None): else: return None, None, None -def calculate_monthly_amount(doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency): + +def calculate_monthly_amount( + doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency +): amount, base_amount = 0, 0 if not last_gl_entry: - total_months = (item.service_end_date.year - item.service_start_date.year) * 12 + \ - (item.service_end_date.month - item.service_start_date.month) + 1 + total_months = ( + (item.service_end_date.year - item.service_start_date.year) * 12 + + (item.service_end_date.month - item.service_start_date.month) + + 1 + ) - prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) \ - / flt(date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date))) + prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) / flt( + date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date)) + ) actual_months = rounded(total_months * prorate_factor, 1) - already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item) + already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount( + doc, item + ) base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount")) if base_amount + already_booked_amount > item.base_net_amount: base_amount = item.base_net_amount - already_booked_amount - if account_currency==doc.company_currency: + if account_currency == doc.company_currency: amount = base_amount else: - amount = flt(item.net_amount/actual_months, item.precision("net_amount")) + amount = flt(item.net_amount / actual_months, item.precision("net_amount")) if amount + already_booked_amount_in_account_currency > item.net_amount: amount = item.net_amount - already_booked_amount_in_account_currency if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date): - partial_month = flt(date_diff(end_date, start_date)) \ - / flt(date_diff(get_last_day(end_date), get_first_day(start_date))) + partial_month = flt(date_diff(end_date, start_date)) / flt( + date_diff(get_last_day(end_date), get_first_day(start_date)) + ) base_amount = rounded(partial_month, 1) * base_amount amount = rounded(partial_month, 1) * amount else: - already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item) - base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount")) - if account_currency==doc.company_currency: + already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount( + doc, item + ) + base_amount = flt( + item.base_net_amount - already_booked_amount, item.precision("base_net_amount") + ) + if account_currency == doc.company_currency: amount = base_amount else: - amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")) + amount = flt( + item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount") + ) return amount, base_amount + def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency): amount, base_amount = 0, 0 if not last_gl_entry: - base_amount = flt(item.base_net_amount*total_booking_days/flt(total_days), item.precision("base_net_amount")) - if account_currency==doc.company_currency: + base_amount = flt( + item.base_net_amount * total_booking_days / flt(total_days), item.precision("base_net_amount") + ) + if account_currency == doc.company_currency: amount = base_amount else: - amount = flt(item.net_amount*total_booking_days/flt(total_days), item.precision("net_amount")) + amount = flt( + item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount") + ) else: - already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item) + already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount( + doc, item + ) - base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount")) - if account_currency==doc.company_currency: + base_amount = flt( + item.base_net_amount - already_booked_amount, item.precision("base_net_amount") + ) + if account_currency == doc.company_currency: amount = base_amount else: - amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")) + amount = flt( + item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount") + ) return amount, base_amount + def get_already_booked_amount(doc, item): if doc.doctype == "Sales Invoice": total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency" @@ -226,21 +291,31 @@ def get_already_booked_amount(doc, item): total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency" deferred_account = "deferred_expense_account" - gl_entries_details = frappe.db.sql(''' + gl_entries_details = frappe.db.sql( + """ select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s and is_cancelled = 0 group by voucher_detail_no - '''.format(total_credit_debit, total_credit_debit_currency), - (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) + """.format( + total_credit_debit, total_credit_debit_currency + ), + (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), + as_dict=True, + ) - journal_entry_details = frappe.db.sql(''' + journal_entry_details = frappe.db.sql( + """ SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s and p.docstatus < 2 group by reference_detail_no - '''.format(total_credit_debit, total_credit_debit_currency), - (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) + """.format( + total_credit_debit, total_credit_debit_currency + ), + (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), + as_dict=True, + ) already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0 already_booked_amount += journal_entry_details[0].total_credit if journal_entry_details else 0 @@ -248,20 +323,29 @@ def get_already_booked_amount(doc, item): if doc.currency == doc.company_currency: already_booked_amount_in_account_currency = already_booked_amount else: - already_booked_amount_in_account_currency = gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0 - already_booked_amount_in_account_currency += journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0 + already_booked_amount_in_account_currency = ( + gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0 + ) + already_booked_amount_in_account_currency += ( + journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0 + ) return already_booked_amount, already_booked_amount_in_account_currency + def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): - enable_check = "enable_deferred_revenue" \ - if doc.doctype=="Sales Invoice" else "enable_deferred_expense" + enable_check = ( + "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" + ) - accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto') + accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto") - def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on): + def _book_deferred_revenue_or_expense( + item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on + ): start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date) - if not (start_date and end_date): return + if not (start_date and end_date): + return account_currency = get_account_currency(item.expense_account or item.income_account) if doc.doctype == "Sales Invoice": @@ -274,12 +358,21 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): total_days = date_diff(item.service_end_date, item.service_start_date) + 1 total_booking_days = date_diff(end_date, start_date) + 1 - if book_deferred_entries_based_on == 'Months': - amount, base_amount = calculate_monthly_amount(doc, item, last_gl_entry, - start_date, end_date, total_days, total_booking_days, account_currency) + if book_deferred_entries_based_on == "Months": + amount, base_amount = calculate_monthly_amount( + doc, + item, + last_gl_entry, + start_date, + end_date, + total_days, + total_booking_days, + account_currency, + ) else: - amount, base_amount = calculate_amount(doc, item, last_gl_entry, - total_days, total_booking_days, account_currency) + amount, base_amount = calculate_amount( + doc, item, last_gl_entry, total_days, total_booking_days, account_currency + ) if not amount: return @@ -289,92 +382,155 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): end_date = get_last_day(add_days(accounts_frozen_upto, 1)) if via_journal_entry: - book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, - base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) + book_revenue_via_journal_entry( + doc, + credit_account, + debit_account, + amount, + base_amount, + end_date, + project, + account_currency, + item.cost_center, + item, + deferred_process, + submit_journal_entry, + ) else: - make_gl_entries(doc, credit_account, debit_account, against, - amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process) + make_gl_entries( + doc, + credit_account, + debit_account, + against, + amount, + base_amount, + end_date, + project, + account_currency, + item.cost_center, + item, + deferred_process, + ) # Returned in case of any errors because it tries to submit the same record again and again in case of errors if frappe.flags.deferred_accounting_error: return if getdate(end_date) < getdate(posting_date) and not last_gl_entry: - _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on) + _book_deferred_revenue_or_expense( + item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on + ) - via_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_via_journal_entry')) - submit_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'submit_journal_entries')) - book_deferred_entries_based_on = frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_based_on') + via_journal_entry = cint( + frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry") + ) + submit_journal_entry = cint( + frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries") + ) + book_deferred_entries_based_on = frappe.db.get_singles_value( + "Accounts Settings", "book_deferred_entries_based_on" + ) - for item in doc.get('items'): + for item in doc.get("items"): if item.get(enable_check): - _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on) + _book_deferred_revenue_or_expense( + item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on + ) + def process_deferred_accounting(posting_date=None): - ''' Converts deferred income/expense into income/expense - Executed via background jobs on every month end ''' + """Converts deferred income/expense into income/expense + Executed via background jobs on every month end""" if not posting_date: posting_date = today() - if not cint(frappe.db.get_singles_value('Accounts Settings', 'automatically_process_deferred_accounting_entry')): + if not cint( + frappe.db.get_singles_value( + "Accounts Settings", "automatically_process_deferred_accounting_entry" + ) + ): return start_date = add_months(today(), -1) end_date = add_days(today(), -1) - companies = frappe.get_all('Company') + companies = frappe.get_all("Company") for company in companies: - for record_type in ('Income', 'Expense'): - doc = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - company=company.name, - posting_date=posting_date, - start_date=start_date, - end_date=end_date, - type=record_type - )) + for record_type in ("Income", "Expense"): + doc = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + company=company.name, + posting_date=posting_date, + start_date=start_date, + end_date=end_date, + type=record_type, + ) + ) doc.insert() doc.submit() -def make_gl_entries(doc, credit_account, debit_account, against, - amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): + +def make_gl_entries( + doc, + credit_account, + debit_account, + against, + amount, + base_amount, + posting_date, + project, + account_currency, + cost_center, + item, + deferred_process=None, +): # GL Entry for crediting the amount in the deferred expense from erpnext.accounts.general_ledger import make_gl_entries - if amount == 0: return + if amount == 0: + return gl_entries = [] gl_entries.append( - doc.get_gl_dict({ - "account": credit_account, - "against": against, - "credit": base_amount, - "credit_in_account_currency": amount, - "cost_center": cost_center, - "voucher_detail_no": item.name, - 'posting_date': posting_date, - 'project': project, - 'against_voucher_type': 'Process Deferred Accounting', - 'against_voucher': deferred_process - }, account_currency, item=item) + doc.get_gl_dict( + { + "account": credit_account, + "against": against, + "credit": base_amount, + "credit_in_account_currency": amount, + "cost_center": cost_center, + "voucher_detail_no": item.name, + "posting_date": posting_date, + "project": project, + "against_voucher_type": "Process Deferred Accounting", + "against_voucher": deferred_process, + }, + account_currency, + item=item, + ) ) # GL Entry to debit the amount from the expense gl_entries.append( - doc.get_gl_dict({ - "account": debit_account, - "against": against, - "debit": base_amount, - "debit_in_account_currency": amount, - "cost_center": cost_center, - "voucher_detail_no": item.name, - 'posting_date': posting_date, - 'project': project, - 'against_voucher_type': 'Process Deferred Accounting', - 'against_voucher': deferred_process - }, account_currency, item=item) + doc.get_gl_dict( + { + "account": debit_account, + "against": against, + "debit": base_amount, + "debit_in_account_currency": amount, + "cost_center": cost_center, + "voucher_detail_no": item.name, + "posting_date": posting_date, + "project": project, + "against_voucher_type": "Process Deferred Accounting", + "against_voucher": deferred_process, + }, + account_currency, + item=item, + ) ) if gl_entries: @@ -384,68 +540,88 @@ def make_gl_entries(doc, credit_account, debit_account, against, except Exception as e: if frappe.flags.in_test: traceback = frappe.get_traceback() - frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) + frappe.log_error( + title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name), + message=traceback, + ) raise e else: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) + frappe.log_error( + title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name), + message=traceback, + ) frappe.flags.deferred_accounting_error = True + def send_mail(deferred_process): title = _("Error while processing deferred accounting for {0}").format(deferred_process) - link = get_link_to_form('Process Deferred Accounting', deferred_process) + link = get_link_to_form("Process Deferred Accounting", deferred_process) content = _("Deferred accounting failed for some invoices:") + "\n" - content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link) + content += _( + "Please check Process Deferred Accounting {0} and submit manually after resolving errors." + ).format(link) sendmail_to_system_managers(title, content) -def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, - amount, base_amount, posting_date, project, account_currency, cost_center, item, - deferred_process=None, submit='No'): - if amount == 0: return +def book_revenue_via_journal_entry( + doc, + credit_account, + debit_account, + amount, + base_amount, + posting_date, + project, + account_currency, + cost_center, + item, + deferred_process=None, + submit="No", +): - journal_entry = frappe.new_doc('Journal Entry') + if amount == 0: + return + + journal_entry = frappe.new_doc("Journal Entry") journal_entry.posting_date = posting_date journal_entry.company = doc.company - journal_entry.voucher_type = 'Deferred Revenue' if doc.doctype == 'Sales Invoice' \ - else 'Deferred Expense' + journal_entry.voucher_type = ( + "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense" + ) + journal_entry.process_deferred_accounting = deferred_process debit_entry = { - 'account': credit_account, - 'credit': base_amount, - 'credit_in_account_currency': amount, - 'account_currency': account_currency, - 'reference_name': doc.name, - 'reference_type': doc.doctype, - 'reference_detail_no': item.name, - 'cost_center': cost_center, - 'project': project, + "account": credit_account, + "credit": base_amount, + "credit_in_account_currency": amount, + "account_currency": account_currency, + "reference_name": doc.name, + "reference_type": doc.doctype, + "reference_detail_no": item.name, + "cost_center": cost_center, + "project": project, } credit_entry = { - 'account': debit_account, - 'debit': base_amount, - 'debit_in_account_currency': amount, - 'account_currency': account_currency, - 'reference_name': doc.name, - 'reference_type': doc.doctype, - 'reference_detail_no': item.name, - 'cost_center': cost_center, - 'project': project, + "account": debit_account, + "debit": base_amount, + "debit_in_account_currency": amount, + "account_currency": account_currency, + "reference_name": doc.name, + "reference_type": doc.doctype, + "reference_detail_no": item.name, + "cost_center": cost_center, + "project": project, } for dimension in get_accounting_dimensions(): - debit_entry.update({ - dimension: item.get(dimension) - }) + debit_entry.update({dimension: item.get(dimension)}) - credit_entry.update({ - dimension: item.get(dimension) - }) + credit_entry.update({dimension: item.get(dimension)}) - journal_entry.append('accounts', debit_entry) - journal_entry.append('accounts', credit_entry) + journal_entry.append("accounts", debit_entry) + journal_entry.append("accounts", credit_entry) try: journal_entry.save() @@ -457,20 +633,30 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, except Exception: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) + frappe.log_error( + title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name), + message=traceback, + ) frappe.flags.deferred_accounting_error = True + def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr): - if doctype == 'Sales Invoice': - credit_account, debit_account = frappe.db.get_value('Sales Invoice Item', {'name': voucher_detail_no}, - ['income_account', 'deferred_revenue_account']) + if doctype == "Sales Invoice": + credit_account, debit_account = frappe.db.get_value( + "Sales Invoice Item", + {"name": voucher_detail_no}, + ["income_account", "deferred_revenue_account"], + ) else: - credit_account, debit_account = frappe.db.get_value('Purchase Invoice Item', {'name': voucher_detail_no}, - ['deferred_expense_account', 'expense_account']) + credit_account, debit_account = frappe.db.get_value( + "Purchase Invoice Item", + {"name": voucher_detail_no}, + ["deferred_expense_account", "expense_account"], + ) - if dr_or_cr == 'Debit': + if dr_or_cr == "Debit": return debit_account else: return credit_account diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index f8a06c7243f..c71ea3648b9 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -10,11 +10,17 @@ from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_ import erpnext -class RootNotEditable(frappe.ValidationError): pass -class BalanceMismatchError(frappe.ValidationError): pass +class RootNotEditable(frappe.ValidationError): + pass + + +class BalanceMismatchError(frappe.ValidationError): + pass + class Account(NestedSet): - nsm_parent_field = 'parent_account' + nsm_parent_field = "parent_account" + def on_update(self): if frappe.local.flags.ignore_update_nsm: return @@ -22,17 +28,20 @@ class Account(NestedSet): super(Account, self).on_update() def onload(self): - frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", "Accounts Settings", - "frozen_accounts_modifier") + frozen_accounts_modifier = frappe.db.get_value( + "Accounts Settings", "Accounts Settings", "frozen_accounts_modifier" + ) if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles(): self.set_onload("can_freeze_account", True) def autoname(self): from erpnext.accounts.utils import get_autoname_with_number + self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) def validate(self): from erpnext.accounts.utils import validate_field_number + if frappe.local.flags.allow_unverified_charts: return self.validate_parent() @@ -49,22 +58,33 @@ class Account(NestedSet): def validate_parent(self): """Fetch Parent Details and validate parent account""" if self.parent_account: - par = frappe.db.get_value("Account", self.parent_account, - ["name", "is_group", "company"], as_dict=1) + par = frappe.db.get_value( + "Account", self.parent_account, ["name", "is_group", "company"], as_dict=1 + ) if not par: - throw(_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account)) + throw( + _("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account) + ) elif par.name == self.name: throw(_("Account {0}: You can not assign itself as parent account").format(self.name)) elif not par.is_group: - throw(_("Account {0}: Parent account {1} can not be a ledger").format(self.name, self.parent_account)) + throw( + _("Account {0}: Parent account {1} can not be a ledger").format( + self.name, self.parent_account + ) + ) elif par.company != self.company: - throw(_("Account {0}: Parent account {1} does not belong to company: {2}") - .format(self.name, self.parent_account, self.company)) + throw( + _("Account {0}: Parent account {1} does not belong to company: {2}").format( + self.name, self.parent_account, self.company + ) + ) def set_root_and_report_type(self): if self.parent_account: - par = frappe.db.get_value("Account", self.parent_account, - ["report_type", "root_type"], as_dict=1) + par = frappe.db.get_value( + "Account", self.parent_account, ["report_type", "root_type"], as_dict=1 + ) if par.report_type: self.report_type = par.report_type @@ -75,15 +95,20 @@ class Account(NestedSet): db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1) if db_value: if self.report_type != db_value.report_type: - frappe.db.sql("update `tabAccount` set report_type=%s where lft > %s and rgt < %s", - (self.report_type, self.lft, self.rgt)) + frappe.db.sql( + "update `tabAccount` set report_type=%s where lft > %s and rgt < %s", + (self.report_type, self.lft, self.rgt), + ) if self.root_type != db_value.root_type: - frappe.db.sql("update `tabAccount` set root_type=%s where lft > %s and rgt < %s", - (self.root_type, self.lft, self.rgt)) + frappe.db.sql( + "update `tabAccount` set root_type=%s where lft > %s and rgt < %s", + (self.root_type, self.lft, self.rgt), + ) if self.root_type and not self.report_type: - self.report_type = "Balance Sheet" \ - if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss" + self.report_type = ( + "Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss" + ) def validate_root_details(self): # does not exists parent @@ -96,21 +121,26 @@ class Account(NestedSet): def validate_root_company_and_sync_account_to_children(self): # ignore validation while creating new compnay or while syncing to child companies - if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation: + if ( + frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation + ): return ancestors = get_root_company(self.company) if ancestors: if frappe.get_value("Company", self.company, "allow_account_creation_against_child_company"): return - if not frappe.db.get_value("Account", - {'account_name': self.account_name, 'company': ancestors[0]}, 'name'): + if not frappe.db.get_value( + "Account", {"account_name": self.account_name, "company": ancestors[0]}, "name" + ): frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0])) elif self.parent_account: - descendants = get_descendants_of('Company', self.company) - if not descendants: return + descendants = get_descendants_of("Company", self.company) + if not descendants: + return parent_acc_name_map = {} - parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \ - ["account_name", "account_number"]) + parent_acc_name, parent_acc_number = frappe.db.get_value( + "Account", self.parent_account, ["account_name", "account_number"] + ) filters = { "company": ["in", descendants], "account_name": parent_acc_name, @@ -118,10 +148,13 @@ class Account(NestedSet): if parent_acc_number: filters["account_number"] = parent_acc_number - for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True): + for d in frappe.db.get_values( + "Account", filters=filters, fieldname=["company", "name"], as_dict=True + ): parent_acc_name_map[d["company"]] = d["name"] - if not parent_acc_name_map: return + if not parent_acc_name_map: + return self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) @@ -142,26 +175,38 @@ class Account(NestedSet): def validate_frozen_accounts_modifier(self): old_value = frappe.db.get_value("Account", self.name, "freeze_account") if old_value and old_value != self.freeze_account: - frozen_accounts_modifier = frappe.db.get_value('Accounts Settings', None, 'frozen_accounts_modifier') - if not frozen_accounts_modifier or \ - frozen_accounts_modifier not in frappe.get_roles(): - throw(_("You are not authorized to set Frozen value")) + frozen_accounts_modifier = frappe.db.get_value( + "Accounts Settings", None, "frozen_accounts_modifier" + ) + if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles(): + throw(_("You are not authorized to set Frozen value")) def validate_balance_must_be_debit_or_credit(self): from erpnext.accounts.utils import get_balance_on + if not self.get("__islocal") and self.balance_must_be: account_balance = get_balance_on(self.name) if account_balance > 0 and self.balance_must_be == "Credit": - frappe.throw(_("Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'")) + frappe.throw( + _( + "Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'" + ) + ) elif account_balance < 0 and self.balance_must_be == "Debit": - frappe.throw(_("Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'")) + frappe.throw( + _( + "Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'" + ) + ) def validate_account_currency(self): if not self.account_currency: - self.account_currency = frappe.get_cached_value('Company', self.company, "default_currency") + self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency") - elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"): + gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency") + + if gl_currency and self.account_currency != gl_currency: if frappe.db.get_value("GL Entry", {"account": self.name}): frappe.throw(_("Currency can not be changed after making entries using some other currency")) @@ -170,45 +215,52 @@ class Account(NestedSet): company_bold = frappe.bold(company) parent_acc_name_bold = frappe.bold(parent_acc_name) if not parent_acc_name_map.get(company): - frappe.throw(_("While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA") - .format(company_bold, parent_acc_name_bold), title=_("Account Not Found")) + frappe.throw( + _( + "While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA" + ).format(company_bold, parent_acc_name_bold), + title=_("Account Not Found"), + ) # validate if parent of child company account to be added is a group - if (frappe.db.get_value("Account", self.parent_account, "is_group") - and not frappe.db.get_value("Account", parent_acc_name_map[company], "is_group")): - msg = _("While creating account for Child Company {0}, parent account {1} found as a ledger account.").format(company_bold, parent_acc_name_bold) + if frappe.db.get_value("Account", self.parent_account, "is_group") and not frappe.db.get_value( + "Account", parent_acc_name_map[company], "is_group" + ): + msg = _( + "While creating account for Child Company {0}, parent account {1} found as a ledger account." + ).format(company_bold, parent_acc_name_bold) msg += "

" - msg += _("Please convert the parent account in corresponding child company to a group account.") + msg += _( + "Please convert the parent account in corresponding child company to a group account." + ) frappe.throw(msg, title=_("Invalid Parent Account")) - filters = { - "account_name": self.account_name, - "company": company - } + filters = {"account_name": self.account_name, "company": company} if self.account_number: filters["account_number"] = self.account_number - child_account = frappe.db.get_value("Account", filters, 'name') + child_account = frappe.db.get_value("Account", filters, "name") if not child_account: doc = frappe.copy_doc(self) doc.flags.ignore_root_company_validation = True - doc.update({ - "company": company, - # parent account's currency should be passed down to child account's curreny - # if it is None, it picks it up from default company currency, which might be unintended - "account_currency": erpnext.get_company_currency(company), - "parent_account": parent_acc_name_map[company] - }) + doc.update( + { + "company": company, + # parent account's currency should be passed down to child account's curreny + # if it is None, it picks it up from default company currency, which might be unintended + "account_currency": erpnext.get_company_currency(company), + "parent_account": parent_acc_name_map[company], + } + ) doc.save() - frappe.msgprint(_("Account {0} is added in the child company {1}") - .format(doc.name, company)) + frappe.msgprint(_("Account {0} is added in the child company {1}").format(doc.name, company)) elif child_account: # update the parent company's value in child companies doc = frappe.get_doc("Account", child_account) parent_value_changed = False - for field in ['account_type', 'freeze_account', 'balance_must_be']: + for field in ["account_type", "freeze_account", "balance_must_be"]: if doc.get(field) != self.get(field): parent_value_changed = True doc.set(field, self.get(field)) @@ -243,8 +295,11 @@ class Account(NestedSet): return frappe.db.get_value("GL Entry", {"account": self.name}) def check_if_child_exists(self): - return frappe.db.sql("""select name from `tabAccount` where parent_account = %s - and docstatus != 2""", self.name) + return frappe.db.sql( + """select name from `tabAccount` where parent_account = %s + and docstatus != 2""", + self.name, + ) def validate_mandatory(self): if not self.root_type: @@ -260,73 +315,99 @@ class Account(NestedSet): super(Account, self).on_trash(True) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_parent_account(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select name from tabAccount + return frappe.db.sql( + """select name from tabAccount where is_group = 1 and docstatus != 2 and company = %s - and %s like %s order by name limit %s, %s""" % - ("%s", searchfield, "%s", "%s", "%s"), - (filters["company"], "%%%s%%" % txt, start, page_len), as_list=1) + and %s like %s order by name limit %s, %s""" + % ("%s", searchfield, "%s", "%s", "%s"), + (filters["company"], "%%%s%%" % txt, start, page_len), + as_list=1, + ) + def get_account_currency(account): """Helper function to get account currency""" if not account: return + def generator(): - account_currency, company = frappe.get_cached_value("Account", account, ["account_currency", "company"]) + account_currency, company = frappe.get_cached_value( + "Account", account, ["account_currency", "company"] + ) if not account_currency: - account_currency = frappe.get_cached_value('Company', company, "default_currency") + account_currency = frappe.get_cached_value("Company", company, "default_currency") return account_currency return frappe.local_cache("account_currency", account, generator) + def on_doctype_update(): frappe.db.add_index("Account", ["lft", "rgt"]) + def get_account_autoname(account_number, account_name, company): # first validate if company exists - company = frappe.get_cached_value('Company', company, ["abbr", "name"], as_dict=True) + company = frappe.get_cached_value("Company", company, ["abbr", "name"], as_dict=True) if not company: - frappe.throw(_('Company {0} does not exist').format(company)) + frappe.throw(_("Company {0} does not exist").format(company)) parts = [account_name.strip(), company.abbr] if cstr(account_number).strip(): parts.insert(0, cstr(account_number).strip()) - return ' - '.join(parts) + return " - ".join(parts) + def validate_account_number(name, account_number, company): if account_number: - account_with_same_number = frappe.db.get_value("Account", - {"account_number": account_number, "company": company, "name": ["!=", name]}) + account_with_same_number = frappe.db.get_value( + "Account", {"account_number": account_number, "company": company, "name": ["!=", name]} + ) if account_with_same_number: - frappe.throw(_("Account Number {0} already used in account {1}") - .format(account_number, account_with_same_number)) + frappe.throw( + _("Account Number {0} already used in account {1}").format( + account_number, account_with_same_number + ) + ) + @frappe.whitelist() def update_account_number(name, account_name, account_number=None, from_descendant=False): account = frappe.db.get_value("Account", name, "company", as_dict=True) - if not account: return + if not account: + return - old_acc_name, old_acc_number = frappe.db.get_value('Account', name, \ - ["account_name", "account_number"]) + old_acc_name, old_acc_number = frappe.db.get_value( + "Account", name, ["account_name", "account_number"] + ) # check if account exists in parent company ancestors = get_ancestors_of("Company", account.company) - allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company") + allow_independent_account_creation = frappe.get_value( + "Company", account.company, "allow_account_creation_against_child_company" + ) if ancestors and not allow_independent_account_creation: for ancestor in ancestors: - if frappe.db.get_value("Account", {'account_name': old_acc_name, 'company': ancestor}, 'name'): + if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"): # same account in parent company exists allow_child_account_creation = _("Allow Account Creation Against Child Company") - message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor)) + message = _("Account {0} exists in parent company {1}.").format( + frappe.bold(old_acc_name), frappe.bold(ancestor) + ) message += "
" - message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(frappe.bold(ancestor)) + message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format( + frappe.bold(ancestor) + ) message += "

" - message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company)) + message += _("To overrule this, enable '{0}' in company {1}").format( + allow_child_account_creation, frappe.bold(account.company) + ) frappe.throw(message, title=_("Rename Not Allowed")) @@ -339,42 +420,53 @@ def update_account_number(name, account_name, account_number=None, from_descenda if not from_descendant: # Update and rename in child company accounts as well - descendants = get_descendants_of('Company', account.company) + descendants = get_descendants_of("Company", account.company) if descendants: - sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number) + sync_update_account_number_in_child( + descendants, old_acc_name, account_name, account_number, old_acc_number + ) new_name = get_account_autoname(account_number, account_name, account.company) if name != new_name: frappe.rename_doc("Account", name, new_name, force=1) return new_name + @frappe.whitelist() def merge_account(old, new, is_group, root_type, company): # Validate properties before merging if not frappe.db.exists("Account", new): throw(_("Account {0} does not exist").format(new)) - val = list(frappe.db.get_value("Account", new, - ["is_group", "root_type", "company"])) + val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"])) if val != [cint(is_group), root_type, company]: - throw(_("""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company""")) + throw( + _( + """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company""" + ) + ) if is_group and frappe.db.get_value("Account", new, "parent_account") == old: - frappe.db.set_value("Account", new, "parent_account", - frappe.db.get_value("Account", old, "parent_account")) + frappe.db.set_value( + "Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account") + ) frappe.rename_doc("Account", old, new, merge=1, force=1) return new + @frappe.whitelist() def get_root_company(company): # return the topmost company in the hierarchy - ancestors = get_ancestors_of('Company', company, "lft asc") + ancestors = get_ancestors_of("Company", company, "lft asc") return [ancestors[0]] if ancestors else [] -def sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number=None, old_acc_number=None): + +def sync_update_account_number_in_child( + descendants, old_acc_name, account_name, account_number=None, old_acc_number=None +): filters = { "company": ["in", descendants], "account_name": old_acc_name, @@ -382,5 +474,7 @@ def sync_update_account_number_in_child(descendants, old_acc_name, account_name, if old_acc_number: filters["account_number"] = old_acc_number - for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True): - update_account_number(d["name"], account_name, account_number, from_descendant=True) + for d in frappe.db.get_values( + "Account", filters=filters, fieldname=["company", "name"], as_dict=True + ): + update_account_number(d["name"], account_name, account_number, from_descendant=True) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 3a5514388ca..fd3c19149ba 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -11,7 +11,9 @@ from six import iteritems from unidecode import unidecode -def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None): +def create_charts( + company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None +): chart = custom_chart or get_chart(chart_template, existing_company) if chart: accounts = [] @@ -21,30 +23,41 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch if root_account: root_type = child.get("root_type") - if account_name not in ["account_name", "account_number", "account_type", - "root_type", "is_group", "tax_rate"]: + if account_name not in [ + "account_name", + "account_number", + "account_type", + "root_type", + "is_group", + "tax_rate", + ]: account_number = cstr(child.get("account_number")).strip() - account_name, account_name_in_db = add_suffix_if_duplicate(account_name, - account_number, accounts) + account_name, account_name_in_db = add_suffix_if_duplicate( + account_name, account_number, accounts + ) is_group = identify_is_group(child) - report_type = "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] \ - else "Profit and Loss" + report_type = ( + "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss" + ) - account = frappe.get_doc({ - "doctype": "Account", - "account_name": child.get('account_name') if from_coa_importer else account_name, - "company": company, - "parent_account": parent, - "is_group": is_group, - "root_type": root_type, - "report_type": report_type, - "account_number": account_number, - "account_type": child.get("account_type"), - "account_currency": child.get('account_currency') or frappe.db.get_value('Company', company, "default_currency"), - "tax_rate": child.get("tax_rate") - }) + account = frappe.get_doc( + { + "doctype": "Account", + "account_name": child.get("account_name") if from_coa_importer else account_name, + "company": company, + "parent_account": parent, + "is_group": is_group, + "root_type": root_type, + "report_type": report_type, + "account_number": account_number, + "account_type": child.get("account_type"), + "account_currency": child.get("account_currency") + or frappe.db.get_value("Company", company, "default_currency"), + "tax_rate": child.get("tax_rate"), + } + ) if root_account or frappe.local.flags.allow_unverified_charts: account.flags.ignore_mandatory = True @@ -64,10 +77,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch rebuild_tree("Account", "parent_account") frappe.local.flags.ignore_update_nsm = False + def add_suffix_if_duplicate(account_name, account_number, accounts): if account_number: - account_name_in_db = unidecode(" - ".join([account_number, - account_name.strip().lower()])) + account_name_in_db = unidecode(" - ".join([account_number, account_name.strip().lower()])) else: account_name_in_db = unidecode(account_name.strip().lower()) @@ -77,16 +90,21 @@ def add_suffix_if_duplicate(account_name, account_number, accounts): return account_name, account_name_in_db + def identify_is_group(child): if child.get("is_group"): is_group = child.get("is_group") - elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])): + elif len( + set(child.keys()) + - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"]) + ): is_group = 1 else: is_group = 0 return is_group + def get_chart(chart_template, existing_company=None): chart = {} if existing_company: @@ -96,11 +114,13 @@ def get_chart(chart_template, existing_company=None): from erpnext.accounts.doctype.account.chart_of_accounts.verified import ( standard_chart_of_accounts, ) + return standard_chart_of_accounts.get() elif chart_template == "Standard with Numbers": from erpnext.accounts.doctype.account.chart_of_accounts.verified import ( standard_chart_of_accounts_with_account_number, ) + return standard_chart_of_accounts_with_account_number.get() else: folders = ("verified",) @@ -116,6 +136,7 @@ def get_chart(chart_template, existing_company=None): if chart and json.loads(chart).get("name") == chart_template: return json.loads(chart).get("tree") + @frappe.whitelist() def get_charts_for_country(country, with_standard=False): charts = [] @@ -123,9 +144,10 @@ def get_charts_for_country(country, with_standard=False): def _get_chart_name(content): if content: content = json.loads(content) - if (content and content.get("disabled", "No") == "No") \ - or frappe.local.flags.allow_unverified_charts: - charts.append(content["name"]) + if ( + content and content.get("disabled", "No") == "No" + ) or frappe.local.flags.allow_unverified_charts: + charts.append(content["name"]) country_code = frappe.db.get_value("Country", country, "code") if country_code: @@ -152,11 +174,21 @@ def get_charts_for_country(country, with_standard=False): def get_account_tree_from_existing_company(existing_company): - all_accounts = frappe.get_all('Account', - filters={'company': existing_company}, - fields = ["name", "account_name", "parent_account", "account_type", - "is_group", "root_type", "tax_rate", "account_number"], - order_by="lft, rgt") + all_accounts = frappe.get_all( + "Account", + filters={"company": existing_company}, + fields=[ + "name", + "account_name", + "parent_account", + "account_type", + "is_group", + "root_type", + "tax_rate", + "account_number", + ], + order_by="lft, rgt", + ) account_tree = {} @@ -165,6 +197,7 @@ def get_account_tree_from_existing_company(existing_company): build_account_tree(account_tree, None, all_accounts) return account_tree + def build_account_tree(tree, parent, all_accounts): # find children parent_account = parent.name if parent else "" @@ -193,27 +226,29 @@ def build_account_tree(tree, parent, all_accounts): # call recursively to build a subtree for current account build_account_tree(tree[child.account_name], child, all_accounts) + @frappe.whitelist() def validate_bank_account(coa, bank_account): accounts = [] chart = get_chart(coa) if chart: + def _get_account_names(account_master): for account_name, child in iteritems(account_master): - if account_name not in ["account_number", "account_type", - "root_type", "is_group", "tax_rate"]: + if account_name not in ["account_number", "account_type", "root_type", "is_group", "tax_rate"]: accounts.append(account_name) _get_account_names(child) _get_account_names(chart) - return (bank_account in accounts) + return bank_account in accounts + @frappe.whitelist() def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False): - ''' get chart template from its folder and parse the json to be rendered as tree ''' + """get chart template from its folder and parse the json to be rendered as tree""" chart = chart_data or get_chart(chart_template) # if no template selected, return as it is @@ -221,22 +256,33 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals return accounts = [] + def _import_accounts(children, parent): - ''' recursively called to form a parent-child based list of dict from chart template ''' + """recursively called to form a parent-child based list of dict from chart template""" for account_name, child in iteritems(children): account = {} - if account_name in ["account_name", "account_number", "account_type",\ - "root_type", "is_group", "tax_rate"]: continue + if account_name in [ + "account_name", + "account_number", + "account_type", + "root_type", + "is_group", + "tax_rate", + ]: + continue if from_coa_importer: - account_name = child['account_name'] + account_name = child["account_name"] - account['parent_account'] = parent - account['expandable'] = True if identify_is_group(child) else False - account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \ - if child.get('account_number') else account_name + account["parent_account"] = parent + account["expandable"] = True if identify_is_group(child) else False + account["value"] = ( + (cstr(child.get("account_number")).strip() + " - " + account_name) + if child.get("account_number") + else account_name + ) accounts.append(account) - _import_accounts(child, account['value']) + _import_accounts(child, account["value"]) _import_accounts(chart, None) return accounts diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py index 7d94c89ad7b..562b00fd000 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py @@ -21,6 +21,7 @@ charts = {} all_account_types = [] all_roots = {} + def go(): global accounts, charts default_account_types = get_default_account_types() @@ -35,14 +36,16 @@ def go(): accounts, charts = {}, {} country_path = os.path.join(path, country_dir) manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read()) - data_files = manifest.get("data", []) + manifest.get("init_xml", []) + \ - manifest.get("update_xml", []) + data_files = ( + manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", []) + ) files_path = [os.path.join(country_path, d) for d in data_files] xml_roots = get_xml_roots(files_path) csv_content = get_csv_contents(files_path) prefix = country_dir if csv_content else None - account_types = get_account_types(xml_roots.get("account.account.type", []), - csv_content.get("account.account.type", []), prefix) + account_types = get_account_types( + xml_roots.get("account.account.type", []), csv_content.get("account.account.type", []), prefix + ) account_types.update(default_account_types) if xml_roots: @@ -55,12 +58,15 @@ def go(): create_all_roots_file() + def get_default_account_types(): default_types_root = [] - default_types_root.append(ET.parse(os.path.join(path, "account", "data", - "data_account_type.xml")).getroot()) + default_types_root.append( + ET.parse(os.path.join(path, "account", "data", "data_account_type.xml")).getroot() + ) return get_account_types(default_types_root, None, prefix="account") + def get_xml_roots(files_path): xml_roots = frappe._dict() for filepath in files_path: @@ -69,64 +75,69 @@ def get_xml_roots(files_path): tree = ET.parse(filepath) root = tree.getroot() for node in root[0].findall("record"): - if node.get("model") in ["account.account.template", - "account.chart.template", "account.account.type"]: + if node.get("model") in [ + "account.account.template", + "account.chart.template", + "account.account.type", + ]: xml_roots.setdefault(node.get("model"), []).append(root) break return xml_roots + def get_csv_contents(files_path): csv_content = {} for filepath in files_path: fname = os.path.basename(filepath) - for file_type in ["account.account.template", "account.account.type", - "account.chart.template"]: + for file_type in ["account.account.template", "account.account.type", "account.chart.template"]: if fname.startswith(file_type) and fname.endswith(".csv"): with open(filepath, "r") as csvfile: try: - csv_content.setdefault(file_type, [])\ - .append(read_csv_content(csvfile.read())) + csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read())) except Exception as e: continue return csv_content + def get_account_types(root_list, csv_content, prefix=None): types = {} account_type_map = { - 'cash': 'Cash', - 'bank': 'Bank', - 'tr_cash': 'Cash', - 'tr_bank': 'Bank', - 'receivable': 'Receivable', - 'tr_receivable': 'Receivable', - 'account rec': 'Receivable', - 'payable': 'Payable', - 'tr_payable': 'Payable', - 'equity': 'Equity', - 'stocks': 'Stock', - 'stock': 'Stock', - 'tax': 'Tax', - 'tr_tax': 'Tax', - 'tax-out': 'Tax', - 'tax-in': 'Tax', - 'charges_personnel': 'Chargeable', - 'fixed asset': 'Fixed Asset', - 'cogs': 'Cost of Goods Sold', - + "cash": "Cash", + "bank": "Bank", + "tr_cash": "Cash", + "tr_bank": "Bank", + "receivable": "Receivable", + "tr_receivable": "Receivable", + "account rec": "Receivable", + "payable": "Payable", + "tr_payable": "Payable", + "equity": "Equity", + "stocks": "Stock", + "stock": "Stock", + "tax": "Tax", + "tr_tax": "Tax", + "tax-out": "Tax", + "tax-in": "Tax", + "charges_personnel": "Chargeable", + "fixed asset": "Fixed Asset", + "cogs": "Cost of Goods Sold", } for root in root_list: for node in root[0].findall("record"): - if node.get("model")=="account.account.type": + if node.get("model") == "account.account.type": data = {} for field in node.findall("field"): - if field.get("name")=="code" and field.text.lower() != "none" \ - and account_type_map.get(field.text): - data["account_type"] = account_type_map[field.text] + if ( + field.get("name") == "code" + and field.text.lower() != "none" + and account_type_map.get(field.text) + ): + data["account_type"] = account_type_map[field.text] node_id = prefix + "." + node.get("id") if prefix else node.get("id") types[node_id] = data - if csv_content and csv_content[0][0]=="id": + if csv_content and csv_content[0][0] == "id": for row in csv_content[1:]: row_dict = dict(zip(csv_content[0], row)) data = {} @@ -137,21 +148,22 @@ def get_account_types(root_list, csv_content, prefix=None): types[node_id] = data return types + def make_maps_for_xml(xml_roots, account_types, country_dir): """make maps for `charts` and `accounts`""" for model, root_list in iteritems(xml_roots): for root in root_list: for node in root[0].findall("record"): - if node.get("model")=="account.account.template": + if node.get("model") == "account.account.template": data = {} for field in node.findall("field"): - if field.get("name")=="name": + if field.get("name") == "name": data["name"] = field.text - if field.get("name")=="parent_id": + if field.get("name") == "parent_id": parent_id = field.get("ref") or field.get("eval") data["parent_id"] = parent_id - if field.get("name")=="user_type": + if field.get("name") == "user_type": value = field.get("ref") if account_types.get(value, {}).get("account_type"): data["account_type"] = account_types[value]["account_type"] @@ -161,16 +173,17 @@ def make_maps_for_xml(xml_roots, account_types, country_dir): data["children"] = [] accounts[node.get("id")] = data - if node.get("model")=="account.chart.template": + if node.get("model") == "account.chart.template": data = {} for field in node.findall("field"): - if field.get("name")=="name": + if field.get("name") == "name": data["name"] = field.text - if field.get("name")=="account_root_id": + if field.get("name") == "account_root_id": data["account_root_id"] = field.get("ref") data["id"] = country_dir charts.setdefault(node.get("id"), {}).update(data) + def make_maps_for_csv(csv_content, account_types, country_dir): for content in csv_content.get("account.account.template", []): for row in content[1:]: @@ -178,7 +191,7 @@ def make_maps_for_csv(csv_content, account_types, country_dir): account = { "name": data.get("name"), "parent_id": data.get("parent_id:id") or data.get("parent_id/id"), - "children": [] + "children": [], } user_type = data.get("user_type/id") or data.get("user_type:id") if account_types.get(user_type, {}).get("account_type"): @@ -195,12 +208,14 @@ def make_maps_for_csv(csv_content, account_types, country_dir): for row in content[1:]: if row: data = dict(zip(content[0], row)) - charts.setdefault(data.get("id"), {}).update({ - "account_root_id": data.get("account_root_id:id") or \ - data.get("account_root_id/id"), - "name": data.get("name"), - "id": country_dir - }) + charts.setdefault(data.get("id"), {}).update( + { + "account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"), + "name": data.get("name"), + "id": country_dir, + } + ) + def make_account_trees(): """build tree hierarchy""" @@ -219,6 +234,7 @@ def make_account_trees(): if "children" in accounts[id] and not accounts[id].get("children"): del accounts[id]["children"] + def make_charts(): """write chart files in app/setup/doctype/company/charts""" for chart_id in charts: @@ -237,34 +253,38 @@ def make_charts(): chart["country_code"] = src["id"][5:] chart["tree"] = accounts[src["account_root_id"]] - for key, val in chart["tree"].items(): if key in ["name", "parent_id"]: chart["tree"].pop(key) if type(val) == dict: val["root_type"] = "" if chart: - fpath = os.path.join("erpnext", "erpnext", "accounts", "doctype", "account", - "chart_of_accounts", filename + ".json") + fpath = os.path.join( + "erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json" + ) with open(fpath, "r") as chartfile: old_content = chartfile.read() - if not old_content or (json.loads(old_content).get("is_active", "No") == "No" \ - and json.loads(old_content).get("disabled", "No") == "No"): + if not old_content or ( + json.loads(old_content).get("is_active", "No") == "No" + and json.loads(old_content).get("disabled", "No") == "No" + ): with open(fpath, "w") as chartfile: chartfile.write(json.dumps(chart, indent=4, sort_keys=True)) all_roots.setdefault(filename, chart["tree"].keys()) + def create_all_roots_file(): - with open('all_roots.txt', 'w') as f: + with open("all_roots.txt", "w") as f: for filename, roots in sorted(all_roots.items()): f.write(filename) - f.write('\n----------------------\n') + f.write("\n----------------------\n") for r in sorted(roots): - f.write(r.encode('utf-8')) - f.write('\n') - f.write('\n\n\n') + f.write(r.encode("utf-8")) + f.write("\n") + f.write("\n\n\n") -if __name__=="__main__": + +if __name__ == "__main__": go() diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py index 9248ffa6e57..e30ad24a374 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py @@ -7,182 +7,103 @@ from frappe import _ def get(): return { - _("Application of Funds (Assets)"): { - _("Current Assets"): { - _("Accounts Receivable"): { - _("Debtors"): { - "account_type": "Receivable" - } - }, - _("Bank Accounts"): { - "account_type": "Bank", - "is_group": 1 - }, - _("Cash In Hand"): { - _("Cash"): { - "account_type": "Cash" - }, - "account_type": "Cash" - }, - _("Loans and Advances (Assets)"): { - _("Employee Advances"): { - }, - }, - _("Securities and Deposits"): { - _("Earnest Money"): {} - }, - _("Stock Assets"): { - _("Stock In Hand"): { - "account_type": "Stock" - }, - "account_type": "Stock", - }, - _("Tax Assets"): { - "is_group": 1 - } - }, - _("Fixed Assets"): { - _("Capital Equipments"): { - "account_type": "Fixed Asset" - }, - _("Electronic Equipments"): { - "account_type": "Fixed Asset" - }, - _("Furnitures and Fixtures"): { - "account_type": "Fixed Asset" - }, - _("Office Equipments"): { - "account_type": "Fixed Asset" - }, - _("Plants and Machineries"): { - "account_type": "Fixed Asset" - }, - _("Buildings"): { - "account_type": "Fixed Asset" + _("Application of Funds (Assets)"): { + _("Current Assets"): { + _("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}}, + _("Bank Accounts"): {"account_type": "Bank", "is_group": 1}, + _("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"}, + _("Loans and Advances (Assets)"): { + _("Employee Advances"): {}, }, - _("Softwares"): { - "account_type": "Fixed Asset" + _("Securities and Deposits"): {_("Earnest Money"): {}}, + _("Stock Assets"): { + _("Stock In Hand"): {"account_type": "Stock"}, + "account_type": "Stock", }, - _("Accumulated Depreciation"): { - "account_type": "Accumulated Depreciation" - }, - _("CWIP Account"): { - "account_type": "Capital Work in Progress", - } - }, - _("Investments"): { - "is_group": 1 - }, - _("Temporary Accounts"): { - _("Temporary Opening"): { - "account_type": "Temporary" - } - }, - "root_type": "Asset" - }, - _("Expenses"): { - _("Direct Expenses"): { - _("Stock Expenses"): { - _("Cost of Goods Sold"): { - "account_type": "Cost of Goods Sold" - }, - _("Expenses Included In Asset Valuation"): { - "account_type": "Expenses Included In Asset Valuation" - }, - _("Expenses Included In Valuation"): { - "account_type": "Expenses Included In Valuation" - }, - _("Stock Adjustment"): { - "account_type": "Stock Adjustment" - } - }, - }, - _("Indirect Expenses"): { - _("Administrative Expenses"): {}, - _("Commission on Sales"): {}, - _("Depreciation"): { - "account_type": "Depreciation" - }, - _("Entertainment Expenses"): {}, - _("Freight and Forwarding Charges"): { - "account_type": "Chargeable" - }, - _("Legal Expenses"): {}, - _("Marketing Expenses"): { - "account_type": "Chargeable" - }, - _("Miscellaneous Expenses"): { - "account_type": "Chargeable" - }, - _("Office Maintenance Expenses"): {}, - _("Office Rent"): {}, - _("Postal Expenses"): {}, - _("Print and Stationery"): {}, - _("Round Off"): { - "account_type": "Round Off" - }, - _("Salary"): {}, - _("Sales Expenses"): {}, - _("Telephone Expenses"): {}, - _("Travel Expenses"): {}, - _("Utility Expenses"): {}, + _("Tax Assets"): {"is_group": 1}, + }, + _("Fixed Assets"): { + _("Capital Equipments"): {"account_type": "Fixed Asset"}, + _("Electronic Equipments"): {"account_type": "Fixed Asset"}, + _("Furnitures and Fixtures"): {"account_type": "Fixed Asset"}, + _("Office Equipments"): {"account_type": "Fixed Asset"}, + _("Plants and Machineries"): {"account_type": "Fixed Asset"}, + _("Buildings"): {"account_type": "Fixed Asset"}, + _("Softwares"): {"account_type": "Fixed Asset"}, + _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"}, + _("CWIP Account"): { + "account_type": "Capital Work in Progress", + }, + }, + _("Investments"): {"is_group": 1}, + _("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}}, + "root_type": "Asset", + }, + _("Expenses"): { + _("Direct Expenses"): { + _("Stock Expenses"): { + _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"}, + _("Expenses Included In Asset Valuation"): { + "account_type": "Expenses Included In Asset Valuation" + }, + _("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"}, + _("Stock Adjustment"): {"account_type": "Stock Adjustment"}, + }, + }, + _("Indirect Expenses"): { + _("Administrative Expenses"): {}, + _("Commission on Sales"): {}, + _("Depreciation"): {"account_type": "Depreciation"}, + _("Entertainment Expenses"): {}, + _("Freight and Forwarding Charges"): {"account_type": "Chargeable"}, + _("Legal Expenses"): {}, + _("Marketing Expenses"): {"account_type": "Chargeable"}, + _("Miscellaneous Expenses"): {"account_type": "Chargeable"}, + _("Office Maintenance Expenses"): {}, + _("Office Rent"): {}, + _("Postal Expenses"): {}, + _("Print and Stationery"): {}, + _("Round Off"): {"account_type": "Round Off"}, + _("Salary"): {}, + _("Sales Expenses"): {}, + _("Telephone Expenses"): {}, + _("Travel Expenses"): {}, + _("Utility Expenses"): {}, _("Write Off"): {}, _("Exchange Gain/Loss"): {}, - _("Gain/Loss on Asset Disposal"): {} - }, - "root_type": "Expense" - }, - _("Income"): { - _("Direct Income"): { - _("Sales"): {}, - _("Service"): {} - }, - _("Indirect Income"): { - "is_group": 1 - }, - "root_type": "Income" - }, - _("Source of Funds (Liabilities)"): { - _("Current Liabilities"): { - _("Accounts Payable"): { - _("Creditors"): { - "account_type": "Payable" - }, - _("Payroll Payable"): {}, - }, - _("Stock Liabilities"): { - _("Stock Received But Not Billed"): { - "account_type": "Stock Received But Not Billed" - }, - _("Asset Received But Not Billed"): { - "account_type": "Asset Received But Not Billed" - } - }, - _("Duties and Taxes"): { - "account_type": "Tax", - "is_group": 1 + _("Gain/Loss on Asset Disposal"): {}, + }, + "root_type": "Expense", + }, + _("Income"): { + _("Direct Income"): {_("Sales"): {}, _("Service"): {}}, + _("Indirect Income"): {"is_group": 1}, + "root_type": "Income", + }, + _("Source of Funds (Liabilities)"): { + _("Current Liabilities"): { + _("Accounts Payable"): { + _("Creditors"): {"account_type": "Payable"}, + _("Payroll Payable"): {}, }, + _("Stock Liabilities"): { + _("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"}, + _("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"}, + }, + _("Duties and Taxes"): {"account_type": "Tax", "is_group": 1}, _("Loans (Liabilities)"): { _("Secured Loans"): {}, _("Unsecured Loans"): {}, _("Bank Overdraft Account"): {}, }, - }, - "root_type": "Liability" - }, + }, + "root_type": "Liability", + }, _("Equity"): { - _("Capital Stock"): { - "account_type": "Equity" - }, - _("Dividends Paid"): { - "account_type": "Equity" - }, - _("Opening Balance Equity"): { - "account_type": "Equity" - }, - _("Retained Earnings"): { - "account_type": "Equity" - }, - "root_type": "Equity" - } + _("Capital Stock"): {"account_type": "Equity"}, + _("Dividends Paid"): {"account_type": "Equity"}, + _("Opening Balance Equity"): {"account_type": "Equity"}, + _("Retained Earnings"): {"account_type": "Equity"}, + "root_type": "Equity", + }, } diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py index 31ae17189a7..0e46f1e08a5 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py @@ -6,288 +6,153 @@ from frappe import _ def get(): - return { - _("Application of Funds (Assets)"): { - _("Current Assets"): { - _("Accounts Receivable"): { - _("Debtors"): { - "account_type": "Receivable", - "account_number": "1310" - }, - "account_number": "1300" - }, - _("Bank Accounts"): { - "account_type": "Bank", - "is_group": 1, - "account_number": "1200" - }, - _("Cash In Hand"): { - _("Cash"): { - "account_type": "Cash", - "account_number": "1110" - }, - "account_type": "Cash", - "account_number": "1100" - }, - _("Loans and Advances (Assets)"): { - _("Employee Advances"): { - "account_number": "1610" - }, - "account_number": "1600" - }, - _("Securities and Deposits"): { - _("Earnest Money"): { - "account_number": "1651" - }, - "account_number": "1650" - }, - _("Stock Assets"): { - _("Stock In Hand"): { - "account_type": "Stock", - "account_number": "1410" - }, - "account_type": "Stock", - "account_number": "1400" - }, - _("Tax Assets"): { - "is_group": 1, - "account_number": "1500" - }, - "account_number": "1100-1600" - }, - _("Fixed Assets"): { - _("Capital Equipments"): { - "account_type": "Fixed Asset", - "account_number": "1710" - }, - _("Electronic Equipments"): { - "account_type": "Fixed Asset", - "account_number": "1720" - }, - _("Furnitures and Fixtures"): { - "account_type": "Fixed Asset", - "account_number": "1730" - }, - _("Office Equipments"): { - "account_type": "Fixed Asset", - "account_number": "1740" - }, - _("Plants and Machineries"): { - "account_type": "Fixed Asset", - "account_number": "1750" - }, - _("Buildings"): { - "account_type": "Fixed Asset", - "account_number": "1760" - }, - _("Softwares"): { - "account_type": "Fixed Asset", - "account_number": "1770" - }, - _("Accumulated Depreciation"): { - "account_type": "Accumulated Depreciation", - "account_number": "1780" - }, - _("CWIP Account"): { - "account_type": "Capital Work in Progress", - "account_number": "1790" - }, - "account_number": "1700" - }, - _("Investments"): { - "is_group": 1, - "account_number": "1800" - }, - _("Temporary Accounts"): { - _("Temporary Opening"): { - "account_type": "Temporary", - "account_number": "1910" - }, - "account_number": "1900" - }, - "root_type": "Asset", - "account_number": "1000" - }, - _("Expenses"): { - _("Direct Expenses"): { - _("Stock Expenses"): { - _("Cost of Goods Sold"): { - "account_type": "Cost of Goods Sold", - "account_number": "5111" - }, - _("Expenses Included In Asset Valuation"): { - "account_type": "Expenses Included In Asset Valuation", - "account_number": "5112" - }, - _("Expenses Included In Valuation"): { - "account_type": "Expenses Included In Valuation", - "account_number": "5118" - }, - _("Stock Adjustment"): { - "account_type": "Stock Adjustment", - "account_number": "5119" - }, - "account_number": "5110" - }, - "account_number": "5100" - }, - _("Indirect Expenses"): { - _("Administrative Expenses"): { - "account_number": "5201" - }, - _("Commission on Sales"): { - "account_number": "5202" - }, - _("Depreciation"): { - "account_type": "Depreciation", - "account_number": "5203" - }, - _("Entertainment Expenses"): { - "account_number": "5204" - }, - _("Freight and Forwarding Charges"): { - "account_type": "Chargeable", - "account_number": "5205" - }, - _("Legal Expenses"): { - "account_number": "5206" - }, - _("Marketing Expenses"): { - "account_type": "Chargeable", - "account_number": "5207" - }, - _("Office Maintenance Expenses"): { - "account_number": "5208" - }, - _("Office Rent"): { - "account_number": "5209" - }, - _("Postal Expenses"): { - "account_number": "5210" - }, - _("Print and Stationery"): { - "account_number": "5211" - }, - _("Round Off"): { - "account_type": "Round Off", - "account_number": "5212" - }, - _("Salary"): { - "account_number": "5213" - }, - _("Sales Expenses"): { - "account_number": "5214" - }, - _("Telephone Expenses"): { - "account_number": "5215" - }, - _("Travel Expenses"): { - "account_number": "5216" - }, - _("Utility Expenses"): { - "account_number": "5217" - }, - _("Write Off"): { - "account_number": "5218" - }, - _("Exchange Gain/Loss"): { - "account_number": "5219" - }, - _("Gain/Loss on Asset Disposal"): { - "account_number": "5220" - }, - _("Miscellaneous Expenses"): { - "account_type": "Chargeable", - "account_number": "5221" - }, - "account_number": "5200" - }, - "root_type": "Expense", - "account_number": "5000" - }, - _("Income"): { - _("Direct Income"): { - _("Sales"): { - "account_number": "4110" - }, - _("Service"): { - "account_number": "4120" - }, - "account_number": "4100" - }, - _("Indirect Income"): { - "is_group": 1, - "account_number": "4200" - }, - "root_type": "Income", - "account_number": "4000" - }, - _("Source of Funds (Liabilities)"): { - _("Current Liabilities"): { - _("Accounts Payable"): { - _("Creditors"): { - "account_type": "Payable", - "account_number": "2110" - }, - _("Payroll Payable"): { - "account_number": "2120" - }, - "account_number": "2100" - }, - _("Stock Liabilities"): { - _("Stock Received But Not Billed"): { - "account_type": "Stock Received But Not Billed", - "account_number": "2210" - }, - _("Asset Received But Not Billed"): { - "account_type": "Asset Received But Not Billed", - "account_number": "2211" - }, - "account_number": "2200" - }, - _("Duties and Taxes"): { - _("TDS Payable"): { - "account_number": "2310" - }, - "account_type": "Tax", - "is_group": 1, - "account_number": "2300" - }, - _("Loans (Liabilities)"): { - _("Secured Loans"): { - "account_number": "2410" - }, - _("Unsecured Loans"): { - "account_number": "2420" - }, - _("Bank Overdraft Account"): { - "account_number": "2430" - }, - "account_number": "2400" - }, - "account_number": "2100-2400" - }, - "root_type": "Liability", - "account_number": "2000" - }, - _("Equity"): { - _("Capital Stock"): { - "account_type": "Equity", - "account_number": "3100" - }, - _("Dividends Paid"): { - "account_type": "Equity", - "account_number": "3200" - }, - _("Opening Balance Equity"): { - "account_type": "Equity", - "account_number": "3300" - }, - _("Retained Earnings"): { - "account_type": "Equity", - "account_number": "3400" - }, - "root_type": "Equity", - "account_number": "3000" - } - } + return { + _("Application of Funds (Assets)"): { + _("Current Assets"): { + _("Accounts Receivable"): { + _("Debtors"): {"account_type": "Receivable", "account_number": "1310"}, + "account_number": "1300", + }, + _("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"}, + _("Cash In Hand"): { + _("Cash"): {"account_type": "Cash", "account_number": "1110"}, + "account_type": "Cash", + "account_number": "1100", + }, + _("Loans and Advances (Assets)"): { + _("Employee Advances"): {"account_number": "1610"}, + "account_number": "1600", + }, + _("Securities and Deposits"): { + _("Earnest Money"): {"account_number": "1651"}, + "account_number": "1650", + }, + _("Stock Assets"): { + _("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"}, + "account_type": "Stock", + "account_number": "1400", + }, + _("Tax Assets"): {"is_group": 1, "account_number": "1500"}, + "account_number": "1100-1600", + }, + _("Fixed Assets"): { + _("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"}, + _("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"}, + _("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"}, + _("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"}, + _("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"}, + _("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"}, + _("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"}, + _("Accumulated Depreciation"): { + "account_type": "Accumulated Depreciation", + "account_number": "1780", + }, + _("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"}, + "account_number": "1700", + }, + _("Investments"): {"is_group": 1, "account_number": "1800"}, + _("Temporary Accounts"): { + _("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"}, + "account_number": "1900", + }, + "root_type": "Asset", + "account_number": "1000", + }, + _("Expenses"): { + _("Direct Expenses"): { + _("Stock Expenses"): { + _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"}, + _("Expenses Included In Asset Valuation"): { + "account_type": "Expenses Included In Asset Valuation", + "account_number": "5112", + }, + _("Expenses Included In Valuation"): { + "account_type": "Expenses Included In Valuation", + "account_number": "5118", + }, + _("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"}, + "account_number": "5110", + }, + "account_number": "5100", + }, + _("Indirect Expenses"): { + _("Administrative Expenses"): {"account_number": "5201"}, + _("Commission on Sales"): {"account_number": "5202"}, + _("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"}, + _("Entertainment Expenses"): {"account_number": "5204"}, + _("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"}, + _("Legal Expenses"): {"account_number": "5206"}, + _("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"}, + _("Office Maintenance Expenses"): {"account_number": "5208"}, + _("Office Rent"): {"account_number": "5209"}, + _("Postal Expenses"): {"account_number": "5210"}, + _("Print and Stationery"): {"account_number": "5211"}, + _("Round Off"): {"account_type": "Round Off", "account_number": "5212"}, + _("Salary"): {"account_number": "5213"}, + _("Sales Expenses"): {"account_number": "5214"}, + _("Telephone Expenses"): {"account_number": "5215"}, + _("Travel Expenses"): {"account_number": "5216"}, + _("Utility Expenses"): {"account_number": "5217"}, + _("Write Off"): {"account_number": "5218"}, + _("Exchange Gain/Loss"): {"account_number": "5219"}, + _("Gain/Loss on Asset Disposal"): {"account_number": "5220"}, + _("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"}, + "account_number": "5200", + }, + "root_type": "Expense", + "account_number": "5000", + }, + _("Income"): { + _("Direct Income"): { + _("Sales"): {"account_number": "4110"}, + _("Service"): {"account_number": "4120"}, + "account_number": "4100", + }, + _("Indirect Income"): {"is_group": 1, "account_number": "4200"}, + "root_type": "Income", + "account_number": "4000", + }, + _("Source of Funds (Liabilities)"): { + _("Current Liabilities"): { + _("Accounts Payable"): { + _("Creditors"): {"account_type": "Payable", "account_number": "2110"}, + _("Payroll Payable"): {"account_number": "2120"}, + "account_number": "2100", + }, + _("Stock Liabilities"): { + _("Stock Received But Not Billed"): { + "account_type": "Stock Received But Not Billed", + "account_number": "2210", + }, + _("Asset Received But Not Billed"): { + "account_type": "Asset Received But Not Billed", + "account_number": "2211", + }, + "account_number": "2200", + }, + _("Duties and Taxes"): { + _("TDS Payable"): {"account_number": "2310"}, + "account_type": "Tax", + "is_group": 1, + "account_number": "2300", + }, + _("Loans (Liabilities)"): { + _("Secured Loans"): {"account_number": "2410"}, + _("Unsecured Loans"): {"account_number": "2420"}, + _("Bank Overdraft Account"): {"account_number": "2430"}, + "account_number": "2400", + }, + "account_number": "2100-2400", + }, + "root_type": "Liability", + "account_number": "2000", + }, + _("Equity"): { + _("Capital Stock"): {"account_type": "Equity", "account_number": "3100"}, + _("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"}, + _("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"}, + _("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"}, + "root_type": "Equity", + "account_number": "3000", + }, + } diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 0715823b300..f9c9173af08 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -20,8 +20,9 @@ class TestAccount(unittest.TestCase): acc.company = "_Test Company" acc.insert() - account_number, account_name = frappe.db.get_value("Account", "1210 - Debtors - _TC", - ["account_number", "account_name"]) + account_number, account_name = frappe.db.get_value( + "Account", "1210 - Debtors - _TC", ["account_number", "account_name"] + ) self.assertEqual(account_number, "1210") self.assertEqual(account_name, "Debtors") @@ -30,8 +31,12 @@ class TestAccount(unittest.TestCase): update_account_number("1210 - Debtors - _TC", new_account_name, new_account_number) - new_acc = frappe.db.get_value("Account", "1211-11-4 - 6 - - Debtors 1 - Test - - _TC", - ["account_name", "account_number"], as_dict=1) + new_acc = frappe.db.get_value( + "Account", + "1211-11-4 - 6 - - Debtors 1 - Test - - _TC", + ["account_name", "account_number"], + as_dict=1, + ) self.assertEqual(new_acc.account_name, "Debtors 1 - Test -") self.assertEqual(new_acc.account_number, "1211-11-4 - 6 -") @@ -79,7 +84,9 @@ class TestAccount(unittest.TestCase): self.assertEqual(parent, "Securities and Deposits - _TC") - merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company) + merge_account( + "Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company + ) parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account") # Parent account of the child account changes after merging @@ -91,14 +98,28 @@ class TestAccount(unittest.TestCase): doc = frappe.get_doc("Account", "Current Assets - _TC") # Raise error as is_group property doesn't match - self.assertRaises(frappe.ValidationError, merge_account, "Current Assets - _TC",\ - "Accumulated Depreciation - _TC", doc.is_group, doc.root_type, doc.company) + self.assertRaises( + frappe.ValidationError, + merge_account, + "Current Assets - _TC", + "Accumulated Depreciation - _TC", + doc.is_group, + doc.root_type, + doc.company, + ) doc = frappe.get_doc("Account", "Capital Stock - _TC") # Raise error as root_type property doesn't match - self.assertRaises(frappe.ValidationError, merge_account, "Capital Stock - _TC",\ - "Softwares - _TC", doc.is_group, doc.root_type, doc.company) + self.assertRaises( + frappe.ValidationError, + merge_account, + "Capital Stock - _TC", + "Softwares - _TC", + doc.is_group, + doc.root_type, + doc.company, + ) def test_account_sync(self): frappe.local.flags.pop("ignore_root_company_validation", None) @@ -109,8 +130,12 @@ class TestAccount(unittest.TestCase): acc.company = "_Test Company 3" acc.insert() - acc_tc_4 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 4"}) - acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 5"}) + acc_tc_4 = frappe.db.get_value( + "Account", {"account_name": "Test Sync Account", "company": "_Test Company 4"} + ) + acc_tc_5 = frappe.db.get_value( + "Account", {"account_name": "Test Sync Account", "company": "_Test Company 5"} + ) self.assertEqual(acc_tc_4, "Test Sync Account - _TC4") self.assertEqual(acc_tc_5, "Test Sync Account - _TC5") @@ -138,8 +163,26 @@ class TestAccount(unittest.TestCase): update_account_number(acc.name, "Test Rename Sync Account", "1234") # Check if renamed in children - self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"})) - self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"})) + self.assertTrue( + frappe.db.exists( + "Account", + { + "account_name": "Test Rename Sync Account", + "company": "_Test Company 4", + "account_number": "1234", + }, + ) + ) + self.assertTrue( + frappe.db.exists( + "Account", + { + "account_name": "Test Rename Sync Account", + "company": "_Test Company 5", + "account_number": "1234", + }, + ) + ) frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4") @@ -155,25 +198,71 @@ class TestAccount(unittest.TestCase): acc.company = "_Test Company 3" acc.insert() - self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"})) - self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"})) + self.assertTrue( + frappe.db.exists( + "Account", {"account_name": "Test Group Account", "company": "_Test Company 4"} + ) + ) + self.assertTrue( + frappe.db.exists( + "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"} + ) + ) # Try renaming child company account - acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"}) - self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account") + acc_tc_5 = frappe.db.get_value( + "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"} + ) + self.assertRaises( + frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account" + ) # Rename child company account with allow_account_creation_against_child_company enabled - frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1) + frappe.db.set_value( + "Company", "_Test Company 5", "allow_account_creation_against_child_company", 1 + ) update_account_number(acc_tc_5, "Test Modified Account") - self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"})) + self.assertTrue( + frappe.db.exists( + "Account", {"name": "Test Modified Account - _TC5", "company": "_Test Company 5"} + ) + ) - frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0) + frappe.db.set_value( + "Company", "_Test Company 5", "allow_account_creation_against_child_company", 0 + ) - to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"] + to_delete = [ + "Test Group Account - _TC3", + "Test Group Account - _TC4", + "Test Modified Account - _TC5", + ] for doc in to_delete: frappe.delete_doc("Account", doc) + def test_validate_account_currency(self): + from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry + + if not frappe.db.get_value("Account", "Test Currency Account - _TC"): + acc = frappe.new_doc("Account") + acc.account_name = "Test Currency Account" + acc.parent_account = "Tax Assets - _TC" + acc.company = "_Test Company" + acc.insert() + else: + acc = frappe.get_doc("Account", "Test Currency Account - _TC") + + self.assertEqual(acc.account_currency, "INR") + + # Make a JV against this account + make_journal_entry( + "Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True + ) + + acc.account_currency = "USD" + self.assertRaises(frappe.ValidationError, acc.save) + def _make_test_records(verbose=None): from frappe.test_runner import make_test_objects @@ -184,20 +273,16 @@ def _make_test_records(verbose=None): ["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"], ["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"], ["_Test Cash", "Cash In Hand", 0, "Cash", None], - ["_Test Account Stock Expenses", "Direct Expenses", 1, None, None], ["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None], ["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None], ["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None], ["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None], ["_Test Employee Advance", "Current Liabilities", 0, None, None], - ["_Test Account Tax Assets", "Current Assets", 1, None, None], ["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None], ["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None], - ["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None], - ["_Test Account Cost for Goods Sold", "Expenses", 0, None, None], ["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None], ["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None], @@ -206,38 +291,45 @@ def _make_test_records(verbose=None): ["_Test Account Discount", "Direct Expenses", 0, None, None], ["_Test Write Off", "Indirect Expenses", 0, None, None], ["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None], - ["_Test Account Sales", "Direct Income", 0, None, None], - # related to Account Inventory Integration ["_Test Account Stock In Hand", "Current Assets", 0, None, None], - # fixed asset depreciation ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], ["_Test Depreciations", "Expenses", 0, None, None], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], - # Receivable / Payable Account ["_Test Receivable", "Current Assets", 0, "Receivable", None], ["_Test Payable", "Current Liabilities", 0, "Payable", None], ["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"], - ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"] + ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"], ] - for company, abbr in [["_Test Company", "_TC"], ["_Test Company 1", "_TC1"], ["_Test Company with perpetual inventory", "TCP1"]]: - test_objects = make_test_objects("Account", [{ - "doctype": "Account", - "account_name": account_name, - "parent_account": parent_account + " - " + abbr, - "company": company, - "is_group": is_group, - "account_type": account_type, - "account_currency": currency - } for account_name, parent_account, is_group, account_type, currency in accounts]) + for company, abbr in [ + ["_Test Company", "_TC"], + ["_Test Company 1", "_TC1"], + ["_Test Company with perpetual inventory", "TCP1"], + ]: + test_objects = make_test_objects( + "Account", + [ + { + "doctype": "Account", + "account_name": account_name, + "parent_account": parent_account + " - " + abbr, + "company": company, + "is_group": is_group, + "account_type": account_type, + "account_currency": currency, + } + for account_name, parent_account, is_group, account_type, currency in accounts + ], + ) return test_objects + def get_inventory_account(company, warehouse=None): account = None if warehouse: @@ -247,19 +339,24 @@ def get_inventory_account(company, warehouse=None): return account + def create_account(**kwargs): - account = frappe.db.get_value("Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}) + account = frappe.db.get_value( + "Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")} + ) if account: return account else: - account = frappe.get_doc(dict( - doctype = "Account", - account_name = kwargs.get('account_name'), - account_type = kwargs.get('account_type'), - parent_account = kwargs.get('parent_account'), - company = kwargs.get('company'), - account_currency = kwargs.get('account_currency') - )) + account = frappe.get_doc( + dict( + doctype="Account", + account_name=kwargs.get("account_name"), + account_type=kwargs.get("account_type"), + parent_account=kwargs.get("parent_account"), + company=kwargs.get("company"), + account_currency=kwargs.get("account_currency"), + ) + ) account.save() return account.name diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index b6112e0cc57..3f1998a3b39 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -17,13 +17,21 @@ class AccountingDimension(Document): self.set_fieldname_and_label() def validate(self): - if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', - 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') : + if self.document_type in core_doctypes_list + ( + "Accounting Dimension", + "Project", + "Cost Center", + "Accounting Dimension Detail", + "Company", + "Account", + ): msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) frappe.throw(msg) - exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name']) + exists = frappe.db.get_value( + "Accounting Dimension", {"document_type": self.document_type}, ["name"] + ) if exists and self.is_new(): frappe.throw(_("Document Type already used as a dimension")) @@ -42,13 +50,13 @@ class AccountingDimension(Document): if frappe.flags.in_test: make_dimension_in_accounting_doctypes(doc=self) else: - frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long') + frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long") def on_trash(self): if frappe.flags.in_test: delete_accounting_dimension(doc=self) else: - frappe.enqueue(delete_accounting_dimension, doc=self, queue='long') + frappe.enqueue(delete_accounting_dimension, doc=self, queue="long") def set_fieldname_and_label(self): if not self.label: @@ -60,6 +68,7 @@ class AccountingDimension(Document): def on_update(self): frappe.flags.accounting_dimensions = None + def make_dimension_in_accounting_doctypes(doc, doclist=None): if not doclist: doclist = get_doctypes_with_dimensions() @@ -70,9 +79,9 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None): for doctype in doclist: if (doc_count + 1) % 2 == 0: - insert_after_field = 'dimension_col_break' + insert_after_field = "dimension_col_break" else: - insert_after_field = 'accounting_dimensions_section' + insert_after_field = "accounting_dimensions_section" df = { "fieldname": doc.fieldname, @@ -80,30 +89,33 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None): "fieldtype": "Link", "options": doc.document_type, "insert_after": insert_after_field, - "owner": "Administrator" + "owner": "Administrator", } meta = frappe.get_meta(doctype, cached=False) fieldnames = [d.fieldname for d in meta.get("fields")] - if df['fieldname'] not in fieldnames: + if df["fieldname"] not in fieldnames: if doctype == "Budget": add_dimension_to_budget_doctype(df.copy(), doc) else: - create_custom_field(doctype, df) + create_custom_field(doctype, df, ignore_validate=True) count += 1 - frappe.publish_progress(count*100/len(doclist), title = _("Creating Dimensions...")) + frappe.publish_progress(count * 100 / len(doclist), title=_("Creating Dimensions...")) frappe.clear_cache(doctype=doctype) -def add_dimension_to_budget_doctype(df, doc): - df.update({ - "insert_after": "cost_center", - "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type) - }) - create_custom_field("Budget", df) +def add_dimension_to_budget_doctype(df, doc): + df.update( + { + "insert_after": "cost_center", + "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type), + } + ) + + create_custom_field("Budget", df, ignore_validate=True) property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options") @@ -112,36 +124,44 @@ def add_dimension_to_budget_doctype(df, doc): property_setter_doc.value = property_setter_doc.value + "\n" + doc.document_type property_setter_doc.save() - frappe.clear_cache(doctype='Budget') + frappe.clear_cache(doctype="Budget") else: - frappe.get_doc({ - "doctype": "Property Setter", - "doctype_or_field": "DocField", - "doc_type": "Budget", - "field_name": "budget_against", - "property": "options", - "property_type": "Text", - "value": "\nCost Center\nProject\n" + doc.document_type - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "budget_against", + "property": "options", + "property_type": "Text", + "value": "\nCost Center\nProject\n" + doc.document_type, + } + ).insert(ignore_permissions=True) def delete_accounting_dimension(doc): doclist = get_doctypes_with_dimensions() - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabCustom Field` WHERE fieldname = %s - AND dt IN (%s)""" % #nosec - ('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist)) + AND dt IN (%s)""" + % ("%s", ", ".join(["%s"] * len(doclist))), # nosec + tuple([doc.fieldname] + doclist), + ) - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabProperty Setter` WHERE field_name = %s - AND doc_type IN (%s)""" % #nosec - ('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist)) + AND doc_type IN (%s)""" + % ("%s", ", ".join(["%s"] * len(doclist))), # nosec + tuple([doc.fieldname] + doclist), + ) budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options") - value_list = budget_against_property.value.split('\n')[3:] + value_list = budget_against_property.value.split("\n")[3:] if doc.document_type in value_list: value_list.remove(doc.document_type) @@ -152,6 +172,7 @@ def delete_accounting_dimension(doc): for doctype in doclist: frappe.clear_cache(doctype=doctype) + @frappe.whitelist() def disable_dimension(doc): if frappe.flags.in_test: @@ -159,10 +180,11 @@ def disable_dimension(doc): else: frappe.enqueue(toggle_disabling, doc=doc) + def toggle_disabling(doc): doc = json.loads(doc) - if doc.get('disabled'): + if doc.get("disabled"): df = {"read_only": 1} else: df = {"read_only": 0} @@ -170,7 +192,7 @@ def toggle_disabling(doc): doclist = get_doctypes_with_dimensions() for doctype in doclist: - field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get('fieldname')}) + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get("fieldname")}) if field: custom_field = frappe.get_doc("Custom Field", field) custom_field.update(df) @@ -178,26 +200,40 @@ def toggle_disabling(doc): frappe.clear_cache(doctype=doctype) + def get_doctypes_with_dimensions(): return frappe.get_hooks("accounting_dimension_doctypes") -def get_accounting_dimensions(as_list=True): + +def get_accounting_dimensions(as_list=True, filters=None): + + if not filters: + filters = {"disabled": 0} + if frappe.flags.accounting_dimensions is None: - frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension", - fields=["label", "fieldname", "disabled", "document_type"]) + frappe.flags.accounting_dimensions = frappe.get_all( + "Accounting Dimension", + fields=["label", "fieldname", "disabled", "document_type"], + filters=filters, + ) if as_list: return [d.fieldname for d in frappe.flags.accounting_dimensions] else: return frappe.flags.accounting_dimensions + def get_checks_for_pl_and_bs_accounts(): - dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs + dimensions = frappe.db.sql( + """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c - WHERE p.name = c.parent""", as_dict=1) + WHERE p.name = c.parent""", + as_dict=1, + ) return dimensions + def get_dimension_with_children(doctype, dimension): if isinstance(dimension, list): @@ -205,34 +241,39 @@ def get_dimension_with_children(doctype, dimension): all_dimensions = [] lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"]) - children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft") + children = frappe.get_all( + doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft" + ) all_dimensions += [c.name for c in children] return all_dimensions + @frappe.whitelist() def get_dimensions(with_cost_center_and_project=False): - dimension_filters = frappe.db.sql(""" + dimension_filters = frappe.db.sql( + """ SELECT label, fieldname, document_type FROM `tabAccounting Dimension` WHERE disabled = 0 - """, as_dict=1) + """, + as_dict=1, + ) - default_dimensions = frappe.db.sql("""SELECT p.fieldname, c.company, c.default_dimension + default_dimensions = frappe.db.sql( + """SELECT p.fieldname, c.company, c.default_dimension FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p - WHERE c.parent = p.name""", as_dict=1) + WHERE c.parent = p.name""", + as_dict=1, + ) if with_cost_center_and_project: - dimension_filters.extend([ - { - 'fieldname': 'cost_center', - 'document_type': 'Cost Center' - }, - { - 'fieldname': 'project', - 'document_type': 'Project' - } - ]) + dimension_filters.extend( + [ + {"fieldname": "cost_center", "document_type": "Cost Center"}, + {"fieldname": "project", "document_type": "Project"}, + ] + ) default_dimensions_map = {} for dimension in default_dimensions: diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index f781a221ddf..25ef2ea5c2c 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -8,7 +8,8 @@ import frappe 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 -test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department'] +test_dependencies = ["Cost Center", "Location", "Warehouse", "Department"] + class TestAccountingDimension(unittest.TestCase): def setUp(self): @@ -18,24 +19,27 @@ class TestAccountingDimension(unittest.TestCase): si = create_sales_invoice(do_not_save=1) si.location = "Block 1" - si.append("items", { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "rate": 100, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - "department": "_Test Department - _TC", - "location": "Block 1" - }) + si.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": 100, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + "department": "_Test Department - _TC", + "location": "Block 1", + }, + ) si.save() si.submit() gle = frappe.get_doc("GL Entry", {"voucher_no": si.name, "account": "Sales - _TC"}) - self.assertEqual(gle.get('department'), "_Test Department - _TC") + self.assertEqual(gle.get("department"), "_Test Department - _TC") def test_dimension_against_journal_entry(self): je = make_journal_entry("Sales - _TC", "Sales Expenses - _TC", 500, save=False) @@ -50,21 +54,24 @@ class TestAccountingDimension(unittest.TestCase): gle = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales - _TC"}) gle1 = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales Expenses - _TC"}) - self.assertEqual(gle.get('department'), "_Test Department - _TC") - self.assertEqual(gle1.get('department'), "_Test Department - _TC") + self.assertEqual(gle.get("department"), "_Test Department - _TC") + self.assertEqual(gle1.get("department"), "_Test Department - _TC") def test_mandatory(self): si = create_sales_invoice(do_not_save=1) - si.append("items", { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "rate": 100, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - "location": "" - }) + si.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": 100, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + "location": "", + }, + ) si.save() self.assertRaises(frappe.ValidationError, si.submit) @@ -72,31 +79,39 @@ class TestAccountingDimension(unittest.TestCase): def tearDown(self): disable_dimension() + def create_dimension(): frappe.set_user("Administrator") if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - frappe.get_doc({ - "doctype": "Accounting Dimension", - "document_type": "Department", - }).insert() + frappe.get_doc( + { + "doctype": "Accounting Dimension", + "document_type": "Department", + } + ).insert() else: dimension = frappe.get_doc("Accounting Dimension", "Department") dimension.disabled = 0 dimension.save() if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): - dimension1 = frappe.get_doc({ - "doctype": "Accounting Dimension", - "document_type": "Location", - }) + dimension1 = frappe.get_doc( + { + "doctype": "Accounting Dimension", + "document_type": "Location", + } + ) - dimension1.append("dimension_defaults", { - "company": "_Test Company", - "reference_document": "Location", - "default_dimension": "Block 1", - "mandatory_for_bs": 1 - }) + dimension1.append( + "dimension_defaults", + { + "company": "_Test Company", + "reference_document": "Location", + "default_dimension": "Block 1", + "mandatory_for_bs": 1, + }, + ) dimension1.insert() dimension1.save() @@ -105,6 +120,7 @@ def create_dimension(): dimension1.disabled = 0 dimension1.save() + def disable_dimension(): dimension1 = frappe.get_doc("Accounting Dimension", "Department") dimension1.disabled = 1 diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 7d32bad0e78..80f736fa5bb 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -19,17 +19,27 @@ class AccountingDimensionFilter(Document): WHERE d.name = a.parent and d.name != %s and d.accounting_dimension = %s - """, (self.name, self.accounting_dimension), as_dict=1) + """, + (self.name, self.accounting_dimension), + as_dict=1, + ) account_list = [d.account for d in accounts] - for account in self.get('accounts'): + for account in self.get("accounts"): if account.applicable_on_account in account_list: - frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format( - account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension))) + frappe.throw( + _("Row {0}: {1} account already applied for Accounting Dimension {2}").format( + account.idx, + frappe.bold(account.applicable_on_account), + frappe.bold(self.accounting_dimension), + ) + ) + def get_dimension_filter_map(): - filters = frappe.db.sql(""" + filters = frappe.db.sql( + """ SELECT a.applicable_on_account, d.dimension_value, p.accounting_dimension, p.allow_or_restrict, a.is_mandatory @@ -40,22 +50,30 @@ def get_dimension_filter_map(): p.name = a.parent AND p.disabled = 0 AND p.name = d.parent - """, as_dict=1) + """, + as_dict=1, + ) dimension_filter_map = {} for f in filters: f.fieldname = scrub(f.accounting_dimension) - build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value, - f.allow_or_restrict, f.is_mandatory) + build_map( + dimension_filter_map, + f.fieldname, + f.applicable_on_account, + f.dimension_value, + f.allow_or_restrict, + f.is_mandatory, + ) return dimension_filter_map + def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): - map_object.setdefault((dimension, account), { - 'allowed_dimensions': [], - 'is_mandatory': is_mandatory, - 'allow_or_restrict': allow_or_restrict - }) - map_object[(dimension, account)]['allowed_dimensions'].append(filter_value) + map_object.setdefault( + (dimension, account), + {"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict}, + ) + map_object[(dimension, account)]["allowed_dimensions"].append(filter_value) 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 e2f85ba21a9..f13f2f9f279 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 @@ -12,7 +12,8 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError -test_dependencies = ['Location', 'Cost Center', 'Department'] +test_dependencies = ["Location", "Cost Center", "Department"] + class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): @@ -22,9 +23,9 @@ class TestAccountingDimensionFilter(unittest.TestCase): def test_allowed_dimension_validation(self): si = create_sales_invoice(do_not_save=1) - si.items[0].cost_center = 'Main - _TC' - si.department = 'Accounts - _TC' - si.location = 'Block 1' + si.items[0].cost_center = "Main - _TC" + si.department = "Accounts - _TC" + si.location = "Block 1" si.save() self.assertRaises(InvalidAccountDimensionError, si.submit) @@ -32,12 +33,12 @@ class TestAccountingDimensionFilter(unittest.TestCase): def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) - si.department = '' - si.location = 'Block 1' + si.department = "" + si.location = "Block 1" # Test with no department for Sales Account - si.items[0].department = '' - si.items[0].cost_center = '_Test Cost Center 2 - _TC' + si.items[0].department = "" + si.items[0].cost_center = "_Test Cost Center 2 - _TC" si.save() self.assertRaises(MandatoryAccountDimensionError, si.submit) @@ -52,53 +53,54 @@ class TestAccountingDimensionFilter(unittest.TestCase): if si.docstatus == 1: si.cancel() + def create_accounting_dimension_filter(): - if not frappe.db.get_value('Accounting Dimension Filter', - {'accounting_dimension': 'Cost Center'}): - frappe.get_doc({ - 'doctype': 'Accounting Dimension Filter', - 'accounting_dimension': 'Cost Center', - 'allow_or_restrict': 'Allow', - 'company': '_Test Company', - 'accounts': [{ - 'applicable_on_account': 'Sales - _TC', - }], - 'dimensions': [{ - 'accounting_dimension': 'Cost Center', - 'dimension_value': '_Test Cost Center 2 - _TC' - }] - }).insert() + if not frappe.db.get_value( + "Accounting Dimension Filter", {"accounting_dimension": "Cost Center"} + ): + frappe.get_doc( + { + "doctype": "Accounting Dimension Filter", + "accounting_dimension": "Cost Center", + "allow_or_restrict": "Allow", + "company": "_Test Company", + "accounts": [ + { + "applicable_on_account": "Sales - _TC", + } + ], + "dimensions": [ + {"accounting_dimension": "Cost Center", "dimension_value": "_Test Cost Center 2 - _TC"} + ], + } + ).insert() else: - doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) + doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}) doc.disabled = 0 doc.save() - if not frappe.db.get_value('Accounting Dimension Filter', - {'accounting_dimension': 'Department'}): - frappe.get_doc({ - 'doctype': 'Accounting Dimension Filter', - 'accounting_dimension': 'Department', - 'allow_or_restrict': 'Allow', - 'company': '_Test Company', - 'accounts': [{ - 'applicable_on_account': 'Sales - _TC', - 'is_mandatory': 1 - }], - 'dimensions': [{ - 'accounting_dimension': 'Department', - 'dimension_value': 'Accounts - _TC' - }] - }).insert() + if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Department"}): + frappe.get_doc( + { + "doctype": "Accounting Dimension Filter", + "accounting_dimension": "Department", + "allow_or_restrict": "Allow", + "company": "_Test Company", + "accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}], + "dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}], + } + ).insert() else: - doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) + doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"}) doc.disabled = 0 doc.save() + def disable_dimension_filter(): - doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) + doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}) doc.disabled = 1 doc.save() - doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) + doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"}) doc.disabled = 1 doc.save() diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py index e2949378e5f..0c15d6a207d 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py @@ -7,7 +7,9 @@ from frappe import _ from frappe.model.document import Document -class OverlapError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + class AccountingPeriod(Document): def validate(self): @@ -17,11 +19,12 @@ class AccountingPeriod(Document): self.bootstrap_doctypes_for_closing() def autoname(self): - company_abbr = frappe.get_cached_value('Company', self.company, "abbr") + company_abbr = frappe.get_cached_value("Company", self.company, "abbr") self.name = " - ".join([self.period_name, company_abbr]) def validate_overlap(self): - existing_accounting_period = frappe.db.sql("""select name from `tabAccounting Period` + existing_accounting_period = frappe.db.sql( + """select name from `tabAccounting Period` where ( (%(start_date)s between start_date and end_date) or (%(end_date)s between start_date and end_date) @@ -32,18 +35,29 @@ class AccountingPeriod(Document): "start_date": self.start_date, "end_date": self.end_date, "name": self.name, - "company": self.company - }, as_dict=True) + "company": self.company, + }, + as_dict=True, + ) if len(existing_accounting_period) > 0: - frappe.throw(_("Accounting Period overlaps with {0}") - .format(existing_accounting_period[0].get("name")), OverlapError) + frappe.throw( + _("Accounting Period overlaps with {0}").format(existing_accounting_period[0].get("name")), + OverlapError, + ) @frappe.whitelist() def get_doctypes_for_closing(self): docs_for_closing = [] - doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \ - "Bank Clearance", "Asset", "Stock Entry"] + doctypes = [ + "Sales Invoice", + "Purchase Invoice", + "Journal Entry", + "Payroll Entry", + "Bank Clearance", + "Asset", + "Stock Entry", + ] closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes] for closed_doctype in closed_doctypes: docs_for_closing.append(closed_doctype) @@ -53,7 +67,7 @@ class AccountingPeriod(Document): def bootstrap_doctypes_for_closing(self): if len(self.closed_documents) == 0: for doctype_for_closing in self.get_doctypes_for_closing(): - self.append('closed_documents', { - "document_type": doctype_for_closing.document_type, - "closed": doctype_for_closing.closed - }) + self.append( + "closed_documents", + {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed}, + ) diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index c06c2e0338b..85025d190f5 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -10,29 +10,38 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.general_ledger import ClosedAccountingPeriod -test_dependencies = ['Item'] +test_dependencies = ["Item"] + class TestAccountingPeriod(unittest.TestCase): def test_overlap(self): - ap1 = create_accounting_period(start_date = "2018-04-01", - end_date = "2018-06-30", company = "Wind Power LLC") + ap1 = create_accounting_period( + start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC" + ) ap1.save() - ap2 = create_accounting_period(start_date = "2018-06-30", - end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1") + ap2 = create_accounting_period( + start_date="2018-06-30", + end_date="2018-07-10", + company="Wind Power LLC", + period_name="Test Accounting Period 1", + ) self.assertRaises(OverlapError, ap2.save) def test_accounting_period(self): - ap1 = create_accounting_period(period_name = "Test Accounting Period 2") + ap1 = create_accounting_period(period_name="Test Accounting Period 2") ap1.save() - doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC") + doc = create_sales_invoice( + do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" + ) self.assertRaises(ClosedAccountingPeriod, doc.submit) def tearDown(self): for d in frappe.get_all("Accounting Period"): frappe.delete_doc("Accounting Period", d.name) + def create_accounting_period(**args): args = frappe._dict(args) @@ -41,8 +50,6 @@ def create_accounting_period(**args): 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.append("closed_documents", { - "document_type": 'Sales Invoice', "closed": 1 - }) + accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1}) return accounting_period diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 48392074102..835498176c7 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -18,11 +18,13 @@ class AccountsSettings(Document): frappe.clear_cache() def validate(self): - frappe.db.set_default("add_taxes_from_item_tax_template", - self.get("add_taxes_from_item_tax_template", 0)) + frappe.db.set_default( + "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0) + ) - frappe.db.set_default("enable_common_party_accounting", - self.get("enable_common_party_accounting", 0)) + frappe.db.set_default( + "enable_common_party_accounting", self.get("enable_common_party_accounting", 0) + ) self.validate_stale_days() self.enable_payment_schedule_in_print() @@ -32,34 +34,91 @@ class AccountsSettings(Document): def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: frappe.msgprint( - _("Stale Days should start from 1."), title='Error', indicator='red', - raise_exception=1) + _("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1 + ) def enable_payment_schedule_in_print(self): show_in_print = cint(self.show_payment_schedule_in_print) for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"): - make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, + "payment_schedule", + "print_hide", + 0 if show_in_print else 1, + "Check", + validate_fields_for_doctype=False, + ) def toggle_discount_accounting_fields(self): enable_discount_accounting = cint(self.enable_discount_accounting) for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: - make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) if enable_discount_accounting: - make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) else: - make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) for doctype in ["Sales Invoice", "Purchase Invoice"]: - make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "additional_discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) if enable_discount_accounting: - make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "additional_discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) else: - make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) - - make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "additional_discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Item", + "default_discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) def validate_pending_reposts(self): if self.acc_frozen_upto: diff --git a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py index eb90fc7a861..a350cc385da 100644 --- a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -8,12 +7,12 @@ class TestAccountsSettings(unittest.TestCase): def tearDown(self): # Just in case `save` method succeeds, we need to take things back to default so that other tests # don't break - cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") cur_settings.allow_stale = 1 cur_settings.save() def test_stale_days(self): - cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") cur_settings.allow_stale = 0 cur_settings.stale_days = 0 diff --git a/erpnext/accounts/doctype/bank/bank.py b/erpnext/accounts/doctype/bank/bank.py index f111433c321..d44be9af23e 100644 --- a/erpnext/accounts/doctype/bank/bank.py +++ b/erpnext/accounts/doctype/bank/bank.py @@ -15,4 +15,4 @@ class Bank(Document): load_address_and_contact(self) def on_trash(self): - delete_contact_and_address('Bank', self.name) + delete_contact_and_address("Bank", self.name) diff --git a/erpnext/accounts/doctype/bank/bank_dashboard.py b/erpnext/accounts/doctype/bank/bank_dashboard.py index e7ef6aa25f5..7e40a1a6b8e 100644 --- a/erpnext/accounts/doctype/bank/bank_dashboard.py +++ b/erpnext/accounts/doctype/bank/bank_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'bank', - 'transactions': [ - { - 'label': _('Bank Details'), - 'items': ['Bank Account', 'Bank Guarantee'] - } - ] + "fieldname": "bank", + "transactions": [{"label": _("Bank Details"), "items": ["Bank Account", "Bank Guarantee"]}], } diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index f9140c31d64..addcf62e5b6 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -20,7 +20,7 @@ class BankAccount(Document): self.name = self.account_name + " - " + self.bank def on_trash(self): - delete_contact_and_address('BankAccount', self.name) + delete_contact_and_address("BankAccount", self.name) def validate(self): self.validate_company() @@ -31,9 +31,9 @@ class BankAccount(Document): frappe.throw(_("Company is manadatory for company account")) def validate_iban(self): - ''' + """ Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN - ''' + """ # IBAN field is optional if not self.iban: return @@ -43,7 +43,7 @@ class BankAccount(Document): return str(9 + ord(c) - 64) # remove whitespaces, upper case to get the right number from ord() - iban = ''.join(self.iban.split(' ')).upper() + iban = "".join(self.iban.split(" ")).upper() # Move country code and checksum from the start to the end flipped = iban[4:] + iban[:4] @@ -52,12 +52,12 @@ class BankAccount(Document): encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped] try: - to_check = int(''.join(encoded)) + to_check = int("".join(encoded)) except ValueError: - frappe.throw(_('IBAN is not valid')) + frappe.throw(_("IBAN is not valid")) if to_check % 97 != 1: - frappe.throw(_('IBAN is not valid')) + frappe.throw(_("IBAN is not valid")) @frappe.whitelist() @@ -69,12 +69,14 @@ def make_bank_account(doctype, docname): return doc + @frappe.whitelist() def get_party_bank_account(party_type, party): - return frappe.db.get_value(party_type, - party, 'default_bank_account') + return frappe.db.get_value(party_type, party, "default_bank_account") + @frappe.whitelist() def get_bank_account_details(bank_account): - return frappe.db.get_value("Bank Account", - bank_account, ['account', 'bank', 'bank_account_no'], as_dict=1) + return frappe.db.get_value( + "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 + ) diff --git a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py index bc08eab0351..8bf8d8a5cd0 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py +++ b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py @@ -1,28 +1,20 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'bank_account', - 'non_standard_fieldnames': { - 'Customer': 'default_bank_account', - 'Supplier': 'default_bank_account', + "fieldname": "bank_account", + "non_standard_fieldnames": { + "Customer": "default_bank_account", + "Supplier": "default_bank_account", }, - 'transactions': [ + "transactions": [ { - 'label': _('Payments'), - 'items': ['Payment Entry', 'Payment Request', 'Payment Order', 'Payroll Entry'] + "label": _("Payments"), + "items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"], }, - { - 'label': _('Party'), - 'items': ['Customer', 'Supplier'] - }, - { - 'items': ['Bank Guarantee'] - }, - { - 'items': ['Journal Entry'] - } - ] + {"label": _("Party"), "items": ["Customer", "Supplier"]}, + {"items": ["Bank Guarantee"]}, + {"items": ["Journal Entry"]}, + ], } diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.py b/erpnext/accounts/doctype/bank_account/test_bank_account.py index 5f23f88af6c..8949524a561 100644 --- a/erpnext/accounts/doctype/bank_account/test_bank_account.py +++ b/erpnext/accounts/doctype/bank_account/test_bank_account.py @@ -8,28 +8,28 @@ from frappe import ValidationError # test_records = frappe.get_test_records('Bank Account') -class TestBankAccount(unittest.TestCase): +class TestBankAccount(unittest.TestCase): def test_validate_iban(self): valid_ibans = [ - 'GB82 WEST 1234 5698 7654 32', - 'DE91 1000 0000 0123 4567 89', - 'FR76 3000 6000 0112 3456 7890 189' + "GB82 WEST 1234 5698 7654 32", + "DE91 1000 0000 0123 4567 89", + "FR76 3000 6000 0112 3456 7890 189", ] invalid_ibans = [ # wrong checksum (3rd place) - 'GB72 WEST 1234 5698 7654 32', - 'DE81 1000 0000 0123 4567 89', - 'FR66 3000 6000 0112 3456 7890 189' + "GB72 WEST 1234 5698 7654 32", + "DE81 1000 0000 0123 4567 89", + "FR66 3000 6000 0112 3456 7890 189", ] - bank_account = frappe.get_doc({'doctype':'Bank Account'}) + bank_account = frappe.get_doc({"doctype": "Bank Account"}) try: bank_account.validate_iban() except AttributeError: - msg = 'BankAccount.validate_iban() failed for empty IBAN' + msg = "BankAccount.validate_iban() failed for empty IBAN" self.fail(msg=msg) for iban in valid_ibans: @@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase): try: bank_account.validate_iban() except ValidationError: - msg = 'BankAccount.validate_iban() failed for valid IBAN {}'.format(iban) + msg = "BankAccount.validate_iban() failed for valid IBAN {}".format(iban) self.fail(msg=msg) for not_iban in invalid_ibans: bank_account.iban = not_iban - msg = 'BankAccount.validate_iban() accepted invalid IBAN {}'.format(not_iban) + msg = "BankAccount.validate_iban() accepted invalid IBAN {}".format(not_iban) with self.assertRaises(ValidationError, msg=msg): bank_account.validate_iban() diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index a3bbb2288d3..98ba399a35d 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -5,11 +5,13 @@ import frappe from frappe import _, msgprint from frappe.model.document import Document -from frappe.utils import flt, fmt_money, getdate, nowdate +from frappe.query_builder.custom import ConstantColumn +from frappe.utils import flt, fmt_money, getdate + +import erpnext + +form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"} -form_grid_templates = { - "journal_entries": "templates/form_grid/bank_reconciliation_grid.html" -} class BankClearance(Document): @frappe.whitelist() @@ -24,7 +26,8 @@ class BankClearance(Document): if not self.include_reconciled_entries: condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')" - journal_entries = frappe.db.sql(""" + journal_entries = frappe.db.sql( + """ select "Journal Entry" as payment_document, t1.name as payment_entry, t1.cheque_no as cheque_number, t1.cheque_date, @@ -38,12 +41,18 @@ class BankClearance(Document): and ifnull(t1.is_opening, 'No') = 'No' {condition} group by t2.account, t1.name order by t1.posting_date ASC, t1.name DESC - """.format(condition=condition), {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1) + """.format( + condition=condition + ), + {"account": self.account, "from": self.from_date, "to": self.to_date}, + as_dict=1, + ) if self.bank_account: - condition += 'and bank_account = %(bank_account)s' + condition += "and bank_account = %(bank_account)s" - payment_entries = frappe.db.sql(""" + payment_entries = frappe.db.sql( + """ select "Payment Entry" as payment_document, name as payment_entry, reference_no as cheque_number, reference_date as cheque_date, @@ -58,12 +67,69 @@ class BankClearance(Document): {condition} order by posting_date ASC, name DESC - """.format(condition=condition), {"account": self.account, "from":self.from_date, - "to": self.to_date, "bank_account": self.bank_account}, as_dict=1) + """.format( + condition=condition + ), + { + "account": self.account, + "from": self.from_date, + "to": self.to_date, + "bank_account": self.bank_account, + }, + as_dict=1, + ) + + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + + loan_disbursements = ( + frappe.qb.from_(loan_disbursement) + .select( + ConstantColumn("Loan Disbursement").as_("payment_document"), + loan_disbursement.name.as_("payment_entry"), + loan_disbursement.disbursed_amount.as_("credit"), + ConstantColumn(0).as_("debit"), + loan_disbursement.reference_number.as_("cheque_number"), + loan_disbursement.reference_date.as_("cheque_date"), + loan_disbursement.disbursement_date.as_("posting_date"), + loan_disbursement.applicant.as_("against_account"), + ) + .where(loan_disbursement.docstatus == 1) + .where(loan_disbursement.disbursement_date >= self.from_date) + .where(loan_disbursement.disbursement_date <= self.to_date) + .where(loan_disbursement.clearance_date.isnull()) + .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) + .orderby(loan_disbursement.disbursement_date) + .orderby(loan_disbursement.name, frappe.qb.desc) + ).run(as_dict=1) + + loan_repayment = frappe.qb.DocType("Loan Repayment") + + loan_repayments = ( + frappe.qb.from_(loan_repayment) + .select( + ConstantColumn("Loan Repayment").as_("payment_document"), + loan_repayment.name.as_("payment_entry"), + loan_repayment.amount_paid.as_("debit"), + ConstantColumn(0).as_("credit"), + loan_repayment.reference_number.as_("cheque_number"), + loan_repayment.reference_date.as_("cheque_date"), + loan_repayment.applicant.as_("against_account"), + loan_repayment.posting_date, + ) + .where(loan_repayment.docstatus == 1) + .where(loan_repayment.clearance_date.isnull()) + .where(loan_repayment.repay_from_salary == 0) + .where(loan_repayment.posting_date >= self.from_date) + .where(loan_repayment.posting_date <= self.to_date) + .where(loan_repayment.payment_account.isin([self.bank_account, self.account])) + .orderby(loan_repayment.posting_date) + .orderby(loan_repayment.name, frappe.qb.desc) + ).run(as_dict=1) pos_sales_invoices, pos_purchase_invoices = [], [] if self.include_pos_transactions: - pos_sales_invoices = frappe.db.sql(""" + pos_sales_invoices = frappe.db.sql( + """ select "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, si.posting_date, si.customer as against_account, sip.clearance_date, @@ -74,9 +140,13 @@ class BankClearance(Document): and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s order by si.posting_date ASC, si.name DESC - """, {"account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1) + """, + {"account": self.account, "from": self.from_date, "to": self.to_date}, + as_dict=1, + ) - pos_purchase_invoices = frappe.db.sql(""" + pos_purchase_invoices = frappe.db.sql( + """ select "Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit, pi.posting_date, pi.supplier as against_account, pi.clearance_date, @@ -87,21 +157,36 @@ class BankClearance(Document): and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s order by pi.posting_date ASC, pi.name DESC - """, {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1) + """, + {"account": self.account, "from": self.from_date, "to": self.to_date}, + as_dict=1, + ) - entries = sorted(list(payment_entries) + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), - key=lambda k: k['posting_date'] or getdate(nowdate())) + entries = sorted( + list(payment_entries) + + list(journal_entries) + + list(pos_sales_invoices) + + list(pos_purchase_invoices) + + list(loan_disbursements) + + list(loan_repayments), + key=lambda k: getdate(k["posting_date"]), + ) - self.set('payment_entries', []) + self.set("payment_entries", []) self.total_amount = 0.0 + default_currency = erpnext.get_default_currency() for d in entries: - row = self.append('payment_entries', {}) + row = self.append("payment_entries", {}) - amount = flt(d.get('debit', 0)) - flt(d.get('credit', 0)) + amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0)) + + if not d.get("account_currency"): + d.account_currency = default_currency formatted_amount = fmt_money(abs(amount), 2, d.account_currency) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) + d.posting_date = getdate(d.posting_date) d.pop("credit") d.pop("debit") @@ -112,21 +197,24 @@ class BankClearance(Document): @frappe.whitelist() def update_clearance_date(self): clearance_date_updated = False - for d in self.get('payment_entries'): + for d in self.get("payment_entries"): if d.clearance_date: if not d.payment_document: frappe.throw(_("Row #{0}: Payment document is required to complete the transaction")) if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date): - frappe.throw(_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}") - .format(d.idx, d.clearance_date, d.cheque_date)) + frappe.throw( + _("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format( + d.idx, d.clearance_date, d.cheque_date + ) + ) if d.clearance_date or self.include_reconciled_entries: if not d.clearance_date: d.clearance_date = None payment_entry = frappe.get_doc(d.payment_document, d.payment_entry) - payment_entry.db_set('clearance_date', d.clearance_date) + payment_entry.db_set("clearance_date", d.clearance_date) clearance_date_updated = True diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index 706fbbe245c..c1e55f6f723 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -1,9 +1,96 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import add_months, getdate + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.loan_management.doctype.loan.test_loan import ( + create_loan, + create_loan_accounts, + create_loan_type, + create_repayment_entry, + make_loan_disbursement_entry, +) + class TestBankClearance(unittest.TestCase): - pass + @classmethod + def setUpClass(cls): + make_bank_account() + create_loan_accounts() + create_loan_masters() + add_transactions() + + # Basic test case to test if bank clearance tool doesn't break + # Detailed test can be added later + def test_bank_clearance(self): + bank_clearance = frappe.get_doc("Bank Clearance") + bank_clearance.account = "_Test Bank Clearance - _TC" + bank_clearance.from_date = add_months(getdate(), -1) + bank_clearance.to_date = getdate() + bank_clearance.get_payment_entries() + self.assertEqual(len(bank_clearance.payment_entries), 3) + + +def make_bank_account(): + if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"): + frappe.get_doc( + { + "doctype": "Account", + "account_type": "Bank", + "account_name": "_Test Bank Clearance", + "company": "_Test Company", + "parent_account": "Bank Accounts - _TC", + } + ).insert() + + +def create_loan_masters(): + create_loan_type( + "Clearance Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "_Test Bank Clearance - _TC", + "_Test Bank Clearance - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) + + +def add_transactions(): + make_payment_entry() + make_loan() + + +def make_loan(): + loan = create_loan( + "_Test Customer", + "Clearance Loan", + 280000, + "Repay Over Number of Periods", + 20, + applicant_type="Customer", + ) + loan.submit() + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate()) + repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount) + repayment_entry.save() + repayment_entry.submit() + + +def make_payment_entry(): + pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690) + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC") + pe.reference_no = "Conrad Oct 18" + pe.reference_date = "2018-10-24" + pe.insert() + pe.submit() diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py index cfbcf16b91e..9144a29c6ef 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py @@ -23,10 +23,16 @@ class BankGuarantee(Document): if not self.bank: frappe.throw(_("Enter the name of the bank or lending institution before submittting.")) + @frappe.whitelist() def get_vouchar_detials(column_list, doctype, docname): column_list = json.loads(column_list) for col in column_list: sanitize_searchfield(col) - return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s''' - .format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0] + return frappe.db.sql( + """ select {columns} from `tab{doctype}` where name=%s""".format( + columns=", ".join(column_list), doctype=doctype + ), + docname, + as_dict=1, + )[0] diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 4211bd0169d..0efe086d94e 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt from erpnext import get_company_currency @@ -21,48 +22,63 @@ from erpnext.accounts.utils import get_balance_on class BankReconciliationTool(Document): pass + @frappe.whitelist() -def get_bank_transactions(bank_account, from_date = None, to_date = None): +def get_bank_transactions(bank_account, from_date=None, to_date=None): # returns bank transactions for a bank account filters = [] - filters.append(['bank_account', '=', bank_account]) - filters.append(['docstatus', '=', 1]) - filters.append(['unallocated_amount', '>', 0]) + filters.append(["bank_account", "=", bank_account]) + filters.append(["docstatus", "=", 1]) + filters.append(["unallocated_amount", ">", 0]) if to_date: - filters.append(['date', '<=', to_date]) + filters.append(["date", "<=", to_date]) if from_date: - filters.append(['date', '>=', from_date]) + filters.append(["date", ">=", from_date]) transactions = frappe.get_all( - 'Bank Transaction', - fields = ['date', 'deposit', 'withdrawal', 'currency', - 'description', 'name', 'bank_account', 'company', - 'unallocated_amount', 'reference_number', 'party_type', 'party'], - filters = filters + "Bank Transaction", + fields=[ + "date", + "deposit", + "withdrawal", + "currency", + "description", + "name", + "bank_account", + "company", + "unallocated_amount", + "reference_number", + "party_type", + "party", + ], + filters=filters, ) return transactions + @frappe.whitelist() def get_account_balance(bank_account, till_date): # returns account balance till the specified date - account = frappe.db.get_value('Bank Account', bank_account, 'account') - filters = frappe._dict({ - "account": account, - "report_date": till_date, - "include_pos_transactions": 1 - }) + account = frappe.db.get_value("Bank Account", bank_account, "account") + filters = frappe._dict( + {"account": account, "report_date": till_date, "include_pos_transactions": 1} + ) data = get_entries(filters) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) - total_debit, total_credit = 0,0 + total_debit, total_credit = 0, 0 for d in data: total_debit += flt(d.debit) total_credit += flt(d.credit) amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) - bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \ + bank_bal = ( + flt(balance_as_per_system) + - flt(total_debit) + + flt(total_credit) + amounts_not_reflected_in_system + ) return bank_bal @@ -75,71 +91,94 @@ def update_bank_transaction(bank_transaction_name, reference_number, party_type= bank_transaction.party_type = party_type bank_transaction.party = party bank_transaction.save() - return frappe.db.get_all('Bank Transaction', - filters={ - 'name': bank_transaction_name - }, - fields=['date', 'deposit', 'withdrawal', 'currency', - 'description', 'name', 'bank_account', 'company', - 'unallocated_amount', 'reference_number', - 'party_type', 'party'], + return frappe.db.get_all( + "Bank Transaction", + filters={"name": bank_transaction_name}, + fields=[ + "date", + "deposit", + "withdrawal", + "currency", + "description", + "name", + "bank_account", + "company", + "unallocated_amount", + "reference_number", + "party_type", + "party", + ], )[0] @frappe.whitelist() -def create_journal_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, posting_date=None, entry_type=None, - second_account=None, mode_of_payment=None, party_type=None, party=None, allow_edit=None): +def create_journal_entry_bts( + bank_transaction_name, + reference_number=None, + reference_date=None, + posting_date=None, + entry_type=None, + second_account=None, + mode_of_payment=None, + party_type=None, + party=None, + allow_edit=None, +): # Create a new journal entry based on the bank transaction bank_transaction = frappe.db.get_values( - "Bank Transaction", bank_transaction_name, - fieldname=["name", "deposit", "withdrawal", "bank_account"] , - as_dict=True + "Bank Transaction", + bank_transaction_name, + fieldname=["name", "deposit", "withdrawal", "bank_account"], + as_dict=True, )[0] company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") account_type = frappe.db.get_value("Account", second_account, "account_type") if account_type in ["Receivable", "Payable"]: if not (party_type and party): - frappe.throw(_("Party Type and Party is required for Receivable / Payable account {0}").format( second_account)) + frappe.throw( + _("Party Type and Party is required for Receivable / Payable account {0}").format( + second_account + ) + ) accounts = [] # Multi Currency? - accounts.append({ + accounts.append( + { "account": second_account, - "credit_in_account_currency": bank_transaction.deposit - if bank_transaction.deposit > 0 - else 0, - "debit_in_account_currency":bank_transaction.withdrawal - if bank_transaction.withdrawal > 0 - else 0, - "party_type":party_type, - "party":party, - }) + "credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, + "debit_in_account_currency": bank_transaction.withdrawal + if bank_transaction.withdrawal > 0 + else 0, + "party_type": party_type, + "party": party, + } + ) - accounts.append({ + accounts.append( + { "account": company_account, "bank_account": bank_transaction.bank_account, "credit_in_account_currency": bank_transaction.withdrawal - if bank_transaction.withdrawal > 0 - else 0, - "debit_in_account_currency":bank_transaction.deposit - if bank_transaction.deposit > 0 - else 0, - }) + if bank_transaction.withdrawal > 0 + else 0, + "debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, + } + ) company = frappe.get_value("Account", company_account, "company") journal_entry_dict = { - "voucher_type" : entry_type, - "company" : company, - "posting_date" : posting_date, - "cheque_date" : reference_date, - "cheque_no" : reference_number, - "mode_of_payment" : mode_of_payment + "voucher_type": entry_type, + "company": company, + "posting_date": posting_date, + "cheque_date": reference_date, + "cheque_no": reference_number, + "mode_of_payment": mode_of_payment, } - journal_entry = frappe.new_doc('Journal Entry') + journal_entry = frappe.new_doc("Journal Entry") journal_entry.update(journal_entry_dict) journal_entry.set("accounts", accounts) - if allow_edit: return journal_entry @@ -151,21 +190,32 @@ def create_journal_entry_bts( bank_transaction_name, reference_number=None, refe else: paid_amount = bank_transaction.withdrawal - vouchers = json.dumps([{ - "payment_doctype":"Journal Entry", - "payment_name":journal_entry.name, - "amount":paid_amount}]) + vouchers = json.dumps( + [{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}] + ) return reconcile_vouchers(bank_transaction.name, vouchers) + @frappe.whitelist() -def create_payment_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, party_type=None, party=None, posting_date=None, - mode_of_payment=None, project=None, cost_center=None, allow_edit=None): +def create_payment_entry_bts( + bank_transaction_name, + reference_number=None, + reference_date=None, + party_type=None, + party=None, + posting_date=None, + mode_of_payment=None, + project=None, + cost_center=None, + allow_edit=None, +): # Create a new payment entry based on the bank transaction bank_transaction = frappe.db.get_values( - "Bank Transaction", bank_transaction_name, - fieldname=["name", "unallocated_amount", "deposit", "bank_account"] , - as_dict=True + "Bank Transaction", + bank_transaction_name, + fieldname=["name", "unallocated_amount", "deposit", "bank_account"], + as_dict=True, )[0] paid_amount = bank_transaction.unallocated_amount payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay" @@ -173,27 +223,26 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") company = frappe.get_value("Account", company_account, "company") payment_entry_dict = { - "company" : company, - "payment_type" : payment_type, - "reference_no" : reference_number, - "reference_date" : reference_date, - "party_type" : party_type, - "party" : party, - "posting_date" : posting_date, + "company": company, + "payment_type": payment_type, + "reference_no": reference_number, + "reference_date": reference_date, + "party_type": party_type, + "party": party, + "posting_date": posting_date, "paid_amount": paid_amount, - "received_amount": paid_amount + "received_amount": paid_amount, } payment_entry = frappe.new_doc("Payment Entry") - payment_entry.update(payment_entry_dict) if mode_of_payment: - payment_entry.mode_of_payment = mode_of_payment + payment_entry.mode_of_payment = mode_of_payment if project: - payment_entry.project = project + payment_entry.project = project if cost_center: - payment_entry.cost_center = cost_center + payment_entry.cost_center = cost_center if payment_type == "Receive": payment_entry.paid_to = company_account else: @@ -207,80 +256,111 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe payment_entry.insert() payment_entry.submit() - vouchers = json.dumps([{ - "payment_doctype":"Payment Entry", - "payment_name":payment_entry.name, - "amount":paid_amount}]) + vouchers = json.dumps( + [{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}] + ) return reconcile_vouchers(bank_transaction.name, vouchers) + @frappe.whitelist() def reconcile_vouchers(bank_transaction_name, vouchers): # updated clear date of all the vouchers based on the bank transaction vouchers = json.loads(vouchers) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) - company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account') + company_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") if transaction.unallocated_amount == 0: frappe.throw(_("This bank transaction is already fully reconciled")) total_amount = 0 for voucher in vouchers: - voucher['payment_entry'] = frappe.get_doc(voucher['payment_doctype'], voucher['payment_name']) - total_amount += get_paid_amount(frappe._dict({ - 'payment_document': voucher['payment_doctype'], - 'payment_entry': voucher['payment_name'], - }), transaction.currency, company_account) + voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"]) + total_amount += get_paid_amount( + frappe._dict( + { + "payment_document": voucher["payment_doctype"], + "payment_entry": voucher["payment_name"], + } + ), + transaction.currency, + company_account, + ) if total_amount > transaction.unallocated_amount: - frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction")) + frappe.throw( + _( + "The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction" + ) + ) account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") for voucher in vouchers: - gl_entry = frappe.db.get_value("GL Entry", dict(account=account, voucher_type=voucher['payment_doctype'], voucher_no=voucher['payment_name']), ['credit', 'debit'], as_dict=1) - gl_amount, transaction_amount = (gl_entry.credit, transaction.deposit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.withdrawal) + gl_entry = frappe.db.get_value( + "GL Entry", + dict( + account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"] + ), + ["credit", "debit"], + as_dict=1, + ) + gl_amount, transaction_amount = ( + (gl_entry.credit, transaction.deposit) + if gl_entry.credit > 0 + else (gl_entry.debit, transaction.withdrawal) + ) allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount - transaction.append("payment_entries", { - "payment_document": voucher['payment_entry'].doctype, - "payment_entry": voucher['payment_entry'].name, - "allocated_amount": allocated_amount - }) + transaction.append( + "payment_entries", + { + "payment_document": voucher["payment_entry"].doctype, + "payment_entry": voucher["payment_entry"].name, + "allocated_amount": allocated_amount, + }, + ) transaction.save() transaction.update_allocations() return frappe.get_doc("Bank Transaction", bank_transaction_name) + @frappe.whitelist() -def get_linked_payments(bank_transaction_name, document_types = None): +def get_linked_payments(bank_transaction_name, document_types=None): # get all matching payments for a bank transaction transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) bank_account = frappe.db.get_values( - "Bank Account", - transaction.bank_account, - ["account", "company"], - as_dict=True)[0] + "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True + )[0] (account, company) = (bank_account.account, bank_account.company) matching = check_matching(account, company, transaction, document_types) return matching + def check_matching(bank_account, company, transaction, document_types): # combine all types of vouchers subquery = get_queries(bank_account, company, transaction, document_types) filters = { - "amount": transaction.unallocated_amount, - "payment_type" : "Receive" if transaction.deposit > 0 else "Pay", - "reference_no": transaction.reference_number, - "party_type": transaction.party_type, - "party": transaction.party, - "bank_account": bank_account - } + "amount": transaction.unallocated_amount, + "payment_type": "Receive" if transaction.deposit > 0 else "Pay", + "reference_no": transaction.reference_number, + "party_type": transaction.party_type, + "party": transaction.party, + "bank_account": bank_account, + } matching_vouchers = [] + + matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters)) + for query in subquery: matching_vouchers.extend( - frappe.db.sql(query, filters,) + frappe.db.sql( + query, + filters, + ) ) - return sorted(matching_vouchers, key = lambda x: x[0], reverse=True) if matching_vouchers else [] + return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else [] + def get_queries(bank_account, company, transaction, document_types): # get queries to get matching vouchers @@ -297,7 +377,7 @@ def get_queries(bank_account, company, transaction, document_types): queries.extend([je_amount_matching]) if transaction.deposit > 0 and "sales_invoice" in document_types: - si_amount_matching = get_si_matching_query(amount_condition) + si_amount_matching = get_si_matching_query(amount_condition) queries.extend([si_amount_matching]) if transaction.withdrawal > 0: @@ -311,13 +391,104 @@ def get_queries(bank_account, company, transaction, document_types): return queries + +def get_loan_vouchers(bank_account, transaction, document_types, filters): + vouchers = [] + amount_condition = True if "exact_match" in document_types else False + + if transaction.withdrawal > 0 and "loan_disbursement" in document_types: + vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters)) + + if transaction.deposit > 0 and "loan_repayment" in document_types: + vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters)) + + return vouchers + + +def get_ld_matching_query(bank_account, amount_condition, filters): + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + matching_reference = loan_disbursement.reference_number == filters.get("reference_number") + matching_party = loan_disbursement.applicant_type == filters.get( + "party_type" + ) and loan_disbursement.applicant == filters.get("party") + + rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0) + + rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0) + + query = ( + frappe.qb.from_(loan_disbursement) + .select( + rank + rank1 + 1, + ConstantColumn("Loan Disbursement").as_("doctype"), + loan_disbursement.name, + loan_disbursement.disbursed_amount, + loan_disbursement.reference_number, + loan_disbursement.reference_date, + loan_disbursement.applicant_type, + loan_disbursement.disbursement_date, + ) + .where(loan_disbursement.docstatus == 1) + .where(loan_disbursement.clearance_date.isnull()) + .where(loan_disbursement.disbursement_account == bank_account) + ) + + if amount_condition: + query.where(loan_disbursement.disbursed_amount == filters.get("amount")) + else: + query.where(loan_disbursement.disbursed_amount <= filters.get("amount")) + + vouchers = query.run(as_list=True) + + return vouchers + + +def get_lr_matching_query(bank_account, amount_condition, filters): + loan_repayment = frappe.qb.DocType("Loan Repayment") + matching_reference = loan_repayment.reference_number == filters.get("reference_number") + matching_party = loan_repayment.applicant_type == filters.get( + "party_type" + ) and loan_repayment.applicant == filters.get("party") + + rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0) + + rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0) + + query = ( + frappe.qb.from_(loan_repayment) + .select( + rank + rank1 + 1, + ConstantColumn("Loan Repayment").as_("doctype"), + loan_repayment.name, + loan_repayment.amount_paid, + loan_repayment.reference_number, + loan_repayment.reference_date, + loan_repayment.applicant_type, + loan_repayment.posting_date, + ) + .where(loan_repayment.docstatus == 1) + .where(loan_repayment.repay_from_salary == 0) + .where(loan_repayment.clearance_date.isnull()) + .where(loan_repayment.payment_account == bank_account) + ) + + if amount_condition: + query.where(loan_repayment.amount_paid == filters.get("amount")) + else: + query.where(loan_repayment.amount_paid <= filters.get("amount")) + + vouchers = query.run() + + return vouchers + + def get_pe_matching_query(amount_condition, account_from_to, transaction): # get matching payment entries query if transaction.deposit > 0: currency_field = "paid_to_account_currency as currency" else: currency_field = "paid_from_account_currency as currency" - return f""" + return f""" SELECT (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END @@ -348,7 +519,6 @@ def get_je_matching_query(amount_condition, transaction): # We have mapping at the bank level # So one bank could have both types of bank accounts like asset and liability # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type - company_account = frappe.get_value("Bank Account", transaction.bank_account, "account") cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" return f""" @@ -407,6 +577,7 @@ def get_si_matching_query(amount_condition): AND si.docstatus = 1 """ + def get_pi_matching_query(amount_condition): # get matching purchase invoice query return f""" @@ -432,11 +603,16 @@ def get_pi_matching_query(amount_condition): AND cash_bank_account = %(bank_account)s """ + def get_ec_matching_query(bank_account, company, amount_condition): # get matching Expense Claim query - mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account", - filters={"default_account": bank_account}, fields=["parent"])] - mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )' + mode_of_payments = [ + x["parent"] + for x in frappe.db.get_all( + "Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"] + ) + ] + mode_of_payments = "('" + "', '".join(mode_of_payments) + "' )" company_currency = get_company_currency(company) return f""" SELECT diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 57434bdd829..0370b3d91db 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -19,6 +19,7 @@ from six import string_types INVALID_VALUES = ("", None) + class BankStatementImport(DataImport): def __init__(self, *args, **kwargs): super(BankStatementImport, self).__init__(*args, **kwargs) @@ -50,16 +51,14 @@ class BankStatementImport(DataImport): self.import_file, self.google_sheets_url ) - if 'Bank Account' not in json.dumps(preview['columns']): + if "Bank Account" not in json.dumps(preview["columns"]): frappe.throw(_("Please add the Bank Account column")) from frappe.core.page.background_jobs.background_jobs import get_info from frappe.utils.scheduler import is_scheduler_inactive if is_scheduler_inactive() and not frappe.flags.in_test: - frappe.throw( - _("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive") - ) + frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) enqueued_jobs = [d.get("job_name") for d in get_info()] @@ -82,21 +81,25 @@ class BankStatementImport(DataImport): return False + @frappe.whitelist() def get_preview_from_template(data_import, import_file=None, google_sheets_url=None): return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template( import_file, google_sheets_url ) + @frappe.whitelist() def form_start_import(data_import): return frappe.get_doc("Bank Statement Import", data_import).start_import() + @frappe.whitelist() def download_errored_template(data_import_name): data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import.export_errored_rows() + def parse_data_from_template(raw_data): data = [] @@ -109,7 +112,10 @@ def parse_data_from_template(raw_data): return data -def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options): + +def start_import( + data_import, bank_account, import_file_path, google_sheets_url, bank, template_options +): """This method runs in background job""" update_mapping_db(bank, template_options) @@ -117,7 +123,7 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url, data_import = frappe.get_doc("Bank Statement Import", data_import) file = import_file_path if import_file_path else google_sheets_url - import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records") + import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records") data = parse_data_from_template(import_file.raw_data) @@ -137,16 +143,18 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url, frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name}) + def update_mapping_db(bank, template_options): bank = frappe.get_doc("Bank", bank) for d in bank.bank_transaction_mapping: d.delete() for d in json.loads(template_options)["column_to_field_map"].items(): - bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1] ,"file_field": d[0]} ) + bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1], "file_field": d[0]}) bank.save() + def add_bank_account(data, bank_account): bank_account_loc = None if "Bank Account" not in data[0]: @@ -162,6 +170,7 @@ def add_bank_account(data, bank_account): else: row.append(bank_account) + def write_files(import_file, data): full_file_path = import_file.file_doc.get_full_path() parts = import_file.file_doc.get_extension() @@ -169,11 +178,12 @@ def write_files(import_file, data): extension = extension.lstrip(".") if extension == "csv": - with open(full_file_path, 'w', newline='') as file: + with open(full_file_path, "w", newline="") as file: writer = csv.writer(file) writer.writerows(data) elif extension == "xlsx" or "xls": - write_xlsx(data, "trans", file_path = full_file_path) + write_xlsx(data, "trans", file_path=full_file_path) + def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None): # from xlsx utils with changes @@ -188,19 +198,21 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None): ws.column_dimensions[get_column_letter(i + 1)].width = column_width row1 = ws.row_dimensions[1] - row1.font = Font(name='Calibri', bold=True) + row1.font = Font(name="Calibri", bold=True) for row in data: clean_row = [] for item in row: - if isinstance(item, string_types) and (sheet_name not in ['Data Import Template', 'Data Export']): + if isinstance(item, string_types) and ( + sheet_name not in ["Data Import Template", "Data Export"] + ): value = handle_html(item) else: value = item if isinstance(item, string_types) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None): # Remove illegal characters from the string - value = re.sub(ILLEGAL_CHARACTERS_RE, '', value) + value = re.sub(ILLEGAL_CHARACTERS_RE, "", value) clean_row.append(value) @@ -209,19 +221,20 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None): wb.save(file_path) return True + @frappe.whitelist() def upload_bank_statement(**args): args = frappe._dict(args) bsi = frappe.new_doc("Bank Statement Import") if args.company: - bsi.update({ - "company": args.company, - }) + bsi.update( + { + "company": args.company, + } + ) if args.bank_account: - bsi.update({ - "bank_account": args.bank_account - }) + bsi.update({"bank_account": args.bank_account}) return bsi diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 44cea31ed38..e43f18b5c74 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -28,17 +28,26 @@ class BankTransaction(StatusUpdater): def update_allocations(self): if self.payment_entries: - allocated_amount = reduce(lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]) + allocated_amount = reduce( + lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries] + ) else: allocated_amount = 0 if allocated_amount: frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount)) - frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount)) + frappe.db.set_value( + self.doctype, + self.name, + "unallocated_amount", + abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount), + ) else: frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0) - frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))) + frappe.db.set_value( + self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) + ) amount = self.deposit or self.withdrawal if amount == self.allocated_amount: @@ -48,7 +57,14 @@ class BankTransaction(StatusUpdater): def clear_linked_payment_entries(self, for_cancel=False): for payment_entry in self.payment_entries: - if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: + if payment_entry.payment_document in [ + "Payment Entry", + "Journal Entry", + "Purchase Invoice", + "Expense Claim", + "Loan Repayment", + "Loan Disbursement", + ]: self.clear_simple_entry(payment_entry, for_cancel=for_cancel) elif payment_entry.payment_document == "Sales Invoice": @@ -56,38 +72,41 @@ class BankTransaction(StatusUpdater): def clear_simple_entry(self, payment_entry, for_cancel=False): if payment_entry.payment_document == "Payment Entry": - if frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") == "Internal Transfer": + if ( + frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") + == "Internal Transfer" + ): if len(get_reconciled_bank_transactions(payment_entry)) < 2: return clearance_date = self.date if not for_cancel else None frappe.db.set_value( - payment_entry.payment_document, payment_entry.payment_entry, - "clearance_date", clearance_date) + payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date + ) def clear_sales_invoice(self, payment_entry, for_cancel=False): clearance_date = self.date if not for_cancel else None frappe.db.set_value( "Sales Invoice Payment", - dict( - parenttype=payment_entry.payment_document, - parent=payment_entry.payment_entry - ), - "clearance_date", clearance_date) + dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry), + "clearance_date", + clearance_date, + ) + def get_reconciled_bank_transactions(payment_entry): reconciled_bank_transactions = frappe.get_all( - 'Bank Transaction Payments', - filters = { - 'payment_entry': payment_entry.payment_entry - }, - fields = ['parent'] + "Bank Transaction Payments", + filters={"payment_entry": payment_entry.payment_entry}, + fields=["parent"], ) return reconciled_bank_transactions + def get_total_allocated_amount(payment_entry): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT SUM(btp.allocated_amount) as allocated_amount, bt.name @@ -100,36 +119,73 @@ def get_total_allocated_amount(payment_entry): AND btp.payment_entry = %s AND - bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) + bt.docstatus = 1""", + (payment_entry.payment_document, payment_entry.payment_entry), + as_dict=True, + ) + def get_paid_amount(payment_entry, currency, bank_account): if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: paid_amount_field = "paid_amount" - if payment_entry.payment_document == 'Payment Entry': + if payment_entry.payment_document == "Payment Entry": doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry) - paid_amount_field = ("base_paid_amount" - if doc.paid_to_account_currency == currency else "paid_amount") - return frappe.db.get_value(payment_entry.payment_document, - payment_entry.payment_entry, paid_amount_field) + if doc.payment_type == "Receive": + paid_amount_field = ( + "received_amount" if doc.paid_to_account_currency == currency else "base_received_amount" + ) + elif doc.payment_type == "Pay": + paid_amount_field = ( + "paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount" + ) + + return frappe.db.get_value( + payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field + ) elif payment_entry.payment_document == "Journal Entry": - return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)") + return frappe.db.get_value( + "Journal Entry Account", + {"parent": payment_entry.payment_entry, "account": bank_account}, + "sum(credit_in_account_currency)", + ) elif payment_entry.payment_document == "Expense Claim": - return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed") + return frappe.db.get_value( + payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed" + ) + + elif payment_entry.payment_document == "Loan Disbursement": + return frappe.db.get_value( + payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount" + ) + + elif payment_entry.payment_document == "Loan Repayment": + return frappe.db.get_value( + payment_entry.payment_document, payment_entry.payment_entry, "amount_paid" + ) else: - frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry)) + frappe.throw( + "Please reconcile {0}: {1} manually".format( + payment_entry.payment_document, payment_entry.payment_entry + ) + ) + @frappe.whitelist() def unclear_reference_payment(doctype, docname): if frappe.db.exists(doctype, docname): doc = frappe.get_doc(doctype, docname) if doctype == "Sales Invoice": - frappe.db.set_value("Sales Invoice Payment", dict(parenttype=doc.payment_document, - parent=doc.payment_entry), "clearance_date", None) + frappe.db.set_value( + "Sales Invoice Payment", + dict(parenttype=doc.payment_document, parent=doc.payment_entry), + "clearance_date", + None, + ) else: frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py index 6125c27cdfd..494459ef0ea 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py @@ -19,12 +19,14 @@ def upload_bank_statement(): fcontent = frappe.local.uploaded_file fname = frappe.local.uploaded_filename - if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')): + if frappe.safe_encode(fname).lower().endswith("csv".encode("utf-8")): from frappe.utils.csvutils import read_csv_content + rows = read_csv_content(fcontent, False) - elif frappe.safe_encode(fname).lower().endswith("xlsx".encode('utf-8')): + elif frappe.safe_encode(fname).lower().endswith("xlsx".encode("utf-8")): from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file + rows = read_xlsx_file_from_attached_file(fcontent=fcontent) columns = rows[0] @@ -44,12 +46,10 @@ def create_bank_entries(columns, data, bank_account): continue fields = {} for key, value in iteritems(header_map): - fields.update({key: d[int(value)-1]}) + fields.update({key: d[int(value) - 1]}) try: - bank_transaction = frappe.get_doc({ - "doctype": "Bank Transaction" - }) + bank_transaction = frappe.get_doc({"doctype": "Bank Transaction"}) bank_transaction.update(fields) bank_transaction.date = getdate(parse_date(bank_transaction.date)) bank_transaction.bank_account = bank_account @@ -62,6 +62,7 @@ def create_bank_entries(columns, data, bank_account): return {"success": success, "errors": errors} + def get_header_mapping(columns, bank_account): mapping = get_bank_mapping(bank_account) @@ -72,10 +73,11 @@ def get_header_mapping(columns, bank_account): return header_map + def get_bank_mapping(bank_account): bank_name = frappe.db.get_value("Bank Account", bank_account, "bank") bank = frappe.get_doc("Bank", bank_name) - mapping = {row.file_field:row.bank_transaction_field for row in bank.bank_transaction_mapping} + mapping = {row.file_field: row.bank_transaction_field for row in bank.bank_transaction_mapping} return mapping diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 72b6893faf5..ad8e73d27bf 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -17,6 +17,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal test_dependencies = ["Item", "Cost Center"] + class TestBankTransaction(unittest.TestCase): @classmethod def setUpClass(cls): @@ -41,21 +42,34 @@ class TestBankTransaction(unittest.TestCase): # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. def test_linked_payments(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic")) - linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"), + ) + linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"]) self.assertTrue(linked_payments[0][6] == "Conrad Electronic") # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment def test_reconcile(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G")) + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"), + ) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700)) - vouchers = json.dumps([{ - "payment_doctype":"Payment Entry", - "payment_name":payment.name, - "amount":bank_transaction.unallocated_amount}]) + vouchers = json.dumps( + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment.name, + "amount": bank_transaction.unallocated_amount, + } + ] + ) reconcile_vouchers(bank_transaction.name, vouchers) - unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount") + unallocated_amount = frappe.db.get_value( + "Bank Transaction", bank_transaction.name, "unallocated_amount" + ) self.assertTrue(unallocated_amount == 0) clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") @@ -69,122 +83,177 @@ class TestBankTransaction(unittest.TestCase): # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) - linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"), + ) + linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"]) self.assertTrue(linked_payments[0][3]) # Check error if already reconciled def test_already_reconciled(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"), + ) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) - vouchers = json.dumps([{ - "payment_doctype":"Payment Entry", - "payment_name":payment.name, - "amount":bank_transaction.unallocated_amount}]) + vouchers = json.dumps( + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment.name, + "amount": bank_transaction.unallocated_amount, + } + ] + ) reconcile_vouchers(bank_transaction.name, vouchers) - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"), + ) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) - vouchers = json.dumps([{ - "payment_doctype":"Payment Entry", - "payment_name":payment.name, - "amount":bank_transaction.unallocated_amount}]) - self.assertRaises(frappe.ValidationError, reconcile_vouchers, bank_transaction_name=bank_transaction.name, vouchers=vouchers) + vouchers = json.dumps( + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment.name, + "amount": bank_transaction.unallocated_amount, + } + ] + ) + self.assertRaises( + frappe.ValidationError, + reconcile_vouchers, + bank_transaction_name=bank_transaction.name, + vouchers=vouchers, + ) # Raise an error if debitor transaction vs debitor payment def test_clear_sales_invoice(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio")) + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"), + ) payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"])) - vouchers = json.dumps([{ - "payment_doctype":"Sales Invoice", - "payment_name":payment.name, - "amount":bank_transaction.unallocated_amount}]) + vouchers = json.dumps( + [ + { + "payment_doctype": "Sales Invoice", + "payment_name": payment.name, + "amount": bank_transaction.unallocated_amount, + } + ] + ) reconcile_vouchers(bank_transaction.name, vouchers=vouchers) - self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0) - self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None) + self.assertEqual( + frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0 + ) + self.assertTrue( + frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") + is not None + ) + def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): try: - frappe.get_doc({ - "doctype": "Bank", - "bank_name":bank_name, - }).insert() + frappe.get_doc( + { + "doctype": "Bank", + "bank_name": bank_name, + } + ).insert() except frappe.DuplicateEntryError: pass try: - frappe.get_doc({ - "doctype": "Bank Account", - "account_name":"Checking Account", - "bank": bank_name, - "account": account_name - }).insert() + frappe.get_doc( + { + "doctype": "Bank Account", + "account_name": "Checking Account", + "bank": bank_name, + "account": account_name, + } + ).insert() except frappe.DuplicateEntryError: pass + def add_transactions(): create_bank_account() - doc = frappe.get_doc({ - "doctype": "Bank Transaction", - "description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", - "date": "2018-10-23", - "deposit": 1200, - "currency": "INR", - "bank_account": "Checking Account - Citi Bank" - }).insert() + doc = frappe.get_doc( + { + "doctype": "Bank Transaction", + "description": "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", + "date": "2018-10-23", + "deposit": 1200, + "currency": "INR", + "bank_account": "Checking Account - Citi Bank", + } + ).insert() doc.submit() - doc = frappe.get_doc({ - "doctype": "Bank Transaction", - "description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G", - "date": "2018-10-23", - "deposit": 1700, - "currency": "INR", - "bank_account": "Checking Account - Citi Bank" - }).insert() + doc = frappe.get_doc( + { + "doctype": "Bank Transaction", + "description": "1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G", + "date": "2018-10-23", + "deposit": 1700, + "currency": "INR", + "bank_account": "Checking Account - Citi Bank", + } + ).insert() doc.submit() - doc = frappe.get_doc({ - "doctype": "Bank Transaction", - "description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic", - "date": "2018-10-26", - "withdrawal": 690, - "currency": "INR", - "bank_account": "Checking Account - Citi Bank" - }).insert() + doc = frappe.get_doc( + { + "doctype": "Bank Transaction", + "description": "Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic", + "date": "2018-10-26", + "withdrawal": 690, + "currency": "INR", + "bank_account": "Checking Account - Citi Bank", + } + ).insert() doc.submit() - doc = frappe.get_doc({ - "doctype": "Bank Transaction", - "description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07", - "date": "2018-10-27", - "deposit": 3900, - "currency": "INR", - "bank_account": "Checking Account - Citi Bank" - }).insert() + doc = frappe.get_doc( + { + "doctype": "Bank Transaction", + "description": "Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07", + "date": "2018-10-27", + "deposit": 3900, + "currency": "INR", + "bank_account": "Checking Account - Citi Bank", + } + ).insert() doc.submit() - doc = frappe.get_doc({ - "doctype": "Bank Transaction", - "description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio", - "date": "2018-10-27", - "withdrawal": 109080, - "currency": "INR", - "bank_account": "Checking Account - Citi Bank" - }).insert() + doc = frappe.get_doc( + { + "doctype": "Bank Transaction", + "description": "I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio", + "date": "2018-10-27", + "withdrawal": 109080, + "currency": "INR", + "bank_account": "Checking Account - Citi Bank", + } + ).insert() doc.submit() def add_vouchers(): try: - frappe.get_doc({ - "doctype": "Supplier", - "supplier_group":"All Supplier Groups", - "supplier_type": "Company", - "supplier_name": "Conrad Electronic" - }).insert() + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_group": "All Supplier Groups", + "supplier_type": "Company", + "supplier_name": "Conrad Electronic", + } + ).insert() except frappe.DuplicateEntryError: pass @@ -198,12 +267,14 @@ def add_vouchers(): pe.submit() try: - frappe.get_doc({ - "doctype": "Supplier", - "supplier_group":"All Supplier Groups", - "supplier_type": "Company", - "supplier_name": "Mr G" - }).insert() + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_group": "All Supplier Groups", + "supplier_type": "Company", + "supplier_name": "Mr G", + } + ).insert() except frappe.DuplicateEntryError: pass @@ -222,26 +293,30 @@ def add_vouchers(): pe.submit() try: - frappe.get_doc({ - "doctype": "Supplier", - "supplier_group":"All Supplier Groups", - "supplier_type": "Company", - "supplier_name": "Poore Simon's" - }).insert() + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_group": "All Supplier Groups", + "supplier_type": "Company", + "supplier_name": "Poore Simon's", + } + ).insert() except frappe.DuplicateEntryError: pass try: - frappe.get_doc({ - "doctype": "Customer", - "customer_group":"All Customer Groups", - "customer_type": "Company", - "customer_name": "Poore Simon's" - }).insert() + frappe.get_doc( + { + "doctype": "Customer", + "customer_group": "All Customer Groups", + "customer_type": "Company", + "customer_name": "Poore Simon's", + } + ).insert() except frappe.DuplicateEntryError: pass - pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save =1) + pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1) pi.cash_bank_account = "_Test Bank - _TC" pi.insert() pi.submit() @@ -261,33 +336,31 @@ def add_vouchers(): pe.submit() try: - frappe.get_doc({ - "doctype": "Customer", - "customer_group":"All Customer Groups", - "customer_type": "Company", - "customer_name": "Fayva" - }).insert() + frappe.get_doc( + { + "doctype": "Customer", + "customer_group": "All Customer Groups", + "customer_type": "Company", + "customer_name": "Fayva", + } + ).insert() except frappe.DuplicateEntryError: pass - mode_of_payment = frappe.get_doc({ - "doctype": "Mode of Payment", - "name": "Cash" - }) + mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"}) - if not frappe.db.get_value('Mode of Payment Account', {'company': "_Test Company", 'parent': "Cash"}): - mode_of_payment.append("accounts", { - "company": "_Test Company", - "default_account": "_Test Bank - _TC" - }) + if not frappe.db.get_value( + "Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"} + ): + mode_of_payment.append( + "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} + ) mode_of_payment.save() si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si.is_pos = 1 - si.append("payments", { - "mode_of_payment": "Cash", - "account": "_Test Bank - _TC", - "amount": 109080 - }) + si.append( + "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080} + ) si.insert() si.submit() diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 492bb365589..5527f9fb99f 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -14,13 +14,19 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.utils import get_fiscal_year -class BudgetError(frappe.ValidationError): pass -class DuplicateBudgetError(frappe.ValidationError): pass +class BudgetError(frappe.ValidationError): + pass + + +class DuplicateBudgetError(frappe.ValidationError): + pass + class Budget(Document): def autoname(self): - self.name = make_autoname(self.get(frappe.scrub(self.budget_against)) - + "/" + self.fiscal_year + "/.###") + self.name = make_autoname( + self.get(frappe.scrub(self.budget_against)) + "/" + self.fiscal_year + "/.###" + ) def validate(self): if not self.get(frappe.scrub(self.budget_against)): @@ -35,34 +41,44 @@ class Budget(Document): budget_against = self.get(budget_against_field) accounts = [d.account for d in self.accounts] or [] - existing_budget = frappe.db.sql(""" + existing_budget = frappe.db.sql( + """ select b.name, ba.account from `tabBudget` b, `tabBudget Account` ba where ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and b.fiscal_year=%s and b.name != %s and ba.account in (%s) """ - % ('%s', budget_against_field, '%s', '%s', '%s', ','.join(['%s'] * len(accounts))), - (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), as_dict=1) + % ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))), + (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), + as_dict=1, + ) for d in existing_budget: - frappe.throw(_("Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}") - .format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), DuplicateBudgetError) + frappe.throw( + _( + "Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}" + ).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), + DuplicateBudgetError, + ) def validate_accounts(self): account_list = [] - for d in self.get('accounts'): + for d in self.get("accounts"): if d.account: - account_details = frappe.db.get_value("Account", d.account, - ["is_group", "company", "report_type"], as_dict=1) + account_details = frappe.db.get_value( + "Account", d.account, ["is_group", "company", "report_type"], as_dict=1 + ) if account_details.is_group: frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account)) elif account_details.company != self.company: - frappe.throw(_("Account {0} does not belongs to company {1}") - .format(d.account, self.company)) + frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company)) elif account_details.report_type != "Profit and Loss": - frappe.throw(_("Budget cannot be assigned against {0}, as it's not an Income or Expense account") - .format(d.account)) + frappe.throw( + _("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format( + d.account + ) + ) if d.account in account_list: frappe.throw(_("Account {0} has been entered multiple times").format(d.account)) @@ -70,51 +86,66 @@ class Budget(Document): account_list.append(d.account) def set_null_value(self): - if self.budget_against == 'Cost Center': + if self.budget_against == "Cost Center": self.project = None else: self.cost_center = None def validate_applicable_for(self): - if (self.applicable_on_material_request - and not (self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses)): - frappe.throw(_("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses")) + if self.applicable_on_material_request and not ( + self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses + ): + frappe.throw( + _("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses") + ) - elif (self.applicable_on_purchase_order - and not (self.applicable_on_booking_actual_expenses)): + elif self.applicable_on_purchase_order and not (self.applicable_on_booking_actual_expenses): frappe.throw(_("Please enable Applicable on Booking Actual Expenses")) - elif not(self.applicable_on_material_request - or self.applicable_on_purchase_order or self.applicable_on_booking_actual_expenses): + elif not ( + self.applicable_on_material_request + or self.applicable_on_purchase_order + or self.applicable_on_booking_actual_expenses + ): self.applicable_on_booking_actual_expenses = 1 + def validate_expense_against_budget(args): args = frappe._dict(args) - if args.get('company') and not args.fiscal_year: - args.fiscal_year = get_fiscal_year(args.get('posting_date'), company=args.get('company'))[0] - frappe.flags.exception_approver_role = frappe.get_cached_value('Company', - args.get('company'), 'exception_budget_approver_role') + if args.get("company") and not args.fiscal_year: + args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] + frappe.flags.exception_approver_role = frappe.get_cached_value( + "Company", args.get("company"), "exception_budget_approver_role" + ) if not args.account: args.account = args.get("expense_account") - if not (args.get('account') and args.get('cost_center')) and args.item_code: + if not (args.get("account") and args.get("cost_center")) and args.item_code: args.cost_center, args.account = get_item_details(args) if not args.account: return - for budget_against in ['project', 'cost_center'] + get_accounting_dimensions(): - if (args.get(budget_against) and args.account - and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})): + for budget_against in ["project", "cost_center"] + get_accounting_dimensions(): + if ( + args.get(budget_against) + and args.account + and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"}) + ): doctype = frappe.unscrub(budget_against) - if frappe.get_cached_value('DocType', doctype, 'is_tree'): + if frappe.get_cached_value("DocType", doctype, "is_tree"): lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) condition = """and exists(select name from `tab%s` - where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec + where lft<=%s and rgt>=%s and name=b.%s)""" % ( + doctype, + lft, + rgt, + budget_against, + ) # nosec args.is_tree = True else: condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against))) @@ -123,7 +154,8 @@ def validate_expense_against_budget(args): args.budget_against_field = budget_against args.budget_against_doctype = doctype - budget_records = frappe.db.sql(""" + budget_records = frappe.db.sql( + """ select b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution, ifnull(b.applicable_on_material_request, 0) as for_material_request, @@ -138,11 +170,17 @@ def validate_expense_against_budget(args): b.name=ba.parent and b.fiscal_year=%s and ba.account=%s and b.docstatus=1 {condition} - """.format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec + """.format( + condition=condition, budget_against_field=budget_against + ), + (args.fiscal_year, args.account), + as_dict=True, + ) # nosec if budget_records: validate_budget_records(args, budget_records) + def validate_budget_records(args, budget_records): for budget in budget_records: if flt(budget.budget_amount): @@ -150,88 +188,118 @@ def validate_budget_records(args, budget_records): yearly_action, monthly_action = get_actions(args, budget) if monthly_action in ["Stop", "Warn"]: - budget_amount = get_accumulated_monthly_budget(budget.monthly_distribution, - args.posting_date, args.fiscal_year, budget.budget_amount) + budget_amount = get_accumulated_monthly_budget( + budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount + ) args["month_end_date"] = get_last_day(args.posting_date) - compare_expense_with_budget(args, budget_amount, - _("Accumulated Monthly"), monthly_action, budget.budget_against, amount) + compare_expense_with_budget( + args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount + ) + + if ( + yearly_action in ("Stop", "Warn") + and monthly_action != "Stop" + and yearly_action != monthly_action + ): + compare_expense_with_budget( + args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount + ) - if yearly_action in ("Stop", "Warn") and monthly_action != "Stop" \ - and yearly_action != monthly_action: - compare_expense_with_budget(args, flt(budget.budget_amount), - _("Annual"), yearly_action, budget.budget_against, amount) def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): actual_expense = amount or get_actual_expense(args) if actual_expense > budget_amount: diff = actual_expense - budget_amount - currency = frappe.get_cached_value('Company', args.company, 'default_currency') + currency = frappe.get_cached_value("Company", args.company, "default_currency") msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format( - _(action_for), frappe.bold(args.account), args.budget_against_field, - frappe.bold(budget_against), - frappe.bold(fmt_money(budget_amount, currency=currency)), - frappe.bold(fmt_money(diff, currency=currency))) + _(action_for), + frappe.bold(args.account), + args.budget_against_field, + frappe.bold(budget_against), + frappe.bold(fmt_money(budget_amount, currency=currency)), + frappe.bold(fmt_money(diff, currency=currency)), + ) - if (frappe.flags.exception_approver_role - and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)): + if ( + frappe.flags.exception_approver_role + and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user) + ): action = "Warn" - if action=="Stop": + if action == "Stop": frappe.throw(msg, BudgetError) else: - frappe.msgprint(msg, indicator='orange') + frappe.msgprint(msg, indicator="orange") + def get_actions(args, budget): yearly_action = budget.action_if_annual_budget_exceeded monthly_action = budget.action_if_accumulated_monthly_budget_exceeded - if args.get('doctype') == 'Material Request' and budget.for_material_request: + if args.get("doctype") == "Material Request" and budget.for_material_request: yearly_action = budget.action_if_annual_budget_exceeded_on_mr monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr - elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order: + elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order: yearly_action = budget.action_if_annual_budget_exceeded_on_po monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po return yearly_action, monthly_action + def get_amount(args, budget): amount = 0 - if args.get('doctype') == 'Material Request' and budget.for_material_request: - amount = (get_requested_amount(args, budget) - + get_ordered_amount(args, budget) + get_actual_expense(args)) + if args.get("doctype") == "Material Request" and budget.for_material_request: + amount = ( + get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args) + ) - elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order: + elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order: amount = get_ordered_amount(args, budget) + get_actual_expense(args) return amount -def get_requested_amount(args, budget): - item_code = args.get('item_code') - condition = get_other_condition(args, budget, 'Material Request') - data = frappe.db.sql(""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount +def get_requested_amount(args, budget): + item_code = args.get("item_code") + condition = get_other_condition(args, budget, "Material Request") + + data = frappe.db.sql( + """ select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and - parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition), item_code, as_list=1) + parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format( + condition + ), + item_code, + as_list=1, + ) return data[0][0] if data else 0 + def get_ordered_amount(args, budget): - item_code = args.get('item_code') - condition = get_other_condition(args, budget, 'Purchase Order') + item_code = args.get("item_code") + condition = get_other_condition(args, budget, "Purchase Order") - data = frappe.db.sql(""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount + data = frappe.db.sql( + """ select ifnull(sum(child.amount - child.billed_amt), 0) as amount from `tabPurchase Order Item` child, `tabPurchase Order` parent where parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt - and parent.status != 'Closed' and {0}""".format(condition), item_code, as_list=1) + and parent.status != 'Closed' and {0}""".format( + condition + ), + item_code, + as_list=1, + ) return data[0][0] if data else 0 + def get_other_condition(args, budget, for_doc): condition = "expense_account = '%s'" % (args.expense_account) budget_against_field = args.get("budget_against_field") @@ -239,41 +307,51 @@ def get_other_condition(args, budget, for_doc): if budget_against_field and args.get(budget_against_field): condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field)) - if args.get('fiscal_year'): - date_field = 'schedule_date' if for_doc == 'Material Request' else 'transaction_date' - start_date, end_date = frappe.db.get_value('Fiscal Year', args.get('fiscal_year'), - ['year_start_date', 'year_end_date']) + if args.get("fiscal_year"): + date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date" + start_date, end_date = frappe.db.get_value( + "Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"] + ) condition += """ and parent.%s - between '%s' and '%s' """ %(date_field, start_date, end_date) + between '%s' and '%s' """ % ( + date_field, + start_date, + end_date, + ) return condition + def get_actual_expense(args): if not args.budget_against_doctype: args.budget_against_doctype = frappe.unscrub(args.budget_against_field) - budget_against_field = args.get('budget_against_field') - condition1 = " and gle.posting_date <= %(month_end_date)s" \ - if args.get("month_end_date") else "" + budget_against_field = args.get("budget_against_field") + condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else "" if args.is_tree: - lft_rgt = frappe.db.get_value(args.budget_against_doctype, - args.get(budget_against_field), ["lft", "rgt"], as_dict=1) + lft_rgt = frappe.db.get_value( + args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1 + ) args.update(lft_rgt) condition2 = """and exists(select name from `tab{doctype}` where lft>=%(lft)s and rgt<=%(rgt)s - and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec - budget_against_field=budget_against_field) + and name=gle.{budget_against_field})""".format( + doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec + ) else: condition2 = """and exists(select name from `tab{doctype}` where name=gle.{budget_against} and - gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype, - budget_against = budget_against_field) + gle.{budget_against} = %({budget_against})s)""".format( + doctype=args.budget_against_doctype, budget_against=budget_against_field + ) - amount = flt(frappe.db.sql(""" + amount = flt( + frappe.db.sql( + """ select sum(gle.debit) - sum(gle.credit) from `tabGL Entry` gle where gle.account=%(account)s @@ -282,46 +360,59 @@ def get_actual_expense(args): and gle.company=%(company)s and gle.docstatus=1 {condition2} - """.format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec + """.format( + condition1=condition1, condition2=condition2 + ), + (args), + )[0][0] + ) # nosec return amount + def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget): distribution = {} if monthly_distribution: - for d in frappe.db.sql("""select mdp.month, mdp.percentage_allocation + for d in frappe.db.sql( + """select mdp.month, mdp.percentage_allocation from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md - where mdp.parent=md.name and md.fiscal_year=%s""", fiscal_year, as_dict=1): - distribution.setdefault(d.month, d.percentage_allocation) + where mdp.parent=md.name and md.fiscal_year=%s""", + fiscal_year, + as_dict=1, + ): + distribution.setdefault(d.month, d.percentage_allocation) dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date") accumulated_percentage = 0.0 - while(dt <= getdate(posting_date)): + while dt <= getdate(posting_date): if monthly_distribution: accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0) else: - accumulated_percentage += 100.0/12 + accumulated_percentage += 100.0 / 12 dt = add_months(dt, 1) return annual_budget * accumulated_percentage / 100 + def get_item_details(args): cost_center, expense_account = None, None - if not args.get('company'): + if not args.get("company"): return cost_center, expense_account if args.item_code: - item_defaults = frappe.db.get_value('Item Default', - {'parent': args.item_code, 'company': args.get('company')}, - ['buying_cost_center', 'expense_account']) + item_defaults = frappe.db.get_value( + "Item Default", + {"parent": args.item_code, "company": args.get("company")}, + ["buying_cost_center", "expense_account"], + ) if item_defaults: cost_center, expense_account = item_defaults if not (cost_center and expense_account): - for doctype in ['Item Group', 'Company']: + for doctype in ["Item Group", "Company"]: data = get_expense_cost_center(doctype, args) if not cost_center and data: @@ -335,11 +426,15 @@ def get_item_details(args): return cost_center, expense_account + def get_expense_cost_center(doctype, args): - if doctype == 'Item Group': - return frappe.db.get_value('Item Default', - {'parent': args.get(frappe.scrub(doctype)), 'company': args.get('company')}, - ['buying_cost_center', 'expense_account']) + if doctype == "Item Group": + return frappe.db.get_value( + "Item Default", + {"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")}, + ["buying_cost_center", "expense_account"], + ) else: - return frappe.db.get_value(doctype, args.get(frappe.scrub(doctype)),\ - ['cost_center', 'default_expense_account']) + return frappe.db.get_value( + doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"] + ) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 9a83a0aa9a6..c48c7d97a2a 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -11,7 +11,8 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order -test_dependencies = ['Monthly Distribution'] +test_dependencies = ["Monthly Distribution"] + class TestBudget(unittest.TestCase): def test_monthly_budget_crossed_ignore(self): @@ -19,11 +20,18 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 40000, + "_Test Cost Center - _TC", + posting_date=nowdate(), + submit=True, + ) - self.assertTrue(frappe.db.get_value("GL Entry", - {"voucher_type": "Journal Entry", "voucher_no": jv.name})) + self.assertTrue( + frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name}) + ) budget.cancel() jv.cancel() @@ -33,10 +41,17 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate()) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 40000, + "_Test Cost Center - _TC", + posting_date=nowdate(), + ) self.assertRaises(BudgetError, jv.submit) @@ -48,49 +63,65 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate()) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 40000, + "_Test Cost Center - _TC", + posting_date=nowdate(), + ) self.assertRaises(BudgetError, jv.submit) - frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', 'Accounts User') + frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "Accounts User") jv.submit() - self.assertEqual(frappe.db.get_value('Journal Entry', jv.name, 'docstatus'), 1) + self.assertEqual(frappe.db.get_value("Journal Entry", jv.name, "docstatus"), 1) jv.cancel() - frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', '') + frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "") budget.load_from_db() budget.cancel() def test_monthly_budget_crossed_for_mr(self): - budget = make_budget(applicable_on_material_request=1, - applicable_on_purchase_order=1, action_if_accumulated_monthly_budget_exceeded_on_mr="Stop", - budget_against="Cost Center") + budget = make_budget( + applicable_on_material_request=1, + applicable_on_purchase_order=1, + action_if_accumulated_monthly_budget_exceeded_on_mr="Stop", + budget_against="Cost Center", + ) fiscal_year = get_fiscal_year(nowdate())[0] - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) - mr = frappe.get_doc({ - "doctype": "Material Request", - "material_request_type": "Purchase", - "transaction_date": nowdate(), - "company": budget.company, - "items": [{ - 'item_code': '_Test Item', - 'qty': 1, - 'uom': "_Test UOM", - 'warehouse': '_Test Warehouse - _TC', - 'schedule_date': nowdate(), - 'rate': 100000, - 'expense_account': '_Test Account Cost for Goods Sold - _TC', - 'cost_center': '_Test Cost Center - _TC' - }] - }) + mr = frappe.get_doc( + { + "doctype": "Material Request", + "material_request_type": "Purchase", + "transaction_date": nowdate(), + "company": budget.company, + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC", + "schedule_date": nowdate(), + "rate": 100000, + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + } + ], + } + ) mr.set_missing_values() @@ -100,11 +131,16 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_crossed_for_po(self): - budget = make_budget(applicable_on_purchase_order=1, - action_if_accumulated_monthly_budget_exceeded_on_po="Stop", budget_against="Cost Center") + budget = make_budget( + applicable_on_purchase_order=1, + action_if_accumulated_monthly_budget_exceeded_on_po="Stop", + budget_against="Cost Center", + ) fiscal_year = get_fiscal_year(nowdate())[0] - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True) @@ -122,12 +158,20 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Project") - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) project = frappe.get_value("Project", {"project_name": "_Test Project"}) - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate()) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 40000, + "_Test Cost Center - _TC", + project=project, + posting_date=nowdate(), + ) self.assertRaises(BudgetError, jv.submit) @@ -139,8 +183,13 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", posting_date=nowdate()) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 250000, + "_Test Cost Center - _TC", + posting_date=nowdate(), + ) self.assertRaises(BudgetError, jv.submit) @@ -153,9 +202,14 @@ class TestBudget(unittest.TestCase): project = frappe.get_value("Project", {"project_name": "_Test Project"}) - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", - project=project, posting_date=nowdate()) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 250000, + "_Test Cost Center - _TC", + project=project, + posting_date=nowdate(), + ) self.assertRaises(BudgetError, jv.submit) @@ -169,14 +223,23 @@ class TestBudget(unittest.TestCase): if month > 9: month = 9 - for i in range(month+1): - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) + for i in range(month + 1): + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 20000, + "_Test Cost Center - _TC", + posting_date=nowdate(), + submit=True, + ) - self.assertTrue(frappe.db.get_value("GL Entry", - {"voucher_type": "Journal Entry", "voucher_no": jv.name})) + self.assertTrue( + frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name}) + ) - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) self.assertRaises(BudgetError, jv.cancel) @@ -193,14 +256,23 @@ class TestBudget(unittest.TestCase): project = frappe.get_value("Project", {"project_name": "_Test Project"}) for i in range(month + 1): - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, - project=project) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 20000, + "_Test Cost Center - _TC", + posting_date=nowdate(), + submit=True, + project=project, + ) - self.assertTrue(frappe.db.get_value("GL Entry", - {"voucher_type": "Journal Entry", "voucher_no": jv.name})) + self.assertTrue( + frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name}) + ) - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) self.assertRaises(BudgetError, jv.cancel) @@ -212,10 +284,17 @@ class TestBudget(unittest.TestCase): set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC") budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC") - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center 2 - _TC", posting_date=nowdate()) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 40000, + "_Test Cost Center 2 - _TC", + posting_date=nowdate(), + ) self.assertRaises(BudgetError, jv.submit) @@ -226,19 +305,28 @@ class TestBudget(unittest.TestCase): cost_center = "_Test Cost Center 3 - _TC" if not frappe.db.exists("Cost Center", cost_center): - frappe.get_doc({ - 'doctype': 'Cost Center', - 'cost_center_name': '_Test Cost Center 3', - 'parent_cost_center': "_Test Company - _TC", - 'company': '_Test Company', - 'is_group': 0 - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Cost Center", + "cost_center_name": "_Test Cost Center 3", + "parent_cost_center": "_Test Company - _TC", + "company": "_Test Company", + "is_group": 0, + } + ).insert(ignore_permissions=True) budget = make_budget(budget_against="Cost Center", cost_center=cost_center) - frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + frappe.db.set_value( + "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop" + ) - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, cost_center, posting_date=nowdate()) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 40000, + cost_center, + posting_date=nowdate(), + ) self.assertRaises(BudgetError, jv.submit) @@ -255,14 +343,16 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again fiscal_year = get_fiscal_year(nowdate())[0] - args = frappe._dict({ - "account": "_Test Account Cost for Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - "monthly_end_date": posting_date, - "company": "_Test Company", - "fiscal_year": fiscal_year, - "budget_against_field": budget_against_field, - }) + args = frappe._dict( + { + "account": "_Test Account Cost for Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + "monthly_end_date": posting_date, + "company": "_Test Company", + "fiscal_year": fiscal_year, + "budget_against_field": budget_against_field, + } + ) if not args.get(budget_against_field): args[budget_against_field] = budget_against @@ -271,26 +361,42 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again if existing_expense: if budget_against_field == "cost_center": - make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) + make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + -existing_expense, + "_Test Cost Center - _TC", + posting_date=nowdate(), + submit=True, + ) elif budget_against_field == "project": - make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate()) + make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + -existing_expense, + "_Test Cost Center - _TC", + submit=True, + project=budget_against, + posting_date=nowdate(), + ) + def make_budget(**args): args = frappe._dict(args) - budget_against=args.budget_against - cost_center=args.cost_center + budget_against = args.budget_against + cost_center = args.cost_center fiscal_year = get_fiscal_year(nowdate())[0] if budget_against == "Project": project_name = "{0}%".format("_Test Project/" + fiscal_year) - budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", project_name)}) + budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)}) else: cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year) - budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", cost_center_name)}) + budget_list = frappe.get_all( + "Budget", fields=["name"], filters={"name": ("like", cost_center_name)} + ) for d in budget_list: frappe.db.sql("delete from `tabBudget` where name = %(name)s", d) frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d) @@ -300,7 +406,7 @@ def make_budget(**args): if budget_against == "Project": budget.project = frappe.get_value("Project", {"project_name": "_Test Project"}) else: - budget.cost_center =cost_center or "_Test Cost Center - _TC" + budget.cost_center = cost_center or "_Test Cost Center - _TC" monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution") monthly_distribution.fiscal_year = fiscal_year @@ -312,20 +418,27 @@ def make_budget(**args): budget.action_if_annual_budget_exceeded = "Stop" budget.action_if_accumulated_monthly_budget_exceeded = "Ignore" budget.budget_against = budget_against - budget.append("accounts", { - "account": "_Test Account Cost for Goods Sold - _TC", - "budget_amount": 200000 - }) + budget.append( + "accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000} + ) if args.applicable_on_material_request: budget.applicable_on_material_request = 1 - budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or 'Warn' - budget.action_if_accumulated_monthly_budget_exceeded_on_mr = args.action_if_accumulated_monthly_budget_exceeded_on_mr or 'Warn' + budget.action_if_annual_budget_exceeded_on_mr = ( + args.action_if_annual_budget_exceeded_on_mr or "Warn" + ) + budget.action_if_accumulated_monthly_budget_exceeded_on_mr = ( + args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn" + ) if args.applicable_on_purchase_order: budget.applicable_on_purchase_order = 1 - budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or 'Warn' - budget.action_if_accumulated_monthly_budget_exceeded_on_po = args.action_if_accumulated_monthly_budget_exceeded_on_po or 'Warn' + budget.action_if_annual_budget_exceeded_on_po = ( + args.action_if_annual_budget_exceeded_on_po or "Warn" + ) + budget.action_if_accumulated_monthly_budget_exceeded_on_po = ( + args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn" + ) budget.insert() budget.submit() diff --git a/erpnext/accounts/doctype/c_form/c_form.py b/erpnext/accounts/doctype/c_form/c_form.py index 61331d32d8e..0de75c78697 100644 --- a/erpnext/accounts/doctype/c_form/c_form.py +++ b/erpnext/accounts/doctype/c_form/c_form.py @@ -11,28 +11,42 @@ from frappe.utils import flt class CForm(Document): def validate(self): """Validate invoice that c-form is applicable - and no other c-form is received for that""" + and no other c-form is received for that""" - for d in self.get('invoices'): + for d in self.get("invoices"): if d.invoice_no: - inv = frappe.db.sql("""select c_form_applicable, c_form_no from - `tabSales Invoice` where name = %s and docstatus = 1""", d.invoice_no) + inv = frappe.db.sql( + """select c_form_applicable, c_form_no from + `tabSales Invoice` where name = %s and docstatus = 1""", + d.invoice_no, + ) - if inv and inv[0][0] != 'Yes': + if inv and inv[0][0] != "Yes": frappe.throw(_("C-form is not applicable for Invoice: {0}").format(d.invoice_no)) elif inv and inv[0][1] and inv[0][1] != self.name: - frappe.throw(_("""Invoice {0} is tagged in another C-form: {1}. + frappe.throw( + _( + """Invoice {0} is tagged in another C-form: {1}. If you want to change C-form no for this invoice, - please remove invoice no from the previous c-form and then try again"""\ - .format(d.invoice_no, inv[0][1]))) + please remove invoice no from the previous c-form and then try again""".format( + d.invoice_no, inv[0][1] + ) + ) + ) elif not inv: - frappe.throw(_("Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \ - Please enter a valid Invoice".format(d.idx, d.invoice_no))) + frappe.throw( + _( + "Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \ + Please enter a valid Invoice".format( + d.idx, d.invoice_no + ) + ) + ) def on_update(self): - """ Update C-Form No on invoices""" + """Update C-Form No on invoices""" self.set_total_invoiced_amount() def on_submit(self): @@ -43,30 +57,40 @@ class CForm(Document): frappe.db.sql("""update `tabSales Invoice` set c_form_no=null where c_form_no=%s""", self.name) def set_cform_in_sales_invoices(self): - inv = [d.invoice_no for d in self.get('invoices')] + inv = [d.invoice_no for d in self.get("invoices")] if inv: - frappe.db.sql("""update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)""" % - ('%s', '%s', ', '.join(['%s'] * len(inv))), tuple([self.name, self.modified] + inv)) + frappe.db.sql( + """update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)""" + % ("%s", "%s", ", ".join(["%s"] * len(inv))), + tuple([self.name, self.modified] + inv), + ) - frappe.db.sql("""update `tabSales Invoice` set c_form_no = null, modified = %s - where name not in (%s) and ifnull(c_form_no, '') = %s""" % - ('%s', ', '.join(['%s']*len(inv)), '%s'), tuple([self.modified] + inv + [self.name])) + frappe.db.sql( + """update `tabSales Invoice` set c_form_no = null, modified = %s + where name not in (%s) and ifnull(c_form_no, '') = %s""" + % ("%s", ", ".join(["%s"] * len(inv)), "%s"), + tuple([self.modified] + inv + [self.name]), + ) else: frappe.throw(_("Please enter atleast 1 invoice in the table")) def set_total_invoiced_amount(self): - total = sum(flt(d.grand_total) for d in self.get('invoices')) - frappe.db.set(self, 'total_invoiced_amount', total) + total = sum(flt(d.grand_total) for d in self.get("invoices")) + frappe.db.set(self, "total_invoiced_amount", total) @frappe.whitelist() def get_invoice_details(self, invoice_no): - """ Pull details from invoices for referrence """ + """Pull details from invoices for referrence""" if invoice_no: - inv = frappe.db.get_value("Sales Invoice", invoice_no, - ["posting_date", "territory", "base_net_total", "base_grand_total"], as_dict=True) + inv = frappe.db.get_value( + "Sales Invoice", + invoice_no, + ["posting_date", "territory", "base_net_total", "base_grand_total"], + as_dict=True, + ) return { - 'invoice_date' : inv.posting_date, - 'territory' : inv.territory, - 'net_total' : inv.base_net_total, - 'grand_total' : inv.base_grand_total + "invoice_date": inv.posting_date, + "territory": inv.territory, + "net_total": inv.base_net_total, + "grand_total": inv.base_grand_total, } diff --git a/erpnext/accounts/doctype/c_form/test_c_form.py b/erpnext/accounts/doctype/c_form/test_c_form.py index fa34c255c66..87ad60fddac 100644 --- a/erpnext/accounts/doctype/c_form/test_c_form.py +++ b/erpnext/accounts/doctype/c_form/test_c_form.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('C-Form') + class TestCForm(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py b/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py index 4465ec6024b..79feb2dae23 100644 --- a/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py +++ b/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py @@ -1,26 +1,25 @@ - DEFAULT_MAPPERS = [ - { - 'doctype': 'Cash Flow Mapper', - 'section_footer': 'Net cash generated by operating activities', - 'section_header': 'Cash flows from operating activities', - 'section_leader': 'Adjustments for', - 'section_name': 'Operating Activities', - 'position': 0, - 'section_subtotal': 'Cash generated from operations', - }, - { - 'doctype': 'Cash Flow Mapper', - 'position': 1, - 'section_footer': 'Net cash used in investing activities', - 'section_header': 'Cash flows from investing activities', - 'section_name': 'Investing Activities' - }, - { - 'doctype': 'Cash Flow Mapper', - 'position': 2, - 'section_footer': 'Net cash used in financing activites', - 'section_header': 'Cash flows from financing activities', - 'section_name': 'Financing Activities', - } + { + "doctype": "Cash Flow Mapper", + "section_footer": "Net cash generated by operating activities", + "section_header": "Cash flows from operating activities", + "section_leader": "Adjustments for", + "section_name": "Operating Activities", + "position": 0, + "section_subtotal": "Cash generated from operations", + }, + { + "doctype": "Cash Flow Mapper", + "position": 1, + "section_footer": "Net cash used in investing activities", + "section_header": "Cash flows from investing activities", + "section_name": "Investing Activities", + }, + { + "doctype": "Cash Flow Mapper", + "position": 2, + "section_footer": "Net cash used in financing activites", + "section_header": "Cash flows from financing activities", + "section_name": "Financing Activities", + }, ] diff --git a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py index cd8381a4bd3..3bce4d51c7a 100644 --- a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py +++ b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py @@ -11,9 +11,11 @@ class CashFlowMapping(Document): self.validate_checked_options() def validate_checked_options(self): - checked_fields = [d for d in self.meta.fields if d.fieldtype == 'Check' and self.get(d.fieldname) == 1] + checked_fields = [ + d for d in self.meta.fields if d.fieldtype == "Check" and self.get(d.fieldname) == 1 + ] if len(checked_fields) > 1: frappe.throw( - frappe._('You can only select a maximum of one option from the list of check boxes.'), - title='Error' + frappe._("You can only select a maximum of one option from the list of check boxes."), + title="Error", ) diff --git a/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py b/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py index abb25670467..19f2425b4ce 100644 --- a/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py +++ b/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py @@ -9,19 +9,16 @@ import frappe class TestCashFlowMapping(unittest.TestCase): def setUp(self): if frappe.db.exists("Cash Flow Mapping", "Test Mapping"): - frappe.delete_doc('Cash Flow Mappping', 'Test Mapping') + frappe.delete_doc("Cash Flow Mappping", "Test Mapping") def tearDown(self): - frappe.delete_doc('Cash Flow Mapping', 'Test Mapping') + frappe.delete_doc("Cash Flow Mapping", "Test Mapping") def test_multiple_selections_not_allowed(self): - doc = frappe.new_doc('Cash Flow Mapping') - doc.mapping_name = 'Test Mapping' - doc.label = 'Test label' - doc.append( - 'accounts', - {'account': 'Accounts Receivable - _TC'} - ) + doc = frappe.new_doc("Cash Flow Mapping") + doc.mapping_name = "Test Mapping" + doc.label = "Test label" + doc.append("accounts", {"account": "Accounts Receivable - _TC"}) doc.is_working_capital = 1 doc.is_finance_cost = 1 diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py index 9fbd0c97c1e..60138077286 100644 --- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py +++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py @@ -17,11 +17,14 @@ class CashierClosing(Document): self.make_calculations() def get_outstanding(self): - values = frappe.db.sql(""" + values = frappe.db.sql( + """ select sum(outstanding_amount) from `tabSales Invoice` where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s - """, (self.date, self.from_time, self.time, self.user)) + """, + (self.date, self.from_time, self.time, self.user), + ) self.outstanding_amount = flt(values[0][0] if values else 0) def make_calculations(self): @@ -29,7 +32,9 @@ class CashierClosing(Document): for i in self.payments: total += flt(i.amount) - self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns) + self.net_amount = ( + total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns) + ) def validate_time(self): if self.from_time >= self.time: 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 aaacce4eb9d..01bf1c23e92 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 @@ -25,33 +25,41 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import class ChartofAccountsImporter(Document): def validate(self): if self.import_file: - get_coa('Chart of Accounts Importer', 'All Accounts', file_name=self.import_file, for_validate=1) + get_coa( + "Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1 + ) + def validate_columns(data): if not data: - frappe.throw(_('No data found. Seems like you uploaded a blank file')) + frappe.throw(_("No data found. Seems like you uploaded a blank file")) no_of_columns = max([len(d) for d in data]) if no_of_columns > 7: - frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'), - title=(_("Wrong Template"))) + frappe.throw( + _("More columns found than expected. Please compare the uploaded file with standard template"), + title=(_("Wrong Template")), + ) + @frappe.whitelist() def validate_company(company): - parent_company, allow_account_creation_against_child_company = frappe.db.get_value('Company', - {'name': company}, ['parent_company', - 'allow_account_creation_against_child_company']) + parent_company, allow_account_creation_against_child_company = frappe.db.get_value( + "Company", {"name": company}, ["parent_company", "allow_account_creation_against_child_company"] + ) if parent_company and (not allow_account_creation_against_child_company): msg = _("{} is a child company.").format(frappe.bold(company)) + " " msg += _("Please import accounts against parent company or enable {} in company master.").format( - frappe.bold('Allow Account Creation Against Child Company')) - frappe.throw(msg, title=_('Wrong Company')) + frappe.bold("Allow Account Creation Against Child Company") + ) + frappe.throw(msg, title=_("Wrong Company")) - if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1): + if frappe.db.get_all("GL Entry", {"company": company}, "name", limit=1): return False + @frappe.whitelist() def import_coa(file_name, company): # delete existing data for accounts @@ -60,7 +68,7 @@ def import_coa(file_name, company): # create accounts file_doc, extension = get_file(file_name) - if extension == 'csv': + if extension == "csv": data = generate_data_from_csv(file_doc) else: data = generate_data_from_excel(file_doc, extension) @@ -72,27 +80,33 @@ def import_coa(file_name, company): # trigger on_update for company to reset default accounts set_default_accounts(company) + def get_file(file_name): file_doc = frappe.get_doc("File", {"file_url": file_name}) parts = file_doc.get_extension() extension = parts[1] extension = extension.lstrip(".") - if extension not in ('csv', 'xlsx', 'xls'): - frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")) + if extension not in ("csv", "xlsx", "xls"): + frappe.throw( + _( + "Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload" + ) + ) + + return file_doc, extension - return file_doc, extension def generate_data_from_csv(file_doc, as_dict=False): - ''' read csv file and return the generated nested tree ''' + """read csv file and return the generated nested tree""" file_path = file_doc.get_full_path() data = [] - with open(file_path, 'r') as in_file: + with open(file_path, "r") as in_file: csv_reader = list(csv.reader(in_file)) headers = csv_reader[0] - del csv_reader[0] # delete top row and headers row + del csv_reader[0] # delete top row and headers row for row in csv_reader: if as_dict: @@ -106,6 +120,7 @@ def generate_data_from_csv(file_doc, as_dict=False): # convert csv data return data + def generate_data_from_excel(file_doc, extension, as_dict=False): content = file_doc.get_content() @@ -123,20 +138,21 @@ def generate_data_from_excel(file_doc, extension, as_dict=False): data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) else: if not row[1]: - row[1] = row[0] - row[3] = row[2] + row[1] = row[0] + row[3] = row[2] data.append(row) return data + @frappe.whitelist() def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0): - ''' called by tree view (to fetch node's children) ''' + """called by tree view (to fetch node's children)""" file_doc, extension = get_file(file_name) - parent = None if parent==_('All Accounts') else parent + parent = None if parent == _("All Accounts") else parent - if extension == 'csv': + if extension == "csv": data = generate_data_from_csv(file_doc) else: data = generate_data_from_excel(file_doc, extension) @@ -146,32 +162,33 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0): if not for_validate: forest = build_forest(data) - accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form + accounts = build_tree_from_json( + "", chart_data=forest, from_coa_importer=True + ) # returns a list of dict in a tree render-able form # filter out to show data for the selected node only - accounts = [d for d in accounts if d['parent_account']==parent] + accounts = [d for d in accounts if d["parent_account"] == parent] return accounts else: - return { - 'show_import_button': 1 - } + return {"show_import_button": 1} + def build_forest(data): - ''' - converts list of list into a nested tree - if a = [[1,1], [1,2], [3,2], [4,4], [5,4]] - tree = { - 1: { - 2: { - 3: {} - } - }, - 4: { - 5: {} - } - } - ''' + """ + converts list of list into a nested tree + if a = [[1,1], [1,2], [3,2], [4,4], [5,4]] + tree = { + 1: { + 2: { + 3: {} + } + }, + 4: { + 5: {} + } + } + """ # set the value of nested dictionary def set_nested(d, path, value): @@ -195,8 +212,11 @@ def build_forest(data): elif account_name == child: parent_account_list = return_parent(data, parent_account) if not parent_account_list and parent_account: - frappe.throw(_("The parent account {0} does not exists in the uploaded template").format( - frappe.bold(parent_account))) + frappe.throw( + _("The parent account {0} does not exists in the uploaded template").format( + frappe.bold(parent_account) + ) + ) return [child] + parent_account_list charts_map, paths = {}, [] @@ -205,7 +225,15 @@ def build_forest(data): error_messages = [] for i in data: - account_name, parent_account, account_number, parent_account_number, is_group, account_type, root_type = i + ( + account_name, + parent_account, + account_number, + parent_account_number, + is_group, + account_type, + root_type, + ) = i if not account_name: error_messages.append("Row {0}: Please enter Account Name".format(line_no)) @@ -216,13 +244,17 @@ def build_forest(data): account_name = "{} - {}".format(account_number, account_name) charts_map[account_name] = {} - charts_map[account_name]['account_name'] = name - if account_number: charts_map[account_name]["account_number"] = account_number - if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group - if account_type: charts_map[account_name]["account_type"] = account_type - if root_type: charts_map[account_name]["root_type"] = root_type + charts_map[account_name]["account_name"] = name + if account_number: + charts_map[account_name]["account_number"] = account_number + if cint(is_group) == 1: + charts_map[account_name]["is_group"] = is_group + if account_type: + charts_map[account_name]["account_type"] = account_type + if root_type: + charts_map[account_name]["root_type"] = root_type path = return_parent(data, account_name)[::-1] - paths.append(path) # List of path is created + paths.append(path) # List of path is created line_no += 1 if error_messages: @@ -231,27 +263,32 @@ def build_forest(data): out = {} for path in paths: for n, account_name in enumerate(path): - set_nested(out, path[:n+1], charts_map[account_name]) # setting the value of nested dictionary. + set_nested( + out, path[: n + 1], charts_map[account_name] + ) # setting the value of nested dictionary. return out + def build_response_as_excel(writer): filename = frappe.generate_hash("", 10) - with open(filename, 'wb') as f: - f.write(cstr(writer.getvalue()).encode('utf-8')) + with open(filename, "wb") as f: + f.write(cstr(writer.getvalue()).encode("utf-8")) f = open(filename) reader = csv.reader(f) from frappe.utils.xlsxutils import make_xlsx + xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template") f.close() os.remove(filename) # write out response as a xlsx type - frappe.response['filename'] = 'coa_importer_template.xlsx' - frappe.response['filecontent'] = xlsx_file.getvalue() - frappe.response['type'] = 'binary' + frappe.response["filename"] = "coa_importer_template.xlsx" + frappe.response["filecontent"] = xlsx_file.getvalue() + frappe.response["type"] = "binary" + @frappe.whitelist() def download_template(file_type, template_type): @@ -259,34 +296,46 @@ def download_template(file_type, template_type): writer = get_template(template_type) - if file_type == 'CSV': + if file_type == "CSV": # download csv file - frappe.response['result'] = cstr(writer.getvalue()) - frappe.response['type'] = 'csv' - frappe.response['doctype'] = 'Chart of Accounts Importer' + frappe.response["result"] = cstr(writer.getvalue()) + frappe.response["type"] = "csv" + frappe.response["doctype"] = "Chart of Accounts Importer" else: build_response_as_excel(writer) + def get_template(template_type): - fields = ["Account Name", "Parent Account", "Account Number", "Parent Account Number", "Is Group", "Account Type", "Root Type"] + fields = [ + "Account Name", + "Parent Account", + "Account Number", + "Parent Account Number", + "Is Group", + "Account Type", + "Root Type", + ] writer = UnicodeWriter() writer.writerow(fields) - if template_type == 'Blank Template': - for root_type in get_root_types(): - writer.writerow(['', '', '', 1, '', root_type]) + if template_type == "Blank Template": + for root_type in get_root_types(): + writer.writerow(["", "", "", 1, "", root_type]) for account in get_mandatory_group_accounts(): - writer.writerow(['', '', '', 1, account, "Asset"]) + writer.writerow(["", "", "", 1, account, "Asset"]) for account_type in get_mandatory_account_types(): - writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')]) + writer.writerow( + ["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] + ) else: writer = get_sample_template(writer) return writer + def get_sample_template(writer): template = [ ["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"], @@ -316,7 +365,7 @@ def get_sample_template(writer): @frappe.whitelist() def validate_accounts(file_doc, extension): - if extension == 'csv': + if extension == "csv": accounts = generate_data_from_csv(file_doc, as_dict=True) else: accounts = generate_data_from_excel(file_doc, extension, as_dict=True) @@ -325,7 +374,9 @@ def validate_accounts(file_doc, extension): for account in accounts: accounts_dict.setdefault(account["account_name"], 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 = _( + "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.") frappe.throw(msg, title=_("Parent Account Missing")) @@ -336,77 +387,106 @@ def validate_accounts(file_doc, extension): return [True, len(accounts)] + def validate_root(accounts): - roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] + roots = [accounts[d] for d in accounts if not accounts[d].get("parent_account")] error_messages = [] for account in roots: if not account.get("root_type") and account.get("account_name"): - error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name"))) + error_messages.append( + _("Please enter Root Type for account- {0}").format(account.get("account_name")) + ) elif account.get("root_type") not in get_root_types() and account.get("account_name"): - error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name"))) + error_messages.append( + _("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format( + account.get("account_name") + ) + ) validate_missing_roots(roots) if error_messages: frappe.throw("
".join(error_messages)) + def validate_missing_roots(roots): - root_types_added = set(d.get('root_type') for d in roots) + root_types_added = set(d.get("root_type") for d in roots) missing = list(set(get_root_types()) - root_types_added) if missing: - frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing))) + frappe.throw(_("Please add Root Account for - {0}").format(" , ".join(missing))) + def get_root_types(): - return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') + return ("Asset", "Liability", "Expense", "Income", "Equity") + def get_report_type(root_type): - if root_type in ('Asset', 'Liability', 'Equity'): - return 'Balance Sheet' + if root_type in ("Asset", "Liability", "Equity"): + return "Balance Sheet" else: - return 'Profit and Loss' + return "Profit and Loss" + def get_mandatory_group_accounts(): - return ('Bank', 'Cash', 'Stock') + return ("Bank", "Cash", "Stock") + def get_mandatory_account_types(): return [ - {'account_type': 'Cost of Goods Sold', 'root_type': 'Expense'}, - {'account_type': 'Depreciation', 'root_type': 'Expense'}, - {'account_type': 'Fixed Asset', 'root_type': 'Asset'}, - {'account_type': 'Payable', 'root_type': 'Liability'}, - {'account_type': 'Receivable', 'root_type': 'Asset'}, - {'account_type': 'Stock Adjustment', 'root_type': 'Expense'}, - {'account_type': 'Bank', 'root_type': 'Asset'}, - {'account_type': 'Cash', 'root_type': 'Asset'}, - {'account_type': 'Stock', 'root_type': 'Asset'} + {"account_type": "Cost of Goods Sold", "root_type": "Expense"}, + {"account_type": "Depreciation", "root_type": "Expense"}, + {"account_type": "Fixed Asset", "root_type": "Asset"}, + {"account_type": "Payable", "root_type": "Liability"}, + {"account_type": "Receivable", "root_type": "Asset"}, + {"account_type": "Stock Adjustment", "root_type": "Expense"}, + {"account_type": "Bank", "root_type": "Asset"}, + {"account_type": "Cash", "root_type": "Asset"}, + {"account_type": "Stock", "root_type": "Asset"}, ] + def unset_existing_data(company): - linked = frappe.db.sql('''select fieldname from tabDocField - where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True) + linked = frappe.db.sql( + '''select fieldname from tabDocField + where fieldtype="Link" and options="Account" and parent="Company"''', + as_dict=True, + ) # remove accounts data from company - update_values = {d.fieldname: '' for d in linked} - frappe.db.set_value('Company', company, update_values, update_values) + update_values = {d.fieldname: "" for d in linked} + frappe.db.set_value("Company", company, update_values, update_values) # remove accounts data from various doctypes - for doctype in ["Account", "Party Account", "Mode of Payment Account", "Tax Withholding Account", - "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: - frappe.db.sql('''delete from `tab{0}` where `company`="%s"''' # nosec - .format(doctype) % (company)) + for doctype in [ + "Account", + "Party Account", + "Mode of Payment Account", + "Tax Withholding Account", + "Sales Taxes and Charges Template", + "Purchase Taxes and Charges Template", + ]: + frappe.db.sql( + '''delete from `tab{0}` where `company`="%s"'''.format(doctype) % (company) # nosec + ) + def set_default_accounts(company): from erpnext.setup.doctype.company.company import install_country_fixtures - company = frappe.get_doc('Company', company) - company.update({ - "default_receivable_account": frappe.db.get_value("Account", - {"company": company.name, "account_type": "Receivable", "is_group": 0}), - "default_payable_account": frappe.db.get_value("Account", - {"company": company.name, "account_type": "Payable", "is_group": 0}) - }) + + company = frappe.get_doc("Company", company) + company.update( + { + "default_receivable_account": frappe.db.get_value( + "Account", {"company": company.name, "account_type": "Receivable", "is_group": 0} + ), + "default_payable_account": frappe.db.get_value( + "Account", {"company": company.name, "account_type": "Payable", "is_group": 0} + ), + } + ) company.save() install_country_fixtures(company.name, company.country) diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py index 20cb42c109c..f8ac66444b4 100644 --- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py +++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py @@ -10,17 +10,20 @@ from frappe.model.document import Document class ChequePrintTemplate(Document): pass + @frappe.whitelist() def create_or_update_cheque_print_format(template_name): if not frappe.db.exists("Print Format", template_name): cheque_print = frappe.new_doc("Print Format") - cheque_print.update({ - "doc_type": "Payment Entry", - "standard": "No", - "custom_format": 1, - "print_format_type": "Jinja", - "name": template_name - }) + cheque_print.update( + { + "doc_type": "Payment Entry", + "standard": "No", + "custom_format": 1, + "print_format_type": "Jinja", + "name": template_name, + } + ) else: cheque_print = frappe.get_doc("Print Format", template_name) @@ -69,10 +72,12 @@ def create_or_update_cheque_print_format(template_name): {{doc.company}} -"""%{ - "starting_position_from_top_edge": doc.starting_position_from_top_edge \ - if doc.cheque_size == "A4" else 0.0, - "cheque_width": doc.cheque_width, "cheque_height": doc.cheque_height, +""" % { + "starting_position_from_top_edge": doc.starting_position_from_top_edge + if doc.cheque_size == "A4" + else 0.0, + "cheque_width": doc.cheque_width, + "cheque_height": doc.cheque_height, "acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge, "acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge, "message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"), @@ -89,7 +94,7 @@ def create_or_update_cheque_print_format(template_name): "amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge, "amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge, "signatory_from_top_edge": doc.signatory_from_top_edge, - "signatory_from_left_edge": doc.signatory_from_left_edge + "signatory_from_left_edge": doc.signatory_from_left_edge, } cheque_print.save(ignore_permissions=True) diff --git a/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py b/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py index 2b323a9bf62..9b003ceaa3e 100644 --- a/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py +++ b/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Cheque Print Template') + class TestChequePrintTemplate(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 7ae0a72e3d1..2d18a2409a2 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -11,11 +11,14 @@ from erpnext.accounts.utils import validate_field_number class CostCenter(NestedSet): - nsm_parent_field = 'parent_cost_center' + nsm_parent_field = "parent_cost_center" def autoname(self): from erpnext.accounts.utils import get_autoname_with_number - self.name = get_autoname_with_number(self.cost_center_number, self.cost_center_name, None, self.company) + + self.name = get_autoname_with_number( + self.cost_center_number, self.cost_center_name, None, self.company + ) def validate(self): self.validate_mandatory() @@ -27,15 +30,31 @@ class CostCenter(NestedSet): if not self.distributed_cost_center: frappe.throw(_("Please enter distributed cost center")) if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100: - frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100")) - if not self.get('__islocal'): - if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \ - and self.check_if_part_of_distributed_cost_center(): - frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center")) + frappe.throw( + _("Total percentage allocation for distributed cost center should be equal to 100") + ) + if not self.get("__islocal"): + if ( + not cint( + frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center") + ) + and self.check_if_part_of_distributed_cost_center() + ): + frappe.throw( + _( + "Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center" + ) + ) if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False): frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center")) - if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)): - frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table.")) + if check_if_distributed_cost_center_enabled( + list(x.cost_center for x in self.distributed_cost_center) + ): + frappe.throw( + _( + "A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table." + ) + ) else: self.distributed_cost_center = [] @@ -47,9 +66,12 @@ class CostCenter(NestedSet): def validate_parent_cost_center(self): if self.parent_cost_center: - if not frappe.db.get_value('Cost Center', self.parent_cost_center, 'is_group'): - frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format( - frappe.bold(self.parent_cost_center))) + if not frappe.db.get_value("Cost Center", self.parent_cost_center, "is_group"): + frappe.throw( + _("{0} is not a group node. Please select a group node as parent cost center").format( + frappe.bold(self.parent_cost_center) + ) + ) @frappe.whitelist() def convert_group_to_ledger(self): @@ -65,9 +87,13 @@ class CostCenter(NestedSet): @frappe.whitelist() def convert_ledger_to_group(self): if cint(self.enable_distributed_cost_center): - frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group")) + frappe.throw( + _("Cost Center with enabled distributed cost center can not be converted to group") + ) if self.check_if_part_of_distributed_cost_center(): - frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group")) + frappe.throw( + _("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group") + ) if self.check_gle_exists(): frappe.throw(_("Cost Center with existing transactions can not be converted to group")) self.is_group = 1 @@ -78,8 +104,11 @@ class CostCenter(NestedSet): return frappe.db.get_value("GL Entry", {"cost_center": self.name}) def check_if_child_exists(self): - return frappe.db.sql("select name from `tabCost Center` where \ - parent_cost_center = %s and docstatus != 2", self.name) + return frappe.db.sql( + "select name from `tabCost Center` where \ + parent_cost_center = %s and docstatus != 2", + self.name, + ) def check_if_part_of_distributed_cost_center(self): return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name}) @@ -87,6 +116,7 @@ class CostCenter(NestedSet): def before_rename(self, olddn, newdn, merge=False): # Add company abbr if not provided from erpnext.setup.doctype.company.company import get_name_with_abbr + new_cost_center = get_name_with_abbr(newdn, self.company) # Validate properties before merging @@ -100,7 +130,9 @@ class CostCenter(NestedSet): super(CostCenter, self).after_rename(olddn, newdn, merge) if not merge: - new_cost_center = frappe.db.get_value("Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1) + new_cost_center = frappe.db.get_value( + "Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1 + ) # exclude company abbr new_parts = newdn.split(" - ")[:-1] @@ -109,7 +141,9 @@ class CostCenter(NestedSet): if len(new_parts) == 1: new_parts = newdn.split(" ") if new_cost_center.cost_center_number != new_parts[0]: - validate_field_number("Cost Center", self.name, new_parts[0], self.company, "cost_center_number") + validate_field_number( + "Cost Center", self.name, new_parts[0], self.company, "cost_center_number" + ) self.cost_center_number = new_parts[0] self.db_set("cost_center_number", new_parts[0]) new_parts = new_parts[1:] @@ -120,14 +154,19 @@ class CostCenter(NestedSet): self.cost_center_name = cost_center_name self.db_set("cost_center_name", cost_center_name) + def on_doctype_update(): frappe.db.add_index("Cost Center", ["lft", "rgt"]) + def get_name_with_number(new_account, account_number): if account_number and not new_account[0].isdigit(): new_account = account_number + " - " + new_account return new_account + def check_if_distributed_cost_center_enabled(cost_center_list): - value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1) + value_list = frappe.get_list( + "Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1 + ) return next((True for x in value_list if x[0]), False) diff --git a/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py b/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py index 0bae8fe1456..5059dc3cc0e 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py +++ b/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'cost_center', - 'reports': [ - { - 'label': _('Reports'), - 'items': ['Budget Variance Report', 'General Ledger'] - } - ] + "fieldname": "cost_center", + "reports": [{"label": _("Reports"), "items": ["Budget Variance Report", "General Ledger"]}], } diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py index f8615ec03a5..a5a2d15ca93 100644 --- a/erpnext/accounts/doctype/cost_center/test_cost_center.py +++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py @@ -5,51 +5,53 @@ import unittest import frappe -test_records = frappe.get_test_records('Cost Center') +test_records = frappe.get_test_records("Cost Center") + class TestCostCenter(unittest.TestCase): def test_cost_center_creation_against_child_node(self): - if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}): + if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}): frappe.get_doc(test_records[1]).insert() - cost_center = frappe.get_doc({ - 'doctype': 'Cost Center', - 'cost_center_name': '_Test Cost Center 3', - 'parent_cost_center': '_Test Cost Center 2 - _TC', - 'is_group': 0, - 'company': '_Test Company' - }) + cost_center = frappe.get_doc( + { + "doctype": "Cost Center", + "cost_center_name": "_Test Cost Center 3", + "parent_cost_center": "_Test Cost Center 2 - _TC", + "is_group": 0, + "company": "_Test Company", + } + ) self.assertRaises(frappe.ValidationError, cost_center.save) def test_validate_distributed_cost_center(self): - if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}): + if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center - _TC"}): frappe.get_doc(test_records[0]).insert() - if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}): + if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}): frappe.get_doc(test_records[1]).insert() - invalid_distributed_cost_center = frappe.get_doc({ - "company": "_Test Company", - "cost_center_name": "_Test Distributed Cost Center", - "doctype": "Cost Center", - "is_group": 0, - "parent_cost_center": "_Test Company - _TC", - "enable_distributed_cost_center": 1, - "distributed_cost_center": [{ - "cost_center": "_Test Cost Center - _TC", - "percentage_allocation": 40 - }, { - "cost_center": "_Test Cost Center 2 - _TC", - "percentage_allocation": 50 - } - ] - }) + invalid_distributed_cost_center = frappe.get_doc( + { + "company": "_Test Company", + "cost_center_name": "_Test Distributed Cost Center", + "doctype": "Cost Center", + "is_group": 0, + "parent_cost_center": "_Test Company - _TC", + "enable_distributed_cost_center": 1, + "distributed_cost_center": [ + {"cost_center": "_Test Cost Center - _TC", "percentage_allocation": 40}, + {"cost_center": "_Test Cost Center 2 - _TC", "percentage_allocation": 50}, + ], + } + ) self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save) + def create_cost_center(**args): args = frappe._dict(args) if args.cost_center_name: diff --git a/erpnext/accounts/doctype/coupon_code/coupon_code.py b/erpnext/accounts/doctype/coupon_code/coupon_code.py index ee32de1cd28..6a0cdf91c0a 100644 --- a/erpnext/accounts/doctype/coupon_code/coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/coupon_code.py @@ -15,7 +15,7 @@ class CouponCode(Document): if not self.coupon_code: if self.coupon_type == "Promotional": - self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper() + self.coupon_code = "".join(i for i in self.coupon_name if not i.isdigit())[0:8].upper() elif self.coupon_type == "Gift Card": self.coupon_code = frappe.generate_hash()[:10].upper() diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py index ca482c8c4ec..b897546b036 100644 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py @@ -7,92 +7,110 @@ import frappe from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -test_dependencies = ['Item'] +test_dependencies = ["Item"] + def test_create_test_data(): frappe.set_user("Administrator") # create test item - if not frappe.db.exists("Item","_Test Tesla Car"): - item = frappe.get_doc({ - "description": "_Test Tesla Car", - "doctype": "Item", - "has_batch_no": 0, - "has_serial_no": 0, - "inspection_required": 0, - "is_stock_item": 1, - "opening_stock":100, - "is_sub_contracted_item": 0, - "item_code": "_Test Tesla Car", - "item_group": "_Test Item Group", - "item_name": "_Test Tesla Car", - "apply_warehouse_wise_reorder_level": 0, - "warehouse":"Stores - _TC", - "gst_hsn_code": "999800", - "valuation_rate": 5000, - "standard_rate":5000, - "item_defaults": [{ - "company": "_Test Company", - "default_warehouse": "Stores - _TC", - "default_price_list":"_Test Price List", - "expense_account": "Cost of Goods Sold - _TC", - "buying_cost_center": "Main - _TC", - "selling_cost_center": "Main - _TC", - "income_account": "Sales - _TC" - }], - }) + if not frappe.db.exists("Item", "_Test Tesla Car"): + item = frappe.get_doc( + { + "description": "_Test Tesla Car", + "doctype": "Item", + "has_batch_no": 0, + "has_serial_no": 0, + "inspection_required": 0, + "is_stock_item": 1, + "opening_stock": 100, + "is_sub_contracted_item": 0, + "item_code": "_Test Tesla Car", + "item_group": "_Test Item Group", + "item_name": "_Test Tesla Car", + "apply_warehouse_wise_reorder_level": 0, + "warehouse": "Stores - _TC", + "gst_hsn_code": "999800", + "valuation_rate": 5000, + "standard_rate": 5000, + "item_defaults": [ + { + "company": "_Test Company", + "default_warehouse": "Stores - _TC", + "default_price_list": "_Test Price List", + "expense_account": "Cost of Goods Sold - _TC", + "buying_cost_center": "Main - _TC", + "selling_cost_center": "Main - _TC", + "income_account": "Sales - _TC", + } + ], + } + ) item.insert() # create test item price - item_price = frappe.get_list('Item Price', filters={'item_code': '_Test Tesla Car', 'price_list': '_Test Price List'}, fields=['name']) - if len(item_price)==0: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "item_code": "_Test Tesla Car", - "price_list": "_Test Price List", - "price_list_rate": 5000 - }) + item_price = frappe.get_list( + "Item Price", + filters={"item_code": "_Test Tesla Car", "price_list": "_Test Price List"}, + fields=["name"], + ) + if len(item_price) == 0: + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "item_code": "_Test Tesla Car", + "price_list": "_Test Price List", + "price_list_rate": 5000, + } + ) item_price.insert() # create test item pricing rule if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}): - item_pricing_rule = frappe.get_doc({ - "doctype": "Pricing Rule", - "title": "_Test Pricing Rule for _Test Item", - "apply_on": "Item Code", - "items": [{ - "item_code": "_Test Tesla Car" - }], - "warehouse":"Stores - _TC", - "coupon_code_based":1, - "selling": 1, - "rate_or_discount": "Discount Percentage", - "discount_percentage": 30, - "company": "_Test Company", - "currency":"INR", - "for_price_list":"_Test Price List" - }) + item_pricing_rule = frappe.get_doc( + { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule for _Test Item", + "apply_on": "Item Code", + "items": [{"item_code": "_Test Tesla Car"}], + "warehouse": "Stores - _TC", + "coupon_code_based": 1, + "selling": 1, + "rate_or_discount": "Discount Percentage", + "discount_percentage": 30, + "company": "_Test Company", + "currency": "INR", + "for_price_list": "_Test Price List", + } + ) item_pricing_rule.insert() # create test item sales partner - if not frappe.db.exists("Sales Partner","_Test Coupon Partner"): - sales_partner = frappe.get_doc({ - "doctype": "Sales Partner", - "partner_name":"_Test Coupon Partner", - "commission_rate":2, - "referral_code": "COPART" - }) + if not frappe.db.exists("Sales Partner", "_Test Coupon Partner"): + sales_partner = frappe.get_doc( + { + "doctype": "Sales Partner", + "partner_name": "_Test Coupon Partner", + "commission_rate": 2, + "referral_code": "COPART", + } + ) sales_partner.insert() # create test item coupon code if not frappe.db.exists("Coupon Code", "SAVE30"): - pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name']) - coupon_code = frappe.get_doc({ - "doctype": "Coupon Code", - "coupon_name":"SAVE30", - "coupon_code":"SAVE30", - "pricing_rule": pricing_rule, - "valid_from": "2014-01-01", - "maximum_use":1, - "used":0 - }) + pricing_rule = frappe.db.get_value( + "Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ["name"] + ) + coupon_code = frappe.get_doc( + { + "doctype": "Coupon Code", + "coupon_name": "SAVE30", + "coupon_code": "SAVE30", + "pricing_rule": pricing_rule, + "valid_from": "2014-01-01", + "maximum_use": 1, + "used": 0, + } + ) coupon_code.insert() + class TestCouponCode(unittest.TestCase): def setUp(self): test_create_test_data() @@ -103,15 +121,21 @@ class TestCouponCode(unittest.TestCase): def test_sales_order_with_coupon_code(self): frappe.db.set_value("Coupon Code", "SAVE30", "used", 0) - so = make_sales_order(company='_Test Company', warehouse='Stores - _TC', - customer="_Test Customer", selling_price_list="_Test Price List", - item_code="_Test Tesla Car", rate=5000, qty=1, - do_not_submit=True) + so = make_sales_order( + company="_Test Company", + warehouse="Stores - _TC", + customer="_Test Customer", + selling_price_list="_Test Price List", + item_code="_Test Tesla Car", + rate=5000, + qty=1, + do_not_submit=True, + ) self.assertEqual(so.items[0].rate, 5000) - so.coupon_code='SAVE30' - so.sales_partner='_Test Coupon Partner' + so.coupon_code = "SAVE30" + so.sales_partner = "_Test Coupon Partner" so.save() # check item price after coupon code is applied diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 02377cd5659..b675bde58af 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -20,78 +20,99 @@ class Dunning(AccountsController): self.validate_overdue_days() self.validate_amount() if not self.income_account: - self.income_account = frappe.db.get_value('Company', self.company, 'default_income_account') + self.income_account = frappe.db.get_value("Company", self.company, "default_income_account") def validate_overdue_days(self): self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 def validate_amount(self): amounts = calculate_interest_and_amount( - self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) - if self.interest_amount != amounts.get('interest_amount'): - self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) - if self.dunning_amount != amounts.get('dunning_amount'): - self.dunning_amount = flt(amounts.get('dunning_amount'), self.precision('dunning_amount')) - if self.grand_total != amounts.get('grand_total'): - self.grand_total = flt(amounts.get('grand_total'), self.precision('grand_total')) + self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days + ) + if self.interest_amount != amounts.get("interest_amount"): + self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount")) + if self.dunning_amount != amounts.get("dunning_amount"): + self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount")) + if self.grand_total != amounts.get("grand_total"): + self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total")) def on_submit(self): self.make_gl_entries() def on_cancel(self): if self.dunning_amount: - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def make_gl_entries(self): if not self.dunning_amount: return gl_entries = [] - invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"] + invoice_fields = [ + "project", + "cost_center", + "debit_to", + "party_account_currency", + "conversion_rate", + "cost_center", + ] inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1) accounting_dimensions = get_accounting_dimensions() invoice_fields.extend(accounting_dimensions) dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate) - default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') + default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center") gl_entries.append( - self.get_gl_dict({ - "account": inv.debit_to, - "party_type": "Customer", - "party": self.customer, - "due_date": self.due_date, - "against": self.income_account, - "debit": dunning_in_company_currency, - "debit_in_account_currency": self.dunning_amount, - "against_voucher": self.name, - "against_voucher_type": "Dunning", - "cost_center": inv.cost_center or default_cost_center, - "project": inv.project - }, inv.party_account_currency, item=inv) + self.get_gl_dict( + { + "account": inv.debit_to, + "party_type": "Customer", + "party": self.customer, + "due_date": self.due_date, + "against": self.income_account, + "debit": dunning_in_company_currency, + "debit_in_account_currency": self.dunning_amount, + "against_voucher": self.name, + "against_voucher_type": "Dunning", + "cost_center": inv.cost_center or default_cost_center, + "project": inv.project, + }, + inv.party_account_currency, + item=inv, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.income_account, - "against": self.customer, - "credit": dunning_in_company_currency, - "cost_center": inv.cost_center or default_cost_center, - "credit_in_account_currency": self.dunning_amount, - "project": inv.project - }, item=inv) + self.get_gl_dict( + { + "account": self.income_account, + "against": self.customer, + "credit": dunning_in_company_currency, + "cost_center": inv.cost_center or default_cost_center, + "credit_in_account_currency": self.dunning_amount, + "project": inv.project, + }, + item=inv, + ) + ) + make_gl_entries( + gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False ) - make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False) def resolve_dunning(doc, state): for reference in doc.references: - if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: - dunnings = frappe.get_list('Dunning', filters={ - 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True) + if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: + dunnings = frappe.get_list( + "Dunning", + filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")}, + ignore_permissions=True, + ) for dunning in dunnings: - frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') + frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") + def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): interest_amount = 0 @@ -102,23 +123,26 @@ def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_ grand_total += flt(interest_amount) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { - 'interest_amount': interest_amount, - 'grand_total': grand_total, - 'dunning_amount': dunning_amount} + "interest_amount": interest_amount, + "grand_total": grand_total, + "dunning_amount": dunning_amount, + } + @frappe.whitelist() def get_dunning_letter_text(dunning_type, doc, language=None): if isinstance(doc, string_types): doc = json.loads(doc) if language: - filters = {'parent': dunning_type, 'language': language} + filters = {"parent": dunning_type, "language": language} else: - filters = {'parent': dunning_type, 'is_default_language': 1} - letter_text = frappe.db.get_value('Dunning Letter Text', filters, - ['body_text', 'closing_text', 'language'], as_dict=1) + filters = {"parent": dunning_type, "is_default_language": 1} + letter_text = frappe.db.get_value( + "Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1 + ) if letter_text: return { - 'body_text': frappe.render_template(letter_text.body_text, doc), - 'closing_text': frappe.render_template(letter_text.closing_text, doc), - 'language': letter_text.language + "body_text": frappe.render_template(letter_text.body_text, doc), + "closing_text": frappe.render_template(letter_text.closing_text, doc), + "language": letter_text.language, } diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py index ebe4efb0985..d1d40314104 100644 --- a/erpnext/accounts/doctype/dunning/dunning_dashboard.py +++ b/erpnext/accounts/doctype/dunning/dunning_dashboard.py @@ -1,18 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'dunning', - 'non_standard_fieldnames': { - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name' + "fieldname": "dunning", + "non_standard_fieldnames": { + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Journal Entry'] - } - ] + "transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}], } diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 27543a89fc0..e1fd1e984f5 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -30,31 +30,35 @@ class TestDunning(unittest.TestCase): def test_dunning(self): dunning = create_dunning() amounts = calculate_interest_and_amount( - dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) - 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) + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days + ) + 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_dunning_with_zero_interest_rate(self): dunning = create_dunning_with_zero_interest_rate() amounts = calculate_interest_and_amount( - dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) - self.assertEqual(round(amounts.get('interest_amount'), 2), 0) - self.assertEqual(round(amounts.get('dunning_amount'), 2), 20) - self.assertEqual(round(amounts.get('grand_total'), 2), 120) - + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days + ) + self.assertEqual(round(amounts.get("interest_amount"), 2), 0) + self.assertEqual(round(amounts.get("dunning_amount"), 2), 20) + self.assertEqual(round(amounts.get("grand_total"), 2), 120) def test_gl_entries(self): dunning = create_dunning() dunning.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s - order by account asc""", dunning.name, as_dict=1) + order by account asc""", + dunning.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - ['Debtors - _TC', 20.44, 0.0], - ['Sales - _TC', 0.0, 20.44] - ]) + expected_values = dict( + (d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]] + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) @@ -72,7 +76,7 @@ class TestDunning(unittest.TestCase): pe.target_exchange_rate = 1 pe.insert() pe.submit() - si_doc = frappe.get_doc('Sales Invoice', dunning.sales_invoice) + si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice) self.assertEqual(si_doc.outstanding_amount, 0) @@ -80,8 +84,9 @@ def create_dunning(): posting_date = add_days(today(), -20) due_date = add_days(today(), -15) sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, status='Overdue') - dunning_type = frappe.get_doc("Dunning Type", 'First Notice') + posting_date=posting_date, due_date=due_date, status="Overdue" + ) + dunning_type = frappe.get_doc("Dunning Type", "First Notice") dunning = frappe.new_doc("Dunning") dunning.sales_invoice = sales_invoice.name dunning.customer_name = sales_invoice.customer_name @@ -91,18 +96,20 @@ def create_dunning(): dunning.company = sales_invoice.company dunning.posting_date = nowdate() dunning.due_date = sales_invoice.due_date - dunning.dunning_type = 'First Notice' + dunning.dunning_type = "First Notice" dunning.rate_of_interest = dunning_type.rate_of_interest dunning.dunning_fee = dunning_type.dunning_fee dunning.save() return dunning + def create_dunning_with_zero_interest_rate(): posting_date = add_days(today(), -20) due_date = add_days(today(), -15) sales_invoice = create_sales_invoice_against_cost_center( - posting_date=posting_date, due_date=due_date, status='Overdue') - dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest') + posting_date=posting_date, due_date=due_date, status="Overdue" + ) + dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest") dunning = frappe.new_doc("Dunning") dunning.sales_invoice = sales_invoice.name dunning.customer_name = sales_invoice.customer_name @@ -112,40 +119,44 @@ def create_dunning_with_zero_interest_rate(): dunning.company = sales_invoice.company dunning.posting_date = nowdate() dunning.due_date = sales_invoice.due_date - dunning.dunning_type = 'First Notice with 0% Rate of Interest' + dunning.dunning_type = "First Notice with 0% Rate of Interest" dunning.rate_of_interest = dunning_type.rate_of_interest dunning.dunning_fee = dunning_type.dunning_fee dunning.save() return dunning + def create_dunning_type(): dunning_type = frappe.new_doc("Dunning Type") - dunning_type.dunning_type = 'First Notice' + dunning_type.dunning_type = "First Notice" dunning_type.start_day = 10 dunning_type.end_day = 20 dunning_type.dunning_fee = 20 dunning_type.rate_of_interest = 8 dunning_type.append( - "dunning_letter_text", { - 'language': 'en', - 'body_text': 'We have still not received payment for our invoice ', - 'closing_text': 'We kindly request that you pay the outstanding amount immediately, including interest and late fees.' - } + "dunning_letter_text", + { + "language": "en", + "body_text": "We have still not received payment for our invoice ", + "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", + }, ) dunning_type.save() + def create_dunning_type_with_zero_interest_rate(): dunning_type = frappe.new_doc("Dunning Type") - dunning_type.dunning_type = 'First Notice with 0% Rate of Interest' + dunning_type.dunning_type = "First Notice with 0% Rate of Interest" dunning_type.start_day = 10 dunning_type.end_day = 20 dunning_type.dunning_fee = 20 dunning_type.rate_of_interest = 0 dunning_type.append( - "dunning_letter_text", { - 'language': 'en', - 'body_text': 'We have still not received payment for our invoice ', - 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.' - } + "dunning_letter_text", + { + "language": "en", + "body_text": "We have still not received payment for our invoice ", + "closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.", + }, ) dunning_type.save() diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 1b13195ce98..2f81c5fb750 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -20,8 +20,9 @@ class ExchangeRateRevaluation(Document): def set_total_gain_loss(self): total_gain_loss = 0 for d in self.accounts: - d.gain_loss = flt(d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")) \ - - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency")) + d.gain_loss = flt( + d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency") + ) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency")) total_gain_loss += flt(d.gain_loss, d.precision("gain_loss")) self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss")) @@ -30,15 +31,15 @@ class ExchangeRateRevaluation(Document): frappe.throw(_("Please select Company and Posting Date to getting entries")) def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry') + self.ignore_linked_doctypes = "GL Entry" @frappe.whitelist() def check_journal_entry_condition(self): - total_debit = frappe.db.get_value("Journal Entry Account", { - 'reference_type': 'Exchange Rate Revaluation', - 'reference_name': self.name, - 'docstatus': 1 - }, "sum(debit) as sum") + total_debit = frappe.db.get_value( + "Journal Entry Account", + {"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1}, + "sum(debit) as sum", + ) total_amt = 0 for d in self.accounts: @@ -54,28 +55,33 @@ class ExchangeRateRevaluation(Document): accounts = [] self.validate_mandatory() company_currency = erpnext.get_company_currency(self.company) - precision = get_field_precision(frappe.get_meta("Exchange Rate Revaluation Account") - .get_field("new_balance_in_base_currency"), company_currency) + precision = get_field_precision( + frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"), + company_currency, + ) account_details = self.get_accounts_from_gle() for d in account_details: - current_exchange_rate = d.balance / d.balance_in_account_currency \ - if d.balance_in_account_currency else 0 + current_exchange_rate = ( + d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0 + ) new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date) new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) if gain_loss: - accounts.append({ - "account": d.account, - "party_type": d.party_type, - "party": d.party, - "account_currency": d.account_currency, - "balance_in_base_currency": d.balance, - "balance_in_account_currency": d.balance_in_account_currency, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency - }) + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + } + ) if not accounts: self.throw_invalid_response_message(account_details) @@ -84,7 +90,8 @@ class ExchangeRateRevaluation(Document): def get_accounts_from_gle(self): company_currency = erpnext.get_company_currency(self.company) - accounts = frappe.db.sql_list(""" + accounts = frappe.db.sql_list( + """ select name from tabAccount where is_group = 0 @@ -93,11 +100,14 @@ class ExchangeRateRevaluation(Document): and account_type != 'Stock' and company=%s and account_currency != %s - order by name""",(self.company, company_currency)) + order by name""", + (self.company, company_currency), + ) account_details = [] if accounts: - account_details = frappe.db.sql(""" + account_details = frappe.db.sql( + """ select account, party_type, party, account_currency, sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency, @@ -109,7 +119,11 @@ class ExchangeRateRevaluation(Document): group by account, NULLIF(party_type,''), NULLIF(party,'') having sum(debit) != sum(credit) order by account - """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1) + """ + % (", ".join(["%s"] * len(accounts)), "%s"), + tuple(accounts + [self.posting_date]), + as_dict=1, + ) return account_details @@ -125,77 +139,107 @@ class ExchangeRateRevaluation(Document): if self.total_gain_loss == 0: return - unrealized_exchange_gain_loss_account = frappe.get_cached_value('Company', self.company, - "unrealized_exchange_gain_loss_account") + unrealized_exchange_gain_loss_account = frappe.get_cached_value( + "Company", self.company, "unrealized_exchange_gain_loss_account" + ) if not unrealized_exchange_gain_loss_account: - frappe.throw(_("Please set Unrealized Exchange Gain/Loss Account in Company {0}") - .format(self.company)) + frappe.throw( + _("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company) + ) - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Exchange Rate Revaluation' + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Rate Revaluation" journal_entry.company = self.company journal_entry.posting_date = self.posting_date journal_entry.multi_currency = 1 journal_entry_accounts = [] for d in self.accounts: - dr_or_cr = "debit_in_account_currency" \ - if d.get("balance_in_account_currency") > 0 else "credit_in_account_currency" + dr_or_cr = ( + "debit_in_account_currency" + if d.get("balance_in_account_currency") > 0 + else "credit_in_account_currency" + ) - reverse_dr_or_cr = "debit_in_account_currency" \ - if dr_or_cr=="credit_in_account_currency" else "credit_in_account_currency" + reverse_dr_or_cr = ( + "debit_in_account_currency" + if dr_or_cr == "credit_in_account_currency" + else "credit_in_account_currency" + ) - journal_entry_accounts.append({ - "account": d.get("account"), - "party_type": d.get("party_type"), - "party": d.get("party"), - "account_currency": d.get("account_currency"), - "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), - dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), - "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), + journal_entry_accounts.append( + { + "account": d.get("account"), + "party_type": d.get("party_type"), + "party": d.get("party"), + "account_currency": d.get("account_currency"), + "balance": flt( + d.get("balance_in_account_currency"), d.precision("balance_in_account_currency") + ), + dr_or_cr: flt( + abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") + ), + "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) + journal_entry_accounts.append( + { + "account": d.get("account"), + "party_type": d.get("party_type"), + "party": d.get("party"), + "account_currency": d.get("account_currency"), + "balance": flt( + d.get("balance_in_account_currency"), d.precision("balance_in_account_currency") + ), + reverse_dr_or_cr: flt( + abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") + ), + "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) + + journal_entry_accounts.append( + { + "account": unrealized_exchange_gain_loss_account, + "balance": get_balance_on(unrealized_exchange_gain_loss_account), + "debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0, + "credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0, + "exchange_rate": 1, "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, - }) - journal_entry_accounts.append({ - "account": d.get("account"), - "party_type": d.get("party_type"), - "party": d.get("party"), - "account_currency": d.get("account_currency"), - "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), - reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), - "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), - "reference_type": "Exchange Rate Revaluation", - "reference_name": self.name - }) - - journal_entry_accounts.append({ - "account": unrealized_exchange_gain_loss_account, - "balance": get_balance_on(unrealized_exchange_gain_loss_account), - "debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0, - "credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0, - "exchange_rate": 1, - "reference_type": "Exchange Rate Revaluation", - "reference_name": self.name, - }) + } + ) journal_entry.set("accounts", journal_entry_accounts) journal_entry.set_amounts_in_company_currency() journal_entry.set_total_debit_credit() return journal_entry.as_dict() + @frappe.whitelist() def get_account_details(account, company, posting_date, party_type=None, party=None): - account_currency, account_type = frappe.db.get_value("Account", account, - ["account_currency", "account_type"]) + account_currency, account_type = frappe.db.get_value( + "Account", account, ["account_currency", "account_type"] + ) if account_type in ["Receivable", "Payable"] and not (party_type and party): frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type)) account_details = {} company_currency = erpnext.get_company_currency(company) - balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False) + balance = get_balance_on( + account, date=posting_date, party_type=party_type, party=party, in_account_currency=False + ) if balance: - balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party) - current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0 + balance_in_account_currency = get_balance_on( + account, date=posting_date, party_type=party_type, party=party + ) + current_exchange_rate = ( + balance / balance_in_account_currency if balance_in_account_currency else 0 + ) new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate account_details = { @@ -204,7 +248,7 @@ def get_account_details(account, company, posting_date, party_type=None, party=N "balance_in_account_currency": balance_in_account_currency, "current_exchange_rate": current_exchange_rate, "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency + "new_balance_in_base_currency": new_balance_in_base_currency, } return account_details diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py index 0efe291d217..7eca9703c24 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'reference_name', - 'transactions': [ - { - 'items': ['Journal Entry'] - } - ] - } + return {"fieldname": "reference_name", "transactions": [{"items": ["Journal Entry"]}]} diff --git a/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py b/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py index 4a56cd39d00..24e6c0c8721 100644 --- a/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py +++ b/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py @@ -1,24 +1,13 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'finance_book', - 'non_standard_fieldnames': { - 'Asset': 'default_finance_book', - 'Company': 'default_finance_book' - }, - 'transactions': [ - { - 'label': _('Assets'), - 'items': ['Asset', 'Asset Value Adjustment'] - }, - { - 'items': ['Company'] - }, - { - 'items': ['Journal Entry'] - } - ] + "fieldname": "finance_book", + "non_standard_fieldnames": {"Asset": "default_finance_book", "Company": "default_finance_book"}, + "transactions": [ + {"label": _("Assets"), "items": ["Asset", "Asset Value Adjustment"]}, + {"items": ["Company"]}, + {"items": ["Journal Entry"]}, + ], } diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py index 8fbf72d7c1c..7b2575d2c32 100644 --- a/erpnext/accounts/doctype/finance_book/test_finance_book.py +++ b/erpnext/accounts/doctype/finance_book/test_finance_book.py @@ -13,31 +13,30 @@ class TestFinanceBook(unittest.TestCase): finance_book = create_finance_book() # create jv entry - jv = make_journal_entry("_Test Bank - _TC", - "_Test Receivable - _TC", 100, save=False) + jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False) - jv.accounts[1].update({ - "party_type": "Customer", - "party": "_Test Customer" - }) + jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer"}) jv.finance_book = finance_book.finance_book_name jv.submit() # check the Finance Book in the GL Entry - gl_entries = frappe.get_all("GL Entry", fields=["name", "finance_book"], - filters={"voucher_type": "Journal Entry", "voucher_no": jv.name}) + gl_entries = frappe.get_all( + "GL Entry", + fields=["name", "finance_book"], + filters={"voucher_type": "Journal Entry", "voucher_no": jv.name}, + ) for gl_entry in gl_entries: self.assertEqual(gl_entry.finance_book, finance_book.name) + def create_finance_book(): if not frappe.db.exists("Finance Book", "_Test Finance Book"): - finance_book = frappe.get_doc({ - "doctype": "Finance Book", - "finance_book_name": "_Test Finance Book" - }).insert() + finance_book = frappe.get_doc( + {"doctype": "Finance Book", "finance_book_name": "_Test Finance Book"} + ).insert() else: finance_book = frappe.get_doc("Finance Book", "_Test Finance Book") - return finance_book \ No newline at end of file + return finance_book diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index dd893f9fc80..069ab5ea843 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -9,7 +9,9 @@ from frappe.model.document import Document from frappe.utils import add_days, add_years, cstr, getdate -class FiscalYearIncorrectDate(frappe.ValidationError): pass +class FiscalYearIncorrectDate(frappe.ValidationError): + pass + class FiscalYear(Document): @frappe.whitelist() @@ -22,19 +24,33 @@ class FiscalYear(Document): # clear cache frappe.clear_cache() - msgprint(_("{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect.").format(self.name)) + msgprint( + _( + "{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect." + ).format(self.name) + ) def validate(self): self.validate_dates() self.validate_overlap() if not self.is_new(): - year_start_end_dates = frappe.db.sql("""select year_start_date, year_end_date - from `tabFiscal Year` where name=%s""", (self.name)) + year_start_end_dates = frappe.db.sql( + """select year_start_date, year_end_date + from `tabFiscal Year` where name=%s""", + (self.name), + ) if year_start_end_dates: - if getdate(self.year_start_date) != year_start_end_dates[0][0] or getdate(self.year_end_date) != year_start_end_dates[0][1]: - frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.")) + if ( + getdate(self.year_start_date) != year_start_end_dates[0][0] + or getdate(self.year_end_date) != year_start_end_dates[0][1] + ): + frappe.throw( + _( + "Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved." + ) + ) def validate_dates(self): if self.is_short_year: @@ -43,14 +59,18 @@ class FiscalYear(Document): return if getdate(self.year_start_date) > getdate(self.year_end_date): - frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"), - FiscalYearIncorrectDate) + frappe.throw( + _("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"), + FiscalYearIncorrectDate, + ) date = getdate(self.year_start_date) + relativedelta(years=1) - relativedelta(days=1) if getdate(self.year_end_date) != date: - frappe.throw(_("Fiscal Year End Date should be one year after Fiscal Year Start Date"), - FiscalYearIncorrectDate) + frappe.throw( + _("Fiscal Year End Date should be one year after Fiscal Year Start Date"), + FiscalYearIncorrectDate, + ) def on_update(self): check_duplicate_fiscal_year(self) @@ -59,11 +79,16 @@ class FiscalYear(Document): def on_trash(self): global_defaults = frappe.get_doc("Global Defaults") if global_defaults.current_fiscal_year == self.name: - frappe.throw(_("You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings").format(self.name)) + frappe.throw( + _( + "You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings" + ).format(self.name) + ) frappe.cache().delete_value("fiscal_years") def validate_overlap(self): - existing_fiscal_years = frappe.db.sql("""select name from `tabFiscal Year` + existing_fiscal_years = frappe.db.sql( + """select name from `tabFiscal Year` where ( (%(year_start_date)s between year_start_date and year_end_date) or (%(year_end_date)s between year_start_date and year_end_date) @@ -73,13 +98,18 @@ class FiscalYear(Document): { "year_start_date": self.year_start_date, "year_end_date": self.year_end_date, - "name": self.name or "No Name" - }, as_dict=True) + "name": self.name or "No Name", + }, + as_dict=True, + ) if existing_fiscal_years: for existing in existing_fiscal_years: - company_for_existing = frappe.db.sql_list("""select company from `tabFiscal Year Company` - where parent=%s""", existing.name) + company_for_existing = frappe.db.sql_list( + """select company from `tabFiscal Year Company` + where parent=%s""", + existing.name, + ) overlap = False if not self.get("companies") or not company_for_existing: @@ -90,20 +120,36 @@ class FiscalYear(Document): overlap = True if overlap: - frappe.throw(_("Year start date or end date is overlapping with {0}. To avoid please set company") - .format(existing.name), frappe.NameError) + frappe.throw( + _("Year start date or end date is overlapping with {0}. To avoid please set company").format( + existing.name + ), + frappe.NameError, + ) + @frappe.whitelist() def check_duplicate_fiscal_year(doc): - year_start_end_dates = frappe.db.sql("""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""", (doc.name)) + year_start_end_dates = frappe.db.sql( + """select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""", + (doc.name), + ) for fiscal_year, ysd, yed in year_start_end_dates: - if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (not frappe.flags.in_test): - frappe.throw(_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(fiscal_year)) + if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and ( + not frappe.flags.in_test + ): + frappe.throw( + _("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format( + fiscal_year + ) + ) @frappe.whitelist() def auto_create_fiscal_year(): - for d in frappe.db.sql("""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""): + for d in frappe.db.sql( + """select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)""" + ): try: current_fy = frappe.get_doc("Fiscal Year", d[0]) @@ -114,16 +160,14 @@ def auto_create_fiscal_year(): start_year = cstr(new_fy.year_start_date.year) end_year = cstr(new_fy.year_end_date.year) - new_fy.year = start_year if start_year==end_year else (start_year + "-" + end_year) + new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year) new_fy.auto_created = 1 new_fy.insert(ignore_permissions=True) except frappe.NameError: pass + def get_from_and_to_date(fiscal_year): - fields = [ - "year_start_date as from_date", - "year_end_date as to_date" - ] + fields = ["year_start_date as from_date", "year_end_date as to_date"] return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py index 3ede25f19d4..bc966916ef3 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py @@ -1,22 +1,15 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'fiscal_year', - 'transactions': [ + "fieldname": "fiscal_year", + "transactions": [ + {"label": _("Budgets"), "items": ["Budget"]}, + {"label": _("References"), "items": ["Period Closing Voucher"]}, { - 'label': _('Budgets'), - 'items': ['Budget'] + "label": _("Target Details"), + "items": ["Sales Person", "Sales Partner", "Territory", "Monthly Distribution"], }, - { - 'label': _('References'), - 'items': ['Period Closing Voucher'] - }, - { - 'label': _('Target Details'), - 'items': ['Sales Person', 'Sales Partner', 'Territory', 'Monthly Distribution'] - } - ] + ], } diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index 69e13a407de..6e946f74660 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -11,43 +11,48 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrect test_ignore = ["Company"] -class TestFiscalYear(unittest.TestCase): +class TestFiscalYear(unittest.TestCase): def test_extra_year(self): if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"): frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000") - fy = frappe.get_doc({ - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2000", - "year_end_date": "2002-12-31", - "year_start_date": "2000-04-01" - }) + fy = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": "_Test Fiscal Year 2000", + "year_end_date": "2002-12-31", + "year_start_date": "2000-04-01", + } + ) self.assertRaises(FiscalYearIncorrectDate, fy.insert) def test_record_generator(): test_records = [ - { - "doctype": "Fiscal Year", - "year": "_Test Short Fiscal Year 2011", - "is_short_year": 1, - "year_end_date": "2011-04-01", - "year_start_date": "2011-12-31" - } + { + "doctype": "Fiscal Year", + "year": "_Test Short Fiscal Year 2011", + "is_short_year": 1, + "year_end_date": "2011-04-01", + "year_start_date": "2011-12-31", + } ] start = 2012 end = now_datetime().year + 5 for year in range(start, end): - test_records.append({ - "doctype": "Fiscal Year", - "year": f"_Test Fiscal Year {year}", - "year_start_date": f"{year}-01-01", - "year_end_date": f"{year}-12-31" - }) + test_records.append( + { + "doctype": "Fiscal Year", + "year": f"_Test Fiscal Year {year}", + "year_start_date": f"{year}-01-01", + "year_end_date": f"{year}-12-31", + } + ) return test_records + test_records = test_record_generator() diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index f184b95a92d..e590beb0261 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -26,6 +26,8 @@ from erpnext.exceptions import ( ) exclude_from_linked_with = True + + class GLEntry(Document): def autoname(self): """ @@ -33,6 +35,8 @@ class GLEntry(Document): name will be changed using autoname options (in a scheduled job) """ self.name = frappe.generate_hash(txt="", length=10) + if self.meta.autoname == "hash": + self.to_rename = 0 def validate(self): self.flags.ignore_submit_comment = True @@ -56,14 +60,18 @@ class GLEntry(Document): validate_frozen_account(self.account, adv_adj) # Update outstanding amt on against voucher - if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] - and self.against_voucher and self.flags.update_outstanding == 'Yes' - and not frappe.flags.is_reverse_depr_entry): - update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, - self.against_voucher) + if ( + self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"] + and self.against_voucher + and self.flags.update_outstanding == "Yes" + and not frappe.flags.is_reverse_depr_entry + ): + update_outstanding_amt( + self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher + ) def check_mandatory(self): - mandatory = ['account','voucher_type','voucher_no','company'] + mandatory = ["account", "voucher_type", "voucher_no", "company"] for k in mandatory: if not self.get(k): frappe.throw(_("{0} is required").format(_(self.meta.get_label(k)))) @@ -71,29 +79,40 @@ class GLEntry(Document): if not (self.party_type and self.party): account_type = frappe.get_cached_value("Account", self.account, "account_type") if account_type == "Receivable": - frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}") - .format(self.voucher_type, self.voucher_no, self.account)) + frappe.throw( + _("{0} {1}: Customer is required against Receivable account {2}").format( + self.voucher_type, self.voucher_no, self.account + ) + ) elif account_type == "Payable": - frappe.throw(_("{0} {1}: Supplier is required against Payable account {2}") - .format(self.voucher_type, self.voucher_no, self.account)) + frappe.throw( + _("{0} {1}: Supplier is required against Payable account {2}").format( + self.voucher_type, self.voucher_no, self.account + ) + ) # Zero value transaction is not allowed if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))): - frappe.throw(_("{0} {1}: Either debit or credit amount is required for {2}") - .format(self.voucher_type, self.voucher_no, self.account)) + frappe.throw( + _("{0} {1}: Either debit or credit amount is required for {2}").format( + self.voucher_type, self.voucher_no, self.account + ) + ) def pl_must_have_cost_center(self): """Validate that profit and loss type account GL entries have a cost center.""" - if self.cost_center or self.voucher_type == 'Period Closing Voucher': + if self.cost_center or self.voucher_type == "Period Closing Voucher": return if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss": msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format( - self.voucher_type, self.voucher_no, self.account) + self.voucher_type, self.voucher_no, self.account + ) msg += " " - msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format( - self.voucher_type) + msg += _( + "Please set the cost center field in {0} or setup a default Cost Center for the Company." + ).format(self.voucher_type) frappe.throw(msg, title=_("Missing Cost Center")) @@ -101,17 +120,31 @@ class GLEntry(Document): account_type = frappe.db.get_value("Account", self.account, "report_type") for dimension in get_checks_for_pl_and_bs_accounts(): - if account_type == "Profit and Loss" \ - and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled: + if ( + account_type == "Profit and Loss" + and self.company == dimension.company + and dimension.mandatory_for_pl + and not dimension.disabled + ): if not self.get(dimension.fieldname): - frappe.throw(_("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.") - .format(dimension.label, self.account)) + frappe.throw( + _("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.").format( + dimension.label, self.account + ) + ) - if account_type == "Balance Sheet" \ - and self.company == dimension.company and dimension.mandatory_for_bs and not dimension.disabled: + if ( + account_type == "Balance Sheet" + and self.company == dimension.company + and dimension.mandatory_for_bs + and not dimension.disabled + ): if not self.get(dimension.fieldname): - frappe.throw(_("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.") - .format(dimension.label, self.account)) + frappe.throw( + _("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.").format( + dimension.label, self.account + ) + ) def validate_allowed_dimensions(self): dimension_filter_map = get_dimension_filter_map() @@ -120,56 +153,97 @@ class GLEntry(Document): account = key[1] if self.account == account: - if value['is_mandatory'] and not self.get(dimension): - frappe.throw(_("{0} is mandatory for account {1}").format( - frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError) + if value["is_mandatory"] and not self.get(dimension): + frappe.throw( + _("{0} is mandatory for account {1}").format( + frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account) + ), + MandatoryAccountDimensionError, + ) - if value['allow_or_restrict'] == 'Allow': - if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: - frappe.throw(_("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) + if value["allow_or_restrict"] == "Allow": + if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(self.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(self.account), + ), + InvalidAccountDimensionError, + ) else: - if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: - frappe.throw(_("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) + if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(self.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(self.account), + ), + InvalidAccountDimensionError, + ) def check_pl_account(self): - if self.is_opening=='Yes' and \ - frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss": - frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") - .format(self.voucher_type, self.voucher_no, self.account)) + if ( + self.is_opening == "Yes" + and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss" + and not self.is_cancelled + ): + frappe.throw( + _("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry").format( + self.voucher_type, self.voucher_no, self.account + ) + ) def validate_account_details(self, adv_adj): """Account must be ledger, active and not freezed""" - ret = frappe.db.sql("""select is_group, docstatus, company - from tabAccount where name=%s""", self.account, as_dict=1)[0] + ret = frappe.db.sql( + """select is_group, docstatus, company + from tabAccount where name=%s""", + self.account, + as_dict=1, + )[0] - if ret.is_group==1: - frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''') - .format(self.voucher_type, self.voucher_no, self.account)) + if ret.is_group == 1: + frappe.throw( + _( + """{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions""" + ).format(self.voucher_type, self.voucher_no, self.account) + ) - if ret.docstatus==2: - frappe.throw(_("{0} {1}: Account {2} is inactive") - .format(self.voucher_type, self.voucher_no, self.account)) + if ret.docstatus == 2: + frappe.throw( + _("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account) + ) if ret.company != self.company: - frappe.throw(_("{0} {1}: Account {2} does not belong to Company {3}") - .format(self.voucher_type, self.voucher_no, self.account, self.company)) + frappe.throw( + _("{0} {1}: Account {2} does not belong to Company {3}").format( + self.voucher_type, self.voucher_no, self.account, self.company + ) + ) def validate_cost_center(self): - if not self.cost_center: return + if not self.cost_center: + return - is_group, company = frappe.get_cached_value('Cost Center', - self.cost_center, ['is_group', 'company']) + is_group, company = frappe.get_cached_value( + "Cost Center", self.cost_center, ["is_group", "company"] + ) if company != self.company: - frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") - .format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) + frappe.throw( + _("{0} {1}: Cost Center {2} does not belong to Company {3}").format( + self.voucher_type, self.voucher_no, self.cost_center, self.company + ) + ) - if (self.voucher_type != 'Period Closing Voucher' and is_group): - frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format( - self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) + if self.voucher_type != "Period Closing Voucher" and is_group: + frappe.throw( + _( + """{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""" + ).format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)) + ) def validate_party(self): validate_party_frozen_disabled(self.party_type, self.party) @@ -182,9 +256,12 @@ class GLEntry(Document): self.account_currency = account_currency or company_currency if account_currency != self.account_currency: - frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}") - .format(self.voucher_type, self.voucher_no, self.account, - (account_currency or company_currency)), InvalidAccountCurrency) + frappe.throw( + _("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}").format( + self.voucher_type, self.voucher_no, self.account, (account_currency or company_currency) + ), + InvalidAccountCurrency, + ) if self.party_type and self.party: validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency) @@ -193,51 +270,80 @@ class GLEntry(Document): if not self.fiscal_year: self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] + def validate_balance_type(account, adv_adj=False): if not adv_adj and account: balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") if balance_must_be: - balance = frappe.db.sql("""select sum(debit) - sum(credit) - from `tabGL Entry` where account = %s""", account)[0][0] + balance = frappe.db.sql( + """select sum(debit) - sum(credit) + from `tabGL Entry` where account = %s""", + account, + )[0][0] - if (balance_must_be=="Debit" and flt(balance) < 0) or \ - (balance_must_be=="Credit" and flt(balance) > 0): - frappe.throw(_("Balance for Account {0} must always be {1}").format(account, _(balance_must_be))) + if (balance_must_be == "Debit" and flt(balance) < 0) or ( + balance_must_be == "Credit" and flt(balance) > 0 + ): + frappe.throw( + _("Balance for Account {0} must always be {1}").format(account, _(balance_must_be)) + ) -def update_outstanding_amt(account, party_type, party, against_voucher_type, against_voucher, on_cancel=False): + +def update_outstanding_amt( + account, party_type, party, against_voucher_type, against_voucher, on_cancel=False +): if party_type and party: - party_condition = " and party_type={0} and party={1}"\ - .format(frappe.db.escape(party_type), frappe.db.escape(party)) + party_condition = " and party_type={0} and party={1}".format( + frappe.db.escape(party_type), frappe.db.escape(party) + ) else: party_condition = "" if against_voucher_type == "Sales Invoice": party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to") - account_condition = "and account in ({0}, {1})".format(frappe.db.escape(account), frappe.db.escape(party_account)) + account_condition = "and account in ({0}, {1})".format( + frappe.db.escape(account), frappe.db.escape(party_account) + ) else: account_condition = " and account = {0}".format(frappe.db.escape(account)) # get final outstanding amt - bal = flt(frappe.db.sql(""" + bal = flt( + frappe.db.sql( + """ select sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where against_voucher_type=%s and against_voucher=%s and voucher_type != 'Invoice Discounting' - {0} {1}""".format(party_condition, account_condition), - (against_voucher_type, against_voucher))[0][0] or 0.0) + {0} {1}""".format( + party_condition, account_condition + ), + (against_voucher_type, against_voucher), + )[0][0] + or 0.0 + ) - if against_voucher_type == 'Purchase Invoice': + if against_voucher_type == "Purchase Invoice": bal = -bal elif against_voucher_type == "Journal Entry": - against_voucher_amount = flt(frappe.db.sql(""" + against_voucher_amount = flt( + frappe.db.sql( + """ select sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s - and account = %s and (against_voucher is null or against_voucher='') {0}""" - .format(party_condition), (against_voucher, account))[0][0]) + and account = %s and (against_voucher is null or against_voucher='') {0}""".format( + party_condition + ), + (against_voucher, account), + )[0][0] + ) if not against_voucher_amount: - frappe.throw(_("Against Journal Entry {0} is already adjusted against some other voucher") - .format(against_voucher)) + frappe.throw( + _("Against Journal Entry {0} is already adjusted against some other voucher").format( + against_voucher + ) + ) bal = against_voucher_amount + bal if against_voucher_amount < 0: @@ -245,44 +351,51 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga # Validation : Outstanding can not be negative for JV if bal < 0 and not on_cancel: - frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))) + frappe.throw( + _("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)) + ) if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]: ref_doc = frappe.get_doc(against_voucher_type, against_voucher) # Didn't use db_set for optimisation purpose ref_doc.outstanding_amount = bal - frappe.db.set_value(against_voucher_type, against_voucher, 'outstanding_amount', bal) + frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal) ref_doc.set_status(update=True) def validate_frozen_account(account, adv_adj=None): frozen_account = frappe.get_cached_value("Account", account, "freeze_account") - if frozen_account == 'Yes' and not adv_adj: - frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None, - 'frozen_accounts_modifier') + if frozen_account == "Yes" and not adv_adj: + frozen_accounts_modifier = frappe.db.get_value( + "Accounts Settings", None, "frozen_accounts_modifier" + ) if not frozen_accounts_modifier: frappe.throw(_("Account {0} is frozen").format(account)) elif frozen_accounts_modifier not in frappe.get_roles(): frappe.throw(_("Not authorized to edit frozen Account {0}").format(account)) + def update_against_account(voucher_type, voucher_no): - entries = frappe.db.get_all("GL Entry", + entries = frappe.db.get_all( + "GL Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, - fields=["name", "party", "against", "debit", "credit", "account", "company"]) + fields=["name", "party", "against", "debit", "credit", "account", "company"], + ) if not entries: return company_currency = erpnext.get_company_currency(entries[0].company) - precision = get_field_precision(frappe.get_meta("GL Entry") - .get_field("debit"), company_currency) + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency) accounts_debited, accounts_credited = [], [] for d in entries: - if flt(d.debit, precision) > 0: accounts_debited.append(d.party or d.account) - if flt(d.credit, precision) > 0: accounts_credited.append(d.party or d.account) + if flt(d.debit, precision) > 0: + accounts_debited.append(d.party or d.account) + if flt(d.credit, precision) > 0: + accounts_credited.append(d.party or d.account) for d in entries: if flt(d.debit, precision) > 0: @@ -293,14 +406,17 @@ def update_against_account(voucher_type, voucher_no): if d.against != new_against: frappe.db.set_value("GL Entry", d.name, "against", new_against) + def on_doctype_update(): frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"]) frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"]) + def rename_gle_sle_docs(): for doctype in ["GL Entry", "Stock Ledger Entry"]: rename_temporarily_named_docs(doctype) + def rename_temporarily_named_docs(doctype): """Rename temporarily named docs using autoname options""" docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000) @@ -311,5 +427,5 @@ def rename_temporarily_named_docs(doctype): frappe.db.sql( "UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype), (newname, oldname), - auto_commit=True + auto_commit=True, ) diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py index 3de23946892..b188b09843a 100644 --- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py @@ -14,48 +14,68 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ class TestGLEntry(unittest.TestCase): def test_round_off_entry(self): frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC") - frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC") + frappe.db.set_value( + "Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC" + ) - jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 100, "_Test Cost Center - _TC", submit=False) + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 100, + "_Test Cost Center - _TC", + submit=False, + ) jv.get("accounts")[0].debit = 100.01 jv.flags.ignore_validate = True jv.submit() - round_off_entry = frappe.db.sql("""select name from `tabGL Entry` + round_off_entry = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no = %s and account='_Test Write Off - _TC' and cost_center='_Test Cost Center - _TC' - and debit = 0 and credit = '.01'""", jv.name) + and debit = 0 and credit = '.01'""", + jv.name, + ) self.assertTrue(round_off_entry) def test_rename_entries(self): - je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True) + je = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True + ) rename_gle_sle_docs() naming_series = parse_naming_series(parts=frappe.get_meta("GL Entry").autoname.split(".")[:-1]) - je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True) + je = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True + ) - gl_entries = frappe.get_all("GL Entry", + gl_entries = frappe.get_all( + "GL Entry", fields=["name", "to_rename"], filters={"voucher_type": "Journal Entry", "voucher_no": je.name}, - order_by="creation" + order_by="creation", ) self.assertTrue(all(entry.to_rename == 1 for entry in gl_entries)) - old_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0] + old_naming_series_current_value = frappe.db.sql( + "SELECT current from tabSeries where name = %s", naming_series + )[0][0] rename_gle_sle_docs() - new_gl_entries = frappe.get_all("GL Entry", + new_gl_entries = frappe.get_all( + "GL Entry", fields=["name", "to_rename"], filters={"voucher_type": "Journal Entry", "voucher_no": je.name}, - order_by="creation" + order_by="creation", ) self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries)) self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries))) - new_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0] + new_naming_series_current_value = frappe.db.sql( + "SELECT current from tabSeries where name = %s", naming_series + )[0][0] self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value) diff --git a/erpnext/accounts/doctype/gst_account/gst_account.json b/erpnext/accounts/doctype/gst_account/gst_account.json index b6ec8844e18..be5124c2d4d 100644 --- a/erpnext/accounts/doctype/gst_account/gst_account.json +++ b/erpnext/accounts/doctype/gst_account/gst_account.json @@ -10,6 +10,7 @@ "sgst_account", "igst_account", "cess_account", + "utgst_account", "is_reverse_charge_account" ], "fields": [ @@ -64,12 +65,18 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Is Reverse Charge Account" + }, + { + "fieldname": "utgst_account", + "fieldtype": "Link", + "label": "UTGST Account", + "options": "Account" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-09 12:30:25.889993", + "modified": "2022-04-07 12:59:14.039768", "modified_by": "Administrator", "module": "Accounts", "name": "GST Account", @@ -78,5 +85,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 09c389de738..5bd4585a9a8 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -33,19 +33,32 @@ class InvoiceDiscounting(AccountsController): frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting")) def validate_invoices(self): - discounted_invoices = [record.sales_invoice for record in - frappe.get_all("Discounted Invoice",fields=["sales_invoice"], filters={"docstatus":1})] + discounted_invoices = [ + record.sales_invoice + for record in frappe.get_all( + "Discounted Invoice", fields=["sales_invoice"], filters={"docstatus": 1} + ) + ] for record in self.invoices: if record.sales_invoice in discounted_invoices: - frappe.throw(_("Row({0}): {1} is already discounted in {2}") - .format(record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent))) + frappe.throw( + _("Row({0}): {1} is already discounted in {2}").format( + record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent) + ) + ) - actual_outstanding = frappe.db.get_value("Sales Invoice", record.sales_invoice,"outstanding_amount") - if record.outstanding_amount > actual_outstanding : - frappe.throw(_ - ("Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}").format( - record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice))) + actual_outstanding = frappe.db.get_value( + "Sales Invoice", record.sales_invoice, "outstanding_amount" + ) + if record.outstanding_amount > actual_outstanding: + frappe.throw( + _( + "Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}" + ).format( + record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice) + ) + ) def calculate_total_amount(self): self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices) @@ -73,24 +86,21 @@ class InvoiceDiscounting(AccountsController): self.status = "Cancelled" if cancel: - self.db_set('status', self.status, update_modified = True) + self.db_set("status", self.status, update_modified=True) def update_sales_invoice(self): for d in self.invoices: if self.docstatus == 1: is_discounted = 1 else: - discounted_invoice = frappe.db.exists({ - "doctype": "Discounted Invoice", - "sales_invoice": d.sales_invoice, - "docstatus": 1 - }) + discounted_invoice = frappe.db.exists( + {"doctype": "Discounted Invoice", "sales_invoice": d.sales_invoice, "docstatus": 1} + ) is_discounted = 1 if discounted_invoice else 0 frappe.db.set_value("Sales Invoice", d.sales_invoice, "is_discounted", is_discounted) def make_gl_entries(self): - company_currency = frappe.get_cached_value('Company', self.company, "default_currency") - + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") gl_entries = [] invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"] @@ -102,135 +112,182 @@ class InvoiceDiscounting(AccountsController): inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1) if d.outstanding_amount: - outstanding_in_company_currency = flt(d.outstanding_amount * inv.conversion_rate, - d.precision("outstanding_amount")) - ar_credit_account_currency = frappe.get_cached_value("Account", self.accounts_receivable_credit, "currency") + outstanding_in_company_currency = flt( + d.outstanding_amount * inv.conversion_rate, d.precision("outstanding_amount") + ) + ar_credit_account_currency = frappe.get_cached_value( + "Account", self.accounts_receivable_credit, "currency" + ) - gl_entries.append(self.get_gl_dict({ - "account": inv.debit_to, - "party_type": "Customer", - "party": d.customer, - "against": self.accounts_receivable_credit, - "credit": outstanding_in_company_currency, - "credit_in_account_currency": outstanding_in_company_currency \ - if inv.party_account_currency==company_currency else d.outstanding_amount, - "cost_center": inv.cost_center, - "against_voucher": d.sales_invoice, - "against_voucher_type": "Sales Invoice" - }, inv.party_account_currency, item=inv)) + gl_entries.append( + self.get_gl_dict( + { + "account": inv.debit_to, + "party_type": "Customer", + "party": d.customer, + "against": self.accounts_receivable_credit, + "credit": outstanding_in_company_currency, + "credit_in_account_currency": outstanding_in_company_currency + if inv.party_account_currency == company_currency + else d.outstanding_amount, + "cost_center": inv.cost_center, + "against_voucher": d.sales_invoice, + "against_voucher_type": "Sales Invoice", + }, + inv.party_account_currency, + item=inv, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": self.accounts_receivable_credit, - "party_type": "Customer", - "party": d.customer, - "against": inv.debit_to, - "debit": outstanding_in_company_currency, - "debit_in_account_currency": outstanding_in_company_currency \ - if ar_credit_account_currency==company_currency else d.outstanding_amount, - "cost_center": inv.cost_center, - "against_voucher": d.sales_invoice, - "against_voucher_type": "Sales Invoice" - }, ar_credit_account_currency, item=inv)) + gl_entries.append( + self.get_gl_dict( + { + "account": self.accounts_receivable_credit, + "party_type": "Customer", + "party": d.customer, + "against": inv.debit_to, + "debit": outstanding_in_company_currency, + "debit_in_account_currency": outstanding_in_company_currency + if ar_credit_account_currency == company_currency + else d.outstanding_amount, + "cost_center": inv.cost_center, + "against_voucher": d.sales_invoice, + "against_voucher_type": "Sales Invoice", + }, + ar_credit_account_currency, + item=inv, + ) + ) - make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No') + make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No") @frappe.whitelist() def create_disbursement_entry(self): je = frappe.new_doc("Journal Entry") - je.voucher_type = 'Journal Entry' + je.voucher_type = "Journal Entry" je.company = self.company - je.remark = 'Loan Disbursement entry against Invoice Discounting: ' + self.name + je.remark = "Loan Disbursement entry against Invoice Discounting: " + self.name - je.append("accounts", { - "account": self.bank_account, - "debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges), - "cost_center": erpnext.get_default_cost_center(self.company) - }) + je.append( + "accounts", + { + "account": self.bank_account, + "debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges), + "cost_center": erpnext.get_default_cost_center(self.company), + }, + ) if self.bank_charges: - je.append("accounts", { - "account": self.bank_charges_account, - "debit_in_account_currency": flt(self.bank_charges), - "cost_center": erpnext.get_default_cost_center(self.company) - }) + je.append( + "accounts", + { + "account": self.bank_charges_account, + "debit_in_account_currency": flt(self.bank_charges), + "cost_center": erpnext.get_default_cost_center(self.company), + }, + ) - je.append("accounts", { - "account": self.short_term_loan, - "credit_in_account_currency": flt(self.total_amount), - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": "Invoice Discounting", - "reference_name": self.name - }) + je.append( + "accounts", + { + "account": self.short_term_loan, + "credit_in_account_currency": flt(self.total_amount), + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": "Invoice Discounting", + "reference_name": self.name, + }, + ) for d in self.invoices: - je.append("accounts", { - "account": self.accounts_receivable_discounted, - "debit_in_account_currency": flt(d.outstanding_amount), - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": "Invoice Discounting", - "reference_name": self.name, - "party_type": "Customer", - "party": d.customer - }) + je.append( + "accounts", + { + "account": self.accounts_receivable_discounted, + "debit_in_account_currency": flt(d.outstanding_amount), + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": "Invoice Discounting", + "reference_name": self.name, + "party_type": "Customer", + "party": d.customer, + }, + ) - je.append("accounts", { - "account": self.accounts_receivable_credit, - "credit_in_account_currency": flt(d.outstanding_amount), - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": "Invoice Discounting", - "reference_name": self.name, - "party_type": "Customer", - "party": d.customer - }) + je.append( + "accounts", + { + "account": self.accounts_receivable_credit, + "credit_in_account_currency": flt(d.outstanding_amount), + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": "Invoice Discounting", + "reference_name": self.name, + "party_type": "Customer", + "party": d.customer, + }, + ) return je @frappe.whitelist() def close_loan(self): je = frappe.new_doc("Journal Entry") - je.voucher_type = 'Journal Entry' + je.voucher_type = "Journal Entry" je.company = self.company - je.remark = 'Loan Settlement entry against Invoice Discounting: ' + self.name + je.remark = "Loan Settlement entry against Invoice Discounting: " + self.name - je.append("accounts", { - "account": self.short_term_loan, - "debit_in_account_currency": flt(self.total_amount), - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": "Invoice Discounting", - "reference_name": self.name, - }) + je.append( + "accounts", + { + "account": self.short_term_loan, + "debit_in_account_currency": flt(self.total_amount), + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": "Invoice Discounting", + "reference_name": self.name, + }, + ) - je.append("accounts", { - "account": self.bank_account, - "credit_in_account_currency": flt(self.total_amount), - "cost_center": erpnext.get_default_cost_center(self.company) - }) + je.append( + "accounts", + { + "account": self.bank_account, + "credit_in_account_currency": flt(self.total_amount), + "cost_center": erpnext.get_default_cost_center(self.company), + }, + ) if getdate(self.loan_end_date) > getdate(nowdate()): for d in self.invoices: - outstanding_amount = frappe.db.get_value("Sales Invoice", d.sales_invoice, "outstanding_amount") + outstanding_amount = frappe.db.get_value( + "Sales Invoice", d.sales_invoice, "outstanding_amount" + ) if flt(outstanding_amount) > 0: - je.append("accounts", { - "account": self.accounts_receivable_discounted, - "credit_in_account_currency": flt(outstanding_amount), - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": "Invoice Discounting", - "reference_name": self.name, - "party_type": "Customer", - "party": d.customer - }) + je.append( + "accounts", + { + "account": self.accounts_receivable_discounted, + "credit_in_account_currency": flt(outstanding_amount), + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": "Invoice Discounting", + "reference_name": self.name, + "party_type": "Customer", + "party": d.customer, + }, + ) - je.append("accounts", { - "account": self.accounts_receivable_unpaid, - "debit_in_account_currency": flt(outstanding_amount), - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": "Invoice Discounting", - "reference_name": self.name, - "party_type": "Customer", - "party": d.customer - }) + je.append( + "accounts", + { + "account": self.accounts_receivable_unpaid, + "debit_in_account_currency": flt(outstanding_amount), + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": "Invoice Discounting", + "reference_name": self.name, + "party_type": "Customer", + "party": d.customer, + }, + ) return je + @frappe.whitelist() def get_invoices(filters): filters = frappe._dict(json.loads(filters)) @@ -250,7 +307,8 @@ def get_invoices(filters): if cond: where_condition += " and " + " and ".join(cond) - return frappe.db.sql(""" + return frappe.db.sql( + """ select name as sales_invoice, customer, @@ -264,17 +322,26 @@ def get_invoices(filters): %s and not exists(select di.name from `tabDiscounted Invoice` di where di.docstatus=1 and di.sales_invoice=si.name) - """ % where_condition, filters, as_dict=1) + """ + % where_condition, + filters, + as_dict=1, + ) + def get_party_account_based_on_invoice_discounting(sales_invoice): party_account = None - invoice_discounting = frappe.db.sql(""" + invoice_discounting = frappe.db.sql( + """ select par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status from `tabInvoice Discounting` par, `tabDiscounted Invoice` ch where par.name=ch.parent and par.docstatus=1 and ch.sales_invoice = %s - """, (sales_invoice), as_dict=1) + """, + (sales_invoice), + as_dict=1, + ) if invoice_discounting: if invoice_discounting[0].status == "Disbursed": party_account = invoice_discounting[0].accounts_receivable_discounted diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py index 771846e508a..a442231d9a4 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'reference_name', - 'internal_links': { - 'Sales Invoice': ['invoices', 'sales_invoice'] - }, - 'transactions': [ - { - 'label': _('Reference'), - 'items': ['Sales Invoice'] - }, - { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Journal Entry'] - } - ] + "fieldname": "reference_name", + "internal_links": {"Sales Invoice": ["invoices", "sales_invoice"]}, + "transactions": [ + {"label": _("Reference"), "items": ["Sales Invoice"]}, + {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}, + ], } diff --git a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py index d1d4be36f17..a85fdfcad7f 100644 --- a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py @@ -14,52 +14,74 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_ class TestInvoiceDiscounting(unittest.TestCase): def setUp(self): - self.ar_credit = create_account(account_name="_Test Accounts Receivable Credit", parent_account = "Accounts Receivable - _TC", company="_Test Company") - self.ar_discounted = create_account(account_name="_Test Accounts Receivable Discounted", parent_account = "Accounts Receivable - _TC", company="_Test Company") - self.ar_unpaid = create_account(account_name="_Test Accounts Receivable Unpaid", parent_account = "Accounts Receivable - _TC", company="_Test Company") - self.short_term_loan = create_account(account_name="_Test Short Term Loan", parent_account = "Source of Funds (Liabilities) - _TC", company="_Test Company") - self.bank_account = create_account(account_name="_Test Bank 2", parent_account = "Bank Accounts - _TC", company="_Test Company") - self.bank_charges_account = create_account(account_name="_Test Bank Charges Account", parent_account = "Expenses - _TC", company="_Test Company") + self.ar_credit = create_account( + account_name="_Test Accounts Receivable Credit", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + ) + self.ar_discounted = create_account( + account_name="_Test Accounts Receivable Discounted", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + ) + self.ar_unpaid = create_account( + account_name="_Test Accounts Receivable Unpaid", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + ) + self.short_term_loan = create_account( + account_name="_Test Short Term Loan", + parent_account="Source of Funds (Liabilities) - _TC", + company="_Test Company", + ) + self.bank_account = create_account( + account_name="_Test Bank 2", parent_account="Bank Accounts - _TC", company="_Test Company" + ) + self.bank_charges_account = create_account( + account_name="_Test Bank Charges Account", + parent_account="Expenses - _TC", + company="_Test Company", + ) frappe.db.set_value("Company", "_Test Company", "default_bank_account", self.bank_account) def test_total_amount(self): inv1 = create_sales_invoice(rate=200) inv2 = create_sales_invoice(rate=500) - inv_disc = create_invoice_discounting([inv1.name, inv2.name], + inv_disc = create_invoice_discounting( + [inv1.name, inv2.name], do_not_submit=True, accounts_receivable_credit=self.ar_credit, accounts_receivable_discounted=self.ar_discounted, accounts_receivable_unpaid=self.ar_unpaid, short_term_loan=self.short_term_loan, bank_charges_account=self.bank_charges_account, - bank_account=self.bank_account - ) + bank_account=self.bank_account, + ) self.assertEqual(inv_disc.total_amount, 700) def test_gl_entries_in_base_currency(self): inv = create_sales_invoice(rate=200) - inv_disc = create_invoice_discounting([inv.name], + inv_disc = create_invoice_discounting( + [inv.name], accounts_receivable_credit=self.ar_credit, accounts_receivable_discounted=self.ar_discounted, accounts_receivable_unpaid=self.ar_unpaid, short_term_loan=self.short_term_loan, bank_charges_account=self.bank_charges_account, - bank_account=self.bank_account - ) + bank_account=self.bank_account, + ) gle = get_gl_entries("Invoice Discounting", inv_disc.name) - expected_gle = { - inv.debit_to: [0.0, 200], - self.ar_credit: [200, 0.0] - } + expected_gle = {inv.debit_to: [0.0, 200], self.ar_credit: [200, 0.0]} for i, gle in enumerate(gle): self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account)) def test_loan_on_submit(self): inv = create_sales_invoice(rate=300) - inv_disc = create_invoice_discounting([inv.name], + inv_disc = create_invoice_discounting( + [inv.name], accounts_receivable_credit=self.ar_credit, accounts_receivable_discounted=self.ar_discounted, accounts_receivable_unpaid=self.ar_unpaid, @@ -67,28 +89,33 @@ class TestInvoiceDiscounting(unittest.TestCase): bank_charges_account=self.bank_charges_account, bank_account=self.bank_account, start=nowdate(), - period=60 - ) + period=60, + ) self.assertEqual(inv_disc.status, "Sanctioned") - self.assertEqual(inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)) - + self.assertEqual( + inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period) + ) def test_on_disbursed(self): inv = create_sales_invoice(rate=500) - inv_disc = create_invoice_discounting([inv.name], + inv_disc = create_invoice_discounting( + [inv.name], accounts_receivable_credit=self.ar_credit, accounts_receivable_discounted=self.ar_discounted, accounts_receivable_unpaid=self.ar_unpaid, short_term_loan=self.short_term_loan, bank_charges_account=self.bank_charges_account, bank_account=self.bank_account, - bank_charges=100 - ) + bank_charges=100, + ) je = inv_disc.create_disbursement_entry() self.assertEqual(je.accounts[0].account, self.bank_account) - self.assertEqual(je.accounts[0].debit_in_account_currency, flt(inv_disc.total_amount) - flt(inv_disc.bank_charges)) + self.assertEqual( + je.accounts[0].debit_in_account_currency, + flt(inv_disc.total_amount) - flt(inv_disc.bank_charges), + ) self.assertEqual(je.accounts[1].account, self.bank_charges_account) self.assertEqual(je.accounts[1].debit_in_account_currency, flt(inv_disc.bank_charges)) @@ -102,7 +129,6 @@ class TestInvoiceDiscounting(unittest.TestCase): self.assertEqual(je.accounts[4].account, self.ar_credit) self.assertEqual(je.accounts[4].credit_in_account_currency, flt(inv.outstanding_amount)) - je.posting_date = nowdate() je.submit() @@ -114,7 +140,8 @@ class TestInvoiceDiscounting(unittest.TestCase): def test_on_close_after_loan_period(self): inv = create_sales_invoice(rate=600) - inv_disc = create_invoice_discounting([inv.name], + inv_disc = create_invoice_discounting( + [inv.name], accounts_receivable_credit=self.ar_credit, accounts_receivable_discounted=self.ar_discounted, accounts_receivable_unpaid=self.ar_unpaid, @@ -122,8 +149,8 @@ class TestInvoiceDiscounting(unittest.TestCase): bank_charges_account=self.bank_charges_account, bank_account=self.bank_account, start=nowdate(), - period=60 - ) + period=60, + ) je1 = inv_disc.create_disbursement_entry() je1.posting_date = nowdate() @@ -151,7 +178,8 @@ class TestInvoiceDiscounting(unittest.TestCase): def test_on_close_after_loan_period_after_inv_payment(self): inv = create_sales_invoice(rate=600) - inv_disc = create_invoice_discounting([inv.name], + inv_disc = create_invoice_discounting( + [inv.name], accounts_receivable_credit=self.ar_credit, accounts_receivable_discounted=self.ar_discounted, accounts_receivable_unpaid=self.ar_unpaid, @@ -159,8 +187,8 @@ class TestInvoiceDiscounting(unittest.TestCase): bank_charges_account=self.bank_charges_account, bank_account=self.bank_account, start=nowdate(), - period=60 - ) + period=60, + ) je1 = inv_disc.create_disbursement_entry() je1.posting_date = nowdate() @@ -183,7 +211,8 @@ class TestInvoiceDiscounting(unittest.TestCase): def test_on_close_before_loan_period(self): inv = create_sales_invoice(rate=700) - inv_disc = create_invoice_discounting([inv.name], + inv_disc = create_invoice_discounting( + [inv.name], accounts_receivable_credit=self.ar_credit, accounts_receivable_discounted=self.ar_discounted, accounts_receivable_unpaid=self.ar_unpaid, @@ -191,7 +220,7 @@ class TestInvoiceDiscounting(unittest.TestCase): bank_charges_account=self.bank_charges_account, bank_account=self.bank_account, start=add_days(nowdate(), -80), - period=60 + period=60, ) je1 = inv_disc.create_disbursement_entry() @@ -209,16 +238,17 @@ class TestInvoiceDiscounting(unittest.TestCase): self.assertEqual(je2.accounts[1].credit_in_account_currency, flt(inv_disc.total_amount)) def test_make_payment_before_loan_period(self): - #it has problem + # it has problem inv = create_sales_invoice(rate=700) - inv_disc = create_invoice_discounting([inv.name], - accounts_receivable_credit=self.ar_credit, - accounts_receivable_discounted=self.ar_discounted, - accounts_receivable_unpaid=self.ar_unpaid, - short_term_loan=self.short_term_loan, - bank_charges_account=self.bank_charges_account, - bank_account=self.bank_account - ) + inv_disc = create_invoice_discounting( + [inv.name], + accounts_receivable_credit=self.ar_credit, + accounts_receivable_discounted=self.ar_discounted, + accounts_receivable_unpaid=self.ar_unpaid, + short_term_loan=self.short_term_loan, + bank_charges_account=self.bank_charges_account, + bank_account=self.bank_account, + ) je = inv_disc.create_disbursement_entry() inv_disc.reload() je.posting_date = nowdate() @@ -232,26 +262,31 @@ class TestInvoiceDiscounting(unittest.TestCase): je_on_payment.submit() self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted) - self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)) + self.assertEqual( + je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount) + ) self.assertEqual(je_on_payment.accounts[1].account, self.bank_account) - self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)) + self.assertEqual( + je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount) + ) inv.reload() self.assertEqual(inv.outstanding_amount, 0) def test_make_payment_before_after_period(self): - #it has problem + # it has problem inv = create_sales_invoice(rate=700) - inv_disc = create_invoice_discounting([inv.name], - accounts_receivable_credit=self.ar_credit, - accounts_receivable_discounted=self.ar_discounted, - accounts_receivable_unpaid=self.ar_unpaid, - short_term_loan=self.short_term_loan, - bank_charges_account=self.bank_charges_account, - bank_account=self.bank_account, - loan_start_date=add_days(nowdate(), -10), - period=5 - ) + inv_disc = create_invoice_discounting( + [inv.name], + accounts_receivable_credit=self.ar_credit, + accounts_receivable_discounted=self.ar_discounted, + accounts_receivable_unpaid=self.ar_unpaid, + short_term_loan=self.short_term_loan, + bank_charges_account=self.bank_charges_account, + bank_account=self.bank_account, + loan_start_date=add_days(nowdate(), -10), + period=5, + ) je = inv_disc.create_disbursement_entry() inv_disc.reload() je.posting_date = nowdate() @@ -269,9 +304,13 @@ class TestInvoiceDiscounting(unittest.TestCase): je_on_payment.submit() self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid) - self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)) + self.assertEqual( + je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount) + ) self.assertEqual(je_on_payment.accounts[1].account, self.bank_account) - self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)) + self.assertEqual( + je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount) + ) inv.reload() self.assertEqual(inv.outstanding_amount, 0) @@ -287,17 +326,15 @@ def create_invoice_discounting(invoices, **args): inv_disc.accounts_receivable_credit = args.accounts_receivable_credit inv_disc.accounts_receivable_discounted = args.accounts_receivable_discounted inv_disc.accounts_receivable_unpaid = args.accounts_receivable_unpaid - inv_disc.short_term_loan=args.short_term_loan - inv_disc.bank_charges_account=args.bank_charges_account - inv_disc.bank_account=args.bank_account + inv_disc.short_term_loan = args.short_term_loan + inv_disc.bank_charges_account = args.bank_charges_account + inv_disc.bank_account = args.bank_account inv_disc.loan_start_date = args.start or nowdate() inv_disc.loan_period = args.period or 30 inv_disc.bank_charges = flt(args.bank_charges) for d in invoices: - inv_disc.append("invoices", { - "sales_invoice": d - }) + inv_disc.append("invoices", {"sales_invoice": d}) inv_disc.insert() if not args.do_not_submit: diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py index 0ceb6a0bc23..23f36ec6d8d 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py @@ -13,20 +13,28 @@ class ItemTaxTemplate(Document): def autoname(self): if self.company and self.title: - abbr = frappe.get_cached_value('Company', self.company, 'abbr') - self.name = '{0} - {1}'.format(self.title, abbr) + abbr = frappe.get_cached_value("Company", self.company, "abbr") + self.name = "{0} - {1}".format(self.title, abbr) def validate_tax_accounts(self): """Check whether Tax Rate is not entered twice for same Tax Type""" check_list = [] - for d in self.get('taxes'): + for d in self.get("taxes"): if d.tax_type: account_type = frappe.db.get_value("Account", d.tax_type, "account_type") - if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']: + if account_type not in [ + "Tax", + "Chargeable", + "Income Account", + "Expense Account", + "Expenses Included In Valuation", + ]: frappe.throw( - _("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format( - d.idx)) + _( + "Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable" + ).format(d.idx) + ) else: if d.tax_type in check_list: frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type)) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py index 71177c25318..5a2bd720dd3 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py @@ -1,26 +1,13 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'item_tax_template', - 'transactions': [ - { - 'label': _('Pre Sales'), - 'items': ['Quotation', 'Supplier Quotation'] - }, - { - 'label': _('Sales'), - 'items': ['Sales Invoice', 'Sales Order', 'Delivery Note'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] - }, - { - 'label': _('Stock'), - 'items': ['Item Groups', 'Item'] - } - ] + "fieldname": "item_tax_template", + "transactions": [ + {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]}, + {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]}, + {"label": _("Stock"), "items": ["Item Groups", "Item"]}, + ], } diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 335fd350def..4493c722544 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -3,7 +3,7 @@ "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-03-25 10:53:52", + "creation": "2022-01-25 10:29:58.717206", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", @@ -13,6 +13,7 @@ "voucher_type", "naming_series", "finance_book", + "process_deferred_accounting", "reversal_of", "tax_withholding_category", "column_break1", @@ -524,13 +525,20 @@ "label": "Reversal Of", "options": "Journal Entry", "read_only": 1 + }, + { + "fieldname": "process_deferred_accounting", + "fieldtype": "Link", + "label": "Process Deferred Accounting", + "options": "Process Deferred Accounting", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, "links": [], - "modified": "2022-01-04 13:39:36.485954", + "modified": "2022-04-06 17:18:46.865259", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", @@ -578,6 +586,7 @@ "search_fields": "voucher_type,posting_date, due_date, cheque_no", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 9c1710217dd..8660c18bf95 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -19,7 +19,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category ) from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import ( - check_if_stock_and_account_balance_synced, get_account_currency, get_balance_on, get_stock_accounts, @@ -29,7 +28,9 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount -class StockAccountInvalidTransaction(frappe.ValidationError): pass +class StockAccountInvalidTransaction(frappe.ValidationError): + pass + class JournalEntry(AccountsController): def __init__(self, *args, **kwargs): @@ -39,11 +40,11 @@ class JournalEntry(AccountsController): return self.voucher_type def validate(self): - if self.voucher_type == 'Opening Entry': - self.is_opening = 'Yes' + if self.voucher_type == "Opening Entry": + self.is_opening = "Yes" if not self.is_opening: - self.is_opening='No' + self.is_opening = "No" self.clearance_date = None @@ -86,15 +87,14 @@ class JournalEntry(AccountsController): self.update_expense_claim() self.update_inter_company_jv() self.update_invoice_discounting() - check_if_stock_and_account_balance_synced(self.posting_date, - self.company, self.doctype, self.name) def on_cancel(self): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries from erpnext.payroll.doctype.salary_slip.salary_slip import unlink_ref_doc_from_salary_slip + unlink_ref_doc_from_payment_entries(self) unlink_ref_doc_from_salary_slip(self.name) - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.make_gl_entries(1) self.update_advance_paid() self.update_expense_claim() @@ -119,10 +119,13 @@ class JournalEntry(AccountsController): frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid() def validate_inter_company_accounts(self): - if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: + if ( + self.voucher_type == "Inter Company Journal Entry" + and self.inter_company_journal_entry_reference + ): doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference) - account_currency = frappe.get_cached_value('Company', self.company, "default_currency") - previous_account_currency = frappe.get_cached_value('Company', doc.company, "default_currency") + account_currency = frappe.get_cached_value("Company", self.company, "default_currency") + previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency") if account_currency == previous_account_currency: if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit: frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry")) @@ -130,45 +133,63 @@ class JournalEntry(AccountsController): def validate_stock_accounts(self): stock_accounts = get_stock_accounts(self.company, self.doctype, self.name) for account in stock_accounts: - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, - self.posting_date, self.company) + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( + account, self.posting_date, self.company + ) if account_bal == stock_bal: - frappe.throw(_("Account: {0} can only be updated via Stock Transactions") - .format(account), StockAccountInvalidTransaction) + frappe.throw( + _("Account: {0} can only be updated via Stock Transactions").format(account), + StockAccountInvalidTransaction, + ) def apply_tax_withholding(self): from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map - if not self.apply_tds or self.voucher_type not in ('Debit Note', 'Credit Note'): + if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"): return - parties = [d.party for d in self.get('accounts') if d.party] + parties = [d.party for d in self.get("accounts") if d.party] parties = list(set(parties)) if len(parties) > 1: frappe.throw(_("Cannot apply TDS against multiple parties in one entry")) account_type_map = get_account_type_map(self.company) - party_type = 'supplier' if self.voucher_type == 'Credit Note' else 'customer' - doctype = 'Purchase Invoice' if self.voucher_type == 'Credit Note' else 'Sales Invoice' - debit_or_credit = 'debit_in_account_currency' if self.voucher_type == 'Credit Note' else 'credit_in_account_currency' - rev_debit_or_credit = 'credit_in_account_currency' if debit_or_credit == 'debit_in_account_currency' else 'debit_in_account_currency' + party_type = "supplier" if self.voucher_type == "Credit Note" else "customer" + doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice" + debit_or_credit = ( + "debit_in_account_currency" + if self.voucher_type == "Credit Note" + else "credit_in_account_currency" + ) + rev_debit_or_credit = ( + "credit_in_account_currency" + if debit_or_credit == "debit_in_account_currency" + else "debit_in_account_currency" + ) party_account = get_party_account(party_type.title(), parties[0], self.company) - net_total = sum(d.get(debit_or_credit) for d in self.get('accounts') if account_type_map.get(d.account) - not in ('Tax', 'Chargeable')) + net_total = sum( + d.get(debit_or_credit) + for d in self.get("accounts") + if account_type_map.get(d.account) not in ("Tax", "Chargeable") + ) - party_amount = sum(d.get(rev_debit_or_credit) for d in self.get('accounts') if d.account == party_account) + party_amount = sum( + d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account + ) - inv = frappe._dict({ - party_type: parties[0], - 'doctype': doctype, - 'company': self.company, - 'posting_date': self.posting_date, - 'net_total': net_total - }) + inv = frappe._dict( + { + party_type: parties[0], + "doctype": doctype, + "company": self.company, + "posting_date": self.posting_date, + "net_total": net_total, + } + ) tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category) @@ -176,45 +197,64 @@ class JournalEntry(AccountsController): return accounts = [] - for d in self.get('accounts'): - if d.get('account') == tax_withholding_details.get("account_head"): - d.update({ - 'account': tax_withholding_details.get("account_head"), - debit_or_credit: tax_withholding_details.get('tax_amount') - }) + for d in self.get("accounts"): + if d.get("account") == tax_withholding_details.get("account_head"): + d.update( + { + "account": tax_withholding_details.get("account_head"), + debit_or_credit: tax_withholding_details.get("tax_amount"), + } + ) - accounts.append(d.get('account')) + accounts.append(d.get("account")) - if d.get('account') == party_account: - d.update({ - rev_debit_or_credit: party_amount - tax_withholding_details.get('tax_amount') - }) + if d.get("account") == party_account: + d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")}) if not accounts or tax_withholding_details.get("account_head") not in accounts: - self.append("accounts", { - 'account': tax_withholding_details.get("account_head"), - rev_debit_or_credit: tax_withholding_details.get('tax_amount'), - 'against_account': parties[0] - }) + self.append( + "accounts", + { + "account": tax_withholding_details.get("account_head"), + rev_debit_or_credit: tax_withholding_details.get("tax_amount"), + "against_account": parties[0], + }, + ) - to_remove = [d for d in self.get('accounts') - if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")] + to_remove = [ + d + for d in self.get("accounts") + if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head") + ] for d in to_remove: self.remove(d) def update_inter_company_jv(self): - if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: - frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\ - "inter_company_journal_entry_reference", self.name) + if ( + self.voucher_type == "Inter Company Journal Entry" + and self.inter_company_journal_entry_reference + ): + frappe.db.set_value( + "Journal Entry", + self.inter_company_journal_entry_reference, + "inter_company_journal_entry_reference", + self.name, + ) def update_invoice_discounting(self): def _validate_invoice_discounting_status(inv_disc, id_status, expected_status, row_id): id_link = get_link_to_form("Invoice Discounting", inv_disc) if id_status != expected_status: - frappe.throw(_("Row #{0}: Status must be {1} for Invoice Discounting {2}").format(d.idx, expected_status, id_link)) + frappe.throw( + _("Row #{0}: Status must be {1} for Invoice Discounting {2}").format( + d.idx, expected_status, id_link + ) + ) - invoice_discounting_list = list(set([d.reference_name for d in self.accounts if d.reference_type=="Invoice Discounting"])) + invoice_discounting_list = list( + set([d.reference_name for d in self.accounts if d.reference_type == "Invoice Discounting"]) + ) for inv_disc in invoice_discounting_list: inv_disc_doc = frappe.get_doc("Invoice Discounting", inv_disc) status = None @@ -238,104 +278,147 @@ class JournalEntry(AccountsController): if status: inv_disc_doc.set_status(status=status) - def unlink_advance_entry_reference(self): for d in self.get("accounts"): if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"): doc = frappe.get_doc(d.reference_type, d.reference_name) doc.delink_advance_entries(self.name) - d.reference_type = '' - d.reference_name = '' + d.reference_type = "" + d.reference_name = "" d.db_update() def unlink_asset_reference(self): for d in self.get("accounts"): - if d.reference_type=="Asset" and d.reference_name: + if d.reference_type == "Asset" and d.reference_name: asset = frappe.get_doc("Asset", d.reference_name) for s in asset.get("schedules"): if s.journal_entry == self.name: s.db_set("journal_entry", None) idx = cint(s.finance_book_id) or 1 - finance_books = asset.get('finance_books')[idx - 1] + finance_books = asset.get("finance_books")[idx - 1] finance_books.value_after_depreciation += s.depreciation_amount finance_books.db_update() asset.set_status() def unlink_inter_company_jv(self): - if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: - frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\ - "inter_company_journal_entry_reference", "") - frappe.db.set_value("Journal Entry", self.name,\ - "inter_company_journal_entry_reference", "") + if ( + self.voucher_type == "Inter Company Journal Entry" + and self.inter_company_journal_entry_reference + ): + frappe.db.set_value( + "Journal Entry", + self.inter_company_journal_entry_reference, + "inter_company_journal_entry_reference", + "", + ) + frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "") def unlink_asset_adjustment_entry(self): - frappe.db.sql(""" update `tabAsset Value Adjustment` - set journal_entry = null where journal_entry = %s""", self.name) + frappe.db.sql( + """ update `tabAsset Value Adjustment` + set journal_entry = null where journal_entry = %s""", + self.name, + ) def validate_party(self): for d in self.get("accounts"): account_type = frappe.db.get_value("Account", d.account, "account_type") if account_type in ["Receivable", "Payable"]: if not (d.party_type and d.party): - frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account)) + frappe.throw( + _("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format( + d.idx, d.account + ) + ) def check_credit_limit(self): - customers = list(set(d.party for d in self.get("accounts") - if d.party_type=="Customer" and d.party and flt(d.debit) > 0)) + customers = list( + set( + d.party + for d in self.get("accounts") + if d.party_type == "Customer" and d.party and flt(d.debit) > 0 + ) + ) if customers: from erpnext.selling.doctype.customer.customer import check_credit_limit + for customer in customers: check_credit_limit(customer, self.company) def validate_cheque_info(self): - if self.voucher_type in ['Bank Entry']: + if self.voucher_type in ["Bank Entry"]: if not self.cheque_no or not self.cheque_date: - msgprint(_("Reference No & Reference Date is required for {0}").format(self.voucher_type), - raise_exception=1) + msgprint( + _("Reference No & Reference Date is required for {0}").format(self.voucher_type), + raise_exception=1, + ) if self.cheque_date and not self.cheque_no: msgprint(_("Reference No is mandatory if you entered Reference Date"), raise_exception=1) def validate_entries_for_advance(self): - for d in self.get('accounts'): + for d in self.get("accounts"): if d.reference_type not in ("Sales Invoice", "Purchase Invoice", "Journal Entry"): - if (d.party_type == 'Customer' and flt(d.credit) > 0) or \ - (d.party_type == 'Supplier' and flt(d.debit) > 0): - if d.is_advance=="No": - msgprint(_("Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.").format(d.idx, d.account), alert=True) + if (d.party_type == "Customer" and flt(d.credit) > 0) or ( + d.party_type == "Supplier" and flt(d.debit) > 0 + ): + if d.is_advance == "No": + msgprint( + _( + "Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry." + ).format(d.idx, d.account), + alert=True, + ) elif d.reference_type in ("Sales Order", "Purchase Order") and d.is_advance != "Yes": - frappe.throw(_("Row {0}: Payment against Sales/Purchase Order should always be marked as advance").format(d.idx)) + frappe.throw( + _( + "Row {0}: Payment against Sales/Purchase Order should always be marked as advance" + ).format(d.idx) + ) if d.is_advance == "Yes": - if d.party_type == 'Customer' and flt(d.debit) > 0: + if d.party_type == "Customer" and flt(d.debit) > 0: frappe.throw(_("Row {0}: Advance against Customer must be credit").format(d.idx)) - elif d.party_type == 'Supplier' and flt(d.credit) > 0: + elif d.party_type == "Supplier" and flt(d.credit) > 0: frappe.throw(_("Row {0}: Advance against Supplier must be debit").format(d.idx)) def validate_against_jv(self): - for d in self.get('accounts'): - if d.reference_type=="Journal Entry": + for d in self.get("accounts"): + if d.reference_type == "Journal Entry": account_root_type = frappe.db.get_value("Account", d.account, "root_type") if account_root_type == "Asset" and flt(d.debit) > 0: - frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets credited") - .format(d.idx, d.account)) + frappe.throw( + _( + "Row #{0}: For {1}, you can select reference document only if account gets credited" + ).format(d.idx, d.account) + ) elif account_root_type == "Liability" and flt(d.credit) > 0: - frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets debited") - .format(d.idx, d.account)) + frappe.throw( + _( + "Row #{0}: For {1}, you can select reference document only if account gets debited" + ).format(d.idx, d.account) + ) if d.reference_name == self.name: frappe.throw(_("You can not enter current voucher in 'Against Journal Entry' column")) - against_entries = frappe.db.sql("""select * from `tabJournal Entry Account` + against_entries = frappe.db.sql( + """select * from `tabJournal Entry Account` where account = %s and docstatus = 1 and parent = %s and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order")) - """, (d.account, d.reference_name), as_dict=True) + """, + (d.account, d.reference_name), + as_dict=True, + ) if not against_entries: - frappe.throw(_("Journal Entry {0} does not have account {1} or already matched against other voucher") - .format(d.reference_name, d.account)) + frappe.throw( + _( + "Journal Entry {0} does not have account {1} or already matched against other voucher" + ).format(d.reference_name, d.account) + ) else: dr_or_cr = "debit" if d.credit > 0 else "credit" valid = False @@ -343,16 +426,19 @@ class JournalEntry(AccountsController): if flt(jvd[dr_or_cr]) > 0: valid = True if not valid: - frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry") - .format(d.reference_name, dr_or_cr)) + frappe.throw( + _("Against Journal Entry {0} does not have any unmatched {1} entry").format( + d.reference_name, dr_or_cr + ) + ) def validate_reference_doc(self): """Validates reference document""" field_dict = { - 'Sales Invoice': ["Customer", "Debit To"], - 'Purchase Invoice': ["Supplier", "Credit To"], - 'Sales Order': ["Customer"], - 'Purchase Order': ["Supplier"] + "Sales Invoice": ["Customer", "Debit To"], + "Purchase Invoice": ["Supplier", "Credit To"], + "Sales Order": ["Customer"], + "Purchase Order": ["Supplier"], } self.reference_totals = {} @@ -365,56 +451,76 @@ class JournalEntry(AccountsController): if not d.reference_name: d.reference_type = None if d.reference_type and d.reference_name and (d.reference_type in list(field_dict)): - dr_or_cr = "credit_in_account_currency" \ - if d.reference_type in ("Sales Order", "Sales Invoice") else "debit_in_account_currency" + dr_or_cr = ( + "credit_in_account_currency" + if d.reference_type in ("Sales Order", "Sales Invoice") + else "debit_in_account_currency" + ) # check debit or credit type Sales / Purchase Order - if d.reference_type=="Sales Order" and flt(d.debit) > 0: - frappe.throw(_("Row {0}: Debit entry can not be linked with a {1}").format(d.idx, d.reference_type)) + if d.reference_type == "Sales Order" and flt(d.debit) > 0: + frappe.throw( + _("Row {0}: Debit entry can not be linked with a {1}").format(d.idx, d.reference_type) + ) if d.reference_type == "Purchase Order" and flt(d.credit) > 0: - frappe.throw(_("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type)) + frappe.throw( + _("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type) + ) # set totals if not d.reference_name in self.reference_totals: self.reference_totals[d.reference_name] = 0.0 - if self.voucher_type not in ('Deferred Revenue', 'Deferred Expense'): + if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"): self.reference_totals[d.reference_name] += flt(d.get(dr_or_cr)) self.reference_types[d.reference_name] = d.reference_type self.reference_accounts[d.reference_name] = d.account - against_voucher = frappe.db.get_value(d.reference_type, d.reference_name, - [scrub(dt) for dt in field_dict.get(d.reference_type)]) + against_voucher = frappe.db.get_value( + d.reference_type, d.reference_name, [scrub(dt) for dt in field_dict.get(d.reference_type)] + ) if not against_voucher: frappe.throw(_("Row {0}: Invalid reference {1}").format(d.idx, d.reference_name)) # check if party and account match if d.reference_type in ("Sales Invoice", "Purchase Invoice"): - if self.voucher_type in ('Deferred Revenue', 'Deferred Expense') and d.reference_detail_no: - debit_or_credit = 'Debit' if d.debit else 'Credit' - party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no, - debit_or_credit) - against_voucher = ['', against_voucher[1]] + if self.voucher_type in ("Deferred Revenue", "Deferred Expense") and d.reference_detail_no: + debit_or_credit = "Debit" if d.debit else "Credit" + party_account = get_deferred_booking_accounts( + d.reference_type, d.reference_detail_no, debit_or_credit + ) + against_voucher = ["", against_voucher[1]] else: if d.reference_type == "Sales Invoice": - party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] + party_account = ( + get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] + ) else: party_account = against_voucher[1] - if (against_voucher[0] != cstr(d.party) or party_account != d.account): - frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}") - .format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1], - d.reference_type, d.reference_name)) + if against_voucher[0] != cstr(d.party) or party_account != d.account: + frappe.throw( + _("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format( + d.idx, + field_dict.get(d.reference_type)[0], + field_dict.get(d.reference_type)[1], + d.reference_type, + d.reference_name, + ) + ) # check if party matches for Sales / Purchase Order if d.reference_type in ("Sales Order", "Purchase Order"): # set totals if against_voucher != d.party: - frappe.throw(_("Row {0}: {1} {2} does not match with {3}") \ - .format(d.idx, d.party_type, d.party, d.reference_type)) + frappe.throw( + _("Row {0}: {1} {2} does not match with {3}").format( + d.idx, d.party_type, d.party, d.reference_type + ) + ) self.validate_orders() self.validate_invoices() @@ -440,62 +546,79 @@ class JournalEntry(AccountsController): account_currency = get_account_currency(account) if account_currency == self.company_currency: voucher_total = order.base_grand_total - formatted_voucher_total = fmt_money(voucher_total, order.precision("base_grand_total"), - currency=account_currency) + formatted_voucher_total = fmt_money( + voucher_total, order.precision("base_grand_total"), currency=account_currency + ) else: voucher_total = order.grand_total - formatted_voucher_total = fmt_money(voucher_total, order.precision("grand_total"), - currency=account_currency) + formatted_voucher_total = fmt_money( + voucher_total, order.precision("grand_total"), currency=account_currency + ) if flt(voucher_total) < (flt(order.advance_paid) + total): - frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total)) + frappe.throw( + _("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format( + reference_type, reference_name, formatted_voucher_total + ) + ) def validate_invoices(self): """Validate totals and docstatus for invoices""" for reference_name, total in iteritems(self.reference_totals): reference_type = self.reference_types[reference_name] - if (reference_type in ("Sales Invoice", "Purchase Invoice") and - self.voucher_type not in ['Debit Note', 'Credit Note']): - invoice = frappe.db.get_value(reference_type, reference_name, - ["docstatus", "outstanding_amount"], as_dict=1) + if reference_type in ("Sales Invoice", "Purchase Invoice") and self.voucher_type not in [ + "Debit Note", + "Credit Note", + ]: + invoice = frappe.db.get_value( + reference_type, reference_name, ["docstatus", "outstanding_amount"], as_dict=1 + ) if invoice.docstatus != 1: frappe.throw(_("{0} {1} is not submitted").format(reference_type, reference_name)) if total and flt(invoice.outstanding_amount) < total: - frappe.throw(_("Payment against {0} {1} cannot be greater than Outstanding Amount {2}") - .format(reference_type, reference_name, invoice.outstanding_amount)) + frappe.throw( + _("Payment against {0} {1} cannot be greater than Outstanding Amount {2}").format( + reference_type, reference_name, invoice.outstanding_amount + ) + ) def set_against_account(self): accounts_debited, accounts_credited = [], [] - if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'): - for d in self.get('accounts'): - if d.reference_type == 'Sales Invoice': - field = 'customer' + if self.voucher_type in ("Deferred Revenue", "Deferred Expense"): + for d in self.get("accounts"): + if d.reference_type == "Sales Invoice": + field = "customer" else: - field = 'supplier' + field = "supplier" d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field) else: for d in self.get("accounts"): - if flt(d.debit > 0): accounts_debited.append(d.party or d.account) - if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) + if flt(d.debit > 0): + accounts_debited.append(d.party or d.account) + if flt(d.credit) > 0: + accounts_credited.append(d.party or d.account) for d in self.get("accounts"): - if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) - if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) + if flt(d.debit > 0): + d.against_account = ", ".join(list(set(accounts_credited))) + if flt(d.credit > 0): + d.against_account = ", ".join(list(set(accounts_debited))) def validate_debit_credit_amount(self): - for d in self.get('accounts'): + for d in self.get("accounts"): if not flt(d.debit) and not flt(d.credit): frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) def validate_total_debit_and_credit(self): self.set_total_debit_credit() if self.difference: - frappe.throw(_("Total Debit must be equal to Total Credit. The difference is {0}") - .format(self.difference)) + frappe.throw( + _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference) + ) def set_total_debit_credit(self): self.total_debit, self.total_credit, self.difference = 0, 0, 0 @@ -506,13 +629,16 @@ class JournalEntry(AccountsController): self.total_debit = flt(self.total_debit) + flt(d.debit, d.precision("debit")) self.total_credit = flt(self.total_credit) + flt(d.credit, d.precision("credit")) - self.difference = flt(self.total_debit, self.precision("total_debit")) - \ - flt(self.total_credit, self.precision("total_credit")) + self.difference = flt(self.total_debit, self.precision("total_debit")) - flt( + self.total_credit, self.precision("total_credit") + ) def validate_multi_currency(self): alternate_currency = [] for d in self.get("accounts"): - account = frappe.db.get_value("Account", d.account, ["account_currency", "account_type"], as_dict=1) + account = frappe.db.get_value( + "Account", d.account, ["account_currency", "account_type"], as_dict=1 + ) if account: d.account_currency = account.account_currency d.account_type = account.account_type @@ -531,8 +657,12 @@ class JournalEntry(AccountsController): def set_amounts_in_company_currency(self): for d in self.get("accounts"): - d.debit_in_account_currency = flt(d.debit_in_account_currency, d.precision("debit_in_account_currency")) - d.credit_in_account_currency = flt(d.credit_in_account_currency, d.precision("credit_in_account_currency")) + d.debit_in_account_currency = flt( + d.debit_in_account_currency, d.precision("debit_in_account_currency") + ) + d.credit_in_account_currency = flt( + d.credit_in_account_currency, d.precision("credit_in_account_currency") + ) d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit")) d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit")) @@ -541,13 +671,28 @@ class JournalEntry(AccountsController): for d in self.get("accounts"): if d.account_currency == self.company_currency: d.exchange_rate = 1 - elif not d.exchange_rate or d.exchange_rate == 1 or \ - (d.reference_type in ("Sales Invoice", "Purchase Invoice") - and d.reference_name and self.posting_date): + elif ( + not d.exchange_rate + or d.exchange_rate == 1 + or ( + d.reference_type in ("Sales Invoice", "Purchase Invoice") + and d.reference_name + and self.posting_date + ) + ): - # Modified to include the posting date for which to retreive the exchange rate - d.exchange_rate = get_exchange_rate(self.posting_date, d.account, d.account_currency, - self.company, d.reference_type, d.reference_name, d.debit, d.credit, d.exchange_rate) + # Modified to include the posting date for which to retreive the exchange rate + d.exchange_rate = get_exchange_rate( + self.posting_date, + d.account, + d.account_currency, + self.company, + d.reference_type, + d.reference_name, + d.debit, + d.credit, + d.exchange_rate, + ) if not d.exchange_rate: frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) @@ -560,55 +705,76 @@ class JournalEntry(AccountsController): if self.cheque_no: if self.cheque_date: - r.append(_('Reference #{0} dated {1}').format(self.cheque_no, formatdate(self.cheque_date))) + r.append(_("Reference #{0} dated {1}").format(self.cheque_no, formatdate(self.cheque_date))) else: msgprint(_("Please enter Reference date"), raise_exception=frappe.MandatoryError) - for d in self.get('accounts'): - if d.reference_type=="Sales Invoice" and d.credit: - r.append(_("{0} against Sales Invoice {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \ - d.reference_name)) + for d in self.get("accounts"): + if d.reference_type == "Sales Invoice" and d.credit: + r.append( + _("{0} against Sales Invoice {1}").format( + fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name + ) + ) - if d.reference_type=="Sales Order" and d.credit: - r.append(_("{0} against Sales Order {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \ - d.reference_name)) + if d.reference_type == "Sales Order" and d.credit: + r.append( + _("{0} against Sales Order {1}").format( + fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name + ) + ) if d.reference_type == "Purchase Invoice" and d.debit: - bill_no = frappe.db.sql("""select bill_no, bill_date - from `tabPurchase Invoice` where name=%s""", d.reference_name) - if bill_no and bill_no[0][0] and bill_no[0][0].lower().strip() \ - not in ['na', 'not applicable', 'none']: - r.append(_('{0} against Bill {1} dated {2}').format(fmt_money(flt(d.debit), currency=self.company_currency), bill_no[0][0], - bill_no[0][1] and formatdate(bill_no[0][1].strftime('%Y-%m-%d')))) + bill_no = frappe.db.sql( + """select bill_no, bill_date + from `tabPurchase Invoice` where name=%s""", + d.reference_name, + ) + if ( + bill_no + and bill_no[0][0] + and bill_no[0][0].lower().strip() not in ["na", "not applicable", "none"] + ): + r.append( + _("{0} against Bill {1} dated {2}").format( + fmt_money(flt(d.debit), currency=self.company_currency), + bill_no[0][0], + bill_no[0][1] and formatdate(bill_no[0][1].strftime("%Y-%m-%d")), + ) + ) if d.reference_type == "Purchase Order" and d.debit: - r.append(_("{0} against Purchase Order {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \ - d.reference_name)) + r.append( + _("{0} against Purchase Order {1}").format( + fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name + ) + ) if r: - self.remark = ("\n").join(r) #User Remarks is not mandatory + self.remark = ("\n").join(r) # User Remarks is not mandatory def set_print_format_fields(self): bank_amount = party_amount = total_amount = 0.0 - currency = bank_account_currency = party_account_currency = pay_to_recd_from= None + currency = bank_account_currency = party_account_currency = pay_to_recd_from = None party_type = None - for d in self.get('accounts'): - if d.party_type in ['Customer', 'Supplier'] and d.party: + for d in self.get("accounts"): + if d.party_type in ["Customer", "Supplier"] and d.party: party_type = d.party_type if not pay_to_recd_from: pay_to_recd_from = d.party if pay_to_recd_from and pay_to_recd_from == d.party: - party_amount += (d.debit_in_account_currency or d.credit_in_account_currency) + party_amount += d.debit_in_account_currency or d.credit_in_account_currency party_account_currency = d.account_currency elif frappe.db.get_value("Account", d.account, "account_type") in ["Bank", "Cash"]: - bank_amount += (d.debit_in_account_currency or d.credit_in_account_currency) + bank_amount += d.debit_in_account_currency or d.credit_in_account_currency bank_account_currency = d.account_currency if party_type and pay_to_recd_from: - self.pay_to_recd_from = frappe.db.get_value(party_type, pay_to_recd_from, - "customer_name" if party_type=="Customer" else "supplier_name") + self.pay_to_recd_from = frappe.db.get_value( + party_type, pay_to_recd_from, "customer_name" if party_type == "Customer" else "supplier_name" + ) if bank_amount: total_amount = bank_amount currency = bank_account_currency @@ -622,6 +788,7 @@ class JournalEntry(AccountsController): self.total_amount = amt self.total_amount_currency = currency from frappe.utils import money_in_words + self.total_amount_in_words = money_in_words(amt, currency) def make_gl_entries(self, cancel=0, adv_adj=0): @@ -635,38 +802,45 @@ class JournalEntry(AccountsController): remarks = "\n".join(r) gl_map.append( - self.get_gl_dict({ - "account": d.account, - "party_type": d.party_type, - "due_date": self.due_date, - "party": d.party, - "against": d.against_account, - "debit": flt(d.debit, d.precision("debit")), - "credit": flt(d.credit, d.precision("credit")), - "account_currency": d.account_currency, - "debit_in_account_currency": flt(d.debit_in_account_currency, d.precision("debit_in_account_currency")), - "credit_in_account_currency": flt(d.credit_in_account_currency, d.precision("credit_in_account_currency")), - "against_voucher_type": d.reference_type, - "against_voucher": d.reference_name, - "remarks": remarks, - "voucher_detail_no": d.reference_detail_no, - "cost_center": d.cost_center, - "project": d.project, - "finance_book": self.finance_book - }, item=d) + self.get_gl_dict( + { + "account": d.account, + "party_type": d.party_type, + "due_date": self.due_date, + "party": d.party, + "against": d.against_account, + "debit": flt(d.debit, d.precision("debit")), + "credit": flt(d.credit, d.precision("credit")), + "account_currency": d.account_currency, + "debit_in_account_currency": flt( + d.debit_in_account_currency, d.precision("debit_in_account_currency") + ), + "credit_in_account_currency": flt( + d.credit_in_account_currency, d.precision("credit_in_account_currency") + ), + "against_voucher_type": d.reference_type, + "against_voucher": d.reference_name, + "remarks": remarks, + "voucher_detail_no": d.reference_detail_no, + "cost_center": d.cost_center, + "project": d.project, + "finance_book": self.finance_book, + }, + item=d, + ) ) - if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'): - update_outstanding = 'No' + if self.voucher_type in ("Deferred Revenue", "Deferred Expense"): + update_outstanding = "No" else: - update_outstanding = 'Yes' + update_outstanding = "Yes" if gl_map: make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) @frappe.whitelist() def get_balance(self): - if not self.get('accounts'): + if not self.get("accounts"): msgprint(_("'Entries' cannot be empty"), raise_exception=True) else: self.total_debit, self.total_credit = 0, 0 @@ -675,18 +849,18 @@ class JournalEntry(AccountsController): # If any row without amount, set the diff on that row if diff: blank_row = None - for d in self.get('accounts'): + for d in self.get("accounts"): if not d.credit_in_account_currency and not d.debit_in_account_currency and diff != 0: blank_row = d if not blank_row: - blank_row = self.append('accounts', {}) + blank_row = self.append("accounts", {}) blank_row.exchange_rate = 1 - if diff>0: + if diff > 0: blank_row.credit_in_account_currency = diff blank_row.credit = diff - elif diff<0: + elif diff < 0: blank_row.debit_in_account_currency = abs(diff) blank_row.debit = abs(diff) @@ -694,76 +868,100 @@ class JournalEntry(AccountsController): @frappe.whitelist() def get_outstanding_invoices(self): - self.set('accounts', []) + self.set("accounts", []) total = 0 for d in self.get_values(): total += flt(d.outstanding_amount, self.precision("credit", "accounts")) - jd1 = self.append('accounts', {}) + jd1 = self.append("accounts", {}) jd1.account = d.account jd1.party = d.party - if self.write_off_based_on == 'Accounts Receivable': + if self.write_off_based_on == "Accounts Receivable": jd1.party_type = "Customer" - jd1.credit_in_account_currency = flt(d.outstanding_amount, self.precision("credit", "accounts")) + jd1.credit_in_account_currency = flt( + d.outstanding_amount, self.precision("credit", "accounts") + ) jd1.reference_type = "Sales Invoice" jd1.reference_name = cstr(d.name) - elif self.write_off_based_on == 'Accounts Payable': + elif self.write_off_based_on == "Accounts Payable": jd1.party_type = "Supplier" jd1.debit_in_account_currency = flt(d.outstanding_amount, self.precision("debit", "accounts")) jd1.reference_type = "Purchase Invoice" jd1.reference_name = cstr(d.name) - jd2 = self.append('accounts', {}) - if self.write_off_based_on == 'Accounts Receivable': + jd2 = self.append("accounts", {}) + if self.write_off_based_on == "Accounts Receivable": jd2.debit_in_account_currency = total - elif self.write_off_based_on == 'Accounts Payable': + elif self.write_off_based_on == "Accounts Payable": jd2.credit_in_account_currency = total self.validate_total_debit_and_credit() - def get_values(self): - cond = " and outstanding_amount <= {0}".format(self.write_off_amount) \ - if flt(self.write_off_amount) > 0 else "" + cond = ( + " and outstanding_amount <= {0}".format(self.write_off_amount) + if flt(self.write_off_amount) > 0 + else "" + ) - if self.write_off_based_on == 'Accounts Receivable': - return frappe.db.sql("""select name, debit_to as account, customer as party, outstanding_amount + if self.write_off_based_on == "Accounts Receivable": + return frappe.db.sql( + """select name, debit_to as account, customer as party, outstanding_amount from `tabSales Invoice` where docstatus = 1 and company = %s - and outstanding_amount > 0 %s""" % ('%s', cond), self.company, as_dict=True) - elif self.write_off_based_on == 'Accounts Payable': - return frappe.db.sql("""select name, credit_to as account, supplier as party, outstanding_amount + and outstanding_amount > 0 %s""" + % ("%s", cond), + self.company, + as_dict=True, + ) + elif self.write_off_based_on == "Accounts Payable": + return frappe.db.sql( + """select name, credit_to as account, supplier as party, outstanding_amount from `tabPurchase Invoice` where docstatus = 1 and company = %s - and outstanding_amount > 0 %s""" % ('%s', cond), self.company, as_dict=True) + and outstanding_amount > 0 %s""" + % ("%s", cond), + self.company, + as_dict=True, + ) def update_expense_claim(self): for d in self.accounts: - if d.reference_type=="Expense Claim" and d.reference_name: + if d.reference_type == "Expense Claim" and d.reference_name: doc = frappe.get_doc("Expense Claim", d.reference_name) if self.docstatus == 2: update_reimbursed_amount(doc, -1 * d.debit) else: update_reimbursed_amount(doc, d.debit) - def validate_expense_claim(self): for d in self.accounts: - if d.reference_type=="Expense Claim": - sanctioned_amount, reimbursed_amount = frappe.db.get_value("Expense Claim", - d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed")) + if d.reference_type == "Expense Claim": + sanctioned_amount, reimbursed_amount = frappe.db.get_value( + "Expense Claim", d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed") + ) pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount) if d.debit > pending_amount: - frappe.throw(_("Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}").format(d.idx, d.reference_name, pending_amount)) + frappe.throw( + _( + "Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}" + ).format(d.idx, d.reference_name, pending_amount) + ) def validate_credit_debit_note(self): if self.stock_entry: if frappe.db.get_value("Stock Entry", self.stock_entry, "docstatus") != 1: frappe.throw(_("Stock Entry {0} is not submitted").format(self.stock_entry)) - if frappe.db.exists({"doctype": "Journal Entry", "stock_entry": self.stock_entry, "docstatus":1}): - frappe.msgprint(_("Warning: Another {0} # {1} exists against stock entry {2}").format(self.voucher_type, self.name, self.stock_entry)) + if frappe.db.exists( + {"doctype": "Journal Entry", "stock_entry": self.stock_entry, "docstatus": 1} + ): + frappe.msgprint( + _("Warning: Another {0} # {1} exists against stock entry {2}").format( + self.voucher_type, self.name, self.stock_entry + ) + ) def validate_empty_accounts_table(self): - if not self.get('accounts'): + if not self.get("accounts"): frappe.throw(_("Accounts table cannot be blank.")) def set_account_and_party_balance(self): @@ -774,54 +972,66 @@ class JournalEntry(AccountsController): account_balance[d.account] = get_balance_on(account=d.account, date=self.posting_date) if (d.party_type, d.party) not in party_balance: - party_balance[(d.party_type, d.party)] = get_balance_on(party_type=d.party_type, - party=d.party, date=self.posting_date, company=self.company) + party_balance[(d.party_type, d.party)] = get_balance_on( + party_type=d.party_type, party=d.party, date=self.posting_date, company=self.company + ) d.account_balance = account_balance[d.account] d.party_balance = party_balance[(d.party_type, d.party)] + @frappe.whitelist() def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account + if mode_of_payment: account = get_bank_cash_account(mode_of_payment, company).get("account") if not account: - ''' - Set the default account first. If the user hasn't set any default account then, he doesn't - want us to set any random account. In this case set the account only if there is single - account (of that type), otherwise return empty dict. - ''' - if account_type=="Bank": - account = frappe.get_cached_value('Company', company, "default_bank_account") + """ + Set the default account first. If the user hasn't set any default account then, he doesn't + want us to set any random account. In this case set the account only if there is single + account (of that type), otherwise return empty dict. + """ + if account_type == "Bank": + account = frappe.get_cached_value("Company", company, "default_bank_account") if not account: - account_list = frappe.get_all("Account", filters = {"company": company, - "account_type": "Bank", "is_group": 0}) + account_list = frappe.get_all( + "Account", filters={"company": company, "account_type": "Bank", "is_group": 0} + ) if len(account_list) == 1: account = account_list[0].name - elif account_type=="Cash": - account = frappe.get_cached_value('Company', company, "default_cash_account") + elif account_type == "Cash": + account = frappe.get_cached_value("Company", company, "default_cash_account") if not account: - account_list = frappe.get_all("Account", filters = {"company": company, - "account_type": "Cash", "is_group": 0}) + account_list = frappe.get_all( + "Account", filters={"company": company, "account_type": "Cash", "is_group": 0} + ) if len(account_list) == 1: account = account_list[0].name if account: - account_details = frappe.db.get_value("Account", account, - ["account_currency", "account_type"], as_dict=1) + account_details = frappe.db.get_value( + "Account", account, ["account_currency", "account_type"], as_dict=1 + ) + + return frappe._dict( + { + "account": account, + "balance": get_balance_on(account), + "account_currency": account_details.account_currency, + "account_type": account_details.account_type, + } + ) + else: + return frappe._dict() - return frappe._dict({ - "account": account, - "balance": get_balance_on(account), - "account_currency": account_details.account_currency, - "account_type": account_details.account_type - }) - else: return frappe._dict() @frappe.whitelist() -def get_payment_entry_against_order(dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None): +def get_payment_entry_against_order( + dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None +): ref_doc = frappe.get_doc(dt, dn) if flt(ref_doc.per_billed, 2) > 0: @@ -845,22 +1055,28 @@ def get_payment_entry_against_order(dt, dn, amount=None, debit_in_account_curren else: amount = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) - return get_payment_entry(ref_doc, { - "party_type": party_type, - "party_account": party_account, - "party_account_currency": party_account_currency, - "amount_field_party": amount_field_party, - "amount_field_bank": amount_field_bank, - "amount": amount, - "debit_in_account_currency": debit_in_account_currency, - "remarks": 'Advance Payment received against {0} {1}'.format(dt, dn), - "is_advance": "Yes", - "bank_account": bank_account, - "journal_entry": journal_entry - }) + return get_payment_entry( + ref_doc, + { + "party_type": party_type, + "party_account": party_account, + "party_account_currency": party_account_currency, + "amount_field_party": amount_field_party, + "amount_field_bank": amount_field_bank, + "amount": amount, + "debit_in_account_currency": debit_in_account_currency, + "remarks": "Advance Payment received against {0} {1}".format(dt, dn), + "is_advance": "Yes", + "bank_account": bank_account, + "journal_entry": journal_entry, + }, + ) + @frappe.whitelist() -def get_payment_entry_against_invoice(dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None): +def get_payment_entry_against_invoice( + dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None +): ref_doc = frappe.get_doc(dt, dn) if dt == "Sales Invoice": party_type = "Customer" @@ -869,73 +1085,91 @@ def get_payment_entry_against_invoice(dt, dn, amount=None, debit_in_account_cur party_type = "Supplier" party_account = ref_doc.credit_to - if (dt == "Sales Invoice" and ref_doc.outstanding_amount > 0) \ - or (dt == "Purchase Invoice" and ref_doc.outstanding_amount < 0): - amount_field_party = "credit_in_account_currency" - amount_field_bank = "debit_in_account_currency" + if (dt == "Sales Invoice" and ref_doc.outstanding_amount > 0) or ( + dt == "Purchase Invoice" and ref_doc.outstanding_amount < 0 + ): + amount_field_party = "credit_in_account_currency" + amount_field_bank = "debit_in_account_currency" else: amount_field_party = "debit_in_account_currency" amount_field_bank = "credit_in_account_currency" - return get_payment_entry(ref_doc, { - "party_type": party_type, - "party_account": party_account, - "party_account_currency": ref_doc.party_account_currency, - "amount_field_party": amount_field_party, - "amount_field_bank": amount_field_bank, - "amount": amount if amount else abs(ref_doc.outstanding_amount), - "debit_in_account_currency": debit_in_account_currency, - "remarks": 'Payment received against {0} {1}. {2}'.format(dt, dn, ref_doc.remarks), - "is_advance": "No", - "bank_account": bank_account, - "journal_entry": journal_entry - }) + return get_payment_entry( + ref_doc, + { + "party_type": party_type, + "party_account": party_account, + "party_account_currency": ref_doc.party_account_currency, + "amount_field_party": amount_field_party, + "amount_field_bank": amount_field_bank, + "amount": amount if amount else abs(ref_doc.outstanding_amount), + "debit_in_account_currency": debit_in_account_currency, + "remarks": "Payment received against {0} {1}. {2}".format(dt, dn, ref_doc.remarks), + "is_advance": "No", + "bank_account": bank_account, + "journal_entry": journal_entry, + }, + ) + def get_payment_entry(ref_doc, args): - cost_center = ref_doc.get("cost_center") or frappe.get_cached_value('Company', ref_doc.company, "cost_center") + cost_center = ref_doc.get("cost_center") or frappe.get_cached_value( + "Company", ref_doc.company, "cost_center" + ) exchange_rate = 1 if args.get("party_account"): # Modified to include the posting date for which the exchange rate is required. # Assumed to be the posting date in the reference document - exchange_rate = get_exchange_rate(ref_doc.get("posting_date") or ref_doc.get("transaction_date"), - args.get("party_account"), args.get("party_account_currency"), - ref_doc.company, ref_doc.doctype, ref_doc.name) + exchange_rate = get_exchange_rate( + ref_doc.get("posting_date") or ref_doc.get("transaction_date"), + args.get("party_account"), + args.get("party_account_currency"), + ref_doc.company, + ref_doc.doctype, + ref_doc.name, + ) je = frappe.new_doc("Journal Entry") - je.update({ - "voucher_type": "Bank Entry", - "company": ref_doc.company, - "remark": args.get("remarks") - }) + je.update( + {"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")} + ) - party_row = je.append("accounts", { - "account": args.get("party_account"), - "party_type": args.get("party_type"), - "party": ref_doc.get(args.get("party_type").lower()), - "cost_center": cost_center, - "account_type": frappe.db.get_value("Account", args.get("party_account"), "account_type"), - "account_currency": args.get("party_account_currency") or \ - get_account_currency(args.get("party_account")), - "balance": get_balance_on(args.get("party_account")), - "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")), - "exchange_rate": exchange_rate, - args.get("amount_field_party"): args.get("amount"), - "is_advance": args.get("is_advance"), - "reference_type": ref_doc.doctype, - "reference_name": ref_doc.name - }) + party_row = je.append( + "accounts", + { + "account": args.get("party_account"), + "party_type": args.get("party_type"), + "party": ref_doc.get(args.get("party_type").lower()), + "cost_center": cost_center, + "account_type": frappe.db.get_value("Account", args.get("party_account"), "account_type"), + "account_currency": args.get("party_account_currency") + or get_account_currency(args.get("party_account")), + "balance": get_balance_on(args.get("party_account")), + "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")), + "exchange_rate": exchange_rate, + args.get("amount_field_party"): args.get("amount"), + "is_advance": args.get("is_advance"), + "reference_type": ref_doc.doctype, + "reference_name": ref_doc.name, + }, + ) bank_row = je.append("accounts") # Make it bank_details - bank_account = get_default_bank_cash_account(ref_doc.company, "Bank", account=args.get("bank_account")) + bank_account = get_default_bank_cash_account( + ref_doc.company, "Bank", account=args.get("bank_account") + ) if bank_account: bank_row.update(bank_account) # Modified to include the posting date for which the exchange rate is required. # Assumed to be the posting date of the reference date - bank_row.exchange_rate = get_exchange_rate(ref_doc.get("posting_date") - or ref_doc.get("transaction_date"), bank_account["account"], - bank_account["account_currency"], ref_doc.company) + bank_row.exchange_rate = get_exchange_rate( + ref_doc.get("posting_date") or ref_doc.get("transaction_date"), + bank_account["account"], + bank_account["account_currency"], + ref_doc.company, + ) bank_row.cost_center = cost_center @@ -947,9 +1181,10 @@ def get_payment_entry(ref_doc, args): bank_row.set(args.get("amount_field_bank"), amount * exchange_rate) # Multi currency check again - if party_row.account_currency != ref_doc.company_currency \ - or (bank_row.account_currency and bank_row.account_currency != ref_doc.company_currency): - je.multi_currency = 1 + if party_row.account_currency != ref_doc.company_currency or ( + bank_row.account_currency and bank_row.account_currency != ref_doc.company_currency + ): + je.multi_currency = 1 je.set_amounts_in_company_currency() je.set_total_debit_credit() @@ -960,13 +1195,17 @@ def get_payment_entry(ref_doc, args): @frappe.whitelist() def get_opening_accounts(company): """get all balance sheet accounts for opening entry""" - accounts = frappe.db.sql_list("""select + accounts = frappe.db.sql_list( + """select name from tabAccount where is_group=0 and report_type='Balance Sheet' and company={0} and name not in (select distinct account from tabWarehouse where account is not null and account != '') - order by name asc""".format(frappe.db.escape(company))) + order by name asc""".format( + frappe.db.escape(company) + ) + ) return [{"account": a, "balance": get_balance_on(a)} for a in accounts] @@ -974,10 +1213,11 @@ def get_opening_accounts(company): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_against_jv(doctype, txt, searchfield, start, page_len, filters): - if not frappe.db.has_column('Journal Entry', searchfield): + if not frappe.db.has_column("Journal Entry", searchfield): return [] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT jv.name, jv.posting_date, jv.user_remark FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail WHERE jv_detail.parent = jv.name @@ -991,14 +1231,17 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters): AND jv.`{0}` LIKE %(txt)s ORDER BY jv.name DESC LIMIT %(offset)s, %(limit)s - """.format(searchfield), dict( - account=filters.get("account"), - party=cstr(filters.get("party")), - txt="%{0}%".format(txt), - offset=start, - limit=page_len - ) - ) + """.format( + searchfield + ), + dict( + account=filters.get("account"), + party=cstr(filters.get("party")), + txt="%{0}%".format(txt), + offset=start, + limit=page_len, + ), + ) @frappe.whitelist() @@ -1014,37 +1257,55 @@ def get_outstanding(args): if args.get("doctype") == "Journal Entry": condition = " and party=%(party)s" if args.get("party") else "" - against_jv_amount = frappe.db.sql(""" + against_jv_amount = frappe.db.sql( + """ select sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {0} - and (reference_type is null or reference_type = '')""".format(condition), args) + and (reference_type is null or reference_type = '')""".format( + condition + ), + args, + ) against_jv_amount = flt(against_jv_amount[0][0]) if against_jv_amount else 0 - amount_field = "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency" - return { - amount_field: abs(against_jv_amount) - } + amount_field = ( + "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency" + ) + return {amount_field: abs(against_jv_amount)} elif args.get("doctype") in ("Sales Invoice", "Purchase Invoice"): party_type = "Customer" if args.get("doctype") == "Sales Invoice" else "Supplier" - invoice = frappe.db.get_value(args["doctype"], args["docname"], - ["outstanding_amount", "conversion_rate", scrub(party_type)], as_dict=1) + invoice = frappe.db.get_value( + args["doctype"], + args["docname"], + ["outstanding_amount", "conversion_rate", scrub(party_type)], + as_dict=1, + ) - exchange_rate = invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1 + exchange_rate = ( + invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1 + ) if args["doctype"] == "Sales Invoice": - amount_field = "credit_in_account_currency" \ - if flt(invoice.outstanding_amount) > 0 else "debit_in_account_currency" + amount_field = ( + "credit_in_account_currency" + if flt(invoice.outstanding_amount) > 0 + else "debit_in_account_currency" + ) else: - amount_field = "debit_in_account_currency" \ - if flt(invoice.outstanding_amount) > 0 else "credit_in_account_currency" + amount_field = ( + "debit_in_account_currency" + if flt(invoice.outstanding_amount) > 0 + else "credit_in_account_currency" + ) return { amount_field: abs(flt(invoice.outstanding_amount)), "exchange_rate": exchange_rate, "party_type": party_type, - "party": invoice.get(scrub(party_type)) + "party": invoice.get(scrub(party_type)), } + @frappe.whitelist() def get_party_account_and_balance(company, party_type, party, cost_center=None): if not frappe.has_permission("Account"): @@ -1053,24 +1314,30 @@ def get_party_account_and_balance(company, party_type, party, cost_center=None): account = get_party_account(party_type, party, company) account_balance = get_balance_on(account=account, cost_center=cost_center) - party_balance = get_balance_on(party_type=party_type, party=party, company=company, cost_center=cost_center) + party_balance = get_balance_on( + party_type=party_type, party=party, company=company, cost_center=cost_center + ) return { "account": account, "balance": account_balance, "party_balance": party_balance, - "account_currency": frappe.db.get_value("Account", account, "account_currency") + "account_currency": frappe.db.get_value("Account", account, "account_currency"), } @frappe.whitelist() -def get_account_balance_and_party_type(account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None): +def get_account_balance_and_party_type( + account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None +): """Returns dict of account balance and party type to be set in Journal Entry on selection of account.""" if not frappe.has_permission("Account"): frappe.msgprint(_("No Permission"), raise_exception=1) company_currency = erpnext.get_company_currency(company) - account_details = frappe.db.get_value("Account", account, ["account_type", "account_currency"], as_dict=1) + account_details = frappe.db.get_value( + "Account", account, ["account_type", "account_currency"], as_dict=1 + ) if not account_details: return @@ -1087,11 +1354,17 @@ def get_account_balance_and_party_type(account, date, company, debit=None, credi "party_type": party_type, "account_type": account_details.account_type, "account_currency": account_details.account_currency or company_currency, - # The date used to retreive the exchange rate here is the date passed in # as an argument to this function. It is assumed to be the date on which the balance is sought - "exchange_rate": get_exchange_rate(date, account, account_details.account_currency, - company, debit=debit, credit=credit, exchange_rate=exchange_rate) + "exchange_rate": get_exchange_rate( + date, + account, + account_details.account_currency, + company, + debit=debit, + credit=credit, + exchange_rate=exchange_rate, + ), } # un-set party if not party type @@ -1102,11 +1375,22 @@ def get_account_balance_and_party_type(account, date, company, debit=None, credi @frappe.whitelist() -def get_exchange_rate(posting_date, account=None, account_currency=None, company=None, - reference_type=None, reference_name=None, debit=None, credit=None, exchange_rate=None): +def get_exchange_rate( + posting_date, + account=None, + account_currency=None, + company=None, + reference_type=None, + reference_name=None, + debit=None, + credit=None, + exchange_rate=None, +): from erpnext.setup.utils import get_exchange_rate - account_details = frappe.db.get_value("Account", account, - ["account_type", "root_type", "account_currency", "company"], as_dict=1) + + account_details = frappe.db.get_value( + "Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1 + ) if not account_details: frappe.throw(_("Please select correct account")) @@ -1125,7 +1409,7 @@ def get_exchange_rate(posting_date, account=None, account_currency=None, company # The date used to retreive the exchange rate here is the date passed # in as an argument to this function. - elif (not exchange_rate or flt(exchange_rate)==1) and account_currency and posting_date: + elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date: exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) else: exchange_rate = 1 @@ -1144,15 +1428,17 @@ def get_average_exchange_rate(account): return exchange_rate + @frappe.whitelist() def make_inter_company_journal_entry(name, voucher_type, company): - journal_entry = frappe.new_doc('Journal Entry') + journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = voucher_type journal_entry.company = company journal_entry.posting_date = nowdate() journal_entry.inter_company_journal_entry_reference = name return journal_entry.as_dict() + @frappe.whitelist() def make_reverse_journal_entry(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc @@ -1160,24 +1446,25 @@ def make_reverse_journal_entry(source_name, target_doc=None): def post_process(source, target): target.reversal_of = source.name - doclist = get_mapped_doc("Journal Entry", source_name, { - "Journal Entry": { - "doctype": "Journal Entry", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Journal Entry", + source_name, + { + "Journal Entry": {"doctype": "Journal Entry", "validation": {"docstatus": ["=", 1]}}, + "Journal Entry Account": { + "doctype": "Journal Entry Account", + "field_map": { + "account_currency": "account_currency", + "exchange_rate": "exchange_rate", + "debit_in_account_currency": "credit_in_account_currency", + "debit": "credit", + "credit_in_account_currency": "debit_in_account_currency", + "credit": "debit", + }, + }, }, - "Journal Entry Account": { - "doctype": "Journal Entry Account", - "field_map": { - "account_currency": "account_currency", - "exchange_rate": "exchange_rate", - "debit_in_account_currency": "credit_in_account_currency", - "debit": "credit", - "credit_in_account_currency": "debit_in_account_currency", - "credit": "debit", - } - }, - }, target_doc, post_process) + target_doc, + post_process, + ) return doclist diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 481462b2aba..2cc5378e927 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -39,14 +39,25 @@ class TestJournalEntry(unittest.TestCase): test_voucher.submit() if test_voucher.doctype == "Journal Entry": - self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` + self.assertTrue( + frappe.db.sql( + """select name from `tabJournal Entry Account` where account = %s and docstatus = 1 and parent = %s""", - ("_Test Receivable - _TC", test_voucher.name))) + ("_Test Receivable - _TC", test_voucher.name), + ) + ) - self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_type = %s and reference_name = %s""", (test_voucher.doctype, test_voucher.name))) + self.assertFalse( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_type = %s and reference_name = %s""", + (test_voucher.doctype, test_voucher.name), + ) + ) - base_jv.get("accounts")[0].is_advance = "Yes" if (test_voucher.doctype in ["Sales Order", "Purchase Order"]) else "No" + base_jv.get("accounts")[0].is_advance = ( + "Yes" if (test_voucher.doctype in ["Sales Order", "Purchase Order"]) else "No" + ) base_jv.get("accounts")[0].set("reference_type", test_voucher.doctype) base_jv.get("accounts")[0].set("reference_name", test_voucher.name) base_jv.insert() @@ -54,18 +65,28 @@ class TestJournalEntry(unittest.TestCase): submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name) - self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_type = %s and reference_name = %s and {0}=400""".format(dr_or_cr), - (submitted_voucher.doctype, submitted_voucher.name))) + self.assertTrue( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_type = %s and reference_name = %s and {0}=400""".format( + dr_or_cr + ), + (submitted_voucher.doctype, submitted_voucher.name), + ) + ) if base_jv.get("accounts")[0].is_advance == "Yes": self.advance_paid_testcase(base_jv, submitted_voucher, dr_or_cr) self.cancel_against_voucher_testcase(submitted_voucher) def advance_paid_testcase(self, base_jv, test_voucher, dr_or_cr): - #Test advance paid field - advance_paid = frappe.db.sql("""select advance_paid from `tab%s` - where name=%s""" % (test_voucher.doctype, '%s'), (test_voucher.name)) + # Test advance paid field + advance_paid = frappe.db.sql( + """select advance_paid from `tab%s` + where name=%s""" + % (test_voucher.doctype, "%s"), + (test_voucher.name), + ) payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr) self.assertTrue(flt(advance_paid[0][0]) == flt(payment_against_order)) @@ -74,13 +95,19 @@ class TestJournalEntry(unittest.TestCase): if test_voucher.doctype == "Journal Entry": # if test_voucher is a Journal Entry, test cancellation of test_voucher test_voucher.cancel() - self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_type='Journal Entry' and reference_name=%s""", test_voucher.name)) + self.assertFalse( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_type='Journal Entry' and reference_name=%s""", + test_voucher.name, + ) + ) elif test_voucher.doctype in ["Sales Order", "Purchase Order"]: # if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 0) + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 + ) submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name) self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel) @@ -89,7 +116,10 @@ class TestJournalEntry(unittest.TestCase): stock_account = get_inventory_account(company) from erpnext.accounts.utils import get_stock_and_account_balance - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company) + + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( + stock_account, nowdate(), company + ) diff = flt(account_bal) - flt(stock_bal) if not diff: @@ -98,19 +128,25 @@ class TestJournalEntry(unittest.TestCase): jv = frappe.new_doc("Journal Entry") jv.company = company jv.posting_date = nowdate() - jv.append("accounts", { - "account": stock_account, - "cost_center": "Main - TCP1", - "debit_in_account_currency": 0 if diff > 0 else abs(diff), - "credit_in_account_currency": diff if diff > 0 else 0 - }) + jv.append( + "accounts", + { + "account": stock_account, + "cost_center": "Main - TCP1", + "debit_in_account_currency": 0 if diff > 0 else abs(diff), + "credit_in_account_currency": diff if diff > 0 else 0, + }, + ) - jv.append("accounts", { - "account": "Stock Adjustment - TCP1", - "cost_center": "Main - TCP1", - "debit_in_account_currency": diff if diff > 0 else 0, - "credit_in_account_currency": 0 if diff > 0 else abs(diff) - }) + jv.append( + "accounts", + { + "account": "Stock Adjustment - TCP1", + "cost_center": "Main - TCP1", + "debit_in_account_currency": diff if diff > 0 else 0, + "credit_in_account_currency": 0 if diff > 0 else abs(diff), + }, + ) jv.insert() if account_bal == stock_bal: @@ -121,16 +157,21 @@ class TestJournalEntry(unittest.TestCase): jv.cancel() def test_multi_currency(self): - jv = make_journal_entry("_Test Bank USD - _TC", - "_Test Bank - _TC", 100, exchange_rate=50, save=False) + jv = make_journal_entry( + "_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False + ) jv.get("accounts")[1].credit_in_account_currency = 5000 jv.submit() - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s - order by account asc""", jv.name, as_dict=1) + order by account asc""", + jv.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -140,33 +181,42 @@ class TestJournalEntry(unittest.TestCase): "debit": 5000, "debit_in_account_currency": 100, "credit": 0, - "credit_in_account_currency": 0 + "credit_in_account_currency": 0, }, "_Test Bank - _TC": { "account_currency": "INR", "debit": 0, "debit_in_account_currency": 0, "credit": 5000, - "credit_in_account_currency": 5000 - } + "credit_in_account_currency": 5000, + }, } - for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): + for field in ( + "account_currency", + "debit", + "debit_in_account_currency", + "credit", + "credit_in_account_currency", + ): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) # cancel jv.cancel() - gle = frappe.db.sql("""select name from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", jv.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + jv.name, + ) self.assertFalse(gle) def test_reverse_journal_entry(self): from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry - jv = make_journal_entry("_Test Bank USD - _TC", - "Sales - _TC", 100, exchange_rate=50, save=False) + + jv = make_journal_entry("_Test Bank USD - _TC", "Sales - _TC", 100, exchange_rate=50, save=False) jv.get("accounts")[1].credit_in_account_currency = 5000 jv.get("accounts")[1].exchange_rate = 1 @@ -176,15 +226,17 @@ class TestJournalEntry(unittest.TestCase): rjv.posting_date = nowdate() rjv.submit() - - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s - order by account asc""", rjv.name, as_dict=1) + order by account asc""", + rjv.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = { "_Test Bank USD - _TC": { "account_currency": "USD", @@ -199,44 +251,38 @@ class TestJournalEntry(unittest.TestCase): "debit_in_account_currency": 5000, "credit": 0, "credit_in_account_currency": 0, - } + }, } - for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): + for field in ( + "account_currency", + "debit", + "debit_in_account_currency", + "credit", + "credit_in_account_currency", + ): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) def test_disallow_change_in_account_currency_for_a_party(self): # create jv in USD - jv = make_journal_entry("_Test Bank USD - _TC", - "_Test Receivable USD - _TC", 100, save=False) + jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False) - jv.accounts[1].update({ - "party_type": "Customer", - "party": "_Test Customer USD" - }) + jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"}) jv.submit() # create jv in USD, but account currency in INR - jv = make_journal_entry("_Test Bank - _TC", - "_Test Receivable - _TC", 100, save=False) + jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False) - jv.accounts[1].update({ - "party_type": "Customer", - "party": "_Test Customer USD" - }) + jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"}) self.assertRaises(InvalidAccountCurrency, jv.submit) # back in USD - jv = make_journal_entry("_Test Bank USD - _TC", - "_Test Receivable USD - _TC", 100, save=False) + jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False) - jv.accounts[1].update({ - "party_type": "Customer", - "party": "_Test Customer USD" - }) + jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"}) jv.submit() @@ -245,13 +291,27 @@ class TestJournalEntry(unittest.TestCase): frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1) frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1) frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1) - jv = make_journal_entry("Sales Expenses - _TC", "Buildings - _TC", 100, posting_date=nowdate(), cost_center = "Main - _TC", save=False) + jv = make_journal_entry( + "Sales Expenses - _TC", + "Buildings - _TC", + 100, + posting_date=nowdate(), + cost_center="Main - _TC", + save=False, + ) jv.voucher_type = "Inter Company Journal Entry" jv.multi_currency = 0 jv.insert() jv.submit() - jv1 = make_journal_entry("Sales Expenses - _TC1", "Buildings - _TC1", 100, posting_date=nowdate(), cost_center = "Main - _TC1", save=False) + jv1 = make_journal_entry( + "Sales Expenses - _TC1", + "Buildings - _TC1", + 100, + posting_date=nowdate(), + cost_center="Main - _TC1", + save=False, + ) jv1.inter_company_journal_entry_reference = jv.name jv1.company = "_Test Company 1" jv1.voucher_type = "Inter Company Journal Entry" @@ -273,9 +333,12 @@ class TestJournalEntry(unittest.TestCase): def test_jv_with_cost_centre(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center = cost_center, save=False) + jv = make_journal_entry( + "_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center=cost_center, save=False + ) jv.voucher_type = "Bank Entry" jv.multi_currency = 0 jv.cheque_no = "112233" @@ -284,17 +347,17 @@ class TestJournalEntry(unittest.TestCase): jv.submit() expected_values = { - "_Test Cash - _TC": { - "cost_center": cost_center - }, - "_Test Bank - _TC": { - "cost_center": cost_center - } + "_Test Cash - _TC": {"cost_center": cost_center}, + "_Test Bank - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, debit, credit + gl_entries = frappe.db.sql( + """select account, cost_center, debit, credit from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s - order by account asc""", jv.name, as_dict=1) + order by account asc""", + jv.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -305,11 +368,13 @@ class TestJournalEntry(unittest.TestCase): from erpnext.projects.doctype.project.test_project import make_project if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}): - project = make_project({ - 'project_name': 'Journal Entry Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2020-01-01' - }) + project = make_project( + { + "project_name": "Journal Entry Project", + "project_template_name": "Test Project Template", + "start_date": "2020-01-01", + } + ) project_name = project.name else: project_name = frappe.get_value("Project", {"project_name": "_Test Project"}) @@ -325,17 +390,17 @@ class TestJournalEntry(unittest.TestCase): jv.submit() expected_values = { - "_Test Cash - _TC": { - "project": project_name - }, - "_Test Bank - _TC": { - "project": project_name - } + "_Test Cash - _TC": {"project": project_name}, + "_Test Bank - _TC": {"project": project_name}, } - gl_entries = frappe.db.sql("""select account, project, debit, credit + gl_entries = frappe.db.sql( + """select account, project, debit, credit from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s - order by account asc""", jv.name, as_dict=1) + order by account asc""", + jv.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -345,9 +410,12 @@ class TestJournalEntry(unittest.TestCase): def test_jv_account_and_party_balance_with_cost_centre(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.utils import get_balance_on + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center = cost_center, save=False) + jv = make_journal_entry( + "_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center=cost_center, save=False + ) account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center) jv.voucher_type = "Bank Entry" jv.multi_currency = 0 @@ -360,7 +428,18 @@ class TestJournalEntry(unittest.TestCase): account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center) self.assertEqual(expected_account_balance, account_balance) -def make_journal_entry(account1, account2, amount, cost_center=None, posting_date=None, exchange_rate=1, save=True, submit=False, project=None): + +def make_journal_entry( + account1, + account2, + amount, + cost_center=None, + posting_date=None, + exchange_rate=1, + save=True, + submit=False, + project=None, +): if not cost_center: cost_center = "_Test Cost Center - _TC" @@ -369,23 +448,27 @@ def make_journal_entry(account1, account2, amount, cost_center=None, posting_dat jv.company = "_Test Company" jv.user_remark = "test" jv.multi_currency = 1 - jv.set("accounts", [ - { - "account": account1, - "cost_center": cost_center, - "project": project, - "debit_in_account_currency": amount if amount > 0 else 0, - "credit_in_account_currency": abs(amount) if amount < 0 else 0, - "exchange_rate": exchange_rate - }, { - "account": account2, - "cost_center": cost_center, - "project": project, - "credit_in_account_currency": amount if amount > 0 else 0, - "debit_in_account_currency": abs(amount) if amount < 0 else 0, - "exchange_rate": exchange_rate - } - ]) + jv.set( + "accounts", + [ + { + "account": account1, + "cost_center": cost_center, + "project": project, + "debit_in_account_currency": amount if amount > 0 else 0, + "credit_in_account_currency": abs(amount) if amount < 0 else 0, + "exchange_rate": exchange_rate, + }, + { + "account": account2, + "cost_center": cost_center, + "project": project, + "credit_in_account_currency": amount if amount > 0 else 0, + "debit_in_account_currency": abs(amount) if amount < 0 else 0, + "exchange_rate": exchange_rate, + }, + ], + ) if save or submit: jv.insert() @@ -394,4 +477,5 @@ def make_journal_entry(account1, account2, amount, cost_center=None, posting_dat return jv -test_records = frappe.get_test_records('Journal Entry') + +test_records = frappe.get_test_records("Journal Entry") diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py index 2da72c20ad8..b8ef3545d33 100644 --- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py +++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py @@ -9,6 +9,7 @@ from frappe.model.document import Document class JournalEntryTemplate(Document): pass + @frappe.whitelist() def get_naming_series(): return frappe.get_meta("Journal Entry").get_field("naming_series").options diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py index f460b9f7953..dcb43fb2cb3 100644 --- a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py +++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py @@ -8,6 +8,7 @@ from frappe.utils import today exclude_from_linked_with = True + class LoyaltyPointEntry(Document): pass @@ -16,18 +17,28 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No if not expiry_date: expiry_date = today() - return frappe.db.sql(''' + return frappe.db.sql( + """ select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice from `tabLoyalty Point Entry` where customer=%s and loyalty_program=%s and expiry_date>=%s and loyalty_points>0 and company=%s order by expiry_date - ''', (customer, loyalty_program, expiry_date, company), as_dict=1) + """, + (customer, loyalty_program, expiry_date, company), + as_dict=1, + ) + def get_redemption_details(customer, loyalty_program, company): - return frappe._dict(frappe.db.sql(''' + return frappe._dict( + frappe.db.sql( + """ select redeem_against, sum(loyalty_points) from `tabLoyalty Point Entry` where customer=%s and loyalty_program=%s and loyalty_points<0 and company=%s group by redeem_against - ''', (customer, loyalty_program, company))) + """, + (customer, loyalty_program, company), + ) + ) diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py index 70da03b27f3..48a25ad6b81 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -12,39 +12,61 @@ class LoyaltyProgram(Document): pass -def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=None, include_expired_entry=False): +def get_loyalty_details( + customer, loyalty_program, expiry_date=None, company=None, include_expired_entry=False +): if not expiry_date: expiry_date = today() - condition = '' + condition = "" if company: condition = " and company=%s " % frappe.db.escape(company) if not include_expired_entry: condition += " and expiry_date>='%s' " % expiry_date - loyalty_point_details = frappe.db.sql('''select sum(loyalty_points) as loyalty_points, + loyalty_point_details = frappe.db.sql( + """select sum(loyalty_points) as loyalty_points, sum(purchase_amount) as total_spent from `tabLoyalty Point Entry` where customer=%s and loyalty_program=%s and posting_date <= %s {condition} - group by customer'''.format(condition=condition), - (customer, loyalty_program, expiry_date), as_dict=1) + group by customer""".format( + condition=condition + ), + (customer, loyalty_program, expiry_date), + as_dict=1, + ) if loyalty_point_details: return loyalty_point_details[0] else: return {"loyalty_points": 0, "total_spent": 0} -@frappe.whitelist() -def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, \ - silent=False, include_expired_entry=False, current_transaction_amount=0): - lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent) - loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program) - lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)) - tier_spent_level = sorted([d.as_dict() for d in loyalty_program.collection_rules], - key=lambda rule:rule.min_spent, reverse=True) +@frappe.whitelist() +def get_loyalty_program_details_with_points( + customer, + loyalty_program=None, + expiry_date=None, + company=None, + silent=False, + include_expired_entry=False, + current_transaction_amount=0, +): + lp_details = get_loyalty_program_details( + customer, loyalty_program, company=company, silent=silent + ) + loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program) + lp_details.update( + get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry) + ) + + tier_spent_level = sorted( + [d.as_dict() for d in loyalty_program.collection_rules], + key=lambda rule: rule.min_spent, + reverse=True, + ) for i, d in enumerate(tier_spent_level): - if i==0 or (lp_details.total_spent+current_transaction_amount) <= d.min_spent: + if i == 0 or (lp_details.total_spent + current_transaction_amount) <= d.min_spent: lp_details.tier_name = d.tier_name lp_details.collection_factor = d.collection_factor else: @@ -52,8 +74,16 @@ def get_loyalty_program_details_with_points(customer, loyalty_program=None, expi return lp_details + @frappe.whitelist() -def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False): +def get_loyalty_program_details( + customer, + loyalty_program=None, + expiry_date=None, + company=None, + silent=False, + include_expired_entry=False, +): lp_details = frappe._dict() if not loyalty_program: @@ -72,6 +102,7 @@ def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None lp_details.update(loyalty_program.as_dict()) return lp_details + @frappe.whitelist() def get_redeemption_factor(loyalty_program=None, customer=None): customer_loyalty_program = None @@ -98,13 +129,16 @@ def validate_loyalty_points(ref_doc, points_to_redeem): else: loyalty_program = frappe.db.get_value("Customer", ref_doc.customer, ["loyalty_program"]) - if loyalty_program and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) !=\ - ref_doc.company: + if ( + loyalty_program + and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) != ref_doc.company + ): frappe.throw(_("The Loyalty Program isn't valid for the selected company")) if loyalty_program and points_to_redeem: - loyalty_program_details = get_loyalty_program_details_with_points(ref_doc.customer, loyalty_program, - posting_date, ref_doc.company) + loyalty_program_details = get_loyalty_program_details_with_points( + ref_doc.customer, loyalty_program, posting_date, ref_doc.company + ) if points_to_redeem > loyalty_program_details.loyalty_points: frappe.throw(_("You don't have enought Loyalty Points to redeem")) diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py index 7652e9691b4..3a4f908a1e9 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): return { - 'fieldname': 'loyalty_program', - 'transactions': [ - { - 'items': ['Sales Invoice', 'Customer'] - } - ] + "fieldname": "loyalty_program", + "transactions": [{"items": ["Sales Invoice", "Customer"]}], } diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py index 82c14324f5c..3641ac4428f 100644 --- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py @@ -19,19 +19,28 @@ class TestLoyaltyProgram(unittest.TestCase): create_records() def test_loyalty_points_earned_single_tier(self): - frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") + frappe.db.set_value( + "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" + ) # create a new sales invoice si_original = create_sales_invoice_record() si_original.insert() si_original.submit() - customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) + customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"}) earned_points = get_points_earned(si_original) - lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) + lpe = frappe.get_doc( + "Loyalty Point Entry", + { + "invoice_type": "Sales Invoice", + "invoice": si_original.name, + "customer": si_original.customer, + }, + ) - self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program) - self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier) + self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program) + self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier) self.assertEqual(lpe.loyalty_points, earned_points) # add redemption point @@ -43,21 +52,31 @@ class TestLoyaltyProgram(unittest.TestCase): earned_after_redemption = get_points_earned(si_redeem) - lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name}) - lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) + lpe_redeem = frappe.get_doc( + "Loyalty Point Entry", + {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "redeem_against": lpe.name}, + ) + lpe_earn = frappe.get_doc( + "Loyalty Point Entry", + {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "name": ["!=", lpe_redeem.name]}, + ) self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption) - self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points)) + self.assertEqual(lpe_redeem.loyalty_points, (-1 * earned_points)) # cancel and delete for d in [si_redeem, si_original]: d.cancel() def test_loyalty_points_earned_multiple_tier(self): - frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty") + frappe.db.set_value( + "Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty" + ) # assign multiple tier program to the customer - customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) - customer.loyalty_program = frappe.get_doc('Loyalty Program', {'loyalty_program_name': 'Test Multiple Loyalty'}).name + customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"}) + customer.loyalty_program = frappe.get_doc( + "Loyalty Program", {"loyalty_program_name": "Test Multiple Loyalty"} + ).name customer.save() # create a new sales invoice @@ -67,10 +86,17 @@ class TestLoyaltyProgram(unittest.TestCase): earned_points = get_points_earned(si_original) - lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) + lpe = frappe.get_doc( + "Loyalty Point Entry", + { + "invoice_type": "Sales Invoice", + "invoice": si_original.name, + "customer": si_original.customer, + }, + ) - self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program) - self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier) + self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program) + self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier) self.assertEqual(lpe.loyalty_points, earned_points) # add redemption point @@ -80,14 +106,20 @@ class TestLoyaltyProgram(unittest.TestCase): si_redeem.insert() si_redeem.submit() - customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) + customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"}) earned_after_redemption = get_points_earned(si_redeem) - lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name}) - lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) + lpe_redeem = frappe.get_doc( + "Loyalty Point Entry", + {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "redeem_against": lpe.name}, + ) + lpe_earn = frappe.get_doc( + "Loyalty Point Entry", + {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "name": ["!=", lpe_redeem.name]}, + ) self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption) - self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points)) + self.assertEqual(lpe_redeem.loyalty_points, (-1 * earned_points)) self.assertEqual(lpe_earn.loyalty_program_tier, customer.loyalty_program_tier) # cancel and delete @@ -95,23 +127,30 @@ class TestLoyaltyProgram(unittest.TestCase): d.cancel() def test_cancel_sales_invoice(self): - ''' cancelling the sales invoice should cancel the earned points''' - frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") + """cancelling the sales invoice should cancel the earned points""" + frappe.db.set_value( + "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" + ) # create a new sales invoice si = create_sales_invoice_record() si.insert() si.submit() - lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si.name, 'customer': si.customer}) + lpe = frappe.get_doc( + "Loyalty Point Entry", + {"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer}, + ) self.assertEqual(True, not (lpe is None)) # cancelling sales invoice si.cancel() - lpe = frappe.db.exists('Loyalty Point Entry', lpe.name) + lpe = frappe.db.exists("Loyalty Point Entry", lpe.name) self.assertEqual(True, (lpe is None)) def test_sales_invoice_return(self): - frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") + frappe.db.set_value( + "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" + ) # create a new sales invoice si_original = create_sales_invoice_record(2) si_original.conversion_rate = flt(1) @@ -119,7 +158,14 @@ class TestLoyaltyProgram(unittest.TestCase): si_original.submit() earned_points = get_points_earned(si_original) - lpe_original = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) + lpe_original = frappe.get_doc( + "Loyalty Point Entry", + { + "invoice_type": "Sales Invoice", + "invoice": si_original.name, + "customer": si_original.customer, + }, + ) self.assertEqual(lpe_original.loyalty_points, earned_points) # create sales invoice return @@ -131,10 +177,17 @@ class TestLoyaltyProgram(unittest.TestCase): si_return.submit() # fetch original invoice again as its status would have been updated - si_original = frappe.get_doc('Sales Invoice', lpe_original.invoice) + si_original = frappe.get_doc("Sales Invoice", lpe_original.invoice) earned_points = get_points_earned(si_original) - lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) + lpe_after_return = frappe.get_doc( + "Loyalty Point Entry", + { + "invoice_type": "Sales Invoice", + "invoice": si_original.name, + "customer": si_original.customer, + }, + ) self.assertEqual(lpe_after_return.loyalty_points, earned_points) self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points)) @@ -143,144 +196,164 @@ class TestLoyaltyProgram(unittest.TestCase): try: d.cancel() except frappe.TimestampMismatchError: - frappe.get_doc('Sales Invoice', d.name).cancel() + frappe.get_doc("Sales Invoice", d.name).cancel() def test_loyalty_points_for_dashboard(self): - doc = frappe.get_doc('Customer', 'Test Loyalty Customer') + doc = frappe.get_doc("Customer", "Test Loyalty Customer") company_wise_info = get_dashboard_info("Customer", doc.name, doc.loyalty_program) for d in company_wise_info: self.assertTrue(d.get("loyalty_points")) + def get_points_earned(self): def get_returned_amount(): - returned_amount = frappe.db.sql(""" + returned_amount = frappe.db.sql( + """ select sum(grand_total) from `tabSales Invoice` where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s - """, self.name) + """, + self.name, + ) return abs(flt(returned_amount[0][0])) if returned_amount else 0 - lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company, - loyalty_program=self.loyalty_program, expiry_date=self.posting_date, include_expired_entry=True) - if lp_details and getdate(lp_details.from_date) <= getdate(self.posting_date) and \ - (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)): + lp_details = get_loyalty_program_details_with_points( + self.customer, + company=self.company, + loyalty_program=self.loyalty_program, + expiry_date=self.posting_date, + include_expired_entry=True, + ) + if ( + lp_details + and getdate(lp_details.from_date) <= getdate(self.posting_date) + and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)) + ): returned_amount = get_returned_amount() eligible_amount = flt(self.grand_total) - cint(self.loyalty_amount) - returned_amount - points_earned = cint(eligible_amount/lp_details.collection_factor) + points_earned = cint(eligible_amount / lp_details.collection_factor) return points_earned or 0 + def create_sales_invoice_record(qty=1): # return sales invoice doc object - return frappe.get_doc({ - "doctype": "Sales Invoice", - "customer": frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}).name, - "company": '_Test Company', - "due_date": today(), - "posting_date": today(), - "currency": "INR", - "taxes_and_charges": "", - "debit_to": "Debtors - _TC", - "taxes": [], - "items": [{ - 'doctype': 'Sales Invoice Item', - 'item_code': frappe.get_doc('Item', {'item_name': 'Loyal Item'}).name, - 'qty': qty, - "rate": 10000, - 'income_account': 'Sales - _TC', - 'cost_center': 'Main - _TC', - 'expense_account': 'Cost of Goods Sold - _TC' - }] - }) + return frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"}).name, + "company": "_Test Company", + "due_date": today(), + "posting_date": today(), + "currency": "INR", + "taxes_and_charges": "", + "debit_to": "Debtors - _TC", + "taxes": [], + "items": [ + { + "doctype": "Sales Invoice Item", + "item_code": frappe.get_doc("Item", {"item_name": "Loyal Item"}).name, + "qty": qty, + "rate": 10000, + "income_account": "Sales - _TC", + "cost_center": "Main - _TC", + "expense_account": "Cost of Goods Sold - _TC", + } + ], + } + ) + def create_records(): # create a new loyalty Account if not frappe.db.exists("Account", "Loyalty - _TC"): - frappe.get_doc({ - "doctype": "Account", - "account_name": "Loyalty", - "parent_account": "Direct Expenses - _TC", - "company": "_Test Company", - "is_group": 0, - "account_type": "Expense Account", - }).insert() + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Loyalty", + "parent_account": "Direct Expenses - _TC", + "company": "_Test Company", + "is_group": 0, + "account_type": "Expense Account", + } + ).insert() # create a new loyalty program Single tier - if not frappe.db.exists("Loyalty Program","Test Single Loyalty"): - frappe.get_doc({ - "doctype": "Loyalty Program", - "loyalty_program_name": "Test Single Loyalty", - "auto_opt_in": 1, - "from_date": today(), - "loyalty_program_type": "Single Tier Program", - "conversion_factor": 1, - "expiry_duration": 10, - "company": "_Test Company", - "cost_center": "Main - _TC", - "expense_account": "Loyalty - _TC", - "collection_rules": [{ - 'tier_name': 'Silver', - 'collection_factor': 1000, - 'min_spent': 1000 - }] - }).insert() + if not frappe.db.exists("Loyalty Program", "Test Single Loyalty"): + frappe.get_doc( + { + "doctype": "Loyalty Program", + "loyalty_program_name": "Test Single Loyalty", + "auto_opt_in": 1, + "from_date": today(), + "loyalty_program_type": "Single Tier Program", + "conversion_factor": 1, + "expiry_duration": 10, + "company": "_Test Company", + "cost_center": "Main - _TC", + "expense_account": "Loyalty - _TC", + "collection_rules": [{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 1000}], + } + ).insert() # create a new customer - if not frappe.db.exists("Customer","Test Loyalty Customer"): - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "Test Loyalty Customer", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert() + if not frappe.db.exists("Customer", "Test Loyalty Customer"): + frappe.get_doc( + { + "customer_group": "_Test Customer Group", + "customer_name": "Test Loyalty Customer", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + } + ).insert() # create a new loyalty program Multiple tier - if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"): - frappe.get_doc({ - "doctype": "Loyalty Program", - "loyalty_program_name": "Test Multiple Loyalty", - "auto_opt_in": 1, - "from_date": today(), - "loyalty_program_type": "Multiple Tier Program", - "conversion_factor": 1, - "expiry_duration": 10, - "company": "_Test Company", - "cost_center": "Main - _TC", - "expense_account": "Loyalty - _TC", - "collection_rules": [ - { - 'tier_name': 'Silver', - 'collection_factor': 1000, - 'min_spent': 10000 - }, - { - 'tier_name': 'Gold', - 'collection_factor': 1000, - 'min_spent': 19000 - } - ] - }).insert() + if not frappe.db.exists("Loyalty Program", "Test Multiple Loyalty"): + frappe.get_doc( + { + "doctype": "Loyalty Program", + "loyalty_program_name": "Test Multiple Loyalty", + "auto_opt_in": 1, + "from_date": today(), + "loyalty_program_type": "Multiple Tier Program", + "conversion_factor": 1, + "expiry_duration": 10, + "company": "_Test Company", + "cost_center": "Main - _TC", + "expense_account": "Loyalty - _TC", + "collection_rules": [ + {"tier_name": "Silver", "collection_factor": 1000, "min_spent": 10000}, + {"tier_name": "Gold", "collection_factor": 1000, "min_spent": 19000}, + ], + } + ).insert() # create an item if not frappe.db.exists("Item", "Loyal Item"): - frappe.get_doc({ - "doctype": "Item", - "item_code": "Loyal Item", - "item_name": "Loyal Item", - "item_group": "All Item Groups", - "company": "_Test Company", - "is_stock_item": 1, - "opening_stock": 100, - "valuation_rate": 10000, - }).insert() + frappe.get_doc( + { + "doctype": "Item", + "item_code": "Loyal Item", + "item_name": "Loyal Item", + "item_group": "All Item Groups", + "company": "_Test Company", + "is_stock_item": 1, + "opening_stock": 100, + "valuation_rate": 10000, + } + ).insert() # create item price - if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}): - frappe.get_doc({ - "doctype": "Item Price", - "price_list": "Standard Selling", - "item_code": "Loyal Item", - "price_list_rate": 10000 - }).insert() + if not frappe.db.exists( + "Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"} + ): + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "Standard Selling", + "item_code": "Loyal Item", + "price_list_rate": 10000, + } + ).insert() diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py index f21d1b9baa9..d0373021a69 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py @@ -19,23 +19,35 @@ class ModeofPayment(Document): for entry in self.accounts: accounts_list.append(entry.company) - if len(accounts_list)!= len(set(accounts_list)): + if len(accounts_list) != len(set(accounts_list)): frappe.throw(_("Same Company is entered more than once")) def validate_accounts(self): for entry in self.accounts: """Error when Company of Ledger account doesn't match with Company Selected""" if frappe.db.get_value("Account", entry.default_account, "company") != entry.company: - frappe.throw(_("Account {0} does not match with Company {1} in Mode of Account: {2}") - .format(entry.default_account, entry.company, self.name)) + frappe.throw( + _("Account {0} does not match with Company {1} in Mode of Account: {2}").format( + entry.default_account, entry.company, self.name + ) + ) def validate_pos_mode_of_payment(self): if not self.enabled: - pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip - WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", (self.name)) + pos_profiles = frappe.db.sql( + """SELECT sip.parent FROM `tabSales Invoice Payment` sip + WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", + (self.name), + ) pos_profiles = list(map(lambda x: x[0], pos_profiles)) if pos_profiles: - message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \ - Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode." + message = ( + "POS Profile " + + frappe.bold(", ".join(pos_profiles)) + + " contains \ + Mode of Payment " + + frappe.bold(str(self.name)) + + ". Please remove them to disable this mode." + ) frappe.throw(_(message), title="Not Allowed") diff --git a/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py index 2ff02a7c4dc..9733fb89e2f 100644 --- a/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py +++ b/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Mode of Payment') + class TestModeofPayment(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py index a8c5f68c110..1d19708eddf 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py @@ -11,13 +11,25 @@ from frappe.utils import add_months, flt class MonthlyDistribution(Document): @frappe.whitelist() def get_months(self): - month_list = ['January','February','March','April','May','June','July','August','September', - 'October','November','December'] - idx =1 + month_list = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ] + idx = 1 for m in month_list: - mnth = self.append('percentages') + mnth = self.append("percentages") mnth.month = m - mnth.percentage_allocation = 100.0/12 + mnth.percentage_allocation = 100.0 / 12 mnth.idx = idx idx += 1 @@ -25,18 +37,15 @@ class MonthlyDistribution(Document): total = sum(flt(d.percentage_allocation) for d in self.get("percentages")) if flt(total, 2) != 100.0: - frappe.throw(_("Percentage Allocation should be equal to 100%") + \ - " ({0}%)".format(str(flt(total, 2)))) + frappe.throw( + _("Percentage Allocation should be equal to 100%") + " ({0}%)".format(str(flt(total, 2))) + ) + def get_periodwise_distribution_data(distribution_id, period_list, periodicity): - doc = frappe.get_doc('Monthly Distribution', distribution_id) + doc = frappe.get_doc("Monthly Distribution", distribution_id) - months_to_add = { - "Yearly": 12, - "Half-Yearly": 6, - "Quarterly": 3, - "Monthly": 1 - }[periodicity] + months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity] period_dict = {} @@ -45,6 +54,7 @@ def get_periodwise_distribution_data(distribution_id, period_list, periodicity): return period_dict + def get_percentage(doc, start_date, period): percentage = 0 months = [start_date.strftime("%B").title()] diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py index 3e6575f2540..ba2cb671d40 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py @@ -1,22 +1,16 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'monthly_distribution', - 'non_standard_fieldnames': { - 'Sales Person': 'distribution_id', - 'Territory': 'distribution_id', - 'Sales Partner': 'distribution_id', + "fieldname": "monthly_distribution", + "non_standard_fieldnames": { + "Sales Person": "distribution_id", + "Territory": "distribution_id", + "Sales Partner": "distribution_id", }, - 'transactions': [ - { - 'label': _('Target Details'), - 'items': ['Sales Person', 'Territory', 'Sales Partner'] - }, - { - 'items': ['Budget'] - } - ] + "transactions": [ + {"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]}, + {"items": ["Budget"]}, + ], } diff --git a/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py index 4a878b2aaf7..848b1f9de76 100644 --- a/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py +++ b/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py @@ -6,7 +6,8 @@ import unittest import frappe -test_records = frappe.get_test_records('Monthly Distribution') +test_records = frappe.get_test_records("Monthly Distribution") + class TestMonthlyDistribution(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json index daee8f8c1ab..c367b360e1f 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json @@ -75,7 +75,7 @@ ], "hide_toolbar": 1, "issingle": 1, - "modified": "2022-01-04 15:25:06.053187", + "modified": "2022-01-04 16:25:06.053187", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool", diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index a33892e044d..0f0ab68dcb8 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -20,9 +20,9 @@ class OpeningInvoiceCreationTool(Document): def onload(self): """Load the Opening Invoice summary""" summary, max_count = self.get_opening_invoice_summary() - self.set_onload('opening_invoices_summary', summary) - self.set_onload('max_count', max_count) - self.set_onload('temporary_opening_account', get_temporary_opening_account(self.company)) + self.set_onload("opening_invoices_summary", summary) + self.set_onload("max_count", max_count) + self.set_onload("temporary_opening_account", get_temporary_opening_account(self.company)) def get_opening_invoice_summary(self): def prepare_invoice_summary(doctype, invoices): @@ -32,10 +32,7 @@ class OpeningInvoiceCreationTool(Document): for invoice in invoices: company = invoice.pop("company") _summary = invoices_summary.get(company, {}) - _summary.update({ - "currency": company_wise_currency.get(company), - doctype: invoice - }) + _summary.update({"currency": company_wise_currency.get(company), doctype: invoice}) invoices_summary.update({company: _summary}) if invoice.paid_amount: @@ -44,17 +41,21 @@ class OpeningInvoiceCreationTool(Document): outstanding_amount.append(invoice.outstanding_amount) if paid_amount or outstanding_amount: - max_count.update({ - doctype: { - "max_paid": max(paid_amount) if paid_amount else 0.0, - "max_due": max(outstanding_amount) if outstanding_amount else 0.0 + max_count.update( + { + doctype: { + "max_paid": max(paid_amount) if paid_amount else 0.0, + "max_due": max(outstanding_amount) if outstanding_amount else 0.0, + } } - }) + ) invoices_summary = {} max_count = {} fields = [ - "company", "count(name) as total_invoices", "sum(outstanding_amount) as outstanding_amount" + "company", + "count(name) as total_invoices", + "sum(outstanding_amount) as outstanding_amount", ] companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"]) if not companies: @@ -62,8 +63,9 @@ class OpeningInvoiceCreationTool(Document): company_wise_currency = {row.company: row.currency for row in companies} for doctype in ["Sales Invoice", "Purchase Invoice"]: - invoices = frappe.get_all(doctype, filters=dict(is_opening="Yes", docstatus=1), - fields=fields, group_by="company") + invoices = frappe.get_all( + doctype, filters=dict(is_opening="Yes", docstatus=1), fields=fields, group_by="company" + ) prepare_invoice_summary(doctype, invoices) return invoices_summary, max_count @@ -74,7 +76,9 @@ class OpeningInvoiceCreationTool(Document): def set_missing_values(self, row): row.qty = row.qty or 1.0 - row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company) + row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account( + self.company + ) row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier" row.item_name = row.item_name or _("Opening Invoice Item") row.posting_date = row.posting_date or nowdate() @@ -85,7 +89,11 @@ class OpeningInvoiceCreationTool(Document): if self.create_missing_party: self.add_party(row.party_type, row.party) else: - frappe.throw(_("Row #{}: {} {} does not exist.").format(row.idx, frappe.bold(row.party_type), frappe.bold(row.party))) + frappe.throw( + _("Row #{}: {} {} does not exist.").format( + row.idx, frappe.bold(row.party_type), frappe.bold(row.party) + ) + ) mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices") for d in ("Party", "Outstanding Amount", "Temporary Opening Account"): @@ -100,12 +108,22 @@ class OpeningInvoiceCreationTool(Document): self.set_missing_values(row) self.validate_mandatory_invoice_fields(row) invoice = self.get_invoice_dict(row) - company_details = frappe.get_cached_value('Company', self.company, ["default_currency", "default_letter_head"], as_dict=1) or {} + company_details = ( + frappe.get_cached_value( + "Company", self.company, ["default_currency", "default_letter_head"], as_dict=1 + ) + or {} + ) + + default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency") + if company_details: - invoice.update({ - "currency": company_details.get("default_currency"), - "letter_head": company_details.get("default_letter_head") - }) + invoice.update( + { + "currency": default_currency or company_details.get("default_currency"), + "letter_head": company_details.get("default_letter_head"), + } + ) invoices.append(invoice) return invoices @@ -127,55 +145,61 @@ class OpeningInvoiceCreationTool(Document): def get_invoice_dict(self, row=None): def get_item_dict(): - cost_center = row.get('cost_center') or frappe.get_cached_value('Company', self.company, "cost_center") + cost_center = row.get("cost_center") or frappe.get_cached_value( + "Company", self.company, "cost_center" + ) if not cost_center: - frappe.throw(_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))) + frappe.throw( + _("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company)) + ) - income_expense_account_field = "income_account" if row.party_type == "Customer" else "expense_account" + income_expense_account_field = ( + "income_account" if row.party_type == "Customer" else "expense_account" + ) default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos") rate = flt(row.outstanding_amount) / flt(row.qty) - item_dict = frappe._dict({ - "uom": default_uom, - "rate": rate or 0.0, - "qty": row.qty, - "conversion_factor": 1.0, - "item_name": row.item_name or "Opening Invoice Item", - "description": row.item_name or "Opening Invoice Item", - income_expense_account_field: row.temporary_opening_account, - "cost_center": cost_center - }) + item_dict = frappe._dict( + { + "uom": default_uom, + "rate": rate or 0.0, + "qty": row.qty, + "conversion_factor": 1.0, + "item_name": row.item_name or "Opening Invoice Item", + "description": row.item_name or "Opening Invoice Item", + income_expense_account_field: row.temporary_opening_account, + "cost_center": cost_center, + } + ) for dimension in get_accounting_dimensions(): - item_dict.update({ - dimension: row.get(dimension) - }) + item_dict.update({dimension: row.get(dimension)}) return item_dict item = get_item_dict() - invoice = frappe._dict({ - "items": [item], - "is_opening": "Yes", - "set_posting_time": 1, - "company": self.company, - "cost_center": self.cost_center, - "due_date": row.due_date, - "posting_date": row.posting_date, - frappe.scrub(row.party_type): row.party, - "is_pos": 0, - "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", - "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559 - "invoice_number": row.invoice_number, - "disable_rounded_total": 1 - }) + invoice = frappe._dict( + { + "items": [item], + "is_opening": "Yes", + "set_posting_time": 1, + "company": self.company, + "cost_center": self.cost_center, + "due_date": row.due_date, + "posting_date": row.posting_date, + frappe.scrub(row.party_type): row.party, + "is_pos": 0, + "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", + "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559 + "invoice_number": row.invoice_number, + "disable_rounded_total": 1, + } + ) accounting_dimension = get_accounting_dimensions() for dimension in accounting_dimension: - invoice.update({ - dimension: self.get(dimension) or item.get(dimension) - }) + invoice.update({dimension: self.get(dimension) or item.get(dimension)}) return invoice @@ -201,9 +225,10 @@ class OpeningInvoiceCreationTool(Document): event="opening_invoice_creation", job_name=self.name, invoices=invoices, - now=frappe.conf.developer_mode or frappe.flags.in_test + now=frappe.conf.developer_mode or frappe.flags.in_test, ) + def start_import(invoices): errors = 0 names = [] @@ -222,35 +247,43 @@ def start_import(invoices): except Exception: errors += 1 frappe.db.rollback() - message = "\n".join(["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) + message = "\n".join( + ["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()] + ) frappe.log_error(title="Error while creating Opening Invoice", message=message) frappe.db.commit() if errors: - frappe.msgprint(_("You had {} errors while creating opening invoices. Check {} for more details") - .format(errors, "Error Log"), indicator="red", title=_("Error Occured")) + frappe.msgprint( + _("You had {} errors while creating opening invoices. Check {} for more details").format( + errors, "Error Log" + ), + indicator="red", + title=_("Error Occured"), + ) return names + def publish(index, total, doctype): - if total < 5: return + if total < 5: + return frappe.publish_realtime( "opening_invoice_creation_progress", dict( title=_("Opening Invoice Creation In Progress"), - message=_('Creating {} out of {} {}').format(index + 1, total, doctype), + message=_("Creating {} out of {} {}").format(index + 1, total, doctype), user=frappe.session.user, - count=index+1, - total=total - )) + count=index + 1, + total=total, + ), + ) + @frappe.whitelist() def get_temporary_opening_account(company=None): if not company: return - accounts = frappe.get_all("Account", filters={ - 'company': company, - 'account_type': 'Temporary' - }) + accounts = frappe.get_all("Account", filters={"company": company, "account_type": "Temporary"}) if not accounts: frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts")) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 3eaf6a28f37..1e22c64c8f2 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( create_dimension, @@ -10,11 +11,11 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] -class TestOpeningInvoiceCreationTool(ERPNextTestCase): + +class TestOpeningInvoiceCreationTool(FrappeTestCase): @classmethod def setUpClass(self): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): @@ -22,10 +23,24 @@ class TestOpeningInvoiceCreationTool(ERPNextTestCase): create_dimension() return super().setUpClass() - def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None): + def make_invoices( + self, + invoice_type="Sales", + company=None, + party_1=None, + party_2=None, + invoice_number=None, + department=None, + ): doc = frappe.get_single("Opening Invoice Creation Tool") - args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company, - party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department) + args = get_opening_invoice_creation_dict( + invoice_type=invoice_type, + company=company, + party_1=party_1, + party_2=party_2, + invoice_number=invoice_number, + department=department, + ) doc.update(args) return doc.make_invoices() @@ -68,15 +83,30 @@ class TestOpeningInvoiceCreationTool(ERPNextTestCase): company = "_Test Opening Invoice Company" party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") - old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account") + old_default_receivable_account = frappe.db.get_value( + "Company", company, "default_receivable_account" + ) frappe.db.set_value("Company", company, "default_receivable_account", "") if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"): - cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company", - "is_group": 1, "company": "_Test Opening Invoice Company"}) + cc = frappe.get_doc( + { + "doctype": "Cost Center", + "cost_center_name": "_Test Opening Invoice Company", + "is_group": 1, + "company": "_Test Opening Invoice Company", + } + ) cc.insert(ignore_mandatory=True) - cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0, - "company": "_Test Opening Invoice Company", "parent_cost_center": cc.name}) + cc2 = frappe.get_doc( + { + "doctype": "Cost Center", + "cost_center_name": "Main", + "is_group": 0, + "company": "_Test Opening Invoice Company", + "parent_cost_center": cc.name, + } + ) cc2.insert() frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC") @@ -84,28 +114,37 @@ class TestOpeningInvoiceCreationTool(ERPNextTestCase): self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2) # Check if missing debit account error raised - error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]}) + error_log = frappe.db.exists( + "Error Log", + {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]}, + ) self.assertTrue(error_log) # teardown - frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) + frappe.db.set_value( + "Company", company, "default_receivable_account", old_default_receivable_account + ) def test_renaming_of_invoice_using_invoice_number_field(self): company = "_Test Opening Invoice Company" party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") - self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11") + self.make_invoices( + company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11" + ) - sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name") - sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name") + sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name") + sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name") self.assertEqual(sales_inv1, "TEST-NEW-INV-11") - #teardown + # teardown for inv in [sales_inv1, sales_inv2]: - doc = frappe.get_doc('Sales Invoice', inv) + doc = frappe.get_doc("Sales Invoice", inv) doc.cancel() def test_opening_invoice_with_accounting_dimension(self): - invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC') + invoices = self.make_invoices( + invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC" + ) expected_value = { "keys": ["customer", "outstanding_amount", "status", "department"], @@ -117,40 +156,44 @@ class TestOpeningInvoiceCreationTool(ERPNextTestCase): def tearDown(self): disable_dimension() + def get_opening_invoice_creation_dict(**args): party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" company = args.get("company", "_Test Company") - invoice_dict = frappe._dict({ - "company": company, - "invoice_type": args.get("invoice_type", "Sales"), - "invoices": [ - { - "qty": 1.0, - "outstanding_amount": 300, - "party": args.get("party_1") or "_Test {0}".format(party), - "item_name": "Opening Item", - "due_date": "2016-09-10", - "posting_date": "2016-09-05", - "temporary_opening_account": get_temporary_opening_account(company), - "invoice_number": args.get("invoice_number") - }, - { - "qty": 2.0, - "outstanding_amount": 250, - "party": args.get("party_2") or "_Test {0} 1".format(party), - "item_name": "Opening Item", - "due_date": "2016-09-10", - "posting_date": "2016-09-05", - "temporary_opening_account": get_temporary_opening_account(company), - "invoice_number": None - } - ] - }) + invoice_dict = frappe._dict( + { + "company": company, + "invoice_type": args.get("invoice_type", "Sales"), + "invoices": [ + { + "qty": 1.0, + "outstanding_amount": 300, + "party": args.get("party_1") or "_Test {0}".format(party), + "item_name": "Opening Item", + "due_date": "2016-09-10", + "posting_date": "2016-09-05", + "temporary_opening_account": get_temporary_opening_account(company), + "invoice_number": args.get("invoice_number"), + }, + { + "qty": 2.0, + "outstanding_amount": 250, + "party": args.get("party_2") or "_Test {0} 1".format(party), + "item_name": "Opening Item", + "due_date": "2016-09-10", + "posting_date": "2016-09-05", + "temporary_opening_account": get_temporary_opening_account(company), + "invoice_number": None, + }, + ], + } + ) invoice_dict.update(args) return invoice_dict + def make_company(): if frappe.db.exists("Company", "_Test Opening Invoice Company"): return frappe.get_doc("Company", "_Test Opening Invoice Company") @@ -163,15 +206,18 @@ def make_company(): company.insert() return company + def make_customer(customer=None): customer_name = customer or "Opening Customer" - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": customer_name, - "customer_group": "All Customer Groups", - "customer_type": "Company", - "territory": "All Territories" - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": customer_name, + "customer_group": "All Customer Groups", + "customer_type": "Company", + "territory": "All Territories", + } + ) if not frappe.db.exists("Customer", customer_name): customer.insert(ignore_permissions=True) return customer.name diff --git a/erpnext/accounts/doctype/party_account/party_account.json b/erpnext/accounts/doctype/party_account/party_account.json index c9f15a6a470..69330577ab3 100644 --- a/erpnext/accounts/doctype/party_account/party_account.json +++ b/erpnext/accounts/doctype/party_account/party_account.json @@ -3,6 +3,7 @@ "creation": "2014-08-29 16:02:39.740505", "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "company", "account" @@ -11,6 +12,7 @@ { "fieldname": "company", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_list_view": 1, "label": "Company", "options": "Company", @@ -27,7 +29,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-07 18:13:08.833822", + "modified": "2022-04-04 12:31:02.994197", "modified_by": "Administrator", "module": "Accounts", "name": "Party Account", @@ -35,5 +37,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py index e9f813c17c6..d766aad9176 100644 --- a/erpnext/accounts/doctype/party_link/party_link.py +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -8,34 +8,43 @@ from frappe.model.document import Document class PartyLink(Document): def validate(self): - if self.primary_role not in ['Customer', 'Supplier']: - frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."), - title=_("Invalid Primary Role")) + if self.primary_role not in ["Customer", "Supplier"]: + frappe.throw( + _( + "Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only." + ), + title=_("Invalid Primary Role"), + ) - existing_party_link = frappe.get_all('Party Link', { - 'primary_party': self.secondary_party - }, pluck="primary_role") + existing_party_link = frappe.get_all( + "Party Link", {"primary_party": self.secondary_party}, pluck="primary_role" + ) if existing_party_link: - frappe.throw(_('{} {} is already linked with another {}') - .format(self.secondary_role, self.secondary_party, existing_party_link[0])) + frappe.throw( + _("{} {} is already linked with another {}").format( + self.secondary_role, self.secondary_party, existing_party_link[0] + ) + ) - existing_party_link = frappe.get_all('Party Link', { - 'secondary_party': self.primary_party - }, pluck="primary_role") + existing_party_link = frappe.get_all( + "Party Link", {"secondary_party": self.primary_party}, pluck="primary_role" + ) if existing_party_link: - frappe.throw(_('{} {} is already linked with another {}') - .format(self.primary_role, self.primary_party, existing_party_link[0])) + frappe.throw( + _("{} {} is already linked with another {}").format( + self.primary_role, self.primary_party, existing_party_link[0] + ) + ) @frappe.whitelist() def create_party_link(primary_role, primary_party, secondary_party): - party_link = frappe.new_doc('Party Link') + party_link = frappe.new_doc("Party Link") party_link.primary_role = primary_role party_link.primary_party = primary_party - party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier' + party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier" party_link.secondary_party = secondary_party party_link.save(ignore_permissions=True) return party_link - diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 345764fb418..3a89ce8cd12 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -226,10 +226,7 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.total_allocated_amount > party_amount))); frm.toggle_display("set_exchange_gain_loss", - (frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount && - ((frm.doc.paid_from_account_currency != company_currency || - frm.doc.paid_to_account_currency != company_currency) && - frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency))); + frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount); frm.refresh_fields(); }, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7c3574266ea..7b1a52986d7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -97,7 +97,7 @@ class PaymentEntry(AccountsController): self.set_status() def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.make_gl_entries(cancel=1) self.update_expense_claim() self.update_outstanding_amounts() @@ -110,6 +110,7 @@ class PaymentEntry(AccountsController): def set_payment_req_status(self): from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status + update_payment_req_status(self, None) def update_outstanding_amounts(self): @@ -119,8 +120,11 @@ class PaymentEntry(AccountsController): reference_names = [] for d in self.get("references"): if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names: - frappe.throw(_("Row #{0}: Duplicate entry in References {1} {2}") - .format(d.idx, d.reference_doctype, d.reference_name)) + frappe.throw( + _("Row #{0}: Duplicate entry in References {1} {2}").format( + d.idx, d.reference_doctype, d.reference_name + ) + ) reference_names.append((d.reference_doctype, d.reference_name, d.payment_term)) def set_bank_account_data(self): @@ -136,20 +140,27 @@ class PaymentEntry(AccountsController): self.set(field, bank_data.account) def validate_payment_type_with_outstanding(self): - total_outstanding = sum(d.allocated_amount for d in self.get('references')) - if total_outstanding < 0 and self.party_type == 'Customer' and self.payment_type == 'Receive': - frappe.throw(_("Cannot receive from customer against negative outstanding"), title=_("Incorrect Payment Type")) + total_outstanding = sum(d.allocated_amount for d in self.get("references")) + if total_outstanding < 0 and self.party_type == "Customer" and self.payment_type == "Receive": + frappe.throw( + _("Cannot receive from customer against negative outstanding"), + title=_("Incorrect Payment Type"), + ) def validate_allocated_amount(self): for d in self.get("references"): - if (flt(d.allocated_amount))> 0: + 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)) + 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)) + 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: @@ -159,9 +170,14 @@ class PaymentEntry(AccountsController): def set_missing_values(self): if self.payment_type == "Internal Transfer": - for field in ("party", "party_balance", "total_allocated_amount", - "base_total_allocated_amount", "unallocated_amount"): - self.set(field, None) + for field in ( + "party", + "party_balance", + "total_allocated_amount", + "base_total_allocated_amount", + "unallocated_amount", + ): + self.set(field, None) self.references = [] else: if not self.party_type: @@ -170,13 +186,16 @@ class PaymentEntry(AccountsController): if not self.party: frappe.throw(_("Party is mandatory")) - _party_name = "title" if self.party_type in ("Student", "Shareholder") else self.party_type.lower() + "_name" + _party_name = ( + "title" if self.party_type in ("Student", "Shareholder") else self.party_type.lower() + "_name" + ) self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name) if self.party: if not self.party_balance: - self.party_balance = get_balance_on(party_type=self.party_type, - party=self.party, date=self.posting_date, company=self.company) + self.party_balance = get_balance_on( + party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company + ) if not self.party_account: party_account = get_party_account(self.party_type, self.party, self.company) @@ -193,16 +212,20 @@ class PaymentEntry(AccountsController): self.paid_to_account_currency = acc.account_currency self.paid_to_account_balance = acc.account_balance - self.party_account_currency = self.paid_from_account_currency \ - if self.payment_type=="Receive" else self.paid_to_account_currency + self.party_account_currency = ( + self.paid_from_account_currency + if self.payment_type == "Receive" + else self.paid_to_account_currency + ) self.set_missing_ref_details() def set_missing_ref_details(self, force=False): for d in self.get("references"): if d.allocated_amount: - ref_details = get_reference_details(d.reference_doctype, - d.reference_name, self.party_account_currency) + ref_details = get_reference_details( + d.reference_doctype, d.reference_name, self.party_account_currency + ) for field, value in iteritems(ref_details): if d.exchange_gain_loss: @@ -212,7 +235,7 @@ class PaymentEntry(AccountsController): # refer -> `update_reference_in_payment_entry()` in utils.py continue - if field == 'exchange_rate' or not d.get(field) or force: + if field == "exchange_rate" or not d.get(field) or force: d.db_set(field, value) def validate_payment_type(self): @@ -225,8 +248,9 @@ class PaymentEntry(AccountsController): frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party)) if self.party_account and self.party_type in ("Customer", "Supplier"): - self.validate_account_type(self.party_account, - [erpnext.get_party_account_type(self.party_type)]) + self.validate_account_type( + self.party_account, [erpnext.get_party_account_type(self.party_type)] + ) def validate_bank_accounts(self): if self.payment_type in ("Pay", "Internal Transfer"): @@ -254,8 +278,9 @@ class PaymentEntry(AccountsController): self.source_exchange_rate = ref_doc.get("exchange_rate") if not self.source_exchange_rate: - self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency, - self.company_currency, self.posting_date) + self.source_exchange_rate = get_exchange_rate( + self.paid_from_account_currency, self.company_currency, self.posting_date + ) def set_target_exchange_rate(self, ref_doc=None): if self.paid_from_account_currency == self.paid_to_account_currency: @@ -266,8 +291,9 @@ class PaymentEntry(AccountsController): self.target_exchange_rate = ref_doc.get("exchange_rate") if not self.target_exchange_rate: - self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency, - self.company_currency, self.posting_date) + self.target_exchange_rate = get_exchange_rate( + self.paid_to_account_currency, self.company_currency, self.posting_date + ) def validate_mandatory(self): for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"): @@ -276,7 +302,7 @@ class PaymentEntry(AccountsController): def validate_reference_documents(self): if self.party_type == "Student": - valid_reference_doctypes = ("Fees") + valid_reference_doctypes = "Fees" elif self.party_type == "Customer": valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") elif self.party_type == "Supplier": @@ -284,16 +310,17 @@ class PaymentEntry(AccountsController): elif self.party_type == "Employee": valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity") elif self.party_type == "Shareholder": - valid_reference_doctypes = ("Journal Entry") + valid_reference_doctypes = "Journal Entry" elif self.party_type == "Donor": - valid_reference_doctypes = ("Donation") + valid_reference_doctypes = "Donation" for d in self.get("references"): if not d.allocated_amount: continue if d.reference_doctype not in valid_reference_doctypes: - frappe.throw(_("Reference Doctype must be one of {0}") - .format(comma_or(valid_reference_doctypes))) + frappe.throw( + _("Reference Doctype must be one of {0}").format(comma_or(valid_reference_doctypes)) + ) elif d.reference_name: if not frappe.db.exists(d.reference_doctype, d.reference_name): @@ -303,28 +330,35 @@ class PaymentEntry(AccountsController): if d.reference_doctype != "Journal Entry": if self.party != ref_doc.get(scrub(self.party_type)): - frappe.throw(_("{0} {1} is not associated with {2} {3}") - .format(d.reference_doctype, d.reference_name, self.party_type, self.party)) + frappe.throw( + _("{0} {1} is not associated with {2} {3}").format( + d.reference_doctype, d.reference_name, self.party_type, self.party + ) + ) else: self.validate_journal_entry() if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Expense Claim", "Fees"): if self.party_type == "Customer": - ref_party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to + ref_party_account = ( + get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to + ) elif self.party_type == "Student": ref_party_account = ref_doc.receivable_account - elif self.party_type=="Supplier": + elif self.party_type == "Supplier": ref_party_account = ref_doc.credit_to - elif self.party_type=="Employee": + elif self.party_type == "Employee": ref_party_account = ref_doc.payable_account if ref_party_account != self.party_account: - frappe.throw(_("{0} {1} is associated with {2}, but Party Account is {3}") - .format(d.reference_doctype, d.reference_name, ref_party_account, self.party_account)) + frappe.throw( + _("{0} {1} is associated with {2}, but Party Account is {3}").format( + d.reference_doctype, d.reference_name, ref_party_account, self.party_account + ) + ) if ref_doc.docstatus != 1: - frappe.throw(_("{0} {1} must be submitted") - .format(d.reference_doctype, d.reference_name)) + frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name)) def validate_paid_invoices(self): no_oustanding_refs = {} @@ -334,28 +368,45 @@ class PaymentEntry(AccountsController): continue if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"): - outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"]) + outstanding_amount, is_return = frappe.get_cached_value( + d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"] + ) if outstanding_amount <= 0 and not is_return: no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) for k, v in no_oustanding_refs.items(): frappe.msgprint( - _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") - .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount"))) - + "

" + _("If this is undesirable please cancel the corresponding Payment Entry."), - title=_("Warning"), indicator="orange") + _( + "{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry." + ).format( + _(k), + frappe.bold(", ".join(d.reference_name for d in v)), + frappe.bold(_("negative outstanding amount")), + ) + + "

" + + _("If this is undesirable please cancel the corresponding Payment Entry."), + title=_("Warning"), + indicator="orange", + ) def validate_journal_entry(self): for d in self.get("references"): if d.allocated_amount and d.reference_doctype == "Journal Entry": - je_accounts = frappe.db.sql("""select debit, credit from `tabJournal Entry Account` + je_accounts = frappe.db.sql( + """select debit, credit from `tabJournal Entry Account` where account = %s and party=%s and docstatus = 1 and parent = %s and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order")) - """, (self.party_account, self.party, d.reference_name), as_dict=True) + """, + (self.party_account, self.party, d.reference_name), + as_dict=True, + ) if not je_accounts: - frappe.throw(_("Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher") - .format(d.idx, d.reference_name, self.party_account)) + frappe.throw( + _( + "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher" + ).format(d.idx, d.reference_name, self.party_account) + ) else: dr_or_cr = "debit" if self.payment_type == "Receive" else "credit" valid = False @@ -363,14 +414,17 @@ class PaymentEntry(AccountsController): if flt(jvd[dr_or_cr]) > 0: valid = True if not valid: - frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry") - .format(d.reference_name, dr_or_cr)) + frappe.throw( + _("Against Journal Entry {0} does not have any unmatched {1} entry").format( + d.reference_name, dr_or_cr + ) + ) def update_payment_schedule(self, cancel=0): invoice_payment_amount_map = {} invoice_paid_amount_map = {} - for ref in self.get('references'): + for ref in self.get("references"): if ref.payment_term and ref.reference_name: key = (ref.payment_term, ref.reference_name) invoice_payment_amount_map.setdefault(key, 0.0) @@ -378,58 +432,68 @@ class PaymentEntry(AccountsController): if not invoice_paid_amount_map.get(key): payment_schedule = frappe.get_all( - 'Payment Schedule', - filters={'parent': ref.reference_name}, - fields=['paid_amount', 'payment_amount', 'payment_term', 'discount', 'outstanding'] + "Payment Schedule", + filters={"parent": ref.reference_name}, + fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"], ) for term in payment_schedule: invoice_key = (term.payment_term, ref.reference_name) invoice_paid_amount_map.setdefault(invoice_key, {}) - invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding - invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100) + invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding + invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( + term.discount / 100 + ) for idx, (key, allocated_amount) in enumerate(iteritems(invoice_payment_amount_map), 1): if not invoice_paid_amount_map.get(key): - frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1])) + frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1])) - outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding')) - discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt')) + outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding")) + discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt")) if cancel: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s, discounted_amount = `discounted_amount` - %s, outstanding = `outstanding` + %s WHERE parent = %s and payment_term = %s""", - (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0])) + (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), + ) else: if allocated_amount > outstanding: - frappe.throw(_('Row #{0}: Cannot allocate more than {1} against payment term {2}').format(idx, outstanding, key[0])) + frappe.throw( + _("Row #{0}: Cannot allocate more than {1} against payment term {2}").format( + idx, outstanding, key[0] + ) + ) if allocated_amount and outstanding: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s, discounted_amount = `discounted_amount` + %s, outstanding = `outstanding` - %s WHERE parent = %s and payment_term = %s""", - (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0])) + (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), + ) def set_status(self): if self.docstatus == 2: - self.status = 'Cancelled' + self.status = "Cancelled" elif self.docstatus == 1: - self.status = 'Submitted' + self.status = "Submitted" else: - self.status = 'Draft' + self.status = "Draft" - self.db_set('status', self.status, update_modified = True) + self.db_set("status", self.status, update_modified=True) def set_tax_withholding(self): - if not self.party_type == 'Supplier': + if not self.party_type == "Supplier": return if not self.apply_tax_withholding_amount: @@ -438,22 +502,24 @@ class PaymentEntry(AccountsController): net_total = self.paid_amount # Adding args as purchase invoice to get TDS amount - args = frappe._dict({ - 'company': self.company, - 'doctype': 'Payment Entry', - 'supplier': self.party, - 'posting_date': self.posting_date, - 'net_total': net_total - }) + args = frappe._dict( + { + "company": self.company, + "doctype": "Payment Entry", + "supplier": self.party, + "posting_date": self.posting_date, + "net_total": net_total, + } + ) tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category) if not tax_withholding_details: return - tax_withholding_details.update({ - 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) - }) + tax_withholding_details.update( + {"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company)} + ) accounts = [] for d in self.taxes: @@ -461,7 +527,7 @@ class PaymentEntry(AccountsController): # Preserve user updated included in paid amount if d.included_in_paid_amount: - tax_withholding_details.update({'included_in_paid_amount': d.included_in_paid_amount}) + tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount}) d.update(tax_withholding_details) accounts.append(d.account_head) @@ -469,8 +535,11 @@ class PaymentEntry(AccountsController): if not accounts or tax_withholding_details.get("account_head") not in accounts: self.append("taxes", tax_withholding_details) - to_remove = [d for d in self.taxes - if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")] + to_remove = [ + d + for d in self.taxes + if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head") + ] for d in to_remove: self.remove(d) @@ -497,40 +566,53 @@ class PaymentEntry(AccountsController): def set_received_amount(self): self.base_received_amount = self.base_paid_amount - if self.paid_from_account_currency == self.paid_to_account_currency \ - and not self.payment_type == 'Internal Transfer': + if ( + self.paid_from_account_currency == self.paid_to_account_currency + and not self.payment_type == "Internal Transfer" + ): self.received_amount = self.paid_amount def set_amounts_after_tax(self): applicable_tax = 0 base_applicable_tax = 0 - for tax in self.get('taxes'): + for tax in self.get("taxes"): if not tax.included_in_paid_amount: - amount = -1 * tax.tax_amount if tax.add_deduct_tax == 'Deduct' else tax.tax_amount - base_amount = -1 * tax.base_tax_amount if tax.add_deduct_tax == 'Deduct' else tax.base_tax_amount + amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount + base_amount = ( + -1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount + ) applicable_tax += amount base_applicable_tax += base_amount - self.paid_amount_after_tax = flt(flt(self.paid_amount) + flt(applicable_tax), - self.precision("paid_amount_after_tax")) - self.base_paid_amount_after_tax = flt(flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate), - self.precision("base_paid_amount_after_tax")) + self.paid_amount_after_tax = flt( + flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax") + ) + self.base_paid_amount_after_tax = flt( + flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate), + self.precision("base_paid_amount_after_tax"), + ) - self.received_amount_after_tax = flt(flt(self.received_amount) + flt(applicable_tax), - self.precision("paid_amount_after_tax")) - self.base_received_amount_after_tax = flt(flt(self.received_amount_after_tax) * flt(self.target_exchange_rate), - self.precision("base_paid_amount_after_tax")) + self.received_amount_after_tax = flt( + flt(self.received_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax") + ) + self.base_received_amount_after_tax = flt( + flt(self.received_amount_after_tax) * flt(self.target_exchange_rate), + self.precision("base_paid_amount_after_tax"), + ) def set_amounts_in_company_currency(self): self.base_paid_amount, self.base_received_amount, self.difference_amount = 0, 0, 0 if self.paid_amount: - self.base_paid_amount = flt(flt(self.paid_amount) * flt(self.source_exchange_rate), - self.precision("base_paid_amount")) + self.base_paid_amount = flt( + flt(self.paid_amount) * flt(self.source_exchange_rate), self.precision("base_paid_amount") + ) if self.received_amount: - self.base_received_amount = flt(flt(self.received_amount) * flt(self.target_exchange_rate), - self.precision("base_received_amount")) + self.base_received_amount = flt( + flt(self.received_amount) * flt(self.target_exchange_rate), + self.precision("base_received_amount"), + ) def set_total_allocated_amount(self): if self.payment_type == "Internal Transfer": @@ -540,8 +622,9 @@ class PaymentEntry(AccountsController): for d in self.get("references"): if d.allocated_amount: total_allocated_amount += flt(d.allocated_amount) - base_total_allocated_amount += flt(flt(d.allocated_amount) * flt(d.exchange_rate), - self.precision("base_paid_amount")) + base_total_allocated_amount += flt( + flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") + ) self.total_allocated_amount = abs(total_allocated_amount) self.base_total_allocated_amount = abs(base_total_allocated_amount) @@ -551,22 +634,33 @@ class PaymentEntry(AccountsController): if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) included_taxes = self.get_included_taxes() - if self.payment_type == "Receive" \ - and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ - and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): - self.unallocated_amount = (self.base_received_amount + total_deductions - - self.base_total_allocated_amount) / self.source_exchange_rate + if ( + self.payment_type == "Receive" + and self.base_total_allocated_amount < self.base_received_amount + total_deductions + and self.total_allocated_amount + < self.paid_amount + (total_deductions / self.source_exchange_rate) + ): + self.unallocated_amount = ( + self.base_received_amount + total_deductions - self.base_total_allocated_amount + ) / self.source_exchange_rate self.unallocated_amount -= included_taxes - elif self.payment_type == "Pay" \ - and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ - and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): - self.unallocated_amount = (self.base_paid_amount - (total_deductions + - self.base_total_allocated_amount)) / self.target_exchange_rate + elif ( + self.payment_type == "Pay" + and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) + and self.total_allocated_amount + < self.received_amount + (total_deductions / self.target_exchange_rate) + ): + self.unallocated_amount = ( + self.base_paid_amount - (total_deductions + self.base_total_allocated_amount) + ) / self.target_exchange_rate self.unallocated_amount -= included_taxes def set_difference_amount(self): - base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate) - if self.payment_type == "Receive" else flt(self.target_exchange_rate)) + base_unallocated_amount = flt(self.unallocated_amount) * ( + flt(self.source_exchange_rate) + if self.payment_type == "Receive" + else flt(self.target_exchange_rate) + ) base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) @@ -580,14 +674,15 @@ class PaymentEntry(AccountsController): total_deductions = sum(flt(d.amount) for d in self.get("deductions")) included_taxes = self.get_included_taxes() - self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes, - self.precision("difference_amount")) + self.difference_amount = flt( + self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount") + ) def get_included_taxes(self): included_taxes = 0 - for tax in self.get('taxes'): + for tax in self.get("taxes"): if tax.included_in_paid_amount: - if tax.add_deduct_tax == 'Add': + if tax.add_deduct_tax == "Add": included_taxes += tax.base_tax_amount else: included_taxes -= tax.base_tax_amount @@ -598,27 +693,41 @@ class PaymentEntry(AccountsController): # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast def clear_unallocated_reference_document_rows(self): self.set("references", self.get("references", {"allocated_amount": ["not in", [0, None, ""]]})) - frappe.db.sql("""delete from `tabPayment Entry Reference` - where parent = %s and allocated_amount = 0""", self.name) + frappe.db.sql( + """delete from `tabPayment Entry Reference` + where parent = %s and allocated_amount = 0""", + self.name, + ) def validate_payment_against_negative_invoice(self): - if ((self.payment_type=="Pay" and self.party_type=="Customer") - or (self.payment_type=="Receive" and self.party_type=="Supplier")): + if (self.payment_type == "Pay" and self.party_type == "Customer") or ( + self.payment_type == "Receive" and self.party_type == "Supplier" + ): - total_negative_outstanding = sum(abs(flt(d.outstanding_amount)) - for d in self.get("references") if flt(d.outstanding_amount) < 0) + total_negative_outstanding = sum( + abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 + ) - paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount + paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount additional_charges = sum([flt(d.amount) for d in self.deductions]) if not total_negative_outstanding: - frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice") - .format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")), - self.party_type), InvalidPaymentEntry) + frappe.throw( + _("Cannot {0} {1} {2} without any negative outstanding invoice").format( + _(self.payment_type), + (_("to") if self.party_type == "Customer" else _("from")), + self.party_type, + ), + InvalidPaymentEntry, + ) elif paid_amount - additional_charges > total_negative_outstanding: - frappe.throw(_("Paid Amount cannot be greater than total negative outstanding amount {0}") - .format(total_negative_outstanding), InvalidPaymentEntry) + frappe.throw( + _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( + total_negative_outstanding + ), + InvalidPaymentEntry, + ) def set_title(self): if frappe.flags.in_import and self.title: @@ -639,33 +748,45 @@ class PaymentEntry(AccountsController): frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) def set_remarks(self): - if self.custom_remarks: return + if self.custom_remarks: + return - if self.payment_type=="Internal Transfer": - remarks = [_("Amount {0} {1} transferred from {2} to {3}") - .format(self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to)] + if self.payment_type == "Internal Transfer": + remarks = [ + _("Amount {0} {1} transferred from {2} to {3}").format( + self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to + ) + ] else: - remarks = [_("Amount {0} {1} {2} {3}").format( - self.party_account_currency, - self.paid_amount if self.payment_type=="Receive" else self.received_amount, - _("received from") if self.payment_type=="Receive" else _("to"), self.party - )] + remarks = [ + _("Amount {0} {1} {2} {3}").format( + self.party_account_currency, + self.paid_amount if self.payment_type == "Receive" else self.received_amount, + _("received from") if self.payment_type == "Receive" else _("to"), + self.party, + ) + ] if self.reference_no: - remarks.append(_("Transaction reference no {0} dated {1}") - .format(self.reference_no, self.reference_date)) + remarks.append( + _("Transaction reference no {0} dated {1}").format(self.reference_no, self.reference_date) + ) if self.payment_type in ["Receive", "Pay"]: for d in self.get("references"): if d.allocated_amount: - remarks.append(_("Amount {0} {1} against {2} {3}").format(self.party_account_currency, - d.allocated_amount, d.reference_doctype, d.reference_name)) + remarks.append( + _("Amount {0} {1} against {2} {3}").format( + self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name + ) + ) for d in self.get("deductions"): if d.amount: - remarks.append(_("Amount {0} {1} deducted against {2}") - .format(self.company_currency, d.amount, d.account)) + remarks.append( + _("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account) + ) self.set("remarks", "\n".join(remarks)) @@ -684,92 +805,110 @@ class PaymentEntry(AccountsController): def add_party_gl_entries(self, gl_entries): if self.party_account: - if self.payment_type=="Receive": + if self.payment_type == "Receive": against_account = self.paid_to else: against_account = self.paid_from - party_gl_dict = self.get_gl_dict({ - "account": self.party_account, - "party_type": self.party_type, - "party": self.party, - "against": against_account, - "account_currency": self.party_account_currency, - "cost_center": self.cost_center - }, item=self) + party_gl_dict = self.get_gl_dict( + { + "account": self.party_account, + "party_type": self.party_type, + "party": self.party, + "against": against_account, + "account_currency": self.party_account_currency, + "cost_center": self.cost_center, + }, + item=self, + ) - dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit" + dr_or_cr = ( + "credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit" + ) for d in self.get("references"): cost_center = self.cost_center if d.reference_doctype == "Sales Invoice" and not cost_center: cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center") gle = party_gl_dict.copy() - gle.update({ - "against_voucher_type": d.reference_doctype, - "against_voucher": d.reference_name, - "cost_center": cost_center - }) + gle.update( + { + "against_voucher_type": d.reference_doctype, + "against_voucher": d.reference_name, + "cost_center": cost_center, + } + ) - allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate), - self.precision("paid_amount")) + allocated_amount_in_company_currency = flt( + flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount") + ) - gle.update({ - dr_or_cr + "_in_account_currency": d.allocated_amount, - dr_or_cr: allocated_amount_in_company_currency - }) + gle.update( + { + dr_or_cr + "_in_account_currency": d.allocated_amount, + dr_or_cr: allocated_amount_in_company_currency, + } + ) gl_entries.append(gle) if self.unallocated_amount: exchange_rate = self.get_exchange_rate() - base_unallocated_amount = (self.unallocated_amount * exchange_rate) + base_unallocated_amount = self.unallocated_amount * exchange_rate gle = party_gl_dict.copy() - gle.update({ - dr_or_cr + "_in_account_currency": self.unallocated_amount, - dr_or_cr: base_unallocated_amount - }) + gle.update( + { + dr_or_cr + "_in_account_currency": self.unallocated_amount, + dr_or_cr: base_unallocated_amount, + } + ) gl_entries.append(gle) def add_bank_gl_entries(self, gl_entries): if self.payment_type in ("Pay", "Internal Transfer"): gl_entries.append( - self.get_gl_dict({ - "account": self.paid_from, - "account_currency": self.paid_from_account_currency, - "against": self.party if self.payment_type=="Pay" else self.paid_to, - "credit_in_account_currency": self.paid_amount, - "credit": self.base_paid_amount, - "cost_center": self.cost_center, - "post_net_value": True - }, item=self) + self.get_gl_dict( + { + "account": self.paid_from, + "account_currency": self.paid_from_account_currency, + "against": self.party if self.payment_type == "Pay" else self.paid_to, + "credit_in_account_currency": self.paid_amount, + "credit": self.base_paid_amount, + "cost_center": self.cost_center, + "post_net_value": True, + }, + item=self, + ) ) if self.payment_type in ("Receive", "Internal Transfer"): gl_entries.append( - self.get_gl_dict({ - "account": self.paid_to, - "account_currency": self.paid_to_account_currency, - "against": self.party if self.payment_type=="Receive" else self.paid_from, - "debit_in_account_currency": self.received_amount, - "debit": self.base_received_amount, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.paid_to, + "account_currency": self.paid_to_account_currency, + "against": self.party if self.payment_type == "Receive" else self.paid_from, + "debit_in_account_currency": self.received_amount, + "debit": self.base_received_amount, + "cost_center": self.cost_center, + }, + item=self, + ) ) def add_tax_gl_entries(self, gl_entries): - for d in self.get('taxes'): + for d in self.get("taxes"): account_currency = get_account_currency(d.account_head) if account_currency != self.company_currency: frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - if self.payment_type in ('Pay', 'Internal Transfer'): + if self.payment_type in ("Pay", "Internal Transfer"): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" against = self.party or self.paid_from - elif self.payment_type == 'Receive': + elif self.payment_type == "Receive": dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" against = self.party or self.paid_to @@ -779,29 +918,39 @@ class PaymentEntry(AccountsController): base_tax_amount = d.base_tax_amount gl_entries.append( - self.get_gl_dict({ - "account": d.account_head, - "against": against, - dr_or_cr: tax_amount, - dr_or_cr + "_in_account_currency": base_tax_amount - if account_currency==self.company_currency - else d.tax_amount, - "cost_center": d.cost_center, - "post_net_value": True, - }, account_currency, item=d)) + self.get_gl_dict( + { + "account": d.account_head, + "against": against, + dr_or_cr: tax_amount, + dr_or_cr + "_in_account_currency": base_tax_amount + if account_currency == self.company_currency + else d.tax_amount, + "cost_center": d.cost_center, + "post_net_value": True, + }, + account_currency, + item=d, + ) + ) if not d.included_in_paid_amount: gl_entries.append( - self.get_gl_dict({ - "account": payment_account, - "against": against, - rev_dr_or_cr: tax_amount, - rev_dr_or_cr + "_in_account_currency": base_tax_amount - if account_currency==self.company_currency - else d.tax_amount, - "cost_center": self.cost_center, - "post_net_value": True, - }, account_currency, item=d)) + self.get_gl_dict( + { + "account": payment_account, + "against": against, + rev_dr_or_cr: tax_amount, + rev_dr_or_cr + "_in_account_currency": base_tax_amount + if account_currency == self.company_currency + else d.tax_amount, + "cost_center": self.cost_center, + "post_net_value": True, + }, + account_currency, + item=d, + ) + ) def add_deductions_gl_entries(self, gl_entries): for d in self.get("deductions"): @@ -811,33 +960,40 @@ class PaymentEntry(AccountsController): frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) gl_entries.append( - self.get_gl_dict({ - "account": d.account, - "account_currency": account_currency, - "against": self.party or self.paid_from, - "debit_in_account_currency": d.amount, - "debit": d.amount, - "cost_center": d.cost_center - }, item=d) + self.get_gl_dict( + { + "account": d.account, + "account_currency": account_currency, + "against": self.party or self.paid_from, + "debit_in_account_currency": d.amount, + "debit": d.amount, + "cost_center": d.cost_center, + }, + item=d, + ) ) def get_party_account_for_taxes(self): - if self.payment_type == 'Receive': + if self.payment_type == "Receive": return self.paid_to - elif self.payment_type in ('Pay', 'Internal Transfer'): + elif self.payment_type in ("Pay", "Internal Transfer"): return self.paid_from def update_advance_paid(self): if self.payment_type in ("Receive", "Pay") and self.party: for d in self.get("references"): - if d.allocated_amount \ - and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance", "Gratuity"): - frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() + if d.allocated_amount and d.reference_doctype in ( + "Sales Order", + "Purchase Order", + "Employee Advance", + "Gratuity", + ): + frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() def update_expense_claim(self): if self.payment_type in ("Pay") and self.party: for d in self.get("references"): - if d.reference_doctype=="Expense Claim" and d.reference_name: + if d.reference_doctype == "Expense Claim" and d.reference_name: doc = frappe.get_doc("Expense Claim", d.reference_name) if self.docstatus == 2: update_reimbursed_amount(doc, -1 * d.allocated_amount) @@ -847,7 +1003,7 @@ class PaymentEntry(AccountsController): def update_donation(self, cancel=0): if self.payment_type == "Receive" and self.party_type == "Donor" and self.party: for d in self.get("references"): - if d.reference_doctype=="Donation" and d.reference_name: + if d.reference_doctype == "Donation" and d.reference_name: is_paid = 0 if cancel else 1 frappe.db.set_value("Donation", d.reference_name, "paid", is_paid) @@ -857,31 +1013,29 @@ class PaymentEntry(AccountsController): def calculate_deductions(self, tax_details): return { - "account": tax_details['tax']['account_head'], - "cost_center": frappe.get_cached_value('Company', self.company, "cost_center"), - "amount": self.total_allocated_amount * (tax_details['tax']['rate'] / 100) + "account": tax_details["tax"]["account_head"], + "cost_center": frappe.get_cached_value("Company", self.company, "cost_center"), + "amount": self.total_allocated_amount * (tax_details["tax"]["rate"] / 100), } def set_gain_or_loss(self, account_details=None): if not self.difference_amount: self.set_difference_amount() - row = { - 'amount': self.difference_amount - } + row = {"amount": self.difference_amount} if account_details: row.update(account_details) - if not row.get('amount'): + if not row.get("amount"): # if no difference amount return - self.append('deductions', row) + self.append("deductions", row) self.set_unallocated_amount() def get_exchange_rate(self): - return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate + return self.source_exchange_rate if self.payment_type == "Receive" else self.target_exchange_rate def initialize_taxes(self): for tax in self.get("taxes"): @@ -905,25 +1059,31 @@ class PaymentEntry(AccountsController): cumulated_tax_fraction = 0 for i, tax in enumerate(self.get("taxes")): tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax) - if i==0: + if i == 0: tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item else: - tax.grand_total_fraction_for_current_item = \ - self.get("taxes")[i-1].grand_total_fraction_for_current_item \ + tax.grand_total_fraction_for_current_item = ( + self.get("taxes")[i - 1].grand_total_fraction_for_current_item + tax.tax_fraction_for_current_item + ) cumulated_tax_fraction += tax.tax_fraction_for_current_item - self.paid_amount_after_tax = flt(self.paid_amount/(1+cumulated_tax_fraction)) + self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction)) def calculate_taxes(self): self.total_taxes_and_charges = 0.0 self.base_total_taxes_and_charges = 0.0 - actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] - for tax in self.get("taxes") if tax.charge_type == "Actual"]) + actual_tax_dict = dict( + [ + [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] + for tax in self.get("taxes") + if tax.charge_type == "Actual" + ] + ) - for i, tax in enumerate(self.get('taxes')): + for i, tax in enumerate(self.get("taxes")): current_tax_amount = self.get_current_tax_amount(tax) if tax.charge_type == "Actual": @@ -942,19 +1102,21 @@ class PaymentEntry(AccountsController): if i == 0: tax.total = flt(self.paid_amount_after_tax + current_tax_amount, self.precision("total", tax)) else: - tax.total = flt(self.get('taxes')[i-1].total + current_tax_amount, self.precision("total", tax)) + tax.total = flt( + self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax) + ) tax.base_total = tax.total * self.source_exchange_rate - if self.payment_type == 'Pay': + if self.payment_type == "Pay": self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) else: self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) - if self.get('taxes'): - self.paid_amount_after_tax = self.get('taxes')[-1].base_total + if self.get("taxes"): + self.paid_amount_after_tax = self.get("taxes")[-1].base_total def get_current_tax_amount(self, tax): tax_rate = tax.rate @@ -962,7 +1124,11 @@ class PaymentEntry(AccountsController): # To set row_id by default as previous row. if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]: if tax.idx == 1: - frappe.throw(_("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row")) + frappe.throw( + _( + "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" + ) + ) if not tax.row_id: tax.row_id = tax.idx - 1 @@ -972,12 +1138,10 @@ class PaymentEntry(AccountsController): elif tax.charge_type == "On Paid Amount": current_tax_amount = (tax_rate / 100.0) * self.paid_amount_after_tax elif tax.charge_type == "On Previous Row Amount": - current_tax_amount = (tax_rate / 100.0) * \ - self.get('taxes')[cint(tax.row_id) - 1].tax_amount + current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].tax_amount elif tax.charge_type == "On Previous Row Total": - current_tax_amount = (tax_rate / 100.0) * \ - self.get('taxes')[cint(tax.row_id) - 1].total + current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].total return current_tax_amount @@ -990,83 +1154,106 @@ class PaymentEntry(AccountsController): if tax.charge_type == "On Paid Amount": current_tax_fraction = tax_rate / 100.0 elif tax.charge_type == "On Previous Row Amount": - current_tax_fraction = (tax_rate / 100.0) * \ - self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item + current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[ + cint(tax.row_id) - 1 + ].tax_fraction_for_current_item elif tax.charge_type == "On Previous Row Total": - current_tax_fraction = (tax_rate / 100.0) * \ - self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item + current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[ + cint(tax.row_id) - 1 + ].grand_total_fraction_for_current_item if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct": current_tax_fraction *= -1.0 return current_tax_fraction + def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): - throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) + throw( + _("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format( + tax.idx, row_range + ) + ) if cint(getattr(tax, "included_in_paid_amount", None)): if tax.charge_type == "Actual": # inclusive tax cannot be of type Actual - throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) - elif tax.charge_type == "On Previous Row Amount" and \ - not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount): + throw( + _("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format( + tax.idx + ) + ) + elif tax.charge_type == "On Previous Row Amount" and not cint( + doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount + ): # referred row should also be inclusive _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + elif tax.charge_type == "On Previous Row Total" and not all( + [cint(t.included_in_paid_amount for t in doc.get("taxes")[: cint(tax.row_id) - 1])] + ): # all rows about the referred tax should be inclusive _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) elif tax.get("category") == "Valuation": frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + @frappe.whitelist() def get_outstanding_reference_documents(args): if isinstance(args, string_types): args = json.loads(args) - if args.get('party_type') == 'Member': + if args.get("party_type") == "Member": return # confirm that Supplier is not blocked - if args.get('party_type') == 'Supplier': - supplier_status = get_supplier_block_status(args['party']) - if supplier_status['on_hold']: - if supplier_status['hold_type'] == 'All': + if args.get("party_type") == "Supplier": + supplier_status = get_supplier_block_status(args["party"]) + if supplier_status["on_hold"]: + if supplier_status["hold_type"] == "All": return [] - elif supplier_status['hold_type'] == 'Payments': - if not supplier_status['release_date'] or getdate(nowdate()) <= supplier_status['release_date']: + elif supplier_status["hold_type"] == "Payments": + if ( + not supplier_status["release_date"] or getdate(nowdate()) <= supplier_status["release_date"] + ): return [] party_account_currency = get_account_currency(args.get("party_account")) - company_currency = frappe.get_cached_value('Company', args.get("company"), "default_currency") + company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency") # Get positive outstanding sales /purchase invoices/ Fees condition = "" if args.get("voucher_type") and args.get("voucher_no"): - condition = " and voucher_type={0} and voucher_no={1}"\ - .format(frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])) + condition = " and voucher_type={0} and voucher_no={1}".format( + frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]) + ) # Add cost center condition if args.get("cost_center"): condition += " and cost_center='%s'" % args.get("cost_center") date_fields_dict = { - 'posting_date': ['from_posting_date', 'to_posting_date'], - 'due_date': ['from_due_date', 'to_due_date'] + "posting_date": ["from_posting_date", "to_posting_date"], + "due_date": ["from_due_date", "to_due_date"], } for fieldname, date_fields in date_fields_dict.items(): if args.get(date_fields[0]) and args.get(date_fields[1]): - condition += " and {0} between '{1}' and '{2}'".format(fieldname, - args.get(date_fields[0]), args.get(date_fields[1])) + condition += " and {0} between '{1}' and '{2}'".format( + fieldname, args.get(date_fields[0]), args.get(date_fields[1]) + ) if args.get("company"): condition += " and company = {0}".format(frappe.db.escape(args.get("company"))) - outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"), - args.get("party_account"), filters=args, condition=condition) + outstanding_invoices = get_outstanding_invoices( + args.get("party_type"), + args.get("party"), + args.get("party_account"), + filters=args, + condition=condition, + ) outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) @@ -1077,28 +1264,44 @@ def get_outstanding_reference_documents(args): d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate") elif d.voucher_type == "Journal Entry": d["exchange_rate"] = get_exchange_rate( - party_account_currency, company_currency, d.posting_date + party_account_currency, company_currency, d.posting_date ) if d.voucher_type in ("Purchase Invoice"): d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") # Get all SO / PO which are not fully billed or against which full advance not paid orders_to_be_billed = [] - if (args.get("party_type") != "Student"): - orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), - args.get("party"), args.get("company"), party_account_currency, company_currency, filters=args) + if args.get("party_type") != "Student": + orders_to_be_billed = get_orders_to_be_billed( + args.get("posting_date"), + args.get("party_type"), + args.get("party"), + args.get("company"), + party_account_currency, + company_currency, + filters=args, + ) # Get negative outstanding sales /purchase invoices negative_outstanding_invoices = [] if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"): - negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"), - args.get("party_account"), party_account_currency, company_currency, condition=condition) + negative_outstanding_invoices = get_negative_outstanding_invoices( + args.get("party_type"), + args.get("party"), + args.get("party_account"), + party_account_currency, + company_currency, + condition=condition, + ) data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed if not data: - frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.") - .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))) + frappe.msgprint( + _( + "No outstanding invoices found for the {0} {1} which qualify the filters you have specified." + ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))) + ) return data @@ -1106,53 +1309,75 @@ def get_outstanding_reference_documents(args): def split_invoices_based_on_payment_terms(outstanding_invoices): invoice_ref_based_on_payment_terms = {} for idx, d in enumerate(outstanding_invoices): - if d.voucher_type in ['Sales Invoice', 'Purchase Invoice']: - payment_term_template = frappe.db.get_value(d.voucher_type, d.voucher_no, 'payment_terms_template') + if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + payment_term_template = frappe.db.get_value( + d.voucher_type, d.voucher_no, "payment_terms_template" + ) if payment_term_template: allocate_payment_based_on_payment_terms = frappe.db.get_value( - 'Payment Terms Template', payment_term_template, 'allocate_payment_based_on_payment_terms') + "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms" + ) if allocate_payment_based_on_payment_terms: - payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': d.voucher_no}, fields=["*"]) + payment_schedule = frappe.get_all( + "Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"] + ) for payment_term in payment_schedule: if payment_term.outstanding > 0.1: invoice_ref_based_on_payment_terms.setdefault(idx, []) - invoice_ref_based_on_payment_terms[idx].append(frappe._dict({ - 'due_date': d.due_date, - 'currency': d.currency, - 'voucher_no': d.voucher_no, - 'voucher_type': d.voucher_type, - 'posting_date': d.posting_date, - 'invoice_amount': flt(d.invoice_amount), - 'outstanding_amount': flt(d.outstanding_amount), - 'payment_amount': payment_term.payment_amount, - 'payment_term': payment_term.payment_term - })) + invoice_ref_based_on_payment_terms[idx].append( + frappe._dict( + { + "due_date": d.due_date, + "currency": d.currency, + "voucher_no": d.voucher_no, + "voucher_type": d.voucher_type, + "posting_date": d.posting_date, + "invoice_amount": flt(d.invoice_amount), + "outstanding_amount": flt(d.outstanding_amount), + "payment_amount": payment_term.payment_amount, + "payment_term": payment_term.payment_term, + } + ) + ) outstanding_invoices_after_split = [] if invoice_ref_based_on_payment_terms: for idx, ref in invoice_ref_based_on_payment_terms.items(): - voucher_no = ref[0]['voucher_no'] - voucher_type = ref[0]['voucher_type'] + voucher_no = ref[0]["voucher_no"] + voucher_type = ref[0]["voucher_type"] - frappe.msgprint(_("Spliting {} {} into {} row(s) as per Payment Terms").format( - voucher_type, voucher_no, len(ref)), alert=True) + frappe.msgprint( + _("Spliting {} {} into {} row(s) as per Payment Terms").format( + voucher_type, voucher_no, len(ref) + ), + alert=True, + ) outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx] - existing_row = list(filter(lambda x: x.get('voucher_no') == voucher_no, outstanding_invoices)) + existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices)) index = outstanding_invoices.index(existing_row[0]) outstanding_invoices.pop(index) outstanding_invoices_after_split += outstanding_invoices return outstanding_invoices_after_split -def get_orders_to_be_billed(posting_date, party_type, party, - company, party_account_currency, company_currency, cost_center=None, filters=None): + +def get_orders_to_be_billed( + posting_date, + party_type, + party, + company, + party_account_currency, + company_currency, + cost_center=None, + filters=None, +): if party_type == "Customer": - voucher_type = 'Sales Order' + voucher_type = "Sales Order" elif party_type == "Supplier": - voucher_type = 'Purchase Order' + voucher_type = "Purchase Order" elif party_type == "Employee": voucher_type = None @@ -1160,7 +1385,7 @@ def get_orders_to_be_billed(posting_date, party_type, party, if voucher_type: doc = frappe.get_doc({"doctype": voucher_type}) condition = "" - if doc and hasattr(doc, 'cost_center'): + if doc and hasattr(doc, "cost_center"): condition = " and cost_center='%s'" % cost_center orders = [] @@ -1172,7 +1397,8 @@ def get_orders_to_be_billed(posting_date, party_type, party, grand_total_field = "grand_total" rounded_total_field = "rounded_total" - orders = frappe.db.sql(""" + orders = frappe.db.sql( + """ select name as voucher_no, if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, @@ -1190,18 +1416,25 @@ def get_orders_to_be_billed(posting_date, party_type, party, {condition} order by transaction_date, name - """.format(**{ - "rounded_total_field": rounded_total_field, - "grand_total_field": grand_total_field, - "voucher_type": voucher_type, - "party_type": scrub(party_type), - "condition": condition - }), (party, company), as_dict=True) + """.format( + **{ + "rounded_total_field": rounded_total_field, + "grand_total_field": grand_total_field, + "voucher_type": voucher_type, + "party_type": scrub(party_type), + "condition": condition, + } + ), + (party, company), + as_dict=True, + ) order_list = [] for d in orders: - if not (flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than")) - and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))): + if not ( + flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than")) + and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than")) + ): continue d["voucher_type"] = voucher_type @@ -1211,8 +1444,16 @@ def get_orders_to_be_billed(posting_date, party_type, party, return order_list -def get_negative_outstanding_invoices(party_type, party, party_account, - party_account_currency, company_currency, cost_center=None, condition=None): + +def get_negative_outstanding_invoices( + party_type, + party, + party_account, + party_account_currency, + company_currency, + cost_center=None, + condition=None, +): voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" supplier_condition = "" if voucher_type == "Purchase Invoice": @@ -1224,7 +1465,8 @@ def get_negative_outstanding_invoices(party_type, party, party_account, grand_total_field = "grand_total" rounded_total_field = "rounded_total" - return frappe.db.sql(""" + return frappe.db.sql( + """ select "{voucher_type}" as voucher_type, name as voucher_no, if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, @@ -1239,21 +1481,26 @@ def get_negative_outstanding_invoices(party_type, party, party_account, {condition} order by posting_date, name - """.format(**{ - "supplier_condition": supplier_condition, - "condition": condition, - "rounded_total_field": rounded_total_field, - "grand_total_field": grand_total_field, - "voucher_type": voucher_type, - "party_type": scrub(party_type), - "party_account": "debit_to" if party_type == "Customer" else "credit_to", - "cost_center": cost_center - }), (party, party_account), as_dict=True) + """.format( + **{ + "supplier_condition": supplier_condition, + "condition": condition, + "rounded_total_field": rounded_total_field, + "grand_total_field": grand_total_field, + "voucher_type": voucher_type, + "party_type": scrub(party_type), + "party_account": "debit_to" if party_type == "Customer" else "credit_to", + "cost_center": cost_center, + } + ), + (party, party_account), + as_dict=True, + ) @frappe.whitelist() def get_party_details(company, party_type, party, date, cost_center=None): - bank_account = '' + bank_account = "" if not frappe.db.exists(party_type, party): frappe.throw(_("Invalid {0}: {1}").format(party_type, party)) @@ -1261,7 +1508,9 @@ def get_party_details(company, party_type, party, date, cost_center=None): account_currency = get_account_currency(party_account) account_balance = get_balance_on(party_account, date, cost_center=cost_center) - _party_name = "title" if party_type in ("Student", "Shareholder") else party_type.lower() + "_name" + _party_name = ( + "title" if party_type in ("Student", "Shareholder") else party_type.lower() + "_name" + ) party_name = frappe.db.get_value(party_type, party, _party_name) party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center) if party_type in ["Customer", "Supplier"]: @@ -1273,61 +1522,68 @@ def get_party_details(company, party_type, party, date, cost_center=None): "party_account_currency": account_currency, "party_balance": party_balance, "account_balance": account_balance, - "bank_account": bank_account + "bank_account": bank_account, } @frappe.whitelist() def get_account_details(account, date, cost_center=None): - frappe.has_permission('Payment Entry', throw=True) + frappe.has_permission("Payment Entry", throw=True) # to check if the passed account is accessible under reference doctype Payment Entry - account_list = frappe.get_list('Account', { - 'name': account - }, reference_doctype='Payment Entry', limit=1) + account_list = frappe.get_list( + "Account", {"name": account}, reference_doctype="Payment Entry", limit=1 + ) # There might be some user permissions which will allow account under certain doctypes # except for Payment Entry, only in such case we should throw permission error if not account_list: - frappe.throw(_('Account: {0} is not permitted under Payment Entry').format(account)) + frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account)) - account_balance = get_balance_on(account, date, cost_center=cost_center, - ignore_account_permission=True) + account_balance = get_balance_on( + account, date, cost_center=cost_center, ignore_account_permission=True + ) - return frappe._dict({ - "account_currency": get_account_currency(account), - "account_balance": account_balance, - "account_type": frappe.db.get_value("Account", account, "account_type") - }) + return frappe._dict( + { + "account_currency": get_account_currency(account), + "account_balance": account_balance, + "account_type": frappe.db.get_value("Account", account, "account_type"), + } + ) @frappe.whitelist() def get_company_defaults(company): fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"] - ret = frappe.get_cached_value('Company', company, fields, as_dict=1) + ret = frappe.get_cached_value("Company", company, fields, as_dict=1) for fieldname in fields: if not ret[fieldname]: - frappe.throw(_("Please set default {0} in Company {1}") - .format(frappe.get_meta("Company").get_label(fieldname), company)) + frappe.throw( + _("Please set default {0} in Company {1}").format( + frappe.get_meta("Company").get_label(fieldname), company + ) + ) return ret def get_outstanding_on_journal_entry(name): res = frappe.db.sql( - 'SELECT ' - 'CASE WHEN party_type IN ("Customer", "Student") ' - 'THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) ' - 'ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) ' - 'END as outstanding_amount ' - 'FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) ' - 'AND party_type IS NOT NULL ' - 'AND party_type != ""', - (name, name), as_dict=1 - ) + "SELECT " + 'CASE WHEN party_type IN ("Customer", "Student") ' + "THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) " + "ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) " + "END as outstanding_amount " + "FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) " + "AND party_type IS NOT NULL " + 'AND party_type != ""', + (name, name), + as_dict=1, + ) - outstanding_amount = res[0].get('outstanding_amount', 0) if res else 0 + outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0 return outstanding_amount @@ -1336,7 +1592,9 @@ def get_outstanding_on_journal_entry(name): def get_reference_details(reference_doctype, reference_name, party_account_currency): total_amount = outstanding_amount = exchange_rate = bill_no = None ref_doc = frappe.get_doc(reference_doctype, reference_name) - company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company) + company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency( + ref_doc.company + ) if reference_doctype == "Fees": total_amount = ref_doc.get("grand_total") @@ -1353,20 +1611,22 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: total_amount = ref_doc.get("total_amount") if ref_doc.multi_currency: - exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + exchange_rate = get_exchange_rate( + party_account_currency, company_currency, ref_doc.posting_date + ) else: exchange_rate = 1 outstanding_amount = get_outstanding_on_journal_entry(reference_name) elif reference_doctype != "Journal Entry": if ref_doc.doctype == "Expense Claim": - total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) + total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) elif ref_doc.doctype == "Employee Advance": total_amount = ref_doc.advance_amount exchange_rate = ref_doc.get("exchange_rate") if party_account_currency != ref_doc.currency: total_amount = flt(total_amount) * flt(exchange_rate) elif ref_doc.doctype == "Gratuity": - total_amount = ref_doc.amount + total_amount = ref_doc.amount if not total_amount: if party_account_currency == company_currency: total_amount = ref_doc.base_grand_total @@ -1376,16 +1636,21 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre if not exchange_rate: # Get the exchange rate from the original ref doc # or get it based on the posting date of the ref doc. - exchange_rate = ref_doc.get("conversion_rate") or \ - get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate( + party_account_currency, company_currency, ref_doc.posting_date + ) if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") bill_no = ref_doc.get("bill_no") elif reference_doctype == "Expense Claim": - outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\ - - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount")) + outstanding_amount = ( + flt(ref_doc.get("total_sanctioned_amount")) + + flt(ref_doc.get("total_taxes_and_charges")) + - flt(ref_doc.get("total_amount_reimbursed")) + - flt(ref_doc.get("total_advance_amount")) + ) elif reference_doctype == "Employee Advance": - outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)) + outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount) if party_account_currency != ref_doc.currency: outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) if party_account_currency == company_currency: @@ -1396,18 +1661,22 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) else: # Get the exchange rate based on the posting date of the ref doc. - exchange_rate = get_exchange_rate(party_account_currency, - company_currency, ref_doc.posting_date) + exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) - return frappe._dict({ - "due_date": ref_doc.get("due_date"), - "total_amount": flt(total_amount), - "outstanding_amount": flt(outstanding_amount), - "exchange_rate": flt(exchange_rate), - "bill_no": bill_no - }) + return frappe._dict( + { + "due_date": ref_doc.get("due_date"), + "total_amount": flt(total_amount), + "outstanding_amount": flt(outstanding_amount), + "exchange_rate": flt(exchange_rate), + "bill_no": bill_no, + } + ) -def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_account_currency, company_currency, reference_name): + +def get_amounts_based_on_reference_doctype( + reference_doctype, ref_doc, party_account_currency, company_currency, reference_name +): total_amount = outstanding_amount = exchange_rate = None if reference_doctype == "Fees": total_amount = ref_doc.get("grand_total") @@ -1420,35 +1689,46 @@ def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_acc elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: total_amount = ref_doc.get("total_amount") if ref_doc.multi_currency: - exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + exchange_rate = get_exchange_rate( + party_account_currency, company_currency, ref_doc.posting_date + ) else: exchange_rate = 1 outstanding_amount = get_outstanding_on_journal_entry(reference_name) return total_amount, outstanding_amount, exchange_rate -def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_currency, company_currency): + +def get_amounts_based_on_ref_doc( + reference_doctype, ref_doc, party_account_currency, company_currency +): total_amount = outstanding_amount = exchange_rate = None if ref_doc.doctype == "Expense Claim": - total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) + total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) elif ref_doc.doctype == "Employee Advance": - total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc) + total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance( + party_account_currency, ref_doc + ) if not total_amount: total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( - party_account_currency, company_currency, ref_doc) + party_account_currency, company_currency, ref_doc + ) if not exchange_rate: # Get the exchange rate from the original ref doc # or get it based on the posting date of the ref doc - exchange_rate = ref_doc.get("conversion_rate") or \ - get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate( + party_account_currency, company_currency, ref_doc.posting_date + ) outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts( - reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency) + reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency + ) return total_amount, outstanding_amount, exchange_rate, bill_no + def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc): total_amount = ref_doc.advance_amount exchange_rate = ref_doc.get("exchange_rate") @@ -1457,7 +1737,10 @@ def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, return total_amount, exchange_rate -def get_total_amount_exchange_rate_base_on_currency(party_account_currency, company_currency, ref_doc): + +def get_total_amount_exchange_rate_base_on_currency( + party_account_currency, company_currency, ref_doc +): exchange_rate = None if party_account_currency == company_currency: total_amount = ref_doc.base_grand_total @@ -1467,16 +1750,23 @@ def get_total_amount_exchange_rate_base_on_currency(party_account_currency, comp return total_amount, exchange_rate -def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency): + +def get_bill_no_and_update_amounts( + reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency +): outstanding_amount = bill_no = None if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") bill_no = ref_doc.get("bill_no") elif reference_doctype == "Expense Claim": - outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\ - - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount")) + outstanding_amount = ( + flt(ref_doc.get("total_sanctioned_amount")) + + flt(ref_doc.get("total_taxes_and_charges")) + - flt(ref_doc.get("total_amount_reimbursed")) + - flt(ref_doc.get("total_advance_amount")) + ) elif reference_doctype == "Employee Advance": - outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)) + outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount) if party_account_currency != ref_doc.currency: outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) if party_account_currency == company_currency: @@ -1498,15 +1788,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= party_account = set_party_account(dt, dn, doc, party_type) party_account_currency = set_party_account_currency(dt, party_account, doc) payment_type = set_payment_type(dt, doc) - grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc) + grand_total, outstanding_amount = set_grand_total_and_outstanding_amount( + party_amount, dt, party_account_currency, doc + ) # bank or cash bank = get_bank_cash_account(doc, bank_account) paid_amount, received_amount = set_paid_amount_and_received_amount( - dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc) + dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc + ) - paid_amount, received_amount, discount_amount = apply_early_payment_discount(paid_amount, received_amount, doc) + paid_amount, received_amount, discount_amount = apply_early_payment_discount( + paid_amount, received_amount, doc + ) pe = frappe.new_doc("Payment Entry") pe.payment_type = payment_type @@ -1520,18 +1815,22 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.contact_email = doc.get("contact_email") pe.ensure_supplier_is_not_blocked() - pe.paid_from = party_account if payment_type=="Receive" else bank.account - pe.paid_to = party_account if payment_type=="Pay" else bank.account - pe.paid_from_account_currency = party_account_currency \ - if payment_type=="Receive" else bank.account_currency - pe.paid_to_account_currency = party_account_currency if payment_type=="Pay" else bank.account_currency + pe.paid_from = party_account if payment_type == "Receive" else bank.account + pe.paid_to = party_account if payment_type == "Pay" else bank.account + pe.paid_from_account_currency = ( + party_account_currency if payment_type == "Receive" else bank.account_currency + ) + pe.paid_to_account_currency = ( + party_account_currency if payment_type == "Pay" else bank.account_currency + ) pe.paid_amount = paid_amount pe.received_amount = received_amount pe.letter_head = doc.get("letter_head") - if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']: - pe.project = (doc.get('project') or - reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items + if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]: + pe.project = doc.get("project") or reduce( + lambda prev, cur: prev or cur, [x.get("project") for x in doc.get("items")], None + ) # get first non-empty project from items if pe.party_type in ["Customer", "Supplier"]: bank_account = get_party_bank_account(pe.party_type, pe.party) @@ -1540,44 +1839,57 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= # only Purchase Invoice can be blocked individually if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked(): - frappe.msgprint(_('{0} is on hold till {1}').format(doc.name, doc.release_date)) + frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date)) else: - if (doc.doctype in ('Sales Invoice', 'Purchase Invoice') - and frappe.get_value('Payment Terms Template', - {'name': doc.payment_terms_template}, 'allocate_payment_based_on_payment_terms')): + if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value( + "Payment Terms Template", + {"name": doc.payment_terms_template}, + "allocate_payment_based_on_payment_terms", + ): - for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount): - pe.append('references', reference) + for reference in get_reference_as_per_payment_terms( + doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount + ): + pe.append("references", reference) else: if dt == "Dunning": - pe.append("references", { - 'reference_doctype': 'Sales Invoice', - 'reference_name': doc.get('sales_invoice'), - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - 'total_amount': doc.get('outstanding_amount'), - 'outstanding_amount': doc.get('outstanding_amount'), - 'allocated_amount': doc.get('outstanding_amount') - }) - pe.append("references", { - 'reference_doctype': dt, - 'reference_name': dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - 'total_amount': doc.get('dunning_amount'), - 'outstanding_amount': doc.get('dunning_amount'), - 'allocated_amount': doc.get('dunning_amount') - }) + pe.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": doc.get("sales_invoice"), + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + "total_amount": doc.get("outstanding_amount"), + "outstanding_amount": doc.get("outstanding_amount"), + "allocated_amount": doc.get("outstanding_amount"), + }, + ) + pe.append( + "references", + { + "reference_doctype": dt, + "reference_name": dn, + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + "total_amount": doc.get("dunning_amount"), + "outstanding_amount": doc.get("dunning_amount"), + "allocated_amount": doc.get("dunning_amount"), + }, + ) else: - pe.append("references", { - 'reference_doctype': dt, - 'reference_name': dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - 'total_amount': grand_total, - 'outstanding_amount': outstanding_amount, - 'allocated_amount': outstanding_amount - }) + pe.append( + "references", + { + "reference_doctype": dt, + "reference_name": dn, + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + "total_amount": grand_total, + "outstanding_amount": outstanding_amount, + "allocated_amount": outstanding_amount, + }, + ) pe.setup_party_account_field() pe.set_missing_values() @@ -1588,25 +1900,32 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() if discount_amount: - pe.set_gain_or_loss(account_details={ - 'account': frappe.get_cached_value('Company', pe.company, "default_discount_account"), - 'cost_center': pe.cost_center or frappe.get_cached_value('Company', pe.company, "cost_center"), - 'amount': discount_amount * (-1 if payment_type == "Pay" else 1) - }) + pe.set_gain_or_loss( + account_details={ + "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), + "cost_center": pe.cost_center + or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": discount_amount * (-1 if payment_type == "Pay" else 1), + } + ) pe.set_difference_amount() return pe + def get_bank_cash_account(doc, bank_account): - bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), - account=bank_account) + bank = get_default_bank_cash_account( + doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account + ) if not bank: - bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), - account=bank_account) + bank = get_default_bank_cash_account( + doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), account=bank_account + ) return bank + def set_party_type(dt): if dt in ("Sales Invoice", "Sales Order", "Dunning"): party_type = "Customer" @@ -1620,6 +1939,7 @@ def set_party_type(dt): party_type = "Donor" return party_type + def set_party_account(dt, dn, doc, party_type): if dt == "Sales Invoice": party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to @@ -1637,6 +1957,7 @@ def set_party_account(dt, dn, doc, party_type): party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) return party_account + def set_party_account_currency(dt, party_account, doc): if dt not in ("Sales Invoice", "Purchase Invoice"): party_account_currency = get_account_currency(party_account) @@ -1644,14 +1965,18 @@ def set_party_account_currency(dt, party_account, doc): party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account) return party_account_currency + def set_payment_type(dt, doc): - if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ - or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): - payment_type = "Receive" + if ( + dt in ("Sales Order", "Donation") + or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0) + ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): + payment_type = "Receive" else: payment_type = "Pay" return payment_type + def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc): grand_total = outstanding_amount = 0 if party_amount: @@ -1664,8 +1989,7 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre outstanding_amount = doc.outstanding_amount elif dt in ("Expense Claim"): grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges - outstanding_amount = doc.grand_total \ - - doc.total_amount_reimbursed + outstanding_amount = doc.grand_total - doc.total_amount_reimbursed elif dt == "Employee Advance": grand_total = flt(doc.advance_amount) outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) @@ -1692,7 +2016,10 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre outstanding_amount = grand_total - flt(doc.advance_paid) return grand_total, outstanding_amount -def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc): + +def set_paid_amount_and_received_amount( + dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc +): paid_amount = received_amount = 0 if party_account_currency == bank.account_currency: paid_amount = received_amount = abs(outstanding_amount) @@ -1701,37 +2028,38 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta if bank_amount: received_amount = bank_amount else: - received_amount = paid_amount * doc.get('conversion_rate', 1) + received_amount = paid_amount * doc.get("conversion_rate", 1) if dt == "Employee Advance": - received_amount = paid_amount * doc.get('exchange_rate', 1) + received_amount = paid_amount * doc.get("exchange_rate", 1) else: received_amount = abs(outstanding_amount) if bank_amount: paid_amount = bank_amount else: # if party account currency and bank currency is different then populate paid amount as well - paid_amount = received_amount * doc.get('conversion_rate', 1) + paid_amount = received_amount * doc.get("conversion_rate", 1) if dt == "Employee Advance": - paid_amount = received_amount * doc.get('exchange_rate', 1) + paid_amount = received_amount * doc.get("exchange_rate", 1) return paid_amount, received_amount + def apply_early_payment_discount(paid_amount, received_amount, doc): total_discount = 0 - eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice'] - has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule + eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] + has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule if doc.doctype in eligible_for_payments and has_payment_schedule: for term in doc.payment_schedule: if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: - if term.discount_type == 'Percentage': - discount_amount = flt(doc.get('grand_total')) * (term.discount / 100) + if term.discount_type == "Percentage": + discount_amount = flt(doc.get("grand_total")) * (term.discount / 100) else: discount_amount = term.discount - discount_amount_in_foreign_currency = discount_amount * doc.get('conversion_rate', 1) + discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1) - if doc.doctype == 'Sales Invoice': + if doc.doctype == "Sales Invoice": paid_amount -= discount_amount received_amount -= discount_amount_in_foreign_currency else: @@ -1741,38 +2069,46 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): total_discount += discount_amount if total_discount: - money = frappe.utils.fmt_money(total_discount, currency=doc.get('currency')) + money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency")) frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) return paid_amount, received_amount, total_discount -def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): + +def get_reference_as_per_payment_terms( + payment_schedule, dt, dn, doc, grand_total, outstanding_amount +): references = [] for payment_term in payment_schedule: - payment_term_outstanding = flt(payment_term.payment_amount - payment_term.paid_amount, - payment_term.precision('payment_amount')) + payment_term_outstanding = flt( + payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount") + ) if payment_term_outstanding: - references.append({ - 'reference_doctype': dt, - 'reference_name': dn, - 'bill_no': doc.get('bill_no'), - 'due_date': doc.get('due_date'), - 'total_amount': grand_total, - 'outstanding_amount': outstanding_amount, - 'payment_term': payment_term.payment_term, - 'allocated_amount': payment_term_outstanding - }) + references.append( + { + "reference_doctype": dt, + "reference_name": dn, + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + "total_amount": grand_total, + "outstanding_amount": outstanding_amount, + "payment_term": payment_term.payment_term, + "allocated_amount": payment_term_outstanding, + } + ) return references + def get_paid_amount(dt, dn, party_type, party, account, due_date): - if party_type=="Customer": + if party_type == "Customer": dr_or_cr = "credit_in_account_currency - debit_in_account_currency" else: dr_or_cr = "debit_in_account_currency - credit_in_account_currency" - paid_amount = frappe.db.sql(""" + paid_amount = frappe.db.sql( + """ select ifnull(sum({dr_or_cr}), 0) as paid_amount from `tabGL Entry` where against_voucher_type = %s @@ -1782,41 +2118,58 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date): and account = %s and due_date = %s and {dr_or_cr} > 0 - """.format(dr_or_cr=dr_or_cr), (dt, dn, party_type, party, account, due_date)) + """.format( + dr_or_cr=dr_or_cr + ), + (dt, dn, party_type, party, account, due_date), + ) return paid_amount[0][0] if paid_amount else 0 + @frappe.whitelist() -def get_party_and_account_balance(company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None): - return frappe._dict({ - "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center), - "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center), - "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center) - }) +def get_party_and_account_balance( + company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None +): + return frappe._dict( + { + "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center), + "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center), + "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center), + } + ) + @frappe.whitelist() def make_payment_order(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc + def set_missing_values(source, target): target.payment_order_type = "Payment Entry" - target.append('references', dict( - reference_doctype="Payment Entry", - reference_name=source.name, - bank_account=source.party_bank_account, - amount=source.paid_amount, - account=source.paid_to, - supplier=source.party, - mode_of_payment=source.mode_of_payment, - )) + target.append( + "references", + dict( + reference_doctype="Payment Entry", + reference_name=source.name, + bank_account=source.party_bank_account, + amount=source.paid_amount, + account=source.paid_to, + supplier=source.party, + mode_of_payment=source.mode_of_payment, + ), + ) - doclist = get_mapped_doc("Payment Entry", source_name, { - "Payment Entry": { - "doctype": "Payment Order", - "validation": { - "docstatus": ["=", 1] - }, - } - - }, target_doc, set_missing_values) + doclist = get_mapped_doc( + "Payment Entry", + source_name, + { + "Payment Entry": { + "doctype": "Payment Order", + "validation": {"docstatus": ["=", 1]}, + } + }, + target_doc, + set_missing_values, + ) return doclist diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 349b8bb5b1b..5b70b510d2b 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -32,10 +32,9 @@ class TestPaymentEntry(unittest.TestCase): pe.insert() pe.submit() - expected_gle = dict((d[0], d) for d in [ - ["Debtors - _TC", 0, 1000, so.name], - ["_Test Cash - _TC", 1000.0, 0, None] - ]) + expected_gle = dict( + (d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]] + ) self.validate_gl_entries(pe.name, expected_gle) @@ -48,9 +47,9 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(so_advance_paid, 0) def test_payment_entry_for_blocked_supplier_invoice(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Invoices' + supplier.hold_type = "Invoices" supplier.save() self.assertRaises(frappe.ValidationError, make_purchase_invoice) @@ -59,32 +58,40 @@ class TestPaymentEntry(unittest.TestCase): supplier.save() def test_payment_entry_for_blocked_supplier_payments(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' + supplier.hold_type = "Payments" supplier.save() pi = make_purchase_invoice() self.assertRaises( - frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, - bank_account="_Test Bank - _TC") + frappe.ValidationError, + get_payment_entry, + dt="Purchase Invoice", + dn=pi.name, + bank_account="_Test Bank - _TC", + ) supplier.on_hold = 0 supplier.save() def test_payment_entry_for_blocked_supplier_payments_today_date(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' + supplier.hold_type = "Payments" supplier.release_date = nowdate() supplier.save() pi = make_purchase_invoice() self.assertRaises( - frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, - bank_account="_Test Bank - _TC") + frappe.ValidationError, + get_payment_entry, + dt="Purchase Invoice", + dn=pi.name, + bank_account="_Test Bank - _TC", + ) supplier.on_hold = 0 supplier.save() @@ -93,15 +100,15 @@ class TestPaymentEntry(unittest.TestCase): # this test is meant to fail only if something fails in the try block with self.assertRaises(Exception): try: - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' - supplier.release_date = '2018-03-01' + supplier.hold_type = "Payments" + supplier.release_date = "2018-03-01" supplier.save() pi = make_purchase_invoice() - get_payment_entry('Purchase Invoice', pi.name, bank_account="_Test Bank - _TC") + get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") supplier.on_hold = 0 supplier.save() @@ -111,8 +118,12 @@ class TestPaymentEntry(unittest.TestCase): raise Exception def test_payment_entry_against_si_usd_to_usd(self): - si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe.reference_no = "1" pe.reference_date = "2016-01-01" @@ -120,10 +131,13 @@ class TestPaymentEntry(unittest.TestCase): pe.insert() pe.submit() - expected_gle = dict((d[0], d) for d in [ - ["_Test Receivable USD - _TC", 0, 5000, si.name], - ["_Test Bank USD - _TC", 5000.0, 0, None] - ]) + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Receivable USD - _TC", 0, 5000, si.name], + ["_Test Bank USD - _TC", 5000.0, 0, None], + ] + ) self.validate_gl_entries(pe.name, expected_gle) @@ -136,8 +150,12 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(outstanding_amount, 100) def test_payment_entry_against_pi(self): - pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", - currency="USD", conversion_rate=50) + pi = make_purchase_invoice( + supplier="_Test Supplier USD", + debit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC") pe.reference_no = "1" pe.reference_date = "2016-01-01" @@ -145,20 +163,26 @@ class TestPaymentEntry(unittest.TestCase): pe.insert() pe.submit() - expected_gle = dict((d[0], d) for d in [ - ["_Test Payable USD - _TC", 12500, 0, pi.name], - ["_Test Bank USD - _TC", 0, 12500, None] - ]) + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Payable USD - _TC", 12500, 0, pi.name], + ["_Test Bank USD - _TC", 0, 12500, None], + ] + ) self.validate_gl_entries(pe.name, expected_gle) outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 0) - def test_payment_against_sales_invoice_to_check_status(self): - si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe.reference_no = "1" @@ -167,28 +191,35 @@ class TestPaymentEntry(unittest.TestCase): pe.insert() pe.submit() - outstanding_amount, status = frappe.db.get_value("Sales Invoice", si.name, ["outstanding_amount", "status"]) + outstanding_amount, status = frappe.db.get_value( + "Sales Invoice", si.name, ["outstanding_amount", "status"] + ) self.assertEqual(flt(outstanding_amount), 0) - self.assertEqual(status, 'Paid') + self.assertEqual(status, "Paid") pe.cancel() - outstanding_amount, status = frappe.db.get_value("Sales Invoice", si.name, ["outstanding_amount", "status"]) + outstanding_amount, status = frappe.db.get_value( + "Sales Invoice", si.name, ["outstanding_amount", "status"] + ) self.assertEqual(flt(outstanding_amount), 100) - self.assertEqual(status, 'Unpaid') + self.assertEqual(status, "Unpaid") def test_payment_entry_against_payment_terms(self): si = create_sales_invoice(do_not_save=1, qty=1, rate=200) create_payment_terms_template() - si.payment_terms_template = 'Test Receivable Template' + si.payment_terms_template = "Test Receivable Template" - si.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 18 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 18, + }, + ) si.save() si.submit() @@ -197,25 +228,28 @@ class TestPaymentEntry(unittest.TestCase): pe.submit() si.load_from_db() - self.assertEqual(pe.references[0].payment_term, 'Basic Amount Receivable') - self.assertEqual(pe.references[1].payment_term, 'Tax Receivable') + self.assertEqual(pe.references[0].payment_term, "Basic Amount Receivable") + self.assertEqual(pe.references[1].payment_term, "Tax Receivable") self.assertEqual(si.payment_schedule[0].paid_amount, 200.0) self.assertEqual(si.payment_schedule[1].paid_amount, 36.0) def test_payment_entry_against_payment_terms_with_discount(self): si = create_sales_invoice(do_not_save=1, qty=1, rate=200) create_payment_terms_template_with_discount() - si.payment_terms_template = 'Test Discount Template' + si.payment_terms_template = "Test Discount Template" - frappe.db.set_value('Company', si.company, 'default_discount_account', 'Write Off - _TC') + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") - si.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 18 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 18, + }, + ) si.save() si.submit() @@ -224,16 +258,19 @@ class TestPaymentEntry(unittest.TestCase): pe.submit() si.load_from_db() - self.assertEqual(pe.references[0].payment_term, '30 Credit Days with 10% Discount') + self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount") self.assertEqual(si.payment_schedule[0].payment_amount, 236.0) self.assertEqual(si.payment_schedule[0].paid_amount, 212.40) self.assertEqual(si.payment_schedule[0].outstanding, 0) self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6) - def test_payment_against_purchase_invoice_to_check_status(self): - pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", - currency="USD", conversion_rate=50) + pi = make_purchase_invoice( + supplier="_Test Supplier USD", + debit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC") pe.reference_no = "1" @@ -242,21 +279,27 @@ class TestPaymentEntry(unittest.TestCase): pe.insert() pe.submit() - outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"]) + outstanding_amount, status = frappe.db.get_value( + "Purchase Invoice", pi.name, ["outstanding_amount", "status"] + ) self.assertEqual(flt(outstanding_amount), 0) - self.assertEqual(status, 'Paid') + self.assertEqual(status, "Paid") pe.cancel() - outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"]) + outstanding_amount, status = frappe.db.get_value( + "Purchase Invoice", pi.name, ["outstanding_amount", "status"] + ) self.assertEqual(flt(outstanding_amount), 250) - self.assertEqual(status, 'Unpaid') + self.assertEqual(status, "Unpaid") def test_payment_entry_against_ec(self): - payable = frappe.get_cached_value('Company', "_Test Company", 'default_payable_account') + payable = frappe.get_cached_value("Company", "_Test Company", "default_payable_account") ec = make_expense_claim(payable, 300, 300, "_Test Company", "Travel Expenses - _TC") - pe = get_payment_entry("Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300) + pe = get_payment_entry( + "Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300 + ) pe.reference_no = "1" pe.reference_date = "2016-01-01" pe.source_exchange_rate = 1 @@ -264,68 +307,87 @@ class TestPaymentEntry(unittest.TestCase): pe.insert() pe.submit() - expected_gle = dict((d[0], d) for d in [ - [payable, 300, 0, ec.name], - ["_Test Bank USD - _TC", 0, 300, None] - ]) + expected_gle = dict( + (d[0], d) for d in [[payable, 300, 0, ec.name], ["_Test Bank USD - _TC", 0, 300, None]] + ) self.validate_gl_entries(pe.name, expected_gle) - outstanding_amount = flt(frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount")) - \ - flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed")) + outstanding_amount = flt( + frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount") + ) - flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed")) self.assertEqual(outstanding_amount, 0) def test_payment_entry_against_si_usd_to_inr(self): - si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) - pe = get_payment_entry("Sales Invoice", si.name, party_amount=20, - bank_account="_Test Bank - _TC", bank_amount=900) + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) + pe = get_payment_entry( + "Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900 + ) pe.reference_no = "1" pe.reference_date = "2016-01-01" self.assertEqual(pe.difference_amount, 100) - pe.append("deductions", { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 100 - }) + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 100, + }, + ) pe.insert() pe.submit() - expected_gle = dict((d[0], d) for d in [ - ["_Test Receivable USD - _TC", 0, 1000, si.name], - ["_Test Bank - _TC", 900, 0, None], - ["_Test Exchange Gain/Loss - _TC", 100.0, 0, None], - ]) + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Receivable USD - _TC", 0, 1000, si.name], + ["_Test Bank - _TC", 900, 0, None], + ["_Test Exchange Gain/Loss - _TC", 100.0, 0, None], + ] + ) self.validate_gl_entries(pe.name, expected_gle) outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 80) - def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency (self): - si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50, do_not_save=1) + def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency(self): + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + do_not_save=1, + ) si.plc_conversion_rate = 50 si.save() si.submit() - pe = get_payment_entry("Sales Invoice", si.name, party_amount=20, - bank_account="_Test Bank USD - _TC", bank_amount=900) + pe = get_payment_entry( + "Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank USD - _TC", bank_amount=900 + ) pe.source_exchange_rate = 45.263 pe.target_exchange_rate = 45.263 pe.reference_no = "1" pe.reference_date = "2016-01-01" - - pe.append("deductions", { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 94.80 - }) + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 94.80, + }, + ) pe.save() @@ -359,8 +421,7 @@ class TestPaymentEntry(unittest.TestCase): pe.set_amounts() self.assertEqual( - pe.source_exchange_rate, 65.1, - "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1) + pe.source_exchange_rate, 65.1, "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1) ) def test_internal_transfer_usd_to_inr(self): @@ -382,20 +443,26 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(pe.difference_amount, 500) - pe.append("deductions", { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 500 - }) + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 500, + }, + ) pe.insert() pe.submit() - expected_gle = dict((d[0], d) for d in [ - ["_Test Bank USD - _TC", 0, 5000, None], - ["_Test Bank - _TC", 4500, 0, None], - ["_Test Exchange Gain/Loss - _TC", 500.0, 0, None], - ]) + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Bank USD - _TC", 0, 5000, None], + ["_Test Bank - _TC", 4500, 0, None], + ["_Test Exchange Gain/Loss - _TC", 500.0, 0, None], + ] + ) self.validate_gl_entries(pe.name, expected_gle) @@ -435,10 +502,9 @@ class TestPaymentEntry(unittest.TestCase): pe3.insert() pe3.submit() - expected_gle = dict((d[0], d) for d in [ - ["Debtors - _TC", 100, 0, si1.name], - ["_Test Cash - _TC", 0, 100, None] - ]) + expected_gle = dict( + (d[0], d) for d in [["Debtors - _TC", 100, 0, si1.name], ["_Test Cash - _TC", 0, 100, None]] + ) self.validate_gl_entries(pe3.name, expected_gle) @@ -462,12 +528,16 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(expected_gle[gle.account][3], gle.against_voucher) def get_gle(self, voucher_no): - return frappe.db.sql("""select account, debit, credit, against_voucher + return frappe.db.sql( + """select account, debit, credit, against_voucher from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", voucher_no, as_dict=1) + order by account asc""", + voucher_no, + as_dict=1, + ) def test_payment_entry_write_off_difference(self): - si = create_sales_invoice() + si = create_sales_invoice() pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") pe.reference_no = "1" pe.reference_date = "2016-01-01" @@ -477,11 +547,10 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(pe.unallocated_amount, 10) pe.received_amount = pe.paid_amount = 95 - pe.append("deductions", { - "account": "_Test Write Off - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 5 - }) + pe.append( + "deductions", + {"account": "_Test Write Off - _TC", "cost_center": "_Test Cost Center - _TC", "amount": 5}, + ) pe.save() self.assertEqual(pe.unallocated_amount, 0) @@ -489,27 +558,37 @@ class TestPaymentEntry(unittest.TestCase): pe.submit() - expected_gle = dict((d[0], d) for d in [ - ["Debtors - _TC", 0, 100, si.name], - ["_Test Cash - _TC", 95, 0, None], - ["_Test Write Off - _TC", 5, 0, None] - ]) + expected_gle = dict( + (d[0], d) + for d in [ + ["Debtors - _TC", 0, 100, si.name], + ["_Test Cash - _TC", 95, 0, None], + ["_Test Write Off - _TC", 5, 0, None], + ] + ) self.validate_gl_entries(pe.name, expected_gle) def test_payment_entry_exchange_gain_loss(self): - si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe.reference_no = "1" pe.reference_date = "2016-01-01" pe.source_exchange_rate = 55 - pe.append("deductions", { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": -500 - }) + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": -500, + }, + ) pe.save() self.assertEqual(pe.unallocated_amount, 0) @@ -517,11 +596,14 @@ class TestPaymentEntry(unittest.TestCase): pe.submit() - expected_gle = dict((d[0], d) for d in [ - ["_Test Receivable USD - _TC", 0, 5000, si.name], - ["_Test Bank USD - _TC", 5500, 0, None], - ["_Test Exchange Gain/Loss - _TC", 0, 500, None], - ]) + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Receivable USD - _TC", 0, 5000, si.name], + ["_Test Bank USD - _TC", 5500, 0, None], + ["_Test Exchange Gain/Loss - _TC", 0, 500, None], + ] + ) self.validate_gl_entries(pe.name, expected_gle) @@ -530,10 +612,11 @@ class TestPaymentEntry(unittest.TestCase): def test_payment_entry_against_sales_invoice_with_cost_centre(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") + si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") self.assertEqual(pe.cost_center, si.cost_center) @@ -546,18 +629,18 @@ class TestPaymentEntry(unittest.TestCase): pe.submit() expected_values = { - "_Test Bank - _TC": { - "cost_center": cost_center - }, - "Debtors - _TC": { - "cost_center": cost_center - } + "_Test Bank - _TC": {"cost_center": cost_center}, + "Debtors - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", pe.name, as_dict=1) + order by account asc""", + pe.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -566,10 +649,13 @@ class TestPaymentEntry(unittest.TestCase): def test_payment_entry_against_purchase_invoice_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC") + pi = make_purchase_invoice_against_cost_center( + cost_center=cost_center, credit_to="Creditors - _TC" + ) pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") self.assertEqual(pe.cost_center, pi.cost_center) @@ -582,18 +668,18 @@ class TestPaymentEntry(unittest.TestCase): pe.submit() expected_values = { - "_Test Bank - _TC": { - "cost_center": cost_center - }, - "Creditors - _TC": { - "cost_center": cost_center - } + "_Test Bank - _TC": {"cost_center": cost_center}, + "Creditors - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", pe.name, as_dict=1) + order by account asc""", + pe.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -603,13 +689,16 @@ class TestPaymentEntry(unittest.TestCase): def test_payment_entry_account_and_party_balance_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.utils import get_balance_on + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") + si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=si.cost_center) - party_balance = get_balance_on(party_type="Customer", party=si.customer, cost_center=si.cost_center) + party_balance = get_balance_on( + party_type="Customer", party=si.customer, cost_center=si.cost_center + ) party_account_balance = get_balance_on(si.debit_to, cost_center=si.cost_center) pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") @@ -634,94 +723,109 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(flt(expected_party_account_balance), party_account_balance) def test_multi_currency_payment_entry_with_taxes(self): - payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC', - save=True) - payment_entry.append('taxes', { - 'account_head': '_Test Account Service Tax - _TC', - 'charge_type': 'Actual', - 'tax_amount': 10, - 'add_deduct_tax': 'Add', - 'description': 'Test' - }) + payment_entry = create_payment_entry( + party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True + ) + payment_entry.append( + "taxes", + { + "account_head": "_Test Account Service Tax - _TC", + "charge_type": "Actual", + "tax_amount": 10, + "add_deduct_tax": "Add", + "description": "Test", + }, + ) payment_entry.save() self.assertEqual(payment_entry.base_total_taxes_and_charges, 10) - self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)) + self.assertEqual( + flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2) + ) + def create_payment_entry(**args): - payment_entry = frappe.new_doc('Payment Entry') - payment_entry.company = args.get('company') or '_Test Company' - payment_entry.payment_type = args.get('payment_type') or 'Pay' - payment_entry.party_type = args.get('party_type') or 'Supplier' - payment_entry.party = args.get('party') or '_Test Supplier' - payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC' - payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC' - payment_entry.paid_amount = args.get('paid_amount') or 1000 + payment_entry = frappe.new_doc("Payment Entry") + payment_entry.company = args.get("company") or "_Test Company" + payment_entry.payment_type = args.get("payment_type") or "Pay" + payment_entry.party_type = args.get("party_type") or "Supplier" + payment_entry.party = args.get("party") or "_Test Supplier" + payment_entry.paid_from = args.get("paid_from") or "_Test Bank - _TC" + payment_entry.paid_to = args.get("paid_to") or "Creditors - _TC" + payment_entry.paid_amount = args.get("paid_amount") or 1000 payment_entry.setup_party_account_field() payment_entry.set_missing_values() payment_entry.set_exchange_rate() payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate - payment_entry.reference_no = 'Test001' + payment_entry.reference_no = "Test001" payment_entry.reference_date = nowdate() - if args.get('save'): + if args.get("save"): payment_entry.save() - if args.get('submit'): + if args.get("submit"): payment_entry.submit() return payment_entry + def create_payment_terms_template(): - create_payment_term('Basic Amount Receivable') - create_payment_term('Tax Receivable') + create_payment_term("Basic Amount Receivable") + create_payment_term("Tax Receivable") - if not frappe.db.exists('Payment Terms Template', 'Test Receivable Template'): - payment_term_template = frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': 'Test Receivable Template', - 'allocate_payment_based_on_payment_terms': 1, - 'terms': [{ - 'doctype': 'Payment Terms Template Detail', - 'payment_term': 'Basic Amount Receivable', - 'invoice_portion': 84.746, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 1 - }, + if not frappe.db.exists("Payment Terms Template", "Test Receivable Template"): + payment_term_template = frappe.get_doc( { - 'doctype': 'Payment Terms Template Detail', - 'payment_term': 'Tax Receivable', - 'invoice_portion': 15.254, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 2 - }] - }).insert() + "doctype": "Payment Terms Template", + "template_name": "Test Receivable Template", + "allocate_payment_based_on_payment_terms": 1, + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "payment_term": "Basic Amount Receivable", + "invoice_portion": 84.746, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 1, + }, + { + "doctype": "Payment Terms Template Detail", + "payment_term": "Tax Receivable", + "invoice_portion": 15.254, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 2, + }, + ], + } + ).insert() + def create_payment_terms_template_with_discount(): - create_payment_term('30 Credit Days with 10% Discount') + create_payment_term("30 Credit Days with 10% Discount") + + if not frappe.db.exists("Payment Terms Template", "Test Discount Template"): + payment_term_template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "Test Discount Template", + "allocate_payment_based_on_payment_terms": 1, + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "payment_term": "30 Credit Days with 10% Discount", + "invoice_portion": 100, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 2, + "discount": 10, + "discount_validity_based_on": "Day(s) after invoice date", + "discount_validity": 1, + } + ], + } + ).insert() - if not frappe.db.exists('Payment Terms Template', 'Test Discount Template'): - payment_term_template = frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': 'Test Discount Template', - 'allocate_payment_based_on_payment_terms': 1, - 'terms': [{ - 'doctype': 'Payment Terms Template Detail', - 'payment_term': '30 Credit Days with 10% Discount', - 'invoice_portion': 100, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 2, - 'discount': 10, - 'discount_validity_based_on': 'Day(s) after invoice date', - 'discount_validity': 1 - }] - }).insert() def create_payment_term(name): - if not frappe.db.exists('Payment Term', name): - frappe.get_doc({ - 'doctype': 'Payment Term', - 'payment_term_name': name - }).insert() + if not frappe.db.exists("Payment Term", name): + frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert() diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py index 25dc4e6a60d..ab47b6151cd 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py @@ -18,10 +18,13 @@ class PaymentGatewayAccount(Document): def update_default_payment_gateway(self): if self.is_default: - frappe.db.sql("""update `tabPayment Gateway Account` set is_default = 0 - where is_default = 1 """) + frappe.db.sql( + """update `tabPayment Gateway Account` set is_default = 0 + where is_default = 1 """ + ) def set_as_default_if_not_set(self): - if not frappe.db.get_value("Payment Gateway Account", - {"is_default": 1, "name": ("!=", self.name)}, "name"): + if not frappe.db.get_value( + "Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name" + ): self.is_default = 1 diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py index bb0fc975cac..d0aaee88350 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py +++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py @@ -1,17 +1,6 @@ - - def get_data(): return { - 'fieldname': 'payment_gateway_account', - 'non_standard_fieldnames': { - 'Subscription Plan': 'payment_gateway' - }, - 'transactions': [ - { - 'items': ['Payment Request'] - }, - { - 'items': ['Subscription Plan'] - } - ] + "fieldname": "payment_gateway_account", + "non_standard_fieldnames": {"Subscription Plan": "payment_gateway"}, + "transactions": [{"items": ["Payment Request"]}, {"items": ["Subscription Plan"]}], } diff --git a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py index 1895c12ad7c..7a8cdf73355 100644 --- a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py +++ b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Payment Gateway Account') + class TestPaymentGatewayAccount(unittest.TestCase): pass 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/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index 50a58b8a0ab..3c45d20770d 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -18,9 +18,9 @@ class PaymentOrder(Document): self.update_payment_status(cancel=True) def update_payment_status(self, cancel=False): - status = 'Payment Ordered' + status = "Payment Ordered" if cancel: - status = 'Initiated' + status = "Initiated" if self.payment_order_type == "Payment Request": ref_field = "status" @@ -32,67 +32,67 @@ class PaymentOrder(Document): for d in self.references: frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_mop_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql(""" select mode_of_payment from `tabPayment Order Reference` + return frappe.db.sql( + """ select mode_of_payment from `tabPayment Order Reference` where parent = %(parent)s and mode_of_payment like %(txt)s - limit %(start)s, %(page_len)s""", { - 'parent': filters.get("parent"), - 'start': start, - 'page_len': page_len, - 'txt': "%%%s%%" % txt - }) + limit %(start)s, %(page_len)s""", + {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt}, + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_supplier_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql(""" select supplier from `tabPayment Order Reference` + return frappe.db.sql( + """ select supplier from `tabPayment Order Reference` where parent = %(parent)s and supplier like %(txt)s and (payment_reference is null or payment_reference='') - limit %(start)s, %(page_len)s""", { - 'parent': filters.get("parent"), - 'start': start, - 'page_len': page_len, - 'txt': "%%%s%%" % txt - }) + limit %(start)s, %(page_len)s""", + {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt}, + ) + @frappe.whitelist() def make_payment_records(name, supplier, mode_of_payment=None): - doc = frappe.get_doc('Payment Order', name) + doc = frappe.get_doc("Payment Order", name) make_journal_entry(doc, supplier, mode_of_payment) + def make_journal_entry(doc, supplier, mode_of_payment=None): - je = frappe.new_doc('Journal Entry') + je = frappe.new_doc("Journal Entry") je.payment_order = doc.name je.posting_date = nowdate() - mode_of_payment_type = frappe._dict(frappe.get_all('Mode of Payment', - fields = ["name", "type"], as_list=1)) + mode_of_payment_type = frappe._dict( + frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1) + ) - je.voucher_type = 'Bank Entry' - if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == 'Cash': + je.voucher_type = "Bank Entry" + if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == "Cash": je.voucher_type = "Cash Entry" paid_amt = 0 - party_account = get_party_account('Supplier', supplier, doc.company) + party_account = get_party_account("Supplier", supplier, doc.company) for d in doc.references: - if (d.supplier == supplier - and (not mode_of_payment or mode_of_payment == d.mode_of_payment)): - je.append('accounts', { - 'account': party_account, - 'debit_in_account_currency': d.amount, - 'party_type': 'Supplier', - 'party': supplier, - 'reference_type': d.reference_doctype, - 'reference_name': d.reference_name - }) + if d.supplier == supplier and (not mode_of_payment or mode_of_payment == d.mode_of_payment): + je.append( + "accounts", + { + "account": party_account, + "debit_in_account_currency": d.amount, + "party_type": "Supplier", + "party": supplier, + "reference_type": d.reference_doctype, + "reference_name": d.reference_name, + }, + ) paid_amt += d.amount - je.append('accounts', { - 'account': doc.account, - 'credit_in_account_currency': paid_amt - }) + je.append("accounts", {"account": doc.account, "credit_in_account_currency": paid_amt}) je.flags.ignore_mandatory = True je.save() diff --git a/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py b/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py index 02da9793895..f82886e7c26 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py +++ b/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): return { - 'fieldname': 'payment_order', - 'transactions': [ - { - 'items': ['Payment Entry', 'Journal Entry'] - } - ] + "fieldname": "payment_order", + "transactions": [{"items": ["Payment Entry", "Journal Entry"]}], } diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 3f4d89b4eaf..0dcb1794b9a 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -26,7 +26,9 @@ class TestPaymentOrder(unittest.TestCase): def test_payment_order_creation_against_payment_entry(self): purchase_invoice = make_purchase_invoice() - payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC") + payment_entry = get_payment_entry( + "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC" + ) payment_entry.reference_no = "_Test_Payment_Order" payment_entry.reference_date = getdate() payment_entry.party_bank_account = "Checking Account - Citi Bank" @@ -40,13 +42,16 @@ class TestPaymentOrder(unittest.TestCase): self.assertEqual(reference_doc.supplier, "_Test Supplier") self.assertEqual(reference_doc.amount, 250) + def create_payment_order_against_payment_entry(ref_doc, order_type): - payment_order = frappe.get_doc(dict( - doctype="Payment Order", - company="_Test Company", - payment_order_type=order_type, - company_bank_account="Checking Account - Citi Bank" - )) + payment_order = frappe.get_doc( + dict( + doctype="Payment Order", + company="_Test Company", + payment_order_type=order_type, + company_bank_account="Checking Account - Citi Bank", + ) + ) doc = make_payment_order(ref_doc.name, payment_order) doc.save() doc.submit() diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 648d2da754e..867fcc7f13e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -1,4 +1,4 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors // For license information, please see license.txt frappe.provide("erpnext.accounts"); @@ -38,6 +38,15 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext ] }; }); + + this.frm.set_query("cost_center", () => { + return { + "filters": { + "company": this.frm.doc.company, + "is_group": 0 + } + } + }); }, refresh: function() { diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index eb0c20f92d9..18d34850850 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -24,6 +24,7 @@ "invoice_limit", "payment_limit", "bank_cash_account", + "cost_center", "sec_break1", "invoices", "column_break_15", @@ -178,13 +179,19 @@ { "fieldname": "column_break_11", "fieldtype": "Column Break" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" } ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", "issingle": 1, "links": [], - "modified": "2021-10-04 20:27:11.114194", + "modified": "2022-04-29 15:37:10.246831", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", @@ -209,5 +216,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 548571d1d76..e5b942fb6ef 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -32,30 +32,43 @@ class PaymentReconciliation(Document): non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes if self.payment_limit: - non_reconciled_payments = non_reconciled_payments[:self.payment_limit] + non_reconciled_payments = non_reconciled_payments[: self.payment_limit] - non_reconciled_payments = sorted(non_reconciled_payments, key=lambda k: k['posting_date'] or getdate(nowdate())) + non_reconciled_payments = sorted( + non_reconciled_payments, key=lambda k: k["posting_date"] or getdate(nowdate()) + ) self.add_payment_entries(non_reconciled_payments) def get_payment_entries(self): - order_doctype = "Sales Order" if self.party_type=="Customer" else "Purchase Order" + order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" condition = self.get_conditions(get_payments=True) - payment_entries = get_advance_payment_entries(self.party_type, self.party, - self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.payment_limit, - condition=condition) + payment_entries = get_advance_payment_entries( + self.party_type, + self.party, + self.receivable_payable_account, + order_doctype, + against_all_orders=True, + limit=self.payment_limit, + condition=condition, + ) return payment_entries def get_jv_entries(self): condition = self.get_conditions() - dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' - else "debit_in_account_currency") + dr_or_cr = ( + "credit_in_account_currency" + if erpnext.get_party_account_type(self.party_type) == "Receivable" + else "debit_in_account_currency" + ) - bank_account_condition = "t2.against_account like %(bank_cash_account)s" \ - if self.bank_cash_account else "1=1" + bank_account_condition = ( + "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" + ) - journal_entries = frappe.db.sql(""" + journal_entries = frappe.db.sql( + """ select "Journal Entry" as reference_type, t1.name as reference_name, t1.posting_date, t1.remark as remarks, t2.name as reference_row, @@ -76,31 +89,42 @@ class PaymentReconciliation(Document): ELSE {bank_account_condition} END) order by t1.posting_date - """.format(**{ - "dr_or_cr": dr_or_cr, - "bank_account_condition": bank_account_condition, - "condition": condition - }), { + """.format( + **{ + "dr_or_cr": dr_or_cr, + "bank_account_condition": bank_account_condition, + "condition": condition, + } + ), + { "party_type": self.party_type, "party": self.party, "account": self.receivable_payable_account, - "bank_cash_account": "%%%s%%" % self.bank_cash_account - }, as_dict=1) + "bank_cash_account": "%%%s%%" % self.bank_cash_account, + }, + as_dict=1, + ) return list(journal_entries) def get_dr_or_cr_notes(self): condition = self.get_conditions(get_return_invoices=True) - dr_or_cr = ("credit_in_account_currency" - if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") + dr_or_cr = ( + "credit_in_account_currency" + if erpnext.get_party_account_type(self.party_type) == "Receivable" + else "debit_in_account_currency" + ) - reconciled_dr_or_cr = ("debit_in_account_currency" - if dr_or_cr == "credit_in_account_currency" else "credit_in_account_currency") + reconciled_dr_or_cr = ( + "debit_in_account_currency" + if dr_or_cr == "credit_in_account_currency" + else "credit_in_account_currency" + ) - voucher_type = ('Sales Invoice' - if self.party_type == 'Customer' else "Purchase Invoice") + voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" - return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type, + return frappe.db.sql( + """ SELECT doc.name as reference_name, %(voucher_type)s as reference_type, (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date, account_currency as currency FROM `tab{doc}` doc, `tabGL Entry` gl @@ -117,106 +141,115 @@ class PaymentReconciliation(Document): amount > 0 ORDER BY doc.posting_date """.format( - doc=voucher_type, - dr_or_cr=dr_or_cr, - reconciled_dr_or_cr=reconciled_dr_or_cr, - party_type_field=frappe.scrub(self.party_type), - condition=condition or ""), + doc=voucher_type, + dr_or_cr=dr_or_cr, + reconciled_dr_or_cr=reconciled_dr_or_cr, + party_type_field=frappe.scrub(self.party_type), + condition=condition or "", + ), { - 'party': self.party, - 'party_type': self.party_type, - 'voucher_type': voucher_type, - 'account': self.receivable_payable_account - }, as_dict=1) + "party": self.party, + "party_type": self.party_type, + "voucher_type": voucher_type, + "account": self.receivable_payable_account, + }, + as_dict=1, + ) def add_payment_entries(self, non_reconciled_payments): - self.set('payments', []) + self.set("payments", []) for payment in non_reconciled_payments: - row = self.append('payments', {}) + row = self.append("payments", {}) row.update(payment) def get_invoice_entries(self): - #Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against + # Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against condition = self.get_conditions(get_invoices=True) - non_reconciled_invoices = get_outstanding_invoices(self.party_type, self.party, - self.receivable_payable_account, condition=condition) + non_reconciled_invoices = get_outstanding_invoices( + self.party_type, self.party, self.receivable_payable_account, condition=condition + ) if self.invoice_limit: - non_reconciled_invoices = non_reconciled_invoices[:self.invoice_limit] + non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit] self.add_invoice_entries(non_reconciled_invoices) def add_invoice_entries(self, non_reconciled_invoices): - #Populate 'invoices' with JVs and Invoices to reconcile against - self.set('invoices', []) + # Populate 'invoices' with JVs and Invoices to reconcile against + self.set("invoices", []) for entry in non_reconciled_invoices: - inv = self.append('invoices', {}) - inv.invoice_type = entry.get('voucher_type') - inv.invoice_number = entry.get('voucher_no') - inv.invoice_date = entry.get('posting_date') - inv.amount = flt(entry.get('invoice_amount')) - inv.currency = entry.get('currency') - inv.outstanding_amount = flt(entry.get('outstanding_amount')) + inv = self.append("invoices", {}) + inv.invoice_type = entry.get("voucher_type") + inv.invoice_number = entry.get("voucher_no") + inv.invoice_date = entry.get("posting_date") + inv.amount = flt(entry.get("invoice_amount")) + inv.currency = entry.get("currency") + inv.outstanding_amount = flt(entry.get("outstanding_amount")) @frappe.whitelist() def allocate_entries(self, args): self.validate_entries() entries = [] - for pay in args.get('payments'): - pay.update({'unreconciled_amount': pay.get('amount')}) - for inv in args.get('invoices'): - if pay.get('amount') >= inv.get('outstanding_amount'): - res = self.get_allocated_entry(pay, inv, inv['outstanding_amount']) - pay['amount'] = flt(pay.get('amount')) - flt(inv.get('outstanding_amount')) - inv['outstanding_amount'] = 0 + for pay in args.get("payments"): + pay.update({"unreconciled_amount": pay.get("amount")}) + for inv in args.get("invoices"): + if pay.get("amount") >= inv.get("outstanding_amount"): + res = self.get_allocated_entry(pay, inv, inv["outstanding_amount"]) + pay["amount"] = flt(pay.get("amount")) - flt(inv.get("outstanding_amount")) + inv["outstanding_amount"] = 0 else: - res = self.get_allocated_entry(pay, inv, pay['amount']) - inv['outstanding_amount'] = flt(inv.get('outstanding_amount')) - flt(pay.get('amount')) - pay['amount'] = 0 - if pay.get('amount') == 0: + res = self.get_allocated_entry(pay, inv, pay["amount"]) + inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount")) + pay["amount"] = 0 + if pay.get("amount") == 0: entries.append(res) break - elif inv.get('outstanding_amount') == 0: + elif inv.get("outstanding_amount") == 0: entries.append(res) continue else: break - self.set('allocation', []) + self.set("allocation", []) for entry in entries: - if entry['allocated_amount'] != 0: - row = self.append('allocation', {}) + if entry["allocated_amount"] != 0: + row = self.append("allocation", {}) row.update(entry) def get_allocated_entry(self, pay, inv, allocated_amount): - return frappe._dict({ - 'reference_type': pay.get('reference_type'), - 'reference_name': pay.get('reference_name'), - 'reference_row': pay.get('reference_row'), - 'invoice_type': inv.get('invoice_type'), - 'invoice_number': inv.get('invoice_number'), - 'unreconciled_amount': pay.get('unreconciled_amount'), - 'amount': pay.get('amount'), - 'allocated_amount': allocated_amount, - 'difference_amount': pay.get('difference_amount') - }) + return frappe._dict( + { + "reference_type": pay.get("reference_type"), + "reference_name": pay.get("reference_name"), + "reference_row": pay.get("reference_row"), + "invoice_type": inv.get("invoice_type"), + "invoice_number": inv.get("invoice_number"), + "unreconciled_amount": pay.get("unreconciled_amount"), + "amount": pay.get("amount"), + "allocated_amount": allocated_amount, + "difference_amount": pay.get("difference_amount"), + } + ) @frappe.whitelist() def reconcile(self): self.validate_allocation() - dr_or_cr = ("credit_in_account_currency" - if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") + dr_or_cr = ( + "credit_in_account_currency" + if erpnext.get_party_account_type(self.party_type) == "Receivable" + else "debit_in_account_currency" + ) entry_list = [] dr_or_cr_notes = [] - for row in self.get('allocation'): + for row in self.get("allocation"): reconciled_entry = [] if row.invoice_number and row.allocated_amount: - if row.reference_type in ['Sales Invoice', 'Purchase Invoice']: + if row.reference_type in ["Sales Invoice", "Purchase Invoice"]: reconciled_entry = dr_or_cr_notes else: reconciled_entry = entry_list @@ -233,23 +266,25 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() def get_payment_details(self, row, dr_or_cr): - return frappe._dict({ - 'voucher_type': row.get('reference_type'), - 'voucher_no' : row.get('reference_name'), - 'voucher_detail_no' : row.get('reference_row'), - 'against_voucher_type' : row.get('invoice_type'), - 'against_voucher' : row.get('invoice_number'), - 'account' : self.receivable_payable_account, - 'party_type': self.party_type, - 'party': self.party, - 'is_advance' : row.get('is_advance'), - 'dr_or_cr' : dr_or_cr, - 'unreconciled_amount': flt(row.get('unreconciled_amount')), - 'unadjusted_amount' : flt(row.get('amount')), - 'allocated_amount' : flt(row.get('allocated_amount')), - 'difference_amount': flt(row.get('difference_amount')), - 'difference_account': row.get('difference_account') - }) + return frappe._dict( + { + "voucher_type": row.get("reference_type"), + "voucher_no": row.get("reference_name"), + "voucher_detail_no": row.get("reference_row"), + "against_voucher_type": row.get("invoice_type"), + "against_voucher": row.get("invoice_number"), + "account": self.receivable_payable_account, + "party_type": self.party_type, + "party": self.party, + "is_advance": row.get("is_advance"), + "dr_or_cr": dr_or_cr, + "unreconciled_amount": flt(row.get("unreconciled_amount")), + "unadjusted_amount": flt(row.get("amount")), + "allocated_amount": flt(row.get("allocated_amount")), + "difference_amount": flt(row.get("difference_amount")), + "difference_account": row.get("difference_account"), + } + ) def check_mandatory_to_fetch(self): for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: @@ -267,7 +302,9 @@ class PaymentReconciliation(Document): unreconciled_invoices = frappe._dict() for inv in self.get("invoices"): - unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(inv.invoice_number, inv.outstanding_amount) + unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault( + inv.invoice_number, inv.outstanding_amount + ) invoices_to_reconcile = [] for row in self.get("allocation"): @@ -275,13 +312,19 @@ class PaymentReconciliation(Document): invoices_to_reconcile.append(row.invoice_number) if flt(row.amount) - flt(row.allocated_amount) < 0: - frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}") - .format(row.idx, row.allocated_amount, row.amount)) + frappe.throw( + _( + "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}" + ).format(row.idx, row.allocated_amount, row.amount) + ) invoice_outstanding = unreconciled_invoices.get(row.invoice_type, {}).get(row.invoice_number) if flt(row.allocated_amount) - invoice_outstanding > 0.009: - frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}") - .format(row.idx, row.allocated_amount, invoice_outstanding)) + frappe.throw( + _( + "Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}" + ).format(row.idx, row.allocated_amount, invoice_outstanding) + ) if not invoices_to_reconcile: frappe.throw(_("No records found in Allocation table")) @@ -289,79 +332,134 @@ class PaymentReconciliation(Document): def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False): condition = " and company = '{0}' ".format(self.company) + if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices): + condition = " and cost_center = '{0}' ".format(self.cost_center) + if get_invoices: - condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) if self.from_invoice_date else "" - condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date)) if self.to_invoice_date else "" - dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' - else "credit_in_account_currency") + condition += ( + " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) + if self.from_invoice_date + else "" + ) + condition += ( + " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date)) + if self.to_invoice_date + else "" + ) + dr_or_cr = ( + "debit_in_account_currency" + if erpnext.get_party_account_type(self.party_type) == "Receivable" + else "credit_in_account_currency" + ) if self.minimum_invoice_amount: - condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount)) + condition += " and {dr_or_cr} >= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount) + ) if self.maximum_invoice_amount: - condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount)) + condition += " and {dr_or_cr} <= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount) + ) elif get_return_invoices: condition = " and doc.company = '{0}' ".format(self.company) - condition += " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else "" - condition += " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else "" - dr_or_cr = ("gl.debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' - else "gl.credit_in_account_currency") + condition += ( + " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) + if self.from_payment_date + else "" + ) + condition += ( + " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) + if self.to_payment_date + else "" + ) + dr_or_cr = ( + "debit_in_account_currency" + if erpnext.get_party_account_type(self.party_type) == "Receivable" + else "credit_in_account_currency" + ) if self.minimum_invoice_amount: - condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount)) + condition += " and gl.{dr_or_cr} >= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount) + ) if self.maximum_invoice_amount: - condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount)) + condition += " and gl.{dr_or_cr} <= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount) + ) else: - condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else "" - condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else "" + condition += ( + " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) + if self.from_payment_date + else "" + ) + condition += ( + " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) + if self.to_payment_date + else "" + ) if self.minimum_payment_amount: - condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) if get_payments \ + condition += ( + " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) + if get_payments else " and total_debit >= {0}".format(flt(self.minimum_payment_amount)) + ) if self.maximum_payment_amount: - condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) if get_payments \ + condition += ( + " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) + if get_payments else " and total_debit <= {0}".format(flt(self.maximum_payment_amount)) + ) return condition + def reconcile_dr_cr_note(dr_cr_notes, company): for inv in dr_cr_notes: - voucher_type = ('Credit Note' - if inv.voucher_type == 'Sales Invoice' else 'Debit Note') + voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" - reconcile_dr_or_cr = ('debit_in_account_currency' - if inv.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') + reconcile_dr_or_cr = ( + "debit_in_account_currency" + if inv.dr_or_cr == "credit_in_account_currency" + else "credit_in_account_currency" + ) company_currency = erpnext.get_company_currency(company) - jv = frappe.get_doc({ - "doctype": "Journal Entry", - "voucher_type": voucher_type, - "posting_date": today(), - "company": company, - "multi_currency": 1 if inv.currency != company_currency else 0, - "accounts": [ - { - 'account': inv.account, - 'party': inv.party, - 'party_type': inv.party_type, - inv.dr_or_cr: abs(inv.allocated_amount), - 'reference_type': inv.against_voucher_type, - 'reference_name': inv.against_voucher, - 'cost_center': erpnext.get_default_cost_center(company) - }, - { - 'account': inv.account, - 'party': inv.party, - 'party_type': inv.party_type, - reconcile_dr_or_cr: (abs(inv.allocated_amount) - if abs(inv.unadjusted_amount) > abs(inv.allocated_amount) else abs(inv.unadjusted_amount)), - 'reference_type': inv.voucher_type, - 'reference_name': inv.voucher_no, - 'cost_center': erpnext.get_default_cost_center(company) - } - ] - }) + jv = frappe.get_doc( + { + "doctype": "Journal Entry", + "voucher_type": voucher_type, + "posting_date": today(), + "company": company, + "multi_currency": 1 if inv.currency != company_currency else 0, + "accounts": [ + { + "account": inv.account, + "party": inv.party, + "party_type": inv.party_type, + inv.dr_or_cr: abs(inv.allocated_amount), + "reference_type": inv.against_voucher_type, + "reference_name": inv.against_voucher, + "cost_center": erpnext.get_default_cost_center(company), + }, + { + "account": inv.account, + "party": inv.party, + "party_type": inv.party_type, + reconcile_dr_or_cr: ( + abs(inv.allocated_amount) + if abs(inv.unadjusted_amount) > abs(inv.allocated_amount) + else abs(inv.unadjusted_amount) + ), + "reference_type": inv.voucher_type, + "reference_name": inv.voucher_no, + "cost_center": erpnext.get_default_cost_center(company), + }, + ], + } + ) jv.flags.ignore_mandatory = True jv.submit() diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 2271f48a2b9..d2374b77a63 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1,9 +1,96 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import add_days, getdate + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + class TestPaymentReconciliation(unittest.TestCase): - pass + @classmethod + def setUpClass(cls): + make_customer() + make_invoice_and_payment() + + def test_payment_reconciliation(self): + payment_reco = frappe.get_doc("Payment Reconciliation") + payment_reco.company = "_Test Company" + payment_reco.party_type = "Customer" + payment_reco.party = "_Test Payment Reco Customer" + payment_reco.receivable_payable_account = "Debtors - _TC" + payment_reco.from_invoice_date = add_days(getdate(), -1) + payment_reco.to_invoice_date = getdate() + payment_reco.from_payment_date = add_days(getdate(), -1) + payment_reco.to_payment_date = getdate() + payment_reco.maximum_invoice_amount = 1000 + payment_reco.maximum_payment_amount = 1000 + payment_reco.invoice_limit = 10 + payment_reco.payment_limit = 10 + payment_reco.bank_cash_account = "_Test Bank - _TC" + payment_reco.cost_center = "_Test Cost Center - _TC" + payment_reco.get_unreconciled_entries() + + self.assertEqual(len(payment_reco.get("invoices")), 1) + self.assertEqual(len(payment_reco.get("payments")), 1) + + payment_entry = payment_reco.get("payments")[0].reference_name + invoice = payment_reco.get("invoices")[0].invoice_number + + payment_reco.allocate_entries( + { + "payments": [payment_reco.get("payments")[0].as_dict()], + "invoices": [payment_reco.get("invoices")[0].as_dict()], + } + ) + payment_reco.reconcile() + + payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry) + self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice) + + +def make_customer(): + if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"): + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test Payment Reco Customer", + "customer_type": "Individual", + "customer_group": "_Test Customer Group", + "territory": "_Test Territory", + } + ).insert() + + +def make_invoice_and_payment(): + si = create_sales_invoice( + customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True + ) + si.cost_center = "_Test Cost Center - _TC" + si.save() + si.submit() + + pe = frappe.get_doc( + { + "doctype": "Payment Entry", + "payment_type": "Receive", + "party_type": "Customer", + "party": "_Test Payment Reco Customer", + "company": "_Test Company", + "paid_from_account_currency": "INR", + "paid_to_account_currency": "INR", + "source_exchange_rate": 1, + "target_exchange_rate": 1, + "reference_no": "1", + "reference_date": getdate(), + "received_amount": 690, + "paid_amount": 690, + "paid_from": "Debtors - _TC", + "paid_to": "_Test Bank - _TC", + "cost_center": "_Test Cost Center - _TC", + } + ) + pe.insert() + pe.submit() diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 1a833a4008e..f05dd5ba496 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -24,7 +24,7 @@ from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscr class PaymentRequest(Document): def validate(self): if self.get("__islocal"): - self.status = 'Draft' + self.status = "Draft" self.validate_reference_document() self.validate_payment_request_amount() self.validate_currency() @@ -35,51 +35,67 @@ class PaymentRequest(Document): frappe.throw(_("To create a Payment Request reference document is required")) def validate_payment_request_amount(self): - existing_payment_request_amount = \ - get_existing_payment_request_amount(self.reference_doctype, self.reference_name) + existing_payment_request_amount = get_existing_payment_request_amount( + self.reference_doctype, self.reference_name + ) if existing_payment_request_amount: ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if (hasattr(ref_doc, "order_type") \ - and getattr(ref_doc, "order_type") != "Shopping Cart"): + if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart": ref_amount = get_amount(ref_doc, self.payment_account) - if existing_payment_request_amount + flt(self.grand_total)> ref_amount: - frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount") - .format(self.reference_doctype)) + if existing_payment_request_amount + flt(self.grand_total) > ref_amount: + frappe.throw( + _("Total Payment Request amount cannot be greater than {0} amount").format( + self.reference_doctype + ) + ) def validate_currency(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if self.payment_account and ref_doc.currency != frappe.db.get_value("Account", self.payment_account, "account_currency"): + if self.payment_account and ref_doc.currency != frappe.db.get_value( + "Account", self.payment_account, "account_currency" + ): frappe.throw(_("Transaction currency must be same as Payment Gateway currency")) def validate_subscription_details(self): if self.is_a_subscription: amount = 0 for subscription_plan in self.subscription_plans: - payment_gateway = frappe.db.get_value("Subscription Plan", subscription_plan.plan, "payment_gateway") + payment_gateway = frappe.db.get_value( + "Subscription Plan", subscription_plan.plan, "payment_gateway" + ) if payment_gateway != self.payment_gateway_account: - frappe.throw(_('The payment gateway account in plan {0} is different from the payment gateway account in this payment request').format(subscription_plan.name)) + frappe.throw( + _( + "The payment gateway account in plan {0} is different from the payment gateway account in this payment request" + ).format(subscription_plan.name) + ) rate = get_plan_rate(subscription_plan.plan, quantity=subscription_plan.qty) amount += rate if amount != self.grand_total: - frappe.msgprint(_("The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.").format(self.grand_total, amount)) + frappe.msgprint( + _( + "The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document." + ).format(self.grand_total, amount) + ) def on_submit(self): - if self.payment_request_type == 'Outward': - self.db_set('status', 'Initiated') + if self.payment_request_type == "Outward": + self.db_set("status", "Initiated") return - elif self.payment_request_type == 'Inward': - self.db_set('status', 'Requested') + elif self.payment_request_type == "Inward": + self.db_set("status", "Requested") send_mail = self.payment_gateway_validation() if self.payment_gateway else None ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if (hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart") \ - or self.flags.mute_email: + if ( + hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart" + ) or self.flags.mute_email: send_mail = False if send_mail and self.payment_channel != "Phone": @@ -101,23 +117,27 @@ class PaymentRequest(Document): request_amount=request_amount, sender=self.email_to, currency=self.currency, - payment_gateway=self.payment_gateway + payment_gateway=self.payment_gateway, ) 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, - 'reference_docname': self.name, - 'status': 'Completed' - }, pluck="data") + data_of_completed_requests = frappe.get_all( + "Integration Request", + filters={ + "reference_doctype": self.doctype, + "reference_docname": self.name, + "status": "Completed", + }, + pluck="data", + ) if not data_of_completed_requests: return self.grand_total - request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests) + request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests) return request_amounts def on_cancel(self): @@ -126,8 +146,9 @@ class PaymentRequest(Document): def make_invoice(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if (hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart"): + if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart": from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice + si = make_sales_invoice(self.reference_name, ignore_permissions=True) si.allocate_advances_automatically = True si = si.insert(ignore_permissions=True) @@ -136,7 +157,7 @@ class PaymentRequest(Document): def payment_gateway_validation(self): try: controller = get_payment_gateway_controller(self.payment_gateway) - if hasattr(controller, 'on_payment_request_submission'): + if hasattr(controller, "on_payment_request_submission"): return controller.on_payment_request_submission(self) else: return True @@ -148,36 +169,45 @@ class PaymentRequest(Document): self.payment_url = self.get_payment_url() if self.payment_url: - self.db_set('payment_url', self.payment_url) + self.db_set("payment_url", self.payment_url) - if self.payment_url or not self.payment_gateway_account \ - or (self.payment_gateway_account and self.payment_channel == "Phone"): - self.db_set('status', 'Initiated') + if ( + self.payment_url + or not self.payment_gateway_account + or (self.payment_gateway_account and self.payment_channel == "Phone") + ): + self.db_set("status", "Initiated") def get_payment_url(self): if self.reference_doctype != "Fees": - data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1) + data = frappe.db.get_value( + self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1 + ) else: - data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["student_name"], as_dict=1) + data = frappe.db.get_value( + self.reference_doctype, self.reference_name, ["student_name"], as_dict=1 + ) data.update({"company": frappe.defaults.get_defaults().company}) controller = get_payment_gateway_controller(self.payment_gateway) controller.validate_transaction_currency(self.currency) - if hasattr(controller, 'validate_minimum_transaction_amount'): + if hasattr(controller, "validate_minimum_transaction_amount"): controller.validate_minimum_transaction_amount(self.currency, self.grand_total) - return controller.get_payment_url(**{ - "amount": flt(self.grand_total, self.precision("grand_total")), - "title": data.company.encode("utf-8"), - "description": self.subject.encode("utf-8"), - "reference_doctype": "Payment Request", - "reference_docname": self.name, - "payer_email": self.email_to or frappe.session.user, - "payer_name": frappe.safe_encode(data.customer_name), - "order_id": self.name, - "currency": self.currency - }) + return controller.get_payment_url( + **{ + "amount": flt(self.grand_total, self.precision("grand_total")), + "title": data.company.encode("utf-8"), + "description": self.subject.encode("utf-8"), + "reference_doctype": "Payment Request", + "reference_docname": self.name, + "payer_email": self.email_to or frappe.session.user, + "payer_name": frappe.safe_encode(data.customer_name), + "order_id": self.name, + "currency": self.currency, + } + ) def set_as_paid(self): if self.payment_channel == "Phone": @@ -202,32 +232,47 @@ class PaymentRequest(Document): else: party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company) - party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account) + party_account_currency = ref_doc.get("party_account_currency") or get_account_currency( + party_account + ) bank_amount = self.grand_total - if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: + if ( + party_account_currency == ref_doc.company_currency and party_account_currency != self.currency + ): party_amount = ref_doc.base_grand_total else: party_amount = self.grand_total - payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount, - bank_account=self.payment_account, bank_amount=bank_amount) + payment_entry = get_payment_entry( + self.reference_doctype, + self.reference_name, + party_amount=party_amount, + bank_account=self.payment_account, + bank_amount=bank_amount, + ) - payment_entry.update({ - "reference_no": self.name, - "reference_date": nowdate(), - "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(self.reference_doctype, - self.reference_name, self.name) - }) + payment_entry.update( + { + "reference_no": self.name, + "reference_date": nowdate(), + "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format( + self.reference_doctype, self.reference_name, self.name + ), + } + ) if payment_entry.difference_amount: company_details = get_company_defaults(ref_doc.company) - payment_entry.append("deductions", { - "account": company_details.exchange_gain_loss_account, - "cost_center": company_details.cost_center, - "amount": payment_entry.difference_amount - }) + payment_entry.append( + "deductions", + { + "account": company_details.exchange_gain_loss_account, + "cost_center": company_details.cost_center, + "amount": payment_entry.difference_amount, + }, + ) if submit: payment_entry.insert(ignore_permissions=True) @@ -243,16 +288,23 @@ class PaymentRequest(Document): "subject": self.subject, "message": self.get_message(), "now": True, - "attachments": [frappe.attach_print(self.reference_doctype, self.reference_name, - file_name=self.reference_name, print_format=self.print_format)]} - enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + "attachments": [ + frappe.attach_print( + self.reference_doctype, + self.reference_name, + file_name=self.reference_name, + print_format=self.print_format, + ) + ], + } + enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args) def get_message(self): """return message with payment gateway link""" context = { "doc": frappe.get_doc(self.reference_doctype, self.reference_name), - "payment_url": self.payment_url + "payment_url": self.payment_url, } if self.message: @@ -266,22 +318,26 @@ class PaymentRequest(Document): def check_if_payment_entry_exists(self): if self.status == "Paid": - if frappe.get_all("Payment Entry Reference", + if frappe.get_all( + "Payment Entry Reference", filters={"reference_name": self.reference_name, "docstatus": ["<", 2]}, fields=["parent"], - limit=1): - frappe.throw(_("Payment Entry already exists"), title=_('Error')) + limit=1, + ): + frappe.throw(_("Payment Entry already exists"), title=_("Error")) def make_communication_entry(self): """Make communication entry""" - comm = frappe.get_doc({ - "doctype":"Communication", - "subject": self.subject, - "content": self.get_message(), - "sent_or_received": "Sent", - "reference_doctype": self.reference_doctype, - "reference_name": self.reference_name - }) + comm = frappe.get_doc( + { + "doctype": "Communication", + "subject": self.subject, + "content": self.get_message(), + "sent_or_received": "Sent", + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name, + } + ) comm.insert(ignore_permissions=True) def get_payment_success_url(self): @@ -298,16 +354,17 @@ class PaymentRequest(Document): self.set_as_paid() # if shopping cart enabled and in session - if (shopping_cart_settings.enabled and hasattr(frappe.local, "session") - and frappe.local.session.user != "Guest") and self.payment_channel != "Phone": + if ( + shopping_cart_settings.enabled + and hasattr(frappe.local, "session") + and frappe.local.session.user != "Guest" + ) and self.payment_channel != "Phone": success_url = shopping_cart_settings.payment_success_url if success_url: - redirect_to = ({ - "Orders": "/orders", - "Invoices": "/invoices", - "My Account": "/me" - }).get(success_url, "/me") + redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get( + success_url, "/me" + ) else: redirect_to = get_url("/orders/{0}".format(self.reference_name)) @@ -317,6 +374,7 @@ class PaymentRequest(Document): if payment_provider == "stripe": return create_stripe_subscription(gateway_controller, data) + @frappe.whitelist(allow_guest=True) def make_payment_request(**args): """Make payment request""" @@ -329,55 +387,68 @@ def make_payment_request(**args): grand_total = get_amount(ref_doc, gateway_account.get("payment_account")) if args.loyalty_points and args.dt == "Sales Order": from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points + loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) - frappe.db.set_value("Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False) - frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False) + frappe.db.set_value( + "Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False + ) + frappe.db.set_value( + "Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False + ) grand_total = grand_total - loyalty_amount - bank_account = (get_party_bank_account(args.get('party_type'), args.get('party')) - if args.get('party_type') else '') + bank_account = ( + get_party_bank_account(args.get("party_type"), args.get("party")) + if args.get("party_type") + else "" + ) existing_payment_request = None if args.order_type == "Shopping Cart": - existing_payment_request = frappe.db.get_value("Payment Request", - {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)}) + existing_payment_request = frappe.db.get_value( + "Payment Request", + {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)}, + ) if existing_payment_request: - frappe.db.set_value("Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False) + frappe.db.set_value( + "Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False + ) pr = frappe.get_doc("Payment Request", existing_payment_request) else: if args.order_type != "Shopping Cart": - existing_payment_request_amount = \ - get_existing_payment_request_amount(args.dt, args.dn) + existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) if existing_payment_request_amount: grand_total -= existing_payment_request_amount pr = frappe.new_doc("Payment Request") - pr.update({ - "payment_gateway_account": gateway_account.get("name"), - "payment_gateway": gateway_account.get("payment_gateway"), - "payment_account": gateway_account.get("payment_account"), - "payment_channel": gateway_account.get("payment_channel"), - "payment_request_type": args.get("payment_request_type"), - "currency": ref_doc.currency, - "grand_total": grand_total, - "mode_of_payment": args.mode_of_payment, - "email_to": args.recipient_id or ref_doc.owner, - "subject": _("Payment Request for {0}").format(args.dn), - "message": gateway_account.get("message") or get_dummy_message(ref_doc), - "reference_doctype": args.dt, - "reference_name": args.dn, - "party_type": args.get("party_type") or "Customer", - "party": args.get("party") or ref_doc.get("customer"), - "bank_account": bank_account - }) + pr.update( + { + "payment_gateway_account": gateway_account.get("name"), + "payment_gateway": gateway_account.get("payment_gateway"), + "payment_account": gateway_account.get("payment_account"), + "payment_channel": gateway_account.get("payment_channel"), + "payment_request_type": args.get("payment_request_type"), + "currency": ref_doc.currency, + "grand_total": grand_total, + "mode_of_payment": args.mode_of_payment, + "email_to": args.recipient_id or ref_doc.owner, + "subject": _("Payment Request for {0}").format(args.dn), + "message": gateway_account.get("message") or get_dummy_message(ref_doc), + "reference_doctype": args.dt, + "reference_name": args.dn, + "party_type": args.get("party_type") or "Customer", + "party": args.get("party") or ref_doc.get("customer"), + "bank_account": bank_account, + } + ) if args.order_type == "Shopping Cart" or args.mute_email: pr.flags.mute_email = True - pr.insert(ignore_permissions=True) if args.submit_doc: + pr.insert(ignore_permissions=True) pr.submit() if args.order_type == "Shopping Cart": @@ -390,11 +461,15 @@ def make_payment_request(**args): return pr.as_dict() + def get_amount(ref_doc, payment_account=None): """get amount based on doctype""" dt = ref_doc.doctype if dt in ["Sales Order", "Purchase Order"]: - grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) + if ref_doc.party_account_currency == ref_doc.currency: + grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) + else: + grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) / ref_doc.conversion_rate elif dt in ["Sales Invoice", "Purchase Invoice"]: if ref_doc.party_account_currency == ref_doc.currency: @@ -411,18 +486,20 @@ def get_amount(ref_doc, payment_account=None): elif dt == "Fees": grand_total = ref_doc.outstanding_amount - if grand_total > 0 : + if grand_total > 0: return grand_total else: frappe.throw(_("Payment Entry is already created")) + def get_existing_payment_request_amount(ref_dt, ref_dn): """ Get the existing payment request which are unpaid or partially paid for payment channel other than Phone and get the summation of existing paid payment request for Phone payment channel. """ - existing_payment_request_amount = frappe.db.sql(""" + existing_payment_request_amount = frappe.db.sql( + """ select sum(grand_total) from `tabPayment Request` where @@ -432,9 +509,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn): and (status != 'Paid' or (payment_channel = 'Phone' and status = 'Paid')) - """, (ref_dt, ref_dn)) + """, + (ref_dt, ref_dn), + ) return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0 + def get_gateway_details(args): """return gateway and payment account of default payment gateway""" if args.get("payment_gateway_account"): @@ -448,58 +528,74 @@ def get_gateway_details(args): return gateway_account + def get_payment_gateway_account(args): - return frappe.db.get_value("Payment Gateway Account", args, + return frappe.db.get_value( + "Payment Gateway Account", + args, ["name", "payment_gateway", "payment_account", "message"], - as_dict=1) + as_dict=1, + ) + @frappe.whitelist() def get_print_format_list(ref_doctype): print_format_list = ["Standard"] - print_format_list.extend([p.name for p in frappe.get_all("Print Format", - filters={"doc_type": ref_doctype})]) + print_format_list.extend( + [p.name for p in frappe.get_all("Print Format", filters={"doc_type": ref_doctype})] + ) + + return {"print_format": print_format_list} - return { - "print_format": print_format_list - } @frappe.whitelist(allow_guest=True) def resend_payment_email(docname): return frappe.get_doc("Payment Request", docname).send_email() + @frappe.whitelist() def make_payment_entry(docname): doc = frappe.get_doc("Payment Request", docname) return doc.create_payment_entry(submit=False).as_dict() + def update_payment_req_status(doc, method): from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details for ref in doc.references: - payment_request_name = frappe.db.get_value("Payment Request", - {"reference_doctype": ref.reference_doctype, "reference_name": ref.reference_name, - "docstatus": 1}) + payment_request_name = frappe.db.get_value( + "Payment Request", + { + "reference_doctype": ref.reference_doctype, + "reference_name": ref.reference_name, + "docstatus": 1, + }, + ) if payment_request_name: - ref_details = get_reference_details(ref.reference_doctype, ref.reference_name, doc.party_account_currency) - pay_req_doc = frappe.get_doc('Payment Request', payment_request_name) + ref_details = get_reference_details( + ref.reference_doctype, ref.reference_name, doc.party_account_currency + ) + pay_req_doc = frappe.get_doc("Payment Request", payment_request_name) status = pay_req_doc.status if status != "Paid" and not ref_details.outstanding_amount: - status = 'Paid' + status = "Paid" elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount: - status = 'Partially Paid' + status = "Partially Paid" elif ref_details.outstanding_amount == ref_details.total_amount: - if pay_req_doc.payment_request_type == 'Outward': - status = 'Initiated' - elif pay_req_doc.payment_request_type == 'Inward': - status = 'Requested' + if pay_req_doc.payment_request_type == "Outward": + status = "Initiated" + elif pay_req_doc.payment_request_type == "Inward": + status = "Requested" + + pay_req_doc.db_set("status", status) - pay_req_doc.db_set('status', status) def get_dummy_message(doc): - return frappe.render_template("""{% if doc.contact_person -%} + return frappe.render_template( + """{% if doc.contact_person -%}

Dear {{ doc.contact_person }},

{%- else %}

Hello,

{% endif %} @@ -511,12 +607,19 @@ def get_dummy_message(doc):

{{ _("If you have any questions, please get back to us.") }}

{{ _("Thank you for your business!") }}

-""", dict(doc=doc, payment_url = '{{ payment_url }}')) +""", + dict(doc=doc, payment_url="{{ payment_url }}"), + ) + @frappe.whitelist() def get_subscription_details(reference_doctype, reference_name): if reference_doctype == "Sales Invoice": - subscriptions = frappe.db.sql("""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",reference_name, as_dict=1) + subscriptions = frappe.db.sql( + """SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""", + reference_name, + as_dict=1, + ) subscription_plans = [] for subscription in subscriptions: plans = frappe.get_doc("Subscription", subscription.sub_name).plans @@ -524,38 +627,50 @@ def get_subscription_details(reference_doctype, reference_name): subscription_plans.append(plan) return subscription_plans + @frappe.whitelist() def make_payment_order(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc + def set_missing_values(source, target): target.payment_order_type = "Payment Request" - target.append('references', { - 'reference_doctype': source.reference_doctype, - 'reference_name': source.reference_name, - 'amount': source.grand_total, - 'supplier': source.party, - 'payment_request': source_name, - 'mode_of_payment': source.mode_of_payment, - 'bank_account': source.bank_account, - 'account': source.account - }) + target.append( + "references", + { + "reference_doctype": source.reference_doctype, + "reference_name": source.reference_name, + "amount": source.grand_total, + "supplier": source.party, + "payment_request": source_name, + "mode_of_payment": source.mode_of_payment, + "bank_account": source.bank_account, + "account": source.account, + }, + ) - doclist = get_mapped_doc("Payment Request", source_name, { - "Payment Request": { - "doctype": "Payment Order", - } - }, target_doc, set_missing_values) + doclist = get_mapped_doc( + "Payment Request", + source_name, + { + "Payment Request": { + "doctype": "Payment Order", + } + }, + target_doc, + set_missing_values, + ) return doclist + def validate_payment(doc, method=None): if doc.reference_doctype != "Payment Request" or ( - frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status') - != "Paid" + frappe.db.get_value(doc.reference_doctype, doc.reference_docname, "status") != "Paid" ): return frappe.throw( - _("The Payment Request {0} is already paid, cannot process payment twice") - .format(doc.reference_docname) + _("The Payment Request {0} is already paid, cannot process payment twice").format( + doc.reference_docname + ) ) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index f679ccfe4ff..355784716a0 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -12,10 +12,7 @@ from erpnext.setup.utils import get_exchange_rate test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"] -payment_gateway = { - "doctype": "Payment Gateway", - "gateway": "_Test Gateway" -} +payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"} payment_method = [ { @@ -23,30 +20,38 @@ payment_method = [ "is_default": 1, "payment_gateway": "_Test Gateway", "payment_account": "_Test Bank - _TC", - "currency": "INR" + "currency": "INR", }, { "doctype": "Payment Gateway Account", "payment_gateway": "_Test Gateway", "payment_account": "_Test Bank USD - _TC", - "currency": "USD" - } + "currency": "USD", + }, ] + class TestPaymentRequest(unittest.TestCase): def setUp(self): if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"): frappe.get_doc(payment_gateway).insert(ignore_permissions=True) for method in payment_method: - if not frappe.db.get_value("Payment Gateway Account", {"payment_gateway": method["payment_gateway"], - "currency": method["currency"]}, "name"): + if not frappe.db.get_value( + "Payment Gateway Account", + {"payment_gateway": method["payment_gateway"], "currency": method["currency"]}, + "name", + ): frappe.get_doc(method).insert(ignore_permissions=True) def test_payment_request_linkings(self): so_inr = make_sales_order(currency="INR") - pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", - payment_gateway_account="_Test Gateway - INR") + pr = make_payment_request( + dt="Sales Order", + dn=so_inr.name, + recipient_id="saurabh@erpnext.com", + payment_gateway_account="_Test Gateway - INR", + ) self.assertEqual(pr.reference_doctype, "Sales Order") self.assertEqual(pr.reference_name, so_inr.name) @@ -55,45 +60,75 @@ class TestPaymentRequest(unittest.TestCase): conversion_rate = get_exchange_rate("USD", "INR") si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate) - pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - payment_gateway_account="_Test Gateway - USD") + pr = make_payment_request( + dt="Sales Invoice", + dn=si_usd.name, + recipient_id="saurabh@erpnext.com", + payment_gateway_account="_Test Gateway - USD", + ) self.assertEqual(pr.reference_doctype, "Sales Invoice") self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.currency, "USD") def test_payment_entry(self): - frappe.db.set_value("Company", "_Test Company", - "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC") + frappe.db.set_value( + "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" + ) frappe.db.set_value("Company", "_Test Company", "write_off_account", "_Test Write Off - _TC") frappe.db.set_value("Company", "_Test Company", "cost_center", "_Test Cost Center - _TC") so_inr = make_sales_order(currency="INR") - pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", - mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1) + pr = make_payment_request( + dt="Sales Order", + dn=so_inr.name, + recipient_id="saurabh@erpnext.com", + mute_email=1, + payment_gateway_account="_Test Gateway - INR", + submit_doc=1, + return_doc=1, + ) pe = pr.set_as_paid() so_inr = frappe.get_doc("Sales Order", so_inr.name) self.assertEqual(so_inr.advance_paid, 1000) - si_usd = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si_usd = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) - pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1) + pr = make_payment_request( + dt="Sales Invoice", + dn=si_usd.name, + recipient_id="saurabh@erpnext.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + submit_doc=1, + return_doc=1, + ) pe = pr.set_as_paid() - expected_gle = dict((d[0], d) for d in [ - ["_Test Receivable USD - _TC", 0, 5000, si_usd.name], - [pr.payment_account, 6290.0, 0, None], - ["_Test Exchange Gain/Loss - _TC", 0, 1290, None] - ]) + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Receivable USD - _TC", 0, 5000, si_usd.name], + [pr.payment_account, 6290.0, 0, None], + ["_Test Exchange Gain/Loss - _TC", 0, 1290, None], + ] + ) - gl_entries = frappe.db.sql("""select account, debit, credit, against_voucher + gl_entries = frappe.db.sql( + """select account, debit, credit, against_voucher from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", pe.name, as_dict=1) + order by account asc""", + pe.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -104,35 +139,49 @@ class TestPaymentRequest(unittest.TestCase): self.assertEqual(expected_gle[gle.account][3], gle.against_voucher) def test_status(self): - si_usd = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si_usd = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) - pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1) + pr = make_payment_request( + dt="Sales Invoice", + dn=si_usd.name, + recipient_id="saurabh@erpnext.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + submit_doc=1, + return_doc=1, + ) pe = pr.create_payment_entry() pr.load_from_db() - self.assertEqual(pr.status, 'Paid') + self.assertEqual(pr.status, "Paid") pe.cancel() pr.load_from_db() - self.assertEqual(pr.status, 'Requested') + self.assertEqual(pr.status, "Requested") def test_multiple_payment_entries_against_sales_order(self): # Make Sales Order, grand_total = 1000 so = make_sales_order() # Payment Request amount = 200 - pr1 = make_payment_request(dt="Sales Order", dn=so.name, - recipient_id="nabin@erpnext.com", return_doc=1) + pr1 = make_payment_request( + dt="Sales Order", dn=so.name, recipient_id="nabin@erpnext.com", return_doc=1 + ) pr1.grand_total = 200 + pr1.insert() pr1.submit() # Make a 2nd Payment Request - pr2 = make_payment_request(dt="Sales Order", dn=so.name, - recipient_id="nabin@erpnext.com", return_doc=1) + pr2 = make_payment_request( + dt="Sales Order", dn=so.name, recipient_id="nabin@erpnext.com", return_doc=1 + ) self.assertEqual(pr2.grand_total, 800) diff --git a/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py b/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py index 7f5b96c8a8a..8df97bf6c77 100644 --- a/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py +++ b/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'payment_term', - 'transactions': [ - { - 'label': _('Sales'), - 'items': ['Sales Invoice', 'Sales Order', 'Quotation'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Order'] - }, - { - 'items': ['Payment Terms Template'] - } - ] + "fieldname": "payment_term", + "transactions": [ + {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Quotation"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order"]}, + {"items": ["Payment Terms Template"]}, + ], } diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index 3a6999c5799..ea3b76c5243 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -16,10 +16,12 @@ class PaymentTermsTemplate(Document): def validate_invoice_portion(self): total_portion = 0 for term in self.terms: - total_portion += flt(term.get('invoice_portion', 0)) + total_portion += flt(term.get("invoice_portion", 0)) if flt(total_portion, 2) != 100.00: - frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red') + frappe.msgprint( + _("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red" + ) def check_duplicate_terms(self): terms = [] @@ -27,8 +29,9 @@ class PaymentTermsTemplate(Document): term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on) if term_info in terms: frappe.msgprint( - _('The Payment Term at row {0} is possibly a duplicate.').format(term.idx), - raise_exception=1, indicator='red' + _("The Payment Term at row {0} is possibly a duplicate.").format(term.idx), + raise_exception=1, + indicator="red", ) else: terms.append(term_info) diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py index aa5de2ca3f8..34ac773febc 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py @@ -1,32 +1,19 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'payment_terms_template', - 'non_standard_fieldnames': { - 'Customer Group': 'payment_terms', - 'Supplier Group': 'payment_terms', - 'Supplier': 'payment_terms', - 'Customer': 'payment_terms' + "fieldname": "payment_terms_template", + "non_standard_fieldnames": { + "Customer Group": "payment_terms", + "Supplier Group": "payment_terms", + "Supplier": "payment_terms", + "Customer": "payment_terms", }, - 'transactions': [ - { - 'label': _('Sales'), - 'items': ['Sales Invoice', 'Sales Order', 'Quotation'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Order'] - }, - { - 'label': _('Party'), - 'items': ['Customer', 'Supplier'] - }, - { - 'label': _('Group'), - 'items': ['Customer Group', 'Supplier Group'] - } - ] + "transactions": [ + {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Quotation"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order"]}, + {"label": _("Party"), "items": ["Customer", "Supplier"]}, + {"label": _("Group"), "items": ["Customer Group", "Supplier Group"]}, + ], } diff --git a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py index 8529ef5c3d5..9717f2009a0 100644 --- a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py @@ -8,64 +8,76 @@ import frappe class TestPaymentTermsTemplate(unittest.TestCase): def tearDown(self): - frappe.delete_doc('Payment Terms Template', '_Test Payment Terms Template For Test', force=1) + frappe.delete_doc("Payment Terms Template", "_Test Payment Terms Template For Test", force=1) def test_create_template(self): - template = frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Test', - 'terms': [{ - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - }] - }) + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Test", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + } + ], + } + ) self.assertRaises(frappe.ValidationError, template.insert) - template.append('terms', { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 0 - }) + template.append( + "terms", + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 0, + }, + ) template.insert() def test_credit_days(self): - template = frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Test', - 'terms': [{ - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 100.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': -30 - }] - }) + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Test", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 100.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": -30, + } + ], + } + ) self.assertRaises(frappe.ValidationError, template.insert) def test_duplicate_terms(self): - template = frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Test', - 'terms': [ - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - }, - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - } - - ] - }) + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Test", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + }, + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + }, + ], + } + ) self.assertRaises(frappe.ValidationError, template.insert) 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 d8a024248be..53b1c64c460 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -23,40 +23,52 @@ class PeriodClosingVoucher(AccountsController): self.make_gl_entries() def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") from erpnext.accounts.general_ledger import make_reverse_gl_entries + make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name) def validate_account_head(self): closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type") if closing_account_type not in ["Liability", "Equity"]: - frappe.throw(_("Closing Account {0} must be of type Liability / Equity") - .format(self.closing_account_head)) + frappe.throw( + _("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head) + ) account_currency = get_account_currency(self.closing_account_head) - company_currency = frappe.get_cached_value('Company', self.company, "default_currency") + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") if account_currency != company_currency: frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) def validate_posting_date(self): from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year - validate_fiscal_year(self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self) + validate_fiscal_year( + self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self + ) - self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1] + self.year_start_date = get_fiscal_year( + self.posting_date, self.fiscal_year, company=self.company + )[1] - pce = frappe.db.sql("""select name from `tabPeriod Closing Voucher` + pce = frappe.db.sql( + """select name from `tabPeriod Closing Voucher` where posting_date > %s and fiscal_year = %s and docstatus = 1""", - (self.posting_date, self.fiscal_year)) + (self.posting_date, self.fiscal_year), + ) if pce and pce[0][0]: - frappe.throw(_("Another Period Closing Entry {0} has been made after {1}") - .format(pce[0][0], self.posting_date)) + frappe.throw( + _("Another Period Closing Entry {0} has been made after {1}").format( + pce[0][0], self.posting_date + ) + ) def make_gl_entries(self): gl_entries = self.get_gl_entries() if gl_entries: from erpnext.accounts.general_ledger import make_gl_entries + make_gl_entries(gl_entries) def get_gl_entries(self): @@ -65,16 +77,29 @@ class PeriodClosingVoucher(AccountsController): for acc in pl_accounts: if flt(acc.bal_in_company_currency): - gl_entries.append(self.get_gl_dict({ - "account": acc.account, - "cost_center": acc.cost_center, - "finance_book": acc.finance_book, - "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)) + gl_entries.append( + self.get_gl_dict( + { + "account": acc.account, + "cost_center": acc.cost_center, + "finance_book": acc.finance_book, + "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, + ) + ) if gl_entries: gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts) @@ -89,16 +114,27 @@ class PeriodClosingVoucher(AccountsController): for acc in pl_accounts: if flt(acc.bal_in_company_currency): cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center - gl_entry = self.get_gl_dict({ - "account": self.closing_account_head, - "cost_center": cost_center, - "finance_book": acc.finance_book, - "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) + gl_entry = self.get_gl_dict( + { + "account": self.closing_account_head, + "cost_center": cost_center, + "finance_book": acc.finance_book, + "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) @@ -112,27 +148,31 @@ class PeriodClosingVoucher(AccountsController): _, default_dimensions = get_dimensions() for dimension in self.accounting_dimensions: - gl_entry.update({ - dimension: default_dimensions.get(self.company, {}).get(dimension) - }) + 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', 't1.finance_book'] + dimension_fields = ["t1.cost_center", "t1.finance_book"] self.accounting_dimensions = get_accounting_dimensions() for dimension in self.accounting_dimensions: - dimension_fields.append('t1.{0}'.format(dimension)) + dimension_fields.append("t1.{0}".format(dimension)) - return frappe.db.sql(""" + 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 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' + where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss' and t2.docstatus < 2 and t2.company = %s and t1.posting_date between %s and %s group by t1.account, {dimension_fields} - """.format(dimension_fields = ', '.join(dimension_fields)), (self.company, self.get("year_start_date"), self.posting_date), as_dict=1) + """.format( + dimension_fields=", ".join(dimension_fields) + ), + (self.company, self.get("year_start_date"), self.posting_date), + as_dict=1, + ) 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 030b4caf7ca..8e0e62d5f8c 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 @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import unittest import frappe @@ -19,7 +18,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") company = create_company() - cost_center = create_cost_center('Test Cost Center 1') + cost_center = create_cost_center("Test Cost Center 1") jv1 = make_journal_entry( amount=400, @@ -27,7 +26,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): account2="Sales - TPC", cost_center=cost_center, posting_date=now(), - save=False + save=False, ) jv1.company = company jv1.save() @@ -39,7 +38,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): account2="Cash - TPC", cost_center=cost_center, posting_date=now(), - save=False + save=False, ) jv2.company = company jv2.save() @@ -49,14 +48,17 @@ class TestPeriodClosingVoucher(unittest.TestCase): surplus_account = pcv.closing_account_head expected_gle = ( - ('Cost of Goods Sold - TPC', 0.0, 600.0), + ("Cost of Goods Sold - TPC", 0.0, 600.0), (surplus_account, 600.0, 400.0), - ('Sales - TPC', 400.0, 0.0) + ("Sales - TPC", 400.0, 0.0), ) - pcv_gle = frappe.db.sql(""" + pcv_gle = frappe.db.sql( + """ select account, debit, credit from `tabGL Entry` where voucher_no=%s order by account - """, (pcv.name)) + """, + (pcv.name), + ) self.assertEqual(pcv_gle, expected_gle) @@ -75,7 +77,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", rate=400, - debit_to="Debtors - TPC" + debit_to="Debtors - TPC", ) create_sales_invoice( company=company, @@ -83,7 +85,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", rate=200, - debit_to="Debtors - TPC" + debit_to="Debtors - TPC", ) pcv = self.make_period_closing_voucher(submit=False) @@ -95,15 +97,18 @@ class TestPeriodClosingVoucher(unittest.TestCase): expected_gle = ( (surplus_account, 0.0, 400.0, cost_center1), (surplus_account, 0.0, 200.0, cost_center2), - ('Sales - TPC', 400.0, 0.0, cost_center1), - ('Sales - TPC', 200.0, 0.0, cost_center2), + ("Sales - TPC", 400.0, 0.0, cost_center1), + ("Sales - TPC", 200.0, 0.0, cost_center2), ) - pcv_gle = frappe.db.sql(""" + pcv_gle = frappe.db.sql( + """ select account, debit, credit, cost_center from `tabGL Entry` where voucher_no=%s order by account, cost_center - """, (pcv.name)) + """, + (pcv.name), + ) self.assertEqual(pcv_gle, expected_gle) @@ -120,14 +125,14 @@ class TestPeriodClosingVoucher(unittest.TestCase): expense_account="Cost of Goods Sold - TPC", cost_center=cost_center, rate=400, - debit_to="Debtors - TPC" + debit_to="Debtors - TPC", ) jv = make_journal_entry( account1="Cash - TPC", account2="Sales - TPC", amount=400, cost_center=cost_center, - posting_date=now() + posting_date=now(), ) jv.company = company jv.finance_book = create_finance_book().name @@ -140,69 +145,84 @@ class TestPeriodClosingVoucher(unittest.TestCase): expected_gle = ( (surplus_account, 0.0, 400.0, None), (surplus_account, 0.0, 400.0, jv.finance_book), - ('Sales - TPC', 400.0, 0.0, None), - ('Sales - TPC', 400.0, 0.0, jv.finance_book) + ("Sales - TPC", 400.0, 0.0, None), + ("Sales - TPC", 400.0, 0.0, jv.finance_book), ) - pcv_gle = frappe.db.sql(""" + pcv_gle = frappe.db.sql( + """ select account, debit, credit, finance_book from `tabGL Entry` where voucher_no=%s order by account, finance_book - """, (pcv.name)) + """, + (pcv.name), + ) self.assertEqual(pcv_gle, expected_gle) def make_period_closing_voucher(self, submit=True): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") - pcv = frappe.get_doc({ - "doctype": "Period Closing Voucher", - "transaction_date": today(), - "posting_date": today(), - "company": "Test PCV Company", - "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], - "cost_center": cost_center, - "closing_account_head": surplus_account, - "remarks": "test" - }) + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": today(), + "posting_date": today(), + "company": "Test PCV Company", + "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], + "cost_center": cost_center, + "closing_account_head": surplus_account, + "remarks": "test", + } + ) pcv.insert() if submit: pcv.submit() 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) + 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) + 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) + 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.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 07059cb7aa3..49aab0d0bbf 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -23,21 +23,33 @@ class POSClosingEntry(StatusUpdater): def validate_pos_invoices(self): invalid_rows = [] for d in self.pos_transactions: - invalid_row = {'idx': d.idx} - pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice, - ["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0] + invalid_row = {"idx": d.idx} + pos_invoice = frappe.db.get_values( + "POS Invoice", + d.pos_invoice, + ["consolidated_invoice", "pos_profile", "docstatus", "owner"], + as_dict=1, + )[0] if pos_invoice.consolidated_invoice: - invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated"))) + invalid_row.setdefault("msg", []).append( + _("POS Invoice is {}").format(frappe.bold("already consolidated")) + ) invalid_rows.append(invalid_row) continue if pos_invoice.pos_profile != self.pos_profile: - invalid_row.setdefault('msg', []).append(_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))) + invalid_row.setdefault("msg", []).append( + _("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile)) + ) if pos_invoice.docstatus != 1: - invalid_row.setdefault('msg', []).append(_('POS Invoice is not {}').format(frappe.bold("submitted"))) + invalid_row.setdefault("msg", []).append( + _("POS Invoice is not {}").format(frappe.bold("submitted")) + ) if pos_invoice.owner != self.user: - invalid_row.setdefault('msg', []).append(_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))) + invalid_row.setdefault("msg", []).append( + _("POS Invoice isn't created by user {}").format(frappe.bold(self.owner)) + ) - if invalid_row.get('msg'): + if invalid_row.get("msg"): invalid_rows.append(invalid_row) if not invalid_rows: @@ -45,16 +57,18 @@ class POSClosingEntry(StatusUpdater): error_list = [] for row in invalid_rows: - for msg in row.get('msg'): - error_list.append(_("Row #{}: {}").format(row.get('idx'), msg)) + for msg in row.get("msg"): + error_list.append(_("Row #{}: {}").format(row.get("idx"), msg)) frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) @frappe.whitelist() def get_payment_reconciliation_details(self): - currency = frappe.get_cached_value('Company', self.company, "default_currency") - return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", - {"data": self, "currency": currency}) + currency = frappe.get_cached_value("Company", self.company, "default_currency") + return frappe.render_template( + "erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", + {"data": self, "currency": currency}, + ) def on_submit(self): consolidate_pos_invoices(closing_entry=self) @@ -72,29 +86,38 @@ class POSClosingEntry(StatusUpdater): opening_entry.set_status() opening_entry.save() + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_cashiers(doctype, txt, searchfield, start, page_len, filters): - cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'], as_list=1) + cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=["user"], as_list=1) return [c for c in cashiers_list] + @frappe.whitelist() def get_pos_invoices(start, end, pos_profile, user): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ select name, timestamp(posting_date, posting_time) as "timestamp" from `tabPOS Invoice` where owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = '' - """, (user, pos_profile), as_dict=1) + """, + (user, pos_profile), + as_dict=1, + ) - data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)) + data = list( + filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data) + ) # need to get taxes and payments so can't avoid get_doc data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data] return data + def make_closing_entry_from_opening(opening_entry): closing_entry = frappe.new_doc("POS Closing Entry") closing_entry.pos_opening_entry = opening_entry.name @@ -107,26 +130,38 @@ def make_closing_entry_from_opening(opening_entry): closing_entry.net_total = 0 closing_entry.total_quantity = 0 - invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, - closing_entry.pos_profile, closing_entry.user) + invoices = get_pos_invoices( + closing_entry.period_start_date, + closing_entry.period_end_date, + closing_entry.pos_profile, + closing_entry.user, + ) pos_transactions = [] taxes = [] payments = [] for detail in opening_entry.balance_details: - payments.append(frappe._dict({ - 'mode_of_payment': detail.mode_of_payment, - 'opening_amount': detail.opening_amount, - 'expected_amount': detail.opening_amount - })) + payments.append( + frappe._dict( + { + "mode_of_payment": detail.mode_of_payment, + "opening_amount": detail.opening_amount, + "expected_amount": detail.opening_amount, + } + ) + ) for d in invoices: - pos_transactions.append(frappe._dict({ - 'pos_invoice': d.name, - 'posting_date': d.posting_date, - 'grand_total': d.grand_total, - 'customer': d.customer - })) + pos_transactions.append( + frappe._dict( + { + "pos_invoice": d.name, + "posting_date": d.posting_date, + "grand_total": d.grand_total, + "customer": d.customer, + } + ) + ) closing_entry.grand_total += flt(d.grand_total) closing_entry.net_total += flt(d.net_total) closing_entry.total_quantity += flt(d.total_qty) @@ -134,24 +169,22 @@ def make_closing_entry_from_opening(opening_entry): for t in d.taxes: existing_tax = [tx for tx in taxes if tx.account_head == t.account_head and tx.rate == t.rate] if existing_tax: - existing_tax[0].amount += flt(t.tax_amount); + existing_tax[0].amount += flt(t.tax_amount) else: - taxes.append(frappe._dict({ - 'account_head': t.account_head, - 'rate': t.rate, - 'amount': t.tax_amount - })) + taxes.append( + frappe._dict({"account_head": t.account_head, "rate": t.rate, "amount": t.tax_amount}) + ) for p in d.payments: existing_pay = [pay for pay in payments if pay.mode_of_payment == p.mode_of_payment] if existing_pay: - existing_pay[0].expected_amount += flt(p.amount); + existing_pay[0].expected_amount += flt(p.amount) else: - payments.append(frappe._dict({ - 'mode_of_payment': p.mode_of_payment, - 'opening_amount': 0, - 'expected_amount': p.amount - })) + payments.append( + frappe._dict( + {"mode_of_payment": p.mode_of_payment, "opening_amount": 0, "expected_amount": p.amount} + ) + ) closing_entry.set("pos_transactions", pos_transactions) closing_entry.set("payment_reconciliation", payments) diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index c40cd363d8f..2d4acc4d047 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -28,24 +28,20 @@ class TestPOSClosingEntry(unittest.TestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) - pos_inv1.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500 - }) + pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) payment = pcv_doc.payment_reconciliation[0] - self.assertEqual(payment.mode_of_payment, 'Cash') + self.assertEqual(payment.mode_of_payment, "Cash") for d in pcv_doc.payment_reconciliation: - if d.mode_of_payment == 'Cash': + if d.mode_of_payment == "Cash": d.closing_amount = 6700 pcv_doc.submit() @@ -58,24 +54,20 @@ class TestPOSClosingEntry(unittest.TestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) - pos_inv1.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500 - }) + pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) payment = pcv_doc.payment_reconciliation[0] - self.assertEqual(payment.mode_of_payment, 'Cash') + self.assertEqual(payment.mode_of_payment, "Cash") for d in pcv_doc.payment_reconciliation: - if d.mode_of_payment == 'Cash': + if d.mode_of_payment == "Cash": d.closing_amount = 6700 pcv_doc.submit() @@ -91,22 +83,19 @@ class TestPOSClosingEntry(unittest.TestCase): si_doc.load_from_db() pos_inv1.load_from_db() self.assertEqual(si_doc.docstatus, 2) - self.assertEqual(pos_inv1.status, 'Paid') + self.assertEqual(pos_inv1.status, "Paid") def init_user_and_profile(**args): - user = 'test@example.com' - test_user = frappe.get_doc('User', user) + user = "test@example.com" + test_user = frappe.get_doc("User", user) roles = ("Accounts Manager", "Accounts User", "Sales Manager") test_user.add_roles(*roles) frappe.set_user(user) pos_profile = make_pos_profile(**args) - pos_profile.append('applicable_for_users', { - 'default': 1, - 'user': user - }) + pos_profile.append("applicable_for_users", {"default": 1, "user": user}) pos_profile.save() diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 0c6e7edeb02..b8500270d1a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -264,7 +264,6 @@ "print_hide": 1 }, { - "allow_on_submit": 1, "default": "0", "fieldname": "is_return", "fieldtype": "Check", @@ -1573,7 +1572,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-10-05 12:11:53.871828", + "modified": "2022-03-22 13:00:24.166684", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", @@ -1623,6 +1622,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 9d585411582..96975e9d116 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -17,7 +17,11 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( ) from erpnext.accounts.party import get_due_date, get_party_account from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty -from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivered_serial_nos, + get_pos_reserved_serial_nos, + get_serial_nos, +) class POSInvoice(SalesInvoice): @@ -26,7 +30,9 @@ class POSInvoice(SalesInvoice): def validate(self): if not cint(self.is_pos): - frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))) + frappe.throw( + _("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")) + ) # run on validate method of selling controller super(SalesInvoice, self).validate() @@ -50,11 +56,12 @@ class POSInvoice(SalesInvoice): self.validate_loyalty_transaction() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(self.coupon_code) def on_submit(self): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program - if self.loyalty_program: + if not self.is_return and self.loyalty_program: self.make_loyalty_point_entry() elif self.is_return and self.return_against and self.loyalty_program: against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) @@ -67,28 +74,32 @@ class POSInvoice(SalesInvoice): if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'used') + + update_coupon_code_count(self.coupon_code, "used") def before_cancel(self): - if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1: + if ( + self.consolidated_invoice + and frappe.db.get_value("Sales Invoice", self.consolidated_invoice, "docstatus") == 1 + ): pos_closing_entry = frappe.get_all( "POS Invoice Reference", ignore_permissions=True, - filters={ 'pos_invoice': self.name }, + filters={"pos_invoice": self.name}, pluck="parent", - limit=1 + limit=1, ) frappe.throw( - _('You need to cancel POS Closing Entry {} to be able to cancel this document.').format( + _("You need to cancel POS Closing Entry {} to be able to cancel this document.").format( get_link_to_form("POS Closing Entry", pos_closing_entry[0]) ), - title=_('Not Allowed') + title=_("Not Allowed"), ) def on_cancel(self): # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() - if self.loyalty_program: + if not self.is_return and self.loyalty_program: self.delete_loyalty_point_entry() elif self.is_return and self.return_against and self.loyalty_program: against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) @@ -97,16 +108,22 @@ class POSInvoice(SalesInvoice): if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'cancelled') + + update_coupon_code_count(self.coupon_code, "cancelled") def check_phone_payments(self): for pay in self.payments: if pay.type == "Phone" and pay.amount >= 0: - paid_amt = frappe.db.get_value("Payment Request", + paid_amt = frappe.db.get_value( + "Payment Request", filters=dict( - reference_doctype="POS Invoice", reference_name=self.name, - mode_of_payment=pay.mode_of_payment, status="Paid"), - fieldname="grand_total") + reference_doctype="POS Invoice", + reference_name=self.name, + mode_of_payment=pay.mode_of_payment, + status="Paid", + ), + fieldname="grand_total", + ) if paid_amt and pay.amount != paid_amt: return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) @@ -120,52 +137,73 @@ class POSInvoice(SalesInvoice): reserved_serial_nos = get_pos_reserved_serial_nos(filters) invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos] - bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos)) + bold_invalid_serial_nos = frappe.bold(", ".join(invalid_serial_nos)) if len(invalid_serial_nos) == 1: - frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.") - .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) + frappe.throw( + _( + "Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no." + ).format(item.idx, bold_invalid_serial_nos), + title=_("Item Unavailable"), + ) elif invalid_serial_nos: - frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.") - .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) + frappe.throw( + _( + "Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no." + ).format(item.idx, bold_invalid_serial_nos), + title=_("Item Unavailable"), + ) def validate_pos_reserved_batch_qty(self, item): - filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no} + filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no} available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code) reserved_batch_qty = get_pos_reserved_batch_qty(filters) bold_item_name = frappe.bold(item.item_name) - bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty)) + bold_extra_batch_qty_needed = frappe.bold( + abs(available_batch_qty - reserved_batch_qty - item.qty) + ) bold_invalid_batch_no = frappe.bold(item.batch_no) if (available_batch_qty - reserved_batch_qty) == 0: - frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.") - .format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable")) + frappe.throw( + _( + "Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no." + ).format(item.idx, bold_invalid_batch_no, bold_item_name), + title=_("Item Unavailable"), + ) elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: - frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required") - .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable")) + frappe.throw( + _( + "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" + ).format( + item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed + ), + title=_("Item Unavailable"), + ) def validate_delivered_serial_nos(self, item): - serial_nos = get_serial_nos(item.serial_no) - delivered_serial_nos = frappe.db.get_list('Serial No', { - 'item_code': item.item_code, - 'name': ['in', serial_nos], - 'sales_invoice': ['is', 'set'] - }, pluck='name') + delivered_serial_nos = get_delivered_serial_nos(item.serial_no) if delivered_serial_nos: - bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos)) - frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.") - .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) + bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos)) + frappe.throw( + _( + "Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no." + ).format(item.idx, bold_delivered_serial_nos), + title=_("Item Unavailable"), + ) def validate_invalid_serial_nos(self, item): serial_nos = get_serial_nos(item.serial_no) error_msg = [] invalid_serials, msg = "", "" for serial_no in serial_nos: - if not frappe.db.exists('Serial No', serial_no): + if not frappe.db.exists("Serial No", serial_no): invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no - msg = (_("Row #{}: Following Serial numbers for item {} are Invalid: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials))) + msg = _("Row #{}: Following Serial numbers for item {} are Invalid: {}").format( + item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials) + ) if invalid_serials: error_msg.append(msg) @@ -173,11 +211,18 @@ class POSInvoice(SalesInvoice): frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_stock_availablility(self): - if self.is_return or self.docstatus != 1: + if self.is_return: return - allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') - for d in self.get('items'): - is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item')) + + if self.docstatus == 0 and not frappe.db.get_value( + "POS Profile", self.pos_profile, "validate_stock_on_save" + ): + return + + allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + + for d in self.get("items"): + is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item")) if is_service_item: return if d.serial_no: @@ -192,13 +237,25 @@ class POSInvoice(SalesInvoice): available_stock, is_stock_item = 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) + 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 {}.') - .format(d.idx, item_code, warehouse), title=_("Item Unavailable")) + frappe.throw( + _("Row #{}: Item Code: {} is not available under warehouse {}.").format( + d.idx, item_code, warehouse + ), + title=_("Item Unavailable"), + ) elif flt(available_stock) < flt(d.qty): - frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.') - .format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable")) + frappe.throw( + _( + "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." + ).format(d.idx, item_code, warehouse, available_stock), + title=_("Item Unavailable"), + ) def validate_serialised_or_batched_item(self): error_msg = [] @@ -212,16 +269,21 @@ class POSInvoice(SalesInvoice): item_code = frappe.bold(d.item_code) serial_nos = get_serial_nos(d.serial_no) if serialized and batched and (no_batch_selected or no_serial_selected): - msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.') - .format(d.idx, item_code)) + msg = _( + "Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction." + ).format(d.idx, item_code) elif serialized and no_serial_selected: - msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.') - .format(d.idx, item_code)) + msg = _( + "Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction." + ).format(d.idx, item_code) elif batched and no_batch_selected: - msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.') - .format(d.idx, item_code)) + msg = _( + "Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction." + ).format(d.idx, item_code) elif serialized and not no_serial_selected and len(serial_nos) != d.qty: - msg = (_("Row #{}: You must select {} serial numbers for item {}.").format(d.idx, frappe.bold(cint(d.qty)), item_code)) + msg = _("Row #{}: You must select {} serial numbers for item {}.").format( + d.idx, frappe.bold(cint(d.qty)), item_code + ) if msg: error_msg.append(msg) @@ -230,18 +292,22 @@ class POSInvoice(SalesInvoice): frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_return_items_qty(self): - if not self.get("is_return"): return + if not self.get("is_return"): + return for d in self.get("items"): if d.get("qty") > 0: frappe.throw( - _("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.") - .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item") + _( + "Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return." + ).format(d.idx, frappe.bold(d.item_code)), + title=_("Invalid Item"), ) if d.get("serial_no"): serial_nos = get_serial_nos(d.serial_no) for sr in serial_nos: - serial_no_exists = frappe.db.sql(""" + serial_no_exists = frappe.db.sql( + """ SELECT name FROM `tabPOS Invoice Item` WHERE @@ -251,14 +317,17 @@ class POSInvoice(SalesInvoice): or serial_no like %s or serial_no like %s ) - """, (self.return_against, sr, sr+'\n%', '%\n'+sr, '%\n'+sr+'\n%')) + """, + (self.return_against, sr, sr + "\n%", "%\n" + sr, "%\n" + sr + "\n%"), + ) if not serial_no_exists: bold_return_against = frappe.bold(self.return_against) bold_serial_no = frappe.bold(sr) frappe.throw( - _("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}") - .format(d.idx, bold_serial_no, bold_return_against) + _( + "Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}" + ).format(d.idx, bold_serial_no, bold_return_against) ) def validate_mode_of_payment(self): @@ -266,16 +335,25 @@ class POSInvoice(SalesInvoice): frappe.throw(_("At least one mode of payment is required for POS invoice.")) def validate_change_account(self): - if self.change_amount and self.account_for_change_amount and \ - frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company: - frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company)) + if ( + self.change_amount + and self.account_for_change_amount + and frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company + ): + frappe.throw( + _("The selected change account {} doesn't belongs to Company {}.").format( + self.account_for_change_amount, self.company + ) + ) def validate_change_amount(self): grand_total = flt(self.rounded_total) or flt(self.grand_total) base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total) if not flt(self.change_amount) and grand_total < flt(self.paid_amount): self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount)) - self.base_change_amount = flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount) + self.base_change_amount = ( + flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount) + ) if flt(self.change_amount) and not self.account_for_change_amount: frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1) @@ -295,8 +373,12 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total)) def validate_loyalty_transaction(self): - if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): - expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"]) + if self.redeem_loyalty_points and ( + not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center + ): + expense_account, cost_center = frappe.db.get_value( + "Loyalty Program", self.loyalty_program, ["expense_account", "cost_center"] + ) if not self.loyalty_redemption_account: self.loyalty_redemption_account = expense_account if not self.loyalty_redemption_cost_center: @@ -307,8 +389,8 @@ class POSInvoice(SalesInvoice): def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): - if self.get('amended_from'): - self.status = 'Draft' + if self.get("amended_from"): + self.status = "Draft" return if not status: @@ -317,19 +399,35 @@ class POSInvoice(SalesInvoice): elif self.docstatus == 1: if self.consolidated_invoice: self.status = "Consolidated" - elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed': + elif ( + flt(self.outstanding_amount) > 0 + and getdate(self.due_date) < getdate(nowdate()) + and self.is_discounted + and self.get_discounting_status() == "Disbursed" + ): self.status = "Overdue and Discounted" elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()): self.status = "Overdue" - elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed': + elif ( + flt(self.outstanding_amount) > 0 + and getdate(self.due_date) >= getdate(nowdate()) + and self.is_discounted + and self.get_discounting_status() == "Disbursed" + ): self.status = "Unpaid and Discounted" elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()): self.status = "Unpaid" - elif flt(self.outstanding_amount) <= 0 and self.is_return == 0 and frappe.db.get_value('POS Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + elif ( + flt(self.outstanding_amount) <= 0 + and self.is_return == 0 + and frappe.db.get_value( + "POS Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} + ) + ): self.status = "Credit Note Issued" elif self.is_return == 1: self.status = "Return" - elif flt(self.outstanding_amount)<=0: + elif flt(self.outstanding_amount) <= 0: self.status = "Paid" else: self.status = "Submitted" @@ -337,22 +435,23 @@ class POSInvoice(SalesInvoice): self.status = "Draft" if update: - self.db_set('status', self.status, update_modified = update_modified) + self.db_set("status", self.status, update_modified=update_modified) def set_pos_fields(self, for_validate=False): """Set retail related fields from POS Profiles""" from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details + if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} if not pos_profile: frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) - self.pos_profile = pos_profile.get('name') + self.pos_profile = pos_profile.get("name") profile = {} if self.pos_profile: - profile = frappe.get_doc('POS Profile', self.pos_profile) + profile = frappe.get_doc("POS Profile", self.pos_profile) - if not self.get('payments') and not for_validate: + if not self.get("payments") and not for_validate: update_multi_mode_option(self, profile) if self.is_return and not for_validate: @@ -362,35 +461,55 @@ class POSInvoice(SalesInvoice): if not for_validate and not self.customer: self.customer = profile.customer - self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount - self.set_warehouse = profile.get('warehouse') or self.set_warehouse + self.account_for_change_amount = ( + profile.get("account_for_change_amount") or self.account_for_change_amount + ) + self.set_warehouse = profile.get("warehouse") or self.set_warehouse - for fieldname in ('currency', 'letter_head', 'tc_name', - 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', - 'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category', - 'ignore_pricing_rule', 'company_address', 'update_stock'): - if not for_validate: - self.set(fieldname, profile.get(fieldname)) + for fieldname in ( + "currency", + "letter_head", + "tc_name", + "company", + "select_print_heading", + "write_off_account", + "taxes_and_charges", + "write_off_cost_center", + "apply_discount_on", + "cost_center", + "tax_category", + "ignore_pricing_rule", + "company_address", + "update_stock", + ): + if not for_validate: + self.set(fieldname, profile.get(fieldname)) if self.customer: customer_price_list, customer_group, customer_currency = frappe.db.get_value( - "Customer", self.customer, ['default_price_list', 'customer_group', 'default_currency'] + "Customer", self.customer, ["default_price_list", "customer_group", "default_currency"] ) - customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') - selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list') - if customer_currency != profile.get('currency'): - self.set('currency', customer_currency) + customer_group_price_list = frappe.db.get_value( + "Customer Group", customer_group, "default_price_list" + ) + selling_price_list = ( + customer_price_list or customer_group_price_list or profile.get("selling_price_list") + ) + if customer_currency != profile.get("currency"): + self.set("currency", customer_currency) else: - selling_price_list = profile.get('selling_price_list') + selling_price_list = profile.get("selling_price_list") if selling_price_list: - self.set('selling_price_list', selling_price_list) + self.set("selling_price_list", selling_price_list) # set pos values in items for item in self.get("items"): - if item.get('item_code'): - profile_details = get_pos_profile_item_details(profile.get("company"), frappe._dict(item.as_dict()), profile) + if item.get("item_code"): + profile_details = get_pos_profile_item_details( + profile.get("company"), frappe._dict(item.as_dict()), profile + ) for fname, val in iteritems(profile_details): if (not for_validate) or (for_validate and not item.get(fname)): item.set(fname, val) @@ -404,7 +523,9 @@ class POSInvoice(SalesInvoice): self.set_taxes() if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + self.account_for_change_amount = frappe.get_cached_value( + "Company", self.company, "default_cash_account" + ) return profile @@ -414,27 +535,29 @@ class POSInvoice(SalesInvoice): if not self.debit_to: self.debit_to = get_party_account("Customer", self.customer, self.company) - self.party_account_currency = frappe.db.get_value("Account", self.debit_to, "account_currency", cache=True) + self.party_account_currency = frappe.db.get_value( + "Account", self.debit_to, "account_currency", cache=True + ) if not self.due_date and self.customer: self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company) super(SalesInvoice, self).set_missing_values(for_validate) print_format = profile.get("print_format") if profile else None - if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')): - print_format = 'POS Invoice' + if not print_format and not cint(frappe.db.get_value("Print Format", "POS Invoice", "disabled")): + print_format = "POS Invoice" if profile: return { "print_format": print_format, "campaign": profile.get("campaign"), - "allow_print_before_pay": profile.get("allow_print_before_pay") + "allow_print_before_pay": profile.get("allow_print_before_pay"), } @frappe.whitelist() def reset_mode_of_payments(self): if self.pos_profile: - pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile) + pos_profile = frappe.get_cached_doc("POS Profile", self.pos_profile) update_multi_mode_option(self, pos_profile) self.paid_amount = 0 @@ -456,6 +579,7 @@ class POSInvoice(SalesInvoice): pay_req = self.get_existing_payment_request(pay) if not pay_req: pay_req = self.get_new_payment_request(pay) + pay_req.insert() pay_req.submit() else: pay_req.request_phone_payment() @@ -463,9 +587,13 @@ class POSInvoice(SalesInvoice): return pay_req def get_new_payment_request(self, mop): - payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { - "payment_account": mop.account, - }, ["name"]) + payment_gateway_account = frappe.db.get_value( + "Payment Gateway Account", + { + "payment_account": mop.account, + }, + ["name"], + ) args = { "dt": "POS Invoice", @@ -476,36 +604,41 @@ class POSInvoice(SalesInvoice): "payment_request_type": "Inward", "party_type": "Customer", "party": self.customer, - "return_doc": True + "return_doc": True, } return make_payment_request(**args) def get_existing_payment_request(self, pay): - payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { - "payment_account": pay.account, - }, ["name"]) + payment_gateway_account = frappe.db.get_value( + "Payment Gateway Account", + { + "payment_account": pay.account, + }, + ["name"], + ) args = { - 'doctype': 'Payment Request', - 'reference_doctype': 'POS Invoice', - 'reference_name': self.name, - 'payment_gateway_account': payment_gateway_account, - 'email_to': self.contact_mobile + "doctype": "Payment Request", + "reference_doctype": "POS Invoice", + "reference_name": self.name, + "payment_gateway_account": payment_gateway_account, + "email_to": self.contact_mobile, } pr = frappe.db.exists(args) if pr: - return frappe.get_doc('Payment Request', pr[0][0]) + return frappe.get_doc("Payment Request", pr[0][0]) + @frappe.whitelist() def get_stock_availability(item_code, warehouse): - if frappe.db.get_value('Item', item_code, 'is_stock_item'): + if frappe.db.get_value("Item", item_code, "is_stock_item"): is_stock_item = True bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty - pos_sales_qty, is_stock_item else: is_stock_item = False - if frappe.db.exists('Product Bundle', item_code): + if frappe.db.exists("Product Bundle", item_code): return get_bundle_availability(item_code, warehouse), is_stock_item else: # Is a service item @@ -513,7 +646,7 @@ def get_stock_availability(item_code, warehouse): def get_bundle_availability(bundle_item_code, warehouse): - product_bundle = frappe.get_doc('Product Bundle', bundle_item_code) + product_bundle = frappe.get_doc("Product Bundle", bundle_item_code) bundle_bin_qty = 1000000 for item in product_bundle.items: @@ -528,30 +661,43 @@ def get_bundle_availability(bundle_item_code, warehouse): 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` + 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) + limit 1""", + (item_code, warehouse), + as_dict=1, + ) 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 + reserved_qty = frappe.db.sql( + """select sum(p_item.qty) as qty from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item where p.name = p_item.parent and ifnull(p.consolidated_invoice, '') = '' and p_item.docstatus = 1 and p_item.item_code = %s and p_item.warehouse = %s - """, (item_code, warehouse), as_dict=1) + """, + (item_code, warehouse), + as_dict=1, + ) return reserved_qty[0].qty or 0 if reserved_qty else 0 + @frappe.whitelist() def make_sales_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("POS Invoice", source_name, target_doc) + @frappe.whitelist() def make_merge_log(invoices): import json @@ -562,35 +708,42 @@ def make_merge_log(invoices): invoices = json.loads(invoices) if len(invoices) == 0: - frappe.throw(_('Atleast one invoice has to be selected.')) + frappe.throw(_("Atleast one invoice has to be selected.")) merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log.posting_date = getdate(nowdate()) for inv in invoices: - inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), - ["customer", "posting_date", "grand_total"], as_dict=1)[0] + inv_data = frappe.db.get_values( + "POS Invoice", inv.get("name"), ["customer", "posting_date", "grand_total"], as_dict=1 + )[0] merge_log.customer = inv_data.customer - merge_log.append("pos_invoices", { - 'pos_invoice': inv.get('name'), - 'customer': inv_data.customer, - 'posting_date': inv_data.posting_date, - 'grand_total': inv_data.grand_total - }) + merge_log.append( + "pos_invoices", + { + "pos_invoice": inv.get("name"), + "customer": inv_data.customer, + "posting_date": inv_data.posting_date, + "grand_total": inv_data.grand_total, + }, + ) - if merge_log.get('pos_invoices'): + if merge_log.get("pos_invoices"): return merge_log.as_dict() + def add_return_modes(doc, pos_profile): def append_payment(payment_mode): - payment = doc.append('payments', {}) + payment = doc.append("payments", {}) payment.default = payment_mode.default payment.mode_of_payment = payment_mode.parent payment.account = payment_mode.default_account payment.type = payment_mode.type - for pos_payment_method in pos_profile.get('payments'): + for pos_payment_method in pos_profile.get("payments"): pos_payment_method = pos_payment_method.as_dict() 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]: + 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]) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index cf8affdd010..70f128e0e39 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -35,29 +35,34 @@ class TestPOSInvoice(unittest.TestCase): w2 = frappe.get_doc(w.doctype, w.name) import time + time.sleep(1) w.save() import time + time.sleep(1) self.assertRaises(frappe.TimestampMismatchError, w2.save) def test_change_naming_series(self): inv = create_pos_invoice(do_not_submit=1) - inv.naming_series = 'TEST-' + inv.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, inv.save) def test_discount_and_inclusive_tax(self): inv = create_pos_invoice(qty=100, rate=50, do_not_save=1) - inv.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + inv.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) inv.insert() self.assertEqual(inv.net_total, 4385.96) @@ -66,7 +71,7 @@ class TestPOSInvoice(unittest.TestCase): inv.reload() inv.discount_amount = 100 - inv.apply_discount_on = 'Net Total' + inv.apply_discount_on = "Net Total" inv.payment_schedule = [] inv.save() @@ -77,7 +82,7 @@ class TestPOSInvoice(unittest.TestCase): inv.reload() inv.discount_amount = 100 - inv.apply_discount_on = 'Grand Total' + inv.apply_discount_on = "Grand Total" inv.payment_schedule = [] inv.save() @@ -93,14 +98,17 @@ class TestPOSInvoice(unittest.TestCase): item_row_copy.qty = qty inv.append("items", item_row_copy) - inv.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 19 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 19, + }, + ) inv.insert() self.assertEqual(inv.net_total, 4600) @@ -115,10 +123,10 @@ class TestPOSInvoice(unittest.TestCase): item_row = inv.get("items")[0] add_items = [ - (54, '_Test Account Excise Duty @ 12 - _TC'), - (288, '_Test Account Excise Duty @ 15 - _TC'), - (144, '_Test Account Excise Duty @ 20 - _TC'), - (430, '_Test Item Tax Template 1 - _TC') + (54, "_Test Account Excise Duty @ 12 - _TC"), + (288, "_Test Account Excise Duty @ 15 - _TC"), + (144, "_Test Account Excise Duty @ 20 - _TC"), + (430, "_Test Item Tax Template 1 - _TC"), ] for qty, item_tax_template in add_items: item_row_copy = copy.deepcopy(item_row) @@ -126,30 +134,39 @@ class TestPOSInvoice(unittest.TestCase): item_row_copy.item_tax_template = item_tax_template inv.append("items", item_row_copy) - inv.append("taxes", { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 11 - }) - inv.append("taxes", { - "account_head": "_Test Account Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 0 - }) - inv.append("taxes", { - "account_head": "_Test Account S&H Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "S&H Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 3 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 11, + }, + ) + inv.append( + "taxes", + { + "account_head": "_Test Account Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 0, + }, + ) + inv.append( + "taxes", + { + "account_head": "_Test Account S&H Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "S&H Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 3, + }, + ) inv.insert() self.assertEqual(inv.net_total, 4600) @@ -179,14 +196,17 @@ class TestPOSInvoice(unittest.TestCase): inv.apply_discount_on = "Net Total" inv.discount_amount = 75.0 - inv.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 24 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 24, + }, + ) inv.insert() self.assertEqual(inv.total, 975) @@ -198,11 +218,15 @@ class TestPOSInvoice(unittest.TestCase): self.assertEqual(inv.grand_total, 1116.0) def test_pos_returns_with_repayment(self): - pos = create_pos_invoice(qty = 10, do_not_save=True) + pos = create_pos_invoice(qty=10, do_not_save=True) - pos.set('payments', []) - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1}) + pos.set("payments", []) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500} + ) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500, "default": 1} + ) pos.insert() pos.submit() @@ -211,25 +235,39 @@ class TestPOSInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - self.assertEqual(pos_return.get('payments')[0].amount, -500) - self.assertEqual(pos_return.get('payments')[1].amount, -500) + self.assertEqual(pos_return.get("payments")[0].amount, -500) + self.assertEqual(pos_return.get("payments")[1].amount, -500) def test_pos_return_for_serialized_item(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - se = make_serialized_item(company='_Test Company', - target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) serial_nos = get_serial_nos(se.get("items")[0].serial_no) - pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', - account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', - expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', - item=se.get("items")[0].item_code, rate=1000, do_not_save=1) + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + do_not_save=1, + ) pos.get("items")[0].serial_no = serial_nos[0] - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1}) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} + ) pos.insert() pos.submit() @@ -238,24 +276,39 @@ class TestPOSInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0]) + self.assertEqual(pos_return.get("items")[0].serial_no, serial_nos[0]) def test_partial_pos_returns(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - se = make_serialized_item(company='_Test Company', - target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) serial_nos = get_serial_nos(se.get("items")[0].serial_no) - pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', - account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', - expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', - item=se.get("items")[0].item_code, qty=2, rate=1000, do_not_save=1) + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + qty=2, + rate=1000, + do_not_save=1, + ) pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1] - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1}) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} + ) pos.insert() pos.submit() @@ -263,24 +316,34 @@ class TestPOSInvoice(unittest.TestCase): pos_return1 = make_sales_return(pos.name) # partial return 1 - pos_return1.get('items')[0].qty = -1 - pos_return1.get('items')[0].serial_no = serial_nos[0] + pos_return1.get("items")[0].qty = -1 + pos_return1.get("items")[0].serial_no = serial_nos[0] pos_return1.insert() pos_return1.submit() # partial return 2 pos_return2 = make_sales_return(pos.name) - self.assertEqual(pos_return2.get('items')[0].qty, -1) - self.assertEqual(pos_return2.get('items')[0].serial_no, serial_nos[1]) + self.assertEqual(pos_return2.get("items")[0].qty, -1) + self.assertEqual(pos_return2.get("items")[0].serial_no, serial_nos[1]) def test_pos_change_amount(self): - pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC", - income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, - cost_center = "Main - _TC", do_not_save=True) + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + rate=105, + cost_center="Main - _TC", + do_not_save=True, + ) - pos.set('payments', []) - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1}) + pos.set("payments", []) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50} + ) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60, "default": 1} + ) pos.insert() pos.submit() @@ -298,29 +361,53 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - se = make_serialized_item(company='_Test Company', - target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) serial_nos = get_serial_nos(se.get("items")[0].serial_no) - pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', - account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', - expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', - item=se.get("items")[0].item_code, rate=1000, do_not_save=1) + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + do_not_save=1, + ) pos.get("items")[0].serial_no = serial_nos[0] - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} + ) pos.insert() pos.submit() - pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', - account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', - expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', - item=se.get("items")[0].item_code, rate=1000, do_not_save=1) + pos2 = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + do_not_save=1, + ) pos2.get("items")[0].serial_no = serial_nos[0] - pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) + pos2.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} + ) pos2.insert() self.assertRaises(frappe.ValidationError, pos2.submit) @@ -329,27 +416,50 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - se = make_serialized_item(company='_Test Company', - target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) serial_nos = get_serial_nos(se.get("items")[0].serial_no) - si = create_sales_invoice(company='_Test Company', debit_to='Debtors - _TC', - account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', - expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', - item=se.get("items")[0].item_code, rate=1000, do_not_save=1) + si = create_sales_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + do_not_save=1, + ) si.get("items")[0].serial_no = serial_nos[0] + si.update_stock = 1 si.insert() si.submit() - pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', - account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', - expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', - item=se.get("items")[0].item_code, rate=1000, do_not_save=1) + pos2 = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + do_not_save=1, + ) pos2.get("items")[0].serial_no = serial_nos[0] - pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) + pos2.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} + ) pos2.insert() self.assertRaises(frappe.ValidationError, pos2.submit) @@ -357,17 +467,30 @@ class TestPOSInvoice(unittest.TestCase): def test_invalid_serial_no_validation(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - se = make_serialized_item(company='_Test Company', - target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') - serial_nos = se.get("items")[0].serial_no + 'wrong' + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) + serial_nos = se.get("items")[0].serial_no + "wrong" - pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', - account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', - expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', - item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1) + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + qty=2, + do_not_save=1, + ) - pos.get('items')[0].has_serial_no = 1 - pos.get('items')[0].serial_no = serial_nos + pos.get("items")[0].has_serial_no = 1 + pos.get("items")[0].serial_no = serial_nos pos.insert() self.assertRaises(frappe.ValidationError, pos.submit) @@ -379,20 +502,31 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records create_records() - frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") - before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty") + frappe.db.set_value( + "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" + ) + before_lp_details = get_loyalty_program_details_with_points( + "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" + ) inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000) - lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'POS Invoice', 'invoice': inv.name, 'customer': inv.customer}) - after_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) + lpe = frappe.get_doc( + "Loyalty Point Entry", + {"invoice_type": "POS Invoice", "invoice": inv.name, "customer": inv.customer}, + ) + after_lp_details = get_loyalty_program_details_with_points( + inv.customer, company=inv.company, loyalty_program=inv.loyalty_program + ) - self.assertEqual(inv.get('loyalty_program'), "Test Single Loyalty") + self.assertEqual(inv.get("loyalty_program"), "Test Single Loyalty") self.assertEqual(lpe.loyalty_points, 10) self.assertEqual(after_lp_details.loyalty_points, before_lp_details.loyalty_points + 10) inv.cancel() - after_cancel_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) + after_cancel_lp_details = get_loyalty_program_details_with_points( + inv.customer, company=inv.company, loyalty_program=inv.loyalty_program + ) self.assertEqual(after_cancel_lp_details.loyalty_points, before_lp_details.loyalty_points) def test_loyalty_points_redeemption(self): @@ -403,17 +537,24 @@ class TestPOSInvoice(unittest.TestCase): # add 10 loyalty points create_pos_invoice(customer="Test Loyalty Customer", rate=10000) - before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty") + before_lp_details = get_loyalty_program_details_with_points( + "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" + ) inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) inv.redeem_loyalty_points = 1 inv.loyalty_points = before_lp_details.loyalty_points inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor - inv.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 10000 - inv.loyalty_amount}) + inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000 - inv.loyalty_amount}, + ) inv.paid_amount = 10000 inv.submit() - after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) + after_redeem_lp_details = get_loyalty_program_details_with_points( + inv.customer, company=inv.company, loyalty_program=inv.loyalty_program + ) self.assertEqual(after_redeem_lp_details.loyalty_points, 9) def test_merging_into_sales_invoice_with_discount(self): @@ -427,21 +568,19 @@ class TestPOSInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Invoice`") test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 270 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270}) pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.submit() consolidate_pos_invoices() pos_inv.load_from_db() - rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") + rounded_total = frappe.db.get_value( + "Sales Invoice", pos_inv.consolidated_invoice, "rounded_total" + ) self.assertEqual(rounded_total, 3470) def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): @@ -455,38 +594,42 @@ class TestPOSInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Invoice`") test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) - pos_inv.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) pos_inv.submit() pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1) pos_inv2.additional_discount_percentage = 10 - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 540 - }) - pos_inv2.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 540}) + pos_inv2.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) pos_inv2.submit() consolidate_pos_invoices() pos_inv.load_from_db() - rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") + rounded_total = frappe.db.get_value( + "Sales Invoice", pos_inv.consolidated_invoice, "rounded_total" + ) self.assertEqual(rounded_total, 840) def test_merging_with_validate_selling_price(self): @@ -506,64 +649,75 @@ class TestPOSInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Invoice`") test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) - pos_inv.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) self.assertRaises(frappe.ValidationError, pos_inv.submit) pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400 - }) - pos_inv2.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 400}) + pos_inv2.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) pos_inv2.submit() consolidate_pos_invoices() pos_inv2.load_from_db() - rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") + rounded_total = frappe.db.get_value( + "Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total" + ) self.assertEqual(rounded_total, 400) def test_pos_batch_item_qty_validation(self): from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_batch_item_with_batch, ) - create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01') - item = frappe.get_doc('Item', '_BATCH ITEM') - batch = frappe.get_doc('Batch', 'TestBatch 01') + + create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01") + item = frappe.get_doc("Item", "_BATCH ITEM") + batch = frappe.get_doc("Batch", "TestBatch 01") batch.submit() - item.batch_no = 'TestBatch 01' + item.batch_no = "TestBatch 01" item.save() - se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01') + se = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="_BATCH ITEM", + qty=2, + basic_rate=100, + batch_no="TestBatch 01", + ) pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1) - pos_inv1.items[0].batch_no = 'TestBatch 01' + pos_inv1.items[0].batch_no = "TestBatch 01" pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1) - pos_inv2.items[0].batch_no = 'TestBatch 01' + pos_inv2.items[0].batch_no = "TestBatch 01" pos_inv2.save() self.assertRaises(frappe.ValidationError, pos_inv2.submit) - #teardown + # teardown pos_inv1.reload() pos_inv1.cancel() pos_inv1.delete() @@ -577,12 +731,14 @@ class TestPOSInvoice(unittest.TestCase): def test_ignore_pricing_rule(self): from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule - item_price = frappe.get_doc({ - 'doctype': 'Item Price', - 'item_code': '_Test Item', - 'price_list': '_Test Price List', - 'price_list_rate': '450', - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Price List", + "price_list_rate": "450", + } + ) item_price.insert() pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10) pr.save() @@ -610,6 +766,76 @@ class TestPOSInvoice(unittest.TestCase): pos_inv.delete() pr.delete() + def test_delivered_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint("before_test_delivered_serial_no_case") + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) + + delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") + self.assertEquals(delivery_document_no, dn.name) + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, pos_inv.submit) + + finally: + frappe.db.rollback(save_point="before_test_delivered_serial_no_case") + frappe.set_user("Administrator") + + def test_returned_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint("before_test_returned_serial_no_case") + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + ) + + pos_return = make_sales_return(pos_inv.name) + pos_return.flags.ignore_validate = True + pos_return.insert() + pos_return.submit() + + pos_reserved_serial_nos = get_pos_reserved_serial_nos( + {"item_code": "_Test Serialized Item With Series", "warehouse": "_Test Warehouse - _TC"} + ) + self.assertTrue(serial_no not in pos_reserved_serial_nos) + + finally: + frappe.db.rollback(save_point="before_test_returned_serial_no_case") + frappe.set_user("Administrator") + def create_pos_invoice(**args): args = frappe._dict(args) @@ -633,23 +859,26 @@ def create_pos_invoice(**args): pos_inv.debit_to = args.debit_to or "Debtors - _TC" pos_inv.is_return = args.is_return pos_inv.return_against = args.return_against - pos_inv.currency=args.currency or "INR" + pos_inv.currency = args.currency or "INR" pos_inv.conversion_rate = args.conversion_rate or 1 pos_inv.account_for_change_amount = args.account_for_change_amount or "Cash - _TC" pos_inv.set_missing_values() - pos_inv.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "rate": args.rate if args.get("rate") is not None else 100, - "income_account": args.income_account or "Sales - _TC", - "expense_account": args.expense_account or "Cost of Goods Sold - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no, - "batch_no": args.batch_no - }) + pos_inv.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "rate": args.rate if args.get("rate") is not None else 100, + "income_account": args.income_account or "Sales - _TC", + "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "serial_no": args.serial_no, + "batch_no": args.batch_no, + }, + ) if not args.do_not_save: pos_inv.insert() @@ -662,7 +891,9 @@ def create_pos_invoice(**args): return pos_inv + def make_batch_item(item_name): from erpnext.stock.doctype.item.test_item import make_item + if not frappe.db.exists(item_name): - return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1)) \ No newline at end of file + return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 40ab0c50deb..d3a81fe61dc 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -5,7 +5,6 @@ import json import frappe -import six from frappe import _ from frappe.core.page.background_jobs.background_jobs import get_info from frappe.model.document import Document @@ -21,32 +20,44 @@ class POSInvoiceMergeLog(Document): self.validate_pos_invoice_status() def validate_customer(self): - if self.merge_invoices_based_on == 'Customer Group': + if self.merge_invoices_based_on == "Customer Group": return for d in self.pos_invoices: if d.customer != self.customer: - frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer)) + frappe.throw( + _("Row #{}: POS Invoice {} is not against customer {}").format( + d.idx, d.pos_invoice, self.customer + ) + ) def validate_pos_invoice_status(self): for d in self.pos_invoices: status, docstatus, is_return, return_against = frappe.db.get_value( - 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) + "POS Invoice", d.pos_invoice, ["status", "docstatus", "is_return", "return_against"] + ) bold_pos_invoice = frappe.bold(d.pos_invoice) bold_status = frappe.bold(status) if docstatus != 1: frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice)) if status == "Consolidated": - frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status)) - if is_return and return_against and return_against not in [d.pos_invoice for d in self.pos_invoices]: + frappe.throw( + _("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status) + ) + if ( + is_return + and return_against + and return_against not in [d.pos_invoice for d in self.pos_invoices] + ): bold_return_against = frappe.bold(return_against) - return_against_status = frappe.db.get_value('POS Invoice', return_against, "status") + return_against_status = frappe.db.get_value("POS Invoice", return_against, "status") if return_against_status != "Consolidated": # if return entry is not getting merged in the current pos closing and if it is not consolidated bold_unconsolidated = frappe.bold("not Consolidated") - msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}.") - .format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated)) + msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format( + d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated + ) msg += " " msg += _("Original invoice should be consolidated before or along with the return invoice.") msg += "

" @@ -54,10 +65,12 @@ class POSInvoiceMergeLog(Document): frappe.throw(msg) def on_submit(self): - pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices + ] - returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] - sales = [d for d in pos_invoice_docs if d.get('is_return') == 0] + returns = [d for d in pos_invoice_docs if d.get("is_return") == 1] + sales = [d for d in pos_invoice_docs if d.get("is_return") == 0] sales_invoice, credit_note = "", "" if returns: @@ -66,12 +79,14 @@ class POSInvoiceMergeLog(Document): if sales: sales_invoice = self.process_merging_into_sales_invoice(sales) - self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log + self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) def on_cancel(self): - pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices + ] self.update_pos_invoices(pos_invoice_docs) self.cancel_linked_invoices() @@ -85,20 +100,12 @@ class POSInvoiceMergeLog(Document): sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.save() - self.write_off_fractional_amount(sales_invoice, data) sales_invoice.submit() self.consolidated_invoice = sales_invoice.name return sales_invoice.name - def write_off_fractional_amount(self, invoice, data): - pos_invoice_grand_total = sum(d.grand_total for d in data) - - if abs(pos_invoice_grand_total - invoice.grand_total) < 1: - invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total) - invoice.save() - def process_merging_into_credit_note(self, data): credit_note = self.get_new_sales_invoice() credit_note.is_return = 1 @@ -111,7 +118,6 @@ class POSInvoiceMergeLog(Document): # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() - self.write_off_fractional_amount(credit_note, data) credit_note.submit() self.consolidated_credit_note = credit_note.name @@ -128,9 +134,8 @@ class POSInvoiceMergeLog(Document): loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1 - for doc in data: - map_doc(doc, invoice, table_map={ "doctype": invoice.doctype }) + map_doc(doc, invoice, table_map={"doctype": invoice.doctype}) if doc.redeem_loyalty_points: invoice.loyalty_redemption_account = doc.loyalty_redemption_account @@ -138,11 +143,17 @@ class POSInvoiceMergeLog(Document): loyalty_points_sum += doc.loyalty_points loyalty_amount_sum += doc.loyalty_amount - for item in doc.get('items'): + for item in doc.get("items"): found = False for i in items: - if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and - i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse): + if ( + i.item_code == item.item_code + and not i.serial_no + and not i.batch_no + and i.uom == item.uom + and i.net_rate == item.net_rate + and i.warehouse == item.warehouse + ): found = True i.qty = i.qty + item.qty i.amount = i.amount + item.net_amount @@ -158,7 +169,7 @@ class POSInvoiceMergeLog(Document): si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) items.append(si_item) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): found = False for t in taxes: if t.account_head == tax.account_head and t.cost_center == tax.cost_center: @@ -167,7 +178,7 @@ class POSInvoiceMergeLog(Document): update_item_wise_tax_detail(t, tax) found = True if not found: - tax.charge_type = 'Actual' + tax.charge_type = "Actual" tax.idx = idx idx += 1 tax.included_in_print_rate = 0 @@ -176,7 +187,7 @@ class POSInvoiceMergeLog(Document): tax.item_wise_tax_detail = tax.item_wise_tax_detail taxes.append(tax) - for payment in doc.get('payments'): + for payment in doc.get("payments"): found = False for pay in payments: if pay.account == payment.account and pay.mode_of_payment == payment.mode_of_payment: @@ -191,52 +202,59 @@ class POSInvoiceMergeLog(Document): base_rounding_adjustment += doc.base_rounding_adjustment base_rounded_total += doc.base_rounded_total - if loyalty_points_sum: invoice.redeem_loyalty_points = 1 invoice.loyalty_points = loyalty_points_sum invoice.loyalty_amount = loyalty_amount_sum - invoice.set('items', items) - invoice.set('payments', payments) - invoice.set('taxes', taxes) - invoice.set('rounding_adjustment',rounding_adjustment) - invoice.set('base_rounding_adjustment',base_rounding_adjustment) - invoice.set('rounded_total',rounded_total) - invoice.set('base_rounded_total',base_rounded_total) + invoice.set("items", items) + invoice.set("payments", payments) + invoice.set("taxes", taxes) + invoice.set("rounding_adjustment", rounding_adjustment) + invoice.set("base_rounding_adjustment", base_rounding_adjustment) + invoice.set("rounded_total", rounded_total) + invoice.set("base_rounded_total", base_rounded_total) invoice.additional_discount_percentage = 0 invoice.discount_amount = 0.0 invoice.taxes_and_charges = None invoice.ignore_pricing_rule = 1 invoice.customer = self.customer - if self.merge_invoices_based_on == 'Customer Group': + if self.merge_invoices_based_on == "Customer Group": invoice.flags.ignore_pos_profile = True - invoice.pos_profile = '' + invoice.pos_profile = "" return invoice def get_new_sales_invoice(self): - sales_invoice = frappe.new_doc('Sales Invoice') + sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.customer = self.customer sales_invoice.is_pos = 1 return sales_invoice - def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''): + def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_note=""): for doc in invoice_docs: doc.load_from_db() - doc.update({ 'consolidated_invoice': None if self.docstatus==2 else (credit_note if doc.is_return else sales_invoice) }) + doc.update( + { + "consolidated_invoice": None + if self.docstatus == 2 + else (credit_note if doc.is_return else sales_invoice) + } + ) doc.set_status(update=True) doc.save() def cancel_linked_invoices(self): for si_name in [self.consolidated_invoice, self.consolidated_credit_note]: - if not si_name: continue - si = frappe.get_doc('Sales Invoice', si_name) + if not si_name: + continue + si = frappe.get_doc("Sales Invoice", si_name) si.flags.ignore_validate = True si.cancel() + def update_item_wise_tax_detail(consolidate_tax_row, tax_row): consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail) tax_row_detail = json.loads(tax_row.item_wise_tax_detail) @@ -247,78 +265,150 @@ def update_item_wise_tax_detail(consolidate_tax_row, tax_row): for item_code, tax_data in tax_row_detail.items(): if consolidated_tax_detail.get(item_code): consolidated_tax_data = consolidated_tax_detail.get(item_code) - consolidated_tax_detail.update({ - item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]] - }) + consolidated_tax_detail.update( + {item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]} + ) else: - consolidated_tax_detail.update({ - item_code: [tax_data[0], tax_data[1]] - }) + consolidated_tax_detail.update({item_code: [tax_data[0], tax_data[1]]}) + + consolidate_tax_row.item_wise_tax_detail = json.dumps( + consolidated_tax_detail, separators=(",", ":") + ) - consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':')) def get_all_unconsolidated_invoices(): filters = { - 'consolidated_invoice': [ 'in', [ '', None ]], - 'status': ['not in', ['Consolidated']], - 'docstatus': 1 + "consolidated_invoice": ["in", ["", None]], + "status": ["not in", ["Consolidated"]], + "docstatus": 1, } - pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, - fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer']) + pos_invoices = frappe.db.get_all( + "POS Invoice", + filters=filters, + fields=[ + "name as pos_invoice", + "posting_date", + "grand_total", + "customer", + "is_return", + "return_against", + ], + ) return pos_invoices + def get_invoice_customer_map(pos_invoices): # pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] } pos_invoice_customer_map = {} for invoice in pos_invoices: - customer = invoice.get('customer') + customer = invoice.get("customer") pos_invoice_customer_map.setdefault(customer, []) pos_invoice_customer_map[customer].append(invoice) return pos_invoice_customer_map + def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): - invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) + invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions")) if frappe.flags.in_test and not invoices: invoices = get_all_unconsolidated_invoices() invoice_by_customer = get_invoice_customer_map(invoices) if len(invoices) >= 10 and closing_entry: - closing_entry.set_status(update=True, status='Queued') - enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry) + closing_entry.set_status(update=True, status="Queued") + enqueue_job( + create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry + ) else: create_merge_logs(invoice_by_customer, closing_entry) + def unconsolidate_pos_invoices(closing_entry): merge_logs = frappe.get_all( - 'POS Invoice Merge Log', - filters={ 'pos_closing_entry': closing_entry.name }, - pluck='name' + "POS Invoice Merge Log", filters={"pos_closing_entry": closing_entry.name}, pluck="name" ) if len(merge_logs) >= 10: - closing_entry.set_status(update=True, status='Queued') + closing_entry.set_status(update=True, status="Queued") enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry) else: cancel_merge_logs(merge_logs, closing_entry) + +def split_invoices(invoices): + """ + Splits invoices into multiple groups + Use-case: + If a serial no is sold and later it is returned + then split the invoices such that the selling entry is merged first and then the return entry + """ + # Input + # invoices = [ + # {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}, + # {'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, + # {'pos_invoice': 'Invoice with SR#2', 'is_return': 0} + # ] + # Output + # _invoices = [ + # [{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}], + # [{'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}], + # ] + + _invoices = [] + special_invoices = [] + pos_return_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) + for d in invoices + if d.is_return and d.return_against + ] + for pos_invoice in pos_return_docs: + for item in pos_invoice.items: + if not item.serial_no: + continue + + return_against_is_added = any( + d for d in _invoices if d.pos_invoice == pos_invoice.return_against + ) + if return_against_is_added: + break + + return_against_is_consolidated = ( + frappe.db.get_value("POS Invoice", pos_invoice.return_against, "status", cache=True) + == "Consolidated" + ) + if return_against_is_consolidated: + break + + pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against] + _invoices.append(pos_invoice_row) + special_invoices.append(pos_invoice.return_against) + break + + _invoices.append([d for d in invoices if d.pos_invoice not in special_invoices]) + + return _invoices + + def create_merge_logs(invoice_by_customer, closing_entry=None): try: - for customer, invoices in six.iteritems(invoice_by_customer): - merge_log = frappe.new_doc('POS Invoice Merge Log') - merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() - merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None + for customer, invoices in invoice_by_customer.items(): + for _invoices in split_invoices(invoices): + merge_log = frappe.new_doc("POS Invoice Merge Log") + merge_log.posting_date = ( + getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() + ) + merge_log.customer = customer + merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None - merge_log.set('pos_invoices', invoices) - merge_log.save(ignore_permissions=True) - merge_log.submit() + merge_log.set("pos_invoices", _invoices) + merge_log.save(ignore_permissions=True) + merge_log.submit() if closing_entry: - closing_entry.set_status(update=True, status='Submitted') - closing_entry.db_set('error_message', '') + closing_entry.set_status(update=True, status="Submitted") + closing_entry.db_set("error_message", "") closing_entry.update_opening_entry() except Exception as e: @@ -327,24 +417,25 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): error_message = safe_load_json(message_log) if closing_entry: - closing_entry.set_status(update=True, status='Failed') - closing_entry.db_set('error_message', error_message) + closing_entry.set_status(update=True, status="Failed") + closing_entry.db_set("error_message", error_message) raise finally: frappe.db.commit() - frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user}) + frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user}) + def cancel_merge_logs(merge_logs, closing_entry=None): try: for log in merge_logs: - merge_log = frappe.get_doc('POS Invoice Merge Log', log) + merge_log = frappe.get_doc("POS Invoice Merge Log", log) merge_log.flags.ignore_permissions = True merge_log.cancel() if closing_entry: - closing_entry.set_status(update=True, status='Cancelled') - closing_entry.db_set('error_message', '') + closing_entry.set_status(update=True, status="Cancelled") + closing_entry.db_set("error_message", "") closing_entry.update_opening_entry(for_cancel=True) except Exception as e: @@ -353,18 +444,19 @@ def cancel_merge_logs(merge_logs, closing_entry=None): error_message = safe_load_json(message_log) if closing_entry: - closing_entry.set_status(update=True, status='Submitted') - closing_entry.db_set('error_message', error_message) + closing_entry.set_status(update=True, status="Submitted") + closing_entry.db_set("error_message", error_message) raise finally: frappe.db.commit() - frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user}) + frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user}) + def enqueue_job(job, **kwargs): check_scheduler_status() - closing_entry = kwargs.get('closing_entry') or {} + closing_entry = kwargs.get("closing_entry") or {} job_name = closing_entry.get("name") if not job_already_enqueued(job_name): @@ -379,24 +471,27 @@ def enqueue_job(job, **kwargs): ) if job == create_merge_logs: - msg = _('POS Invoices will be consolidated in a background process') + msg = _("POS Invoices will be consolidated in a background process") else: - msg = _('POS Invoices will be unconsolidated in a background process') + msg = _("POS Invoices will be unconsolidated in a background process") frappe.msgprint(msg, alert=1) + def check_scheduler_status(): if is_scheduler_inactive() and not frappe.flags.in_test: frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) + def job_already_enqueued(job_name): enqueued_jobs = [d.get("job_name") for d in get_info()] if job_name in enqueued_jobs: return True + def safe_load_json(message): try: - json_message = json.loads(message).get('message') + json_message = json.loads(message).get("message") except Exception: json_message = message diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 5930aa097f7..9e696f18b6a 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -5,6 +5,7 @@ import json import unittest import frappe +from frappe.tests.utils import change_settings from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return @@ -23,21 +24,19 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) + pos_inv2.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200} + ) pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) - pos_inv3.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 - }) + pos_inv3.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300} + ) pos_inv3.submit() consolidate_pos_invoices() @@ -62,28 +61,29 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) + pos_inv2.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200} + ) pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) - pos_inv3.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 - }) + pos_inv3.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300} + ) pos_inv3.submit() pos_inv_cn = make_sales_return(pos_inv.name) pos_inv_cn.set("payments", []) - pos_inv_cn.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300 - }) + pos_inv_cn.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -100} + ) + pos_inv_cn.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": -200} + ) pos_inv_cn.paid_amount = -300 pos_inv_cn.submit() @@ -97,7 +97,12 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv_cn.load_from_db() self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) - self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return")) + consolidated_credit_note = frappe.get_doc("Sales Invoice", pos_inv_cn.consolidated_invoice) + self.assertEqual(consolidated_credit_note.is_return, 1) + self.assertEqual(consolidated_credit_note.payments[0].mode_of_payment, "Cash") + self.assertEqual(consolidated_credit_note.payments[0].amount, -100) + self.assertEqual(consolidated_credit_note.payments[1].mode_of_payment, "Bank Draft") + self.assertEqual(consolidated_credit_note.payments[1].amount, -200) finally: frappe.set_user("Administrator") @@ -110,41 +115,47 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): try: inv = create_pos_invoice(qty=1, rate=100, do_not_save=True) - inv.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 9 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 9, + }, + ) inv.insert() inv.submit() inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True) - inv2.get('items')[0].item_code = '_Test Item 2' - inv2.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 5 - }) + inv2.get("items")[0].item_code = "_Test Item 2" + inv2.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 5, + }, + ) inv2.insert() inv2.submit() consolidate_pos_invoices() inv.load_from_db() - consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) - item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail) + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) + item_wise_tax_detail = json.loads(consolidated_invoice.get("taxes")[0].item_wise_tax_detail) - tax_rate, amount = item_wise_tax_detail.get('_Test Item') + tax_rate, amount = item_wise_tax_detail.get("_Test Item") self.assertEqual(tax_rate, 9) self.assertEqual(amount, 9) - tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2') + tax_rate2, amount2 = item_wise_tax_detail.get("_Test Item 2") self.assertEqual(tax_rate2, 5) self.assertEqual(amount2, 5) finally: @@ -152,11 +163,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - def test_consolidation_round_off_error_1(self): - ''' + """ Test round off error in consolidated invoice creation if POS Invoice has inclusive tax - ''' + """ frappe.db.sql("delete from `tabPOS Invoice`") @@ -171,43 +181,45 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): init_user_and_profile() inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True) - inv.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 7.5, - "included_in_print_rate": 1 - }) - inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1, + }, + ) + inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 30000}) inv.insert() inv.submit() inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True) - inv2.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 7.5, - "included_in_print_rate": 1 - }) - inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000 - }) + inv2.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1, + }, + ) + inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 30000}) inv2.insert() inv2.submit() consolidate_pos_invoices() inv.load_from_db() - consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) self.assertEqual(consolidated_invoice.outstanding_amount, 0) - self.assertEqual(consolidated_invoice.status, 'Paid') + self.assertEqual(consolidated_invoice.status, "Paid") finally: frappe.set_user("Administrator") @@ -215,9 +227,9 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.db.sql("delete from `tabPOS Invoice`") def test_consolidation_round_off_error_2(self): - ''' + """ Test the same case as above but with an Unpaid POS Invoice - ''' + """ frappe.db.sql("delete from `tabPOS Invoice`") try: @@ -231,50 +243,207 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): init_user_and_profile() inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True) - inv.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 7.5, - "included_in_print_rate": 1 - }) - inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1, + }, + ) + inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60000}) inv.insert() inv.submit() inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True) - inv2.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 7.5, - "included_in_print_rate": 1 - }) - inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000 - }) + inv2.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1, + }, + ) + inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60000}) inv2.insert() inv2.submit() inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True) - inv3.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000 - }) + inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000}) inv3.insert() inv3.submit() consolidate_pos_invoices() inv.load_from_db() - consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) self.assertEqual(consolidated_invoice.outstanding_amount, 800) - self.assertNotEqual(consolidated_invoice.status, 'Paid') + self.assertNotEqual(consolidated_invoice.status, "Paid") + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + @change_settings( + "System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3} + ) + def test_consolidation_round_off_error_3(self): + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + init_user_and_profile() + + item_rates = [69, 59, 29] + for i in [1, 2]: + inv = create_pos_invoice(is_return=1, do_not_save=1) + inv.items = [] + for rate in item_rates: + inv.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": -1, + "rate": rate, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) + inv.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 15, + "included_in_print_rate": 1, + }, + ) + inv.payments = [] + inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -157}) + inv.paid_amount = -157 + inv.save() + inv.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.status, "Return") + self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_consolidation_rounding_adjustment(self): + """ + Test if the rounding adjustment is calculated correctly + """ + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + + init_user_and_profile() + + inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True) + inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 70}) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True) + inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60}) + inv2.insert() + inv2.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.rounding_adjustment, 1) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_serial_no_case_1(self): + """ + Create a POS Invoice with serial no + Create a Return Invoice with serial no + Create a POS Invoice with serial no again + Consolidate the invoices + + The first POS Invoice should be consolidated with a separate single Merge Log + The second and third POS Invoice should be consolidated with a single Merge Log + """ + + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=1, + ) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv.submit() + + pos_inv_cn = make_sales_return(pos_inv.name) + pos_inv_cn.paid_amount = -100 + pos_inv_cn.submit() + + pos_inv2 = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=1, + ) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv2.submit() + + consolidate_pos_invoices() + + pos_inv.load_from_db() + pos_inv2.load_from_db() + + self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice) finally: frappe.set_user("Administrator") diff --git a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json index 205c4ede901..387c4b0f360 100644 --- a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json +++ b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json @@ -9,7 +9,9 @@ "posting_date", "column_break_3", "customer", - "grand_total" + "grand_total", + "is_return", + "return_against" ], "fields": [ { @@ -48,11 +50,27 @@ "in_list_view": 1, "label": "Amount", "reqd": 1 + }, + { + "default": "0", + "fetch_from": "pos_invoice.is_return", + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "read_only": 1 + }, + { + "fetch_from": "pos_invoice.return_against", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against", + "options": "POS Invoice", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-05-29 15:08:42.194979", + "modified": "2022-03-24 13:32:02.366257", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Reference", @@ -61,5 +79,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py index 0b2e045e5a7..3cd14264bb8 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py @@ -17,7 +17,9 @@ class POSOpeningEntry(StatusUpdater): def validate_pos_profile_and_cashier(self): if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): - frappe.throw(_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company)) + frappe.throw( + _("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company) + ) if not cint(frappe.db.get_value("User", self.user, "enabled")): frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user)) @@ -26,8 +28,11 @@ class POSOpeningEntry(StatusUpdater): invalid_modes = [] for d in self.balance_details: if d.mode_of_payment: - account = frappe.db.get_value("Mode of Payment Account", - {"parent": d.mode_of_payment, "company": self.company}, "default_account") + account = frappe.db.get_value( + "Mode of Payment Account", + {"parent": d.mode_of_payment, "company": self.company}, + "default_account", + ) if not account: invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment)) diff --git a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py index 105d53d00e8..64c658ab151 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py @@ -9,6 +9,7 @@ import frappe class TestPOSOpeningEntry(unittest.TestCase): pass + def create_opening_entry(pos_profile, user): entry = frappe.new_doc("POS Opening Entry") entry.pos_profile = pos_profile.name @@ -16,11 +17,9 @@ def create_opening_entry(pos_profile, user): entry.company = pos_profile.company entry.period_start_date = frappe.utils.get_datetime() - balance_details = []; + balance_details = [] for d in pos_profile.payments: - balance_details.append(frappe._dict({ - 'mode_of_payment': d.mode_of_payment - })) + balance_details.append(frappe._dict({"mode_of_payment": d.mode_of_payment})) entry.set("balance_details", balance_details) entry.submit() diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 9c9f37bba27..11646a6517d 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -22,6 +22,7 @@ "hide_images", "hide_unavailable_items", "auto_add_item_to_cart", + "validate_stock_on_save", "column_break_16", "update_stock", "ignore_pricing_rule", @@ -351,6 +352,12 @@ { "fieldname": "column_break_25", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "validate_stock_on_save", + "fieldtype": "Check", + "label": "Validate Stock on Save" } ], "icon": "icon-cog", @@ -378,10 +385,11 @@ "link_fieldname": "pos_profile" } ], - "modified": "2021-10-14 14:17:00.469298", + "modified": "2022-03-21 13:29:28.480533", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -404,5 +412,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index d80c1b27cce..cb84d2bec6c 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -18,29 +18,42 @@ class POSProfile(Document): def validate_default_profile(self): for row in self.applicable_for_users: - res = frappe.db.sql("""select pf.name + res = frappe.db.sql( + """select pf.name from `tabPOS Profile User` pfu, `tabPOS Profile` pf where pf.name = pfu.parent and pfu.user = %s and pf.name != %s and pf.company = %s - and pfu.default=1 and pf.disabled = 0""", (row.user, self.name, self.company)) + and pfu.default=1 and pf.disabled = 0""", + (row.user, self.name, self.company), + ) if row.default and res: - msgprint(_("Already set default in pos profile {0} for user {1}, kindly disabled default") - .format(res[0][0], row.user), raise_exception=1) + msgprint( + _("Already set default in pos profile {0} for user {1}, kindly disabled default").format( + res[0][0], row.user + ), + raise_exception=1, + ) elif not row.default and not res: - msgprint(_("User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User.") - .format(row.user, row.idx)) + msgprint( + _( + "User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User." + ).format(row.user, row.idx) + ) def validate_all_link_fields(self): - accounts = {"Account": [self.income_account, - self.expense_account], "Cost Center": [self.cost_center], - "Warehouse": [self.warehouse]} + accounts = { + "Account": [self.income_account, self.expense_account], + "Cost Center": [self.cost_center], + "Warehouse": [self.warehouse], + } for link_dt, dn_list in iteritems(accounts): for link_dn in dn_list: - if link_dn and not frappe.db.exists({"doctype": link_dt, - "company": self.company, "name": link_dn}): + if link_dn and not frappe.db.exists( + {"doctype": link_dt, "company": self.company, "name": link_dn} + ): frappe.throw(_("{0} does not belong to Company {1}").format(link_dn, self.company)) def validate_duplicate_groups(self): @@ -48,10 +61,15 @@ class POSProfile(Document): customer_groups = [d.customer_group for d in self.customer_groups] if len(item_groups) != len(set(item_groups)): - frappe.throw(_("Duplicate item group found in the item group table"), title = "Duplicate Item Group") + frappe.throw( + _("Duplicate item group found in the item group table"), title="Duplicate Item Group" + ) if len(customer_groups) != len(set(customer_groups)): - frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group") + frappe.throw( + _("Duplicate customer group found in the cutomer group table"), + title="Duplicate Customer Group", + ) def validate_payment_methods(self): if not self.payments: @@ -69,7 +87,7 @@ class POSProfile(Document): account = frappe.db.get_value( "Mode of Payment Account", {"parent": d.mode_of_payment, "company": self.company}, - "default_account" + "default_account", ) if not account: @@ -92,12 +110,16 @@ class POSProfile(Document): frappe.defaults.clear_default("is_pos") if not include_current_pos: - condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "\'") + condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'") else: condition = " where pfu.default = 1 " - pos_view_users = frappe.db.sql_list("""select pfu.user - from `tabPOS Profile User` as pfu {0}""".format(condition)) + pos_view_users = frappe.db.sql_list( + """select pfu.user + from `tabPOS Profile User` as pfu {0}""".format( + condition + ) + ) for user in pos_view_users: if user: @@ -105,48 +127,62 @@ class POSProfile(Document): else: frappe.defaults.set_global_default("is_pos", 1) + def get_item_groups(pos_profile): item_groups = [] - pos_profile = frappe.get_cached_doc('POS Profile', pos_profile) + pos_profile = frappe.get_cached_doc("POS Profile", pos_profile) - if pos_profile.get('item_groups'): + if pos_profile.get("item_groups"): # Get items based on the item groups defined in the POS profile - for data in pos_profile.get('item_groups'): - item_groups.extend(["%s" % frappe.db.escape(d.name) for d in get_child_nodes('Item Group', data.item_group)]) + for data in pos_profile.get("item_groups"): + item_groups.extend( + ["%s" % frappe.db.escape(d.name) for d in get_child_nodes("Item Group", data.item_group)] + ) return list(set(item_groups)) + def get_child_nodes(group_type, root): lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"]) - return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where - lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1) + return frappe.db.sql( + """ Select name, lft, rgt from `tab{tab}` where + lft >= {lft} and rgt <= {rgt} order by lft""".format( + tab=group_type, lft=lft, rgt=rgt + ), + as_dict=1, + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): - user = frappe.session['user'] - company = filters.get('company') or frappe.defaults.get_user_default('company') + user = frappe.session["user"] + company = filters.get("company") or frappe.defaults.get_user_default("company") args = { - 'user': user, - 'start': start, - 'company': company, - 'page_len': page_len, - 'txt': '%%%s%%' % txt + "user": user, + "start": start, + "company": company, + "page_len": page_len, + "txt": "%%%s%%" % txt, } - pos_profile = frappe.db.sql("""select pf.name + pos_profile = frappe.db.sql( + """select pf.name from `tabPOS Profile` pf, `tabPOS Profile User` pfu where pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s and (pf.name like %(txt)s) - and pf.disabled = 0 limit %(start)s, %(page_len)s""", args) + and pf.disabled = 0 limit %(start)s, %(page_len)s""", + args, + ) if not pos_profile: - del args['user'] + del args["user"] - pos_profile = frappe.db.sql("""select pf.name + pos_profile = frappe.db.sql( + """select pf.name from `tabPOS Profile` pf left join `tabPOS Profile User` pfu on @@ -155,26 +191,37 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): ifnull(pfu.user, '') = '' and pf.company = %(company)s and pf.name like %(txt)s - and pf.disabled = 0""", args) + and pf.disabled = 0""", + args, + ) return pos_profile + @frappe.whitelist() def set_default_profile(pos_profile, company): modified = now() user = frappe.session.user if pos_profile and company: - frappe.db.sql(""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf + frappe.db.sql( + """ update `tabPOS Profile User` pfu, `tabPOS Profile` pf set pfu.default = 0, pf.modified = %s, pf.modified_by = %s where pfu.user = %s and pf.name = pfu.parent and pf.company = %s - and pfu.default = 1""", (modified, user, user, company), auto_commit=1) + and pfu.default = 1""", + (modified, user, user, company), + auto_commit=1, + ) - frappe.db.sql(""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf + frappe.db.sql( + """ update `tabPOS Profile User` pfu, `tabPOS Profile` pf set pfu.default = 1, pf.modified = %s, pf.modified_by = %s where pfu.user = %s and pf.name = pfu.parent and pf.company = %s and pf.name = %s - """, (modified, user, user, company, pos_profile), auto_commit=1) + """, + (modified, user, user, company, pos_profile), + auto_commit=1, + ) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index c8cf0d2a0f1..788aa62701d 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -8,7 +8,8 @@ import frappe from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes from erpnext.stock.get_item_details import get_pos_profile -test_dependencies = ['Item'] +test_dependencies = ["Item"] + class TestPOSProfile(unittest.TestCase): def test_pos_profile(self): @@ -17,46 +18,64 @@ class TestPOSProfile(unittest.TestCase): pos_profile = get_pos_profile("_Test Company") or {} if pos_profile: doc = frappe.get_doc("POS Profile", pos_profile.get("name")) - doc.append('item_groups', {'item_group': '_Test Item Group'}) - doc.append('customer_groups', {'customer_group': '_Test Customer Group'}) + doc.append("item_groups", {"item_group": "_Test Item Group"}) + doc.append("customer_groups", {"customer_group": "_Test Customer Group"}) doc.save() items = get_items_list(doc, doc.company) customers = get_customers_list(doc) - products_count = frappe.db.sql(""" select count(name) from tabItem where item_group = '_Test Item Group'""", as_list=1) - customers_count = frappe.db.sql(""" select count(name) from tabCustomer where customer_group = '_Test Customer Group'""") + products_count = frappe.db.sql( + """ select count(name) from tabItem where item_group = '_Test Item Group'""", as_list=1 + ) + customers_count = frappe.db.sql( + """ select count(name) from tabCustomer where customer_group = '_Test Customer Group'""" + ) self.assertEqual(len(items), products_count[0][0]) self.assertEqual(len(customers), customers_count[0][0]) frappe.db.sql("delete from `tabPOS Profile`") + def get_customers_list(pos_profile=None): if pos_profile is None: pos_profile = {} cond = "1=1" customer_groups = [] - if pos_profile.get('customer_groups'): + if pos_profile.get("customer_groups"): # Get customers based on the customer groups defined in the POS profile - for d in pos_profile.get('customer_groups'): - customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))]) - cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups))) + for d in pos_profile.get("customer_groups"): + customer_groups.extend( + [d.get("name") for d in get_child_nodes("Customer Group", d.get("customer_group"))] + ) + cond = "customer_group in (%s)" % (", ".join(["%s"] * len(customer_groups))) - return frappe.db.sql(""" select name, customer_name, customer_group, + return ( + frappe.db.sql( + """ select name, customer_name, customer_group, territory, customer_pos_id from tabCustomer where disabled = 0 - and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {} + and {cond}""".format( + cond=cond + ), + tuple(customer_groups), + as_dict=1, + ) + or {} + ) + def get_items_list(pos_profile, company): cond = "" args_list = [] - if pos_profile.get('item_groups'): + if pos_profile.get("item_groups"): # Get items based on the item groups defined in the POS profile - for d in pos_profile.get('item_groups'): - args_list.extend([d.name for d in get_child_nodes('Item Group', d.item_group)]) + for d in pos_profile.get("item_groups"): + args_list.extend([d.name for d in get_child_nodes("Item Group", d.item_group)]) if args_list: - cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list))) + cond = "and i.item_group in (%s)" % (", ".join(["%s"] * len(args_list))) - return frappe.db.sql(""" + return frappe.db.sql( + """ select i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no, i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image, @@ -69,7 +88,13 @@ def get_items_list(pos_profile, company): where i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 and i.is_fixed_asset = 0 {cond} - """.format(cond=cond), tuple([company] + args_list), as_dict=1) + """.format( + cond=cond + ), + tuple([company] + args_list), + as_dict=1, + ) + def make_pos_profile(**args): frappe.db.sql("delete from `tabPOS Payment Method`") @@ -77,38 +102,34 @@ def make_pos_profile(**args): args = frappe._dict(args) - pos_profile = frappe.get_doc({ - "company": args.company or "_Test Company", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "currency": args.currency or "INR", - "doctype": "POS Profile", - "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", - "income_account": args.income_account or "Sales - _TC", - "name": args.name or "_Test POS Profile", - "naming_series": "_T-POS Profile-", - "selling_price_list": args.selling_price_list or "_Test Price List", - "territory": args.territory or "_Test Territory", - "customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'), - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "write_off_account": args.write_off_account or "_Test Write Off - _TC", - "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" - }) + pos_profile = frappe.get_doc( + { + "company": args.company or "_Test Company", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "currency": args.currency or "INR", + "doctype": "POS Profile", + "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", + "income_account": args.income_account or "Sales - _TC", + "name": args.name or "_Test POS Profile", + "naming_series": "_T-POS Profile-", + "selling_price_list": args.selling_price_list or "_Test Price List", + "territory": args.territory or "_Test Territory", + "customer_group": frappe.db.get_value("Customer Group", {"is_group": 0}, "name"), + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "write_off_account": args.write_off_account or "_Test Write Off - _TC", + "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC", + } + ) mode_of_payment = frappe.get_doc("Mode of Payment", "Cash") company = args.company or "_Test Company" default_account = args.income_account or "Sales - _TC" if not frappe.db.get_value("Mode of Payment Account", {"company": company, "parent": "Cash"}): - mode_of_payment.append("accounts", { - "company": company, - "default_account": default_account - }) + mode_of_payment.append("accounts", {"company": company, "default_account": default_account}) mode_of_payment.save() - pos_profile.append("payments", { - 'mode_of_payment': 'Cash', - 'default': 1 - }) + pos_profile.append("payments", {"mode_of_payment": "Cash", "default": 1}) if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"): pos_profile.insert() diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index ad60bbad950..db6ec82c83d 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -13,11 +13,11 @@ from frappe.model.document import Document from frappe.utils import cint, flt, getdate from six import string_types -apply_on_dict = {"Item Code": "items", - "Item Group": "item_groups", "Brand": "brands"} +apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"} other_fields = ["other_item_code", "other_item_group", "other_brand"] + class PricingRule(Document): def validate(self): self.validate_mandatory() @@ -32,7 +32,8 @@ class PricingRule(Document): self.validate_dates() self.validate_condition() - if not self.margin_type: self.margin_rate_or_amount = 0.0 + if not self.margin_type: + self.margin_rate_or_amount = 0.0 def validate_duplicate_apply_on(self): field = apply_on_dict.get(self.apply_on) @@ -50,36 +51,51 @@ class PricingRule(Document): throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError) if self.apply_rule_on_other: - o_field = 'other_' + frappe.scrub(self.apply_rule_on_other) + o_field = "other_" + frappe.scrub(self.apply_rule_on_other) if not self.get(o_field) and o_field in other_fields: - frappe.throw(_("For the 'Apply Rule On Other' condition the field {0} is mandatory") - .format(frappe.bold(self.apply_rule_on_other))) + frappe.throw( + _("For the 'Apply Rule On Other' condition the field {0} is mandatory").format( + frappe.bold(self.apply_rule_on_other) + ) + ) - - if self.price_or_product_discount == 'Price' and not self.rate_or_discount: + if self.price_or_product_discount == "Price" and not self.rate_or_discount: throw(_("Rate or Discount is required for the price discount."), frappe.MandatoryError) if self.apply_discount_on_rate: if not self.priority: - throw(_("As the field {0} is enabled, the field {1} is mandatory.") - .format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority"))) + throw( + _("As the field {0} is enabled, the field {1} is mandatory.").format( + frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority") + ) + ) if self.priority and cint(self.priority) == 1: - throw(_("As the field {0} is enabled, the value of the field {1} should be more than 1.") - .format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority"))) + throw( + _("As the field {0} is enabled, the value of the field {1} should be more than 1.").format( + frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority") + ) + ) def validate_applicable_for_selling_or_buying(self): if not self.selling and not self.buying: throw(_("Atleast one of the Selling or Buying must be selected")) - if not self.selling and self.applicable_for in ["Customer", "Customer Group", - "Territory", "Sales Partner", "Campaign"]: - throw(_("Selling must be checked, if Applicable For is selected as {0}") - .format(self.applicable_for)) + if not self.selling and self.applicable_for in [ + "Customer", + "Customer Group", + "Territory", + "Sales Partner", + "Campaign", + ]: + throw( + _("Selling must be checked, if Applicable For is selected as {0}").format(self.applicable_for) + ) if not self.buying and self.applicable_for in ["Supplier", "Supplier Group"]: - throw(_("Buying must be checked, if Applicable For is selected as {0}") - .format(self.applicable_for)) + throw( + _("Buying must be checked, if Applicable For is selected as {0}").format(self.applicable_for) + ) def validate_min_max_qty(self): if self.min_qty and self.max_qty and flt(self.min_qty) > flt(self.max_qty): @@ -96,11 +112,12 @@ class PricingRule(Document): # reset all values except for the logic field options = (self.meta.get_options(logic_field) or "").split("\n") for f in options: - if not f: continue + if not f: + continue scrubbed_f = frappe.scrub(f) - if logic_field == 'apply_on': + if logic_field == "apply_on": apply_on_f = apply_on_dict.get(f, f) else: apply_on_f = scrubbed_f @@ -113,8 +130,11 @@ class PricingRule(Document): apply_rule_on_other = frappe.scrub(self.apply_rule_on_other or "") - cleanup_other_fields = (other_fields if not apply_rule_on_other - else [o_field for o_field in other_fields if o_field != 'other_' + apply_rule_on_other]) + cleanup_other_fields = ( + other_fields + if not apply_rule_on_other + else [o_field for o_field in other_fields if o_field != "other_" + apply_rule_on_other] + ) for other_field in cleanup_other_fields: self.set(other_field, None) @@ -124,7 +144,7 @@ class PricingRule(Document): if flt(self.get(frappe.scrub(field))) < 0: throw(_("{0} can not be negative").format(field)) - if self.price_or_product_discount == 'Product' and not self.free_item: + if self.price_or_product_discount == "Product" and not self.free_item: if self.mixed_conditions: frappe.throw(_("Free item code is not selected")) else: @@ -151,31 +171,37 @@ 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(r'[\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")) -#-------------------------------------------------------------------------------- + +# -------------------------------------------------------------------------------- + @frappe.whitelist() def apply_pricing_rule(args, doc=None): """ - args = { - "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...], - "customer": "something", - "customer_group": "something", - "territory": "something", - "supplier": "something", - "supplier_group": "something", - "currency": "something", - "conversion_rate": "something", - "price_list": "something", - "plc_conversion_rate": "something", - "company": "something", - "transaction_date": "something", - "campaign": "something", - "sales_partner": "something", - "ignore_pricing_rule": "something" - } + args = { + "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...], + "customer": "something", + "customer_group": "something", + "territory": "something", + "supplier": "something", + "supplier_group": "something", + "currency": "something", + "conversion_rate": "something", + "price_list": "something", + "plc_conversion_rate": "something", + "company": "something", + "transaction_date": "something", + "campaign": "something", + "sales_partner": "something", + "ignore_pricing_rule": "something" + } """ if isinstance(args, string_types): @@ -189,16 +215,23 @@ def apply_pricing_rule(args, doc=None): # list of dictionaries out = [] - if args.get("doctype") == "Material Request": return out + if args.get("doctype") == "Material Request": + return out item_list = args.get("items") args.pop("items") - set_serial_nos_based_on_fifo = frappe.db.get_single_value("Stock Settings", - "automatically_set_serial_nos_based_on_fifo") + set_serial_nos_based_on_fifo = frappe.db.get_single_value( + "Stock Settings", "automatically_set_serial_nos_based_on_fifo" + ) - item_code_list = tuple(item.get('item_code') for item in item_list) - query_items = frappe.get_all('Item', fields=['item_code','has_serial_no'], filters=[['item_code','in',item_code_list]],as_list=1) + item_code_list = tuple(item.get("item_code") for item in item_list) + query_items = frappe.get_all( + "Item", + fields=["item_code", "has_serial_no"], + filters=[["item_code", "in", item_code_list]], + as_list=1, + ) serialized_items = dict() for item_code, val in query_items: serialized_items.setdefault(item_code, val) @@ -206,26 +239,31 @@ def apply_pricing_rule(args, doc=None): for item in item_list: args_copy = copy.deepcopy(args) args_copy.update(item) - data = get_pricing_rule_for_item(args_copy, item.get('price_list_rate'), doc=doc) + data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc) out.append(data) - if serialized_items.get(item.get('item_code')) and not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'): + if ( + serialized_items.get(item.get("item_code")) + and not item.get("serial_no") + and set_serial_nos_based_on_fifo + and not args.get("is_return") + ): out[0].update(get_serial_no_for_item(args_copy)) return out + def get_serial_no_for_item(args): from erpnext.stock.get_item_details import get_serial_no - item_details = frappe._dict({ - "doctype": args.doctype, - "name": args.name, - "serial_no": args.serial_no - }) + item_details = frappe._dict( + {"doctype": args.doctype, "name": args.name, "serial_no": args.serial_no} + ) if args.get("parenttype") in ("Sales Invoice", "Delivery Note") and flt(args.stock_qty) > 0: item_details.serial_no = get_serial_no(args) return item_details + def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): from erpnext.accounts.doctype.pricing_rule.utils import ( get_applied_pricing_rules, @@ -240,18 +278,20 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if doc: doc = frappe.get_doc(doc) - if (args.get('is_free_item') or - args.get("parenttype") == "Material Request"): return {} + if args.get("is_free_item") or args.get("parenttype") == "Material Request": + return {} - item_details = frappe._dict({ - "doctype": args.doctype, - "has_margin": False, - "name": args.name, - "free_item_data": [], - "parent": args.parent, - "parenttype": args.parenttype, - "child_docname": args.get('child_docname'), - }) + item_details = frappe._dict( + { + "doctype": args.doctype, + "has_margin": False, + "name": args.name, + "free_item_data": [], + "parent": args.parent, + "parenttype": args.parenttype, + "child_docname": args.get("child_docname"), + } + ) if args.ignore_pricing_rule or not args.item_code: if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"): @@ -265,20 +305,25 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa update_args_for_pricing_rule(args) - pricing_rules = (get_applied_pricing_rules(args.get('pricing_rules')) - if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc)) + pricing_rules = ( + get_applied_pricing_rules(args.get("pricing_rules")) + if for_validate and args.get("pricing_rules") + else get_pricing_rules(args, doc) + ) if pricing_rules: rules = [] for pricing_rule in pricing_rules: - if not pricing_rule: continue + if not pricing_rule: + continue if isinstance(pricing_rule, string_types): pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule) pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) - if pricing_rule.get('suggestion'): continue + if pricing_rule.get("suggestion"): + continue item_details.validate_applied_rule = pricing_rule.get("validate_applied_rule", 0) item_details.price_or_product_discount = pricing_rule.get("price_or_product_discount") @@ -286,14 +331,19 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa rules.append(get_pricing_rule_details(args, pricing_rule)) if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other: - item_details.update({ - 'apply_rule_on_other_items': json.dumps(pricing_rule.apply_rule_on_other_items), - 'price_or_product_discount': pricing_rule.price_or_product_discount, - 'apply_rule_on': (frappe.scrub(pricing_rule.apply_rule_on_other) - if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on'))) - }) + item_details.update( + { + "apply_rule_on_other_items": json.dumps(pricing_rule.apply_rule_on_other_items), + "price_or_product_discount": pricing_rule.price_or_product_discount, + "apply_rule_on": ( + frappe.scrub(pricing_rule.apply_rule_on_other) + if pricing_rule.apply_rule_on_other + else frappe.scrub(pricing_rule.get("apply_on")) + ), + } + ) - if pricing_rule.coupon_code_based==1 and args.coupon_code==None: + if pricing_rule.coupon_code_based == 1 and args.coupon_code == None: return item_details if not pricing_rule.validate_applied_rule: @@ -310,7 +360,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa item_details.pricing_rules = frappe.as_json([d.pricing_rule for d in rules]) - if not doc: return item_details + if not doc: + return item_details elif args.get("pricing_rules"): item_details = remove_pricing_rule_for_item( @@ -322,19 +373,22 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa return item_details + 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"]) + args.item_group, args.brand = frappe.get_cached_value( + "Item", args.item_code, ["item_group", "brand"] + ) except frappe.DoesNotExistError: return if not args.item_group: frappe.throw(_("Item Group not mentioned in item master for item {0}").format(args.item_code)) - if args.transaction_type=="selling": + if args.transaction_type == "selling": if args.customer and not (args.customer_group and args.territory): - if args.quotation_to and args.quotation_to != 'Customer': + if args.quotation_to and args.quotation_to != "Customer": customer = frappe._dict() else: customer = frappe.get_cached_value("Customer", args.customer, ["customer_group", "territory"]) @@ -348,20 +402,25 @@ def update_args_for_pricing_rule(args): args.supplier_group = frappe.get_cached_value("Supplier", args.supplier, "supplier_group") args.customer = args.customer_group = args.territory = None + def get_pricing_rule_details(args, pricing_rule): - return frappe._dict({ - 'pricing_rule': pricing_rule.name, - 'rate_or_discount': pricing_rule.rate_or_discount, - 'margin_type': pricing_rule.margin_type, - 'item_code': args.get("item_code"), - 'child_docname': args.get('child_docname') - }) + return frappe._dict( + { + "pricing_rule": pricing_rule.name, + "rate_or_discount": pricing_rule.rate_or_discount, + "margin_type": pricing_rule.margin_type, + "item_code": args.get("item_code"), + "child_docname": args.get("child_docname"), + } + ) + def apply_price_discount_rule(pricing_rule, item_details, args): item_details.pricing_rule_for = pricing_rule.rate_or_discount - if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency) - or (pricing_rule.margin_type == 'Percentage')): + if ( + pricing_rule.margin_type in ["Amount", "Percentage"] and pricing_rule.currency == args.currency + ) or (pricing_rule.margin_type == "Percentage"): item_details.margin_type = pricing_rule.margin_type item_details.has_margin = True @@ -370,7 +429,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args): else: item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount - if pricing_rule.rate_or_discount == 'Rate': + if pricing_rule.rate_or_discount == "Rate": pricing_rule_rate = 0.0 if pricing_rule.currency == args.currency: pricing_rule_rate = pricing_rule.rate @@ -378,63 +437,71 @@ def apply_price_discount_rule(pricing_rule, item_details, args): if pricing_rule_rate: # Override already set price list rate (from item price) # if pricing_rule_rate > 0 - item_details.update({ - "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1), - }) - item_details.update({ - "discount_percentage": 0.0 - }) + item_details.update( + { + "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1), + } + ) + item_details.update({"discount_percentage": 0.0}) - for apply_on in ['Discount Amount', 'Discount Percentage']: - if pricing_rule.rate_or_discount != apply_on: continue + for apply_on in ["Discount Amount", "Discount Percentage"]: + if pricing_rule.rate_or_discount != apply_on: + continue field = frappe.scrub(apply_on) if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"): # Apply discount on discounted rate - item_details[field] += ((100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)) + item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100) else: if field not in item_details: item_details.setdefault(field, 0) - item_details[field] += (pricing_rule.get(field, 0) - if pricing_rule else args.get(field, 0)) + item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0) + def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None): from erpnext.accounts.doctype.pricing_rule.utils import ( get_applied_pricing_rules, get_pricing_rule_items, ) - for d in get_applied_pricing_rules(pricing_rules): - if not d or not frappe.db.exists("Pricing Rule", d): continue - pricing_rule = frappe.get_cached_doc('Pricing Rule', d) - if pricing_rule.price_or_product_discount == 'Price': - if pricing_rule.rate_or_discount == 'Discount Percentage': + for d in get_applied_pricing_rules(pricing_rules): + if not d or not frappe.db.exists("Pricing Rule", d): + continue + pricing_rule = frappe.get_cached_doc("Pricing Rule", d) + + if pricing_rule.price_or_product_discount == "Price": + if pricing_rule.rate_or_discount == "Discount Percentage": item_details.discount_percentage = 0.0 item_details.discount_amount = 0.0 item_details.rate = rate or 0.0 - if pricing_rule.rate_or_discount == 'Discount Amount': + if pricing_rule.rate_or_discount == "Discount Amount": item_details.discount_amount = 0.0 - if pricing_rule.margin_type in ['Percentage', 'Amount']: + if pricing_rule.margin_type in ["Percentage", "Amount"]: item_details.margin_rate_or_amount = 0.0 item_details.margin_type = None - elif pricing_rule.get('free_item'): - item_details.remove_free_item = (item_code if pricing_rule.get('same_item') - else pricing_rule.get('free_item')) + elif pricing_rule.get("free_item"): + item_details.remove_free_item = ( + item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item") + ) if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"): items = get_pricing_rule_items(pricing_rule) - item_details.apply_on = (frappe.scrub(pricing_rule.apply_rule_on_other) - if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on'))) - item_details.applied_on_items = ','.join(items) + item_details.apply_on = ( + frappe.scrub(pricing_rule.apply_rule_on_other) + if pricing_rule.apply_rule_on_other + else frappe.scrub(pricing_rule.get("apply_on")) + ) + item_details.applied_on_items = ",".join(items) - item_details.pricing_rules = '' + item_details.pricing_rules = "" item_details.pricing_rule_removed = True return item_details + @frappe.whitelist() def remove_pricing_rules(item_list): if isinstance(item_list, string_types): @@ -452,19 +519,26 @@ def remove_pricing_rules(item_list): return out + def set_transaction_type(args): if args.transaction_type: return if args.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"): args.transaction_type = "selling" - elif args.doctype in ("Material Request", "Supplier Quotation", "Purchase Order", - "Purchase Receipt", "Purchase Invoice"): - args.transaction_type = "buying" + elif args.doctype in ( + "Material Request", + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ): + args.transaction_type = "buying" elif args.customer: args.transaction_type = "selling" else: args.transaction_type = "buying" + @frappe.whitelist() def make_pricing_rule(doctype, docname): doc = frappe.new_doc("Pricing Rule") @@ -475,15 +549,18 @@ def make_pricing_rule(doctype, docname): return doc + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_item_uoms(doctype, txt, searchfield, start, page_len, filters): - items = [filters.get('value')] - if filters.get('apply_on') != 'Item Code': - field = frappe.scrub(filters.get('apply_on')) - items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get('value')})] + items = [filters.get("value")] + if filters.get("apply_on") != "Item Code": + field = frappe.scrub(filters.get("apply_on")) + items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get("value")})] - return frappe.get_all('UOM Conversion Detail', filters={ - 'parent': ('in', items), - 'uom': ("like", "{0}%".format(txt)) - }, fields = ["distinct uom"], as_list=1) + return frappe.get_all( + "UOM Conversion Detail", + filters={"parent": ("in", items), "uom": ("like", "{0}%".format(txt))}, + fields=["distinct uom"], + as_list=1, + ) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index f3b3cd4df77..4b81a7d6a23 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import unittest import frappe @@ -32,31 +31,31 @@ class TestPricingRule(unittest.TestCase): "doctype": "Pricing Rule", "title": "_Test Pricing Rule", "apply_on": "Item Code", - "items": [{ - "item_code": "_Test Item" - }], + "items": [{"item_code": "_Test Item"}], "currency": "USD", "selling": 1, "rate_or_discount": "Discount Percentage", "rate": 0, "discount_percentage": 10, - "company": "_Test Company" + "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() - args = frappe._dict({ - "item_code": "_Test Item", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "name": None - }) + args = frappe._dict( + { + "item_code": "_Test Item", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + } + ) details = get_item_details(args) self.assertEqual(details.get("discount_percentage"), 10) @@ -75,9 +74,7 @@ class TestPricingRule(unittest.TestCase): prule = frappe.get_doc(test_record.copy()) prule.apply_on = "Item Group" prule.items = [] - prule.append('item_groups', { - 'item_group': "All Item Groups" - }) + prule.append("item_groups", {"item_group": "All Item Groups"}) prule.title = "_Test Pricing Rule for Item Group" prule.discount_percentage = 15 prule.insert() @@ -100,6 +97,7 @@ class TestPricingRule(unittest.TestCase): frappe.db.sql("update `tabPricing Rule` set priority=NULL where campaign='_Test Campaign'") from erpnext.accounts.doctype.pricing_rule.utils import MultiplePricingRuleConflict + self.assertRaises(MultiplePricingRuleConflict, get_item_details, args) args.item_code = "_Test Item 2" @@ -115,41 +113,47 @@ class TestPricingRule(unittest.TestCase): "doctype": "Pricing Rule", "title": "_Test Pricing Rule", "apply_on": "Item Code", - "items": [{ - "item_code": "_Test FG Item 2", - }], + "items": [ + { + "item_code": "_Test FG Item 2", + } + ], "selling": 1, "currency": "USD", "rate_or_discount": "Discount Percentage", "rate": 0, "margin_type": "Percentage", "margin_rate_or_amount": 10, - "company": "_Test Company" + "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": "_Test Price List 2", - "item_code": "_Test FG Item 2", - "price_list_rate": 100 - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "_Test Price List 2", + "item_code": "_Test FG Item 2", + "price_list_rate": 100, + } + ) item_price.insert(ignore_permissions=True) - args = frappe._dict({ - "item_code": "_Test FG Item 2", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "name": None - }) + args = frappe._dict( + { + "item_code": "_Test FG Item 2", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + } + ) details = get_item_details(args) self.assertEqual(details.get("margin_type"), "Percentage") self.assertEqual(details.get("margin_rate_or_amount"), 10) @@ -178,25 +182,27 @@ class TestPricingRule(unittest.TestCase): "discount_percentage": 10, "applicable_for": "Customer Group", "customer_group": "All Customer Groups", - "company": "_Test Company" + "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() - args = frappe._dict({ - "item_code": "Mixed Cond Item 1", - "item_group": "Products", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "customer_group": "_Test Customer Group", - "name": None - }) + args = frappe._dict( + { + "item_code": "Mixed Cond Item 1", + "item_group": "Products", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "customer_group": "_Test Customer Group", + "name": None, + } + ) details = get_item_details(args) self.assertEqual(details.get("discount_percentage"), 10) @@ -206,72 +212,79 @@ class TestPricingRule(unittest.TestCase): from erpnext.stock.get_item_details import get_item_details if not frappe.db.exists("Item", "Test Variant PRT"): - frappe.get_doc({ - "doctype": "Item", - "item_code": "Test Variant PRT", - "item_name": "Test Variant PRT", - "description": "Test Variant PRT", - "item_group": "_Test Item Group", - "is_stock_item": 1, - "variant_of": "_Test Variant Item", - "default_warehouse": "_Test Warehouse - _TC", - "stock_uom": "_Test UOM", - "attributes": [ + frappe.get_doc( + { + "doctype": "Item", + "item_code": "Test Variant PRT", + "item_name": "Test Variant PRT", + "description": "Test Variant PRT", + "item_group": "_Test Item Group", + "is_stock_item": 1, + "variant_of": "_Test Variant Item", + "default_warehouse": "_Test Warehouse - _TC", + "stock_uom": "_Test UOM", + "attributes": [{"attribute": "Test Size", "attribute_value": "Medium"}], + } + ).insert() + + frappe.get_doc( + { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule 1", + "apply_on": "Item Code", + "currency": "USD", + "items": [ { - "attribute": "Test Size", - "attribute_value": "Medium" + "item_code": "_Test Variant Item", } ], - }).insert() + "selling": 1, + "rate_or_discount": "Discount Percentage", + "rate": 0, + "discount_percentage": 7.5, + "company": "_Test Company", + } + ).insert() - frappe.get_doc({ - "doctype": "Pricing Rule", - "title": "_Test Pricing Rule 1", - "apply_on": "Item Code", - "currency": "USD", - "items": [{ - "item_code": "_Test Variant Item", - }], - "selling": 1, - "rate_or_discount": "Discount Percentage", - "rate": 0, - "discount_percentage": 7.5, - "company": "_Test Company" - }).insert() - - args = frappe._dict({ - "item_code": "Test Variant PRT", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "name": None - }) + args = frappe._dict( + { + "item_code": "Test Variant PRT", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + } + ) details = get_item_details(args) self.assertEqual(details.get("discount_percentage"), 7.5) # add a new pricing rule for that item code, it should take priority - frappe.get_doc({ - "doctype": "Pricing Rule", - "title": "_Test Pricing Rule 2", - "apply_on": "Item Code", - "items": [{ - "item_code": "Test Variant PRT", - }], - "currency": "USD", - "selling": 1, - "rate_or_discount": "Discount Percentage", - "rate": 0, - "discount_percentage": 17.5, - "priority": 1, - "company": "_Test Company" - }).insert() + frappe.get_doc( + { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule 2", + "apply_on": "Item Code", + "items": [ + { + "item_code": "Test Variant PRT", + } + ], + "currency": "USD", + "selling": 1, + "rate_or_discount": "Discount Percentage", + "rate": 0, + "discount_percentage": 17.5, + "priority": 1, + "company": "_Test Company", + } + ).insert() details = get_item_details(args) self.assertEqual(details.get("discount_percentage"), 17.5) @@ -282,33 +295,31 @@ class TestPricingRule(unittest.TestCase): "title": "_Test Pricing Rule", "apply_on": "Item Code", "currency": "USD", - "items": [{ - "item_code": "_Test Item", - }], + "items": [ + { + "item_code": "_Test Item", + } + ], "selling": 1, "rate_or_discount": "Discount Percentage", "rate": 0, "min_qty": 5, "max_qty": 7, "discount_percentage": 17.5, - "company": "_Test Company" + "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() - if not frappe.db.get_value('UOM Conversion Detail', - {'parent': '_Test Item', 'uom': 'box'}): - item = frappe.get_doc('Item', '_Test Item') - item.append('uoms', { - 'uom': 'Box', - 'conversion_factor': 5 - }) + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Test Item", "uom": "box"}): + item = frappe.get_doc("Item", "_Test Item") + item.append("uoms", {"uom": "Box", "conversion_factor": 5}) item.save(ignore_permissions=True) # With pricing rule so = make_sales_order(item_code="_Test Item", qty=1, uom="Box", do_not_submit=True) so.items[0].price_list_rate = 100 so.submit() - so = frappe.get_doc('Sales Order', so.name) + so = frappe.get_doc("Sales Order", so.name) self.assertEqual(so.items[0].discount_percentage, 17.5) self.assertEqual(so.items[0].rate, 82.5) @@ -316,13 +327,15 @@ class TestPricingRule(unittest.TestCase): so = make_sales_order(item_code="_Test Item", qty=2, uom="Box", do_not_submit=True) so.items[0].price_list_rate = 100 so.submit() - so = frappe.get_doc('Sales Order', so.name) + so = frappe.get_doc("Sales Order", so.name) self.assertEqual(so.items[0].discount_percentage, 0) self.assertEqual(so.items[0].rate, 100) def test_pricing_rule_with_margin_and_discount(self): - frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') - make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10, discount_percentage=10) + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + make_pricing_rule( + selling=1, margin_type="Percentage", margin_rate_or_amount=10, discount_percentage=10 + ) si = create_sales_invoice(do_not_save=True) si.items[0].price_list_rate = 1000 si.payment_schedule = [] @@ -336,9 +349,14 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(item.rate, 990) def test_pricing_rule_with_margin_and_discount_amount(self): - frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') - make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10, - rate_or_discount="Discount Amount", discount_amount=110) + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + make_pricing_rule( + selling=1, + margin_type="Percentage", + margin_rate_or_amount=10, + rate_or_discount="Discount Amount", + discount_amount=110, + ) si = create_sales_invoice(do_not_save=True) si.items[0].price_list_rate = 1000 si.payment_schedule = [] @@ -351,15 +369,17 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(item.rate, 990) def test_pricing_rule_for_product_discount_on_same_item(self): - frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") test_record = { "doctype": "Pricing Rule", "title": "_Test Pricing Rule", "apply_on": "Item Code", "currency": "USD", - "items": [{ - "item_code": "_Test Item", - }], + "items": [ + { + "item_code": "_Test Item", + } + ], "selling": 1, "rate_or_discount": "Discount Percentage", "rate": 0, @@ -369,7 +389,7 @@ class TestPricingRule(unittest.TestCase): "price_or_product_discount": "Product", "same_item": 1, "free_qty": 1, - "company": "_Test Company" + "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() @@ -379,17 +399,18 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].item_code, "_Test Item") - def test_pricing_rule_for_product_discount_on_different_item(self): - frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") test_record = { "doctype": "Pricing Rule", "title": "_Test Pricing Rule", "apply_on": "Item Code", "currency": "USD", - "items": [{ - "item_code": "_Test Item", - }], + "items": [ + { + "item_code": "_Test Item", + } + ], "selling": 1, "rate_or_discount": "Discount Percentage", "rate": 0, @@ -400,7 +421,7 @@ class TestPricingRule(unittest.TestCase): "same_item": 0, "free_item": "_Test Item 2", "free_qty": 1, - "company": "_Test Company" + "company": "_Test Company", } frappe.get_doc(test_record.copy()).insert() @@ -411,15 +432,17 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(so.items[1].item_code, "_Test Item 2") def test_cumulative_pricing_rule(self): - frappe.delete_doc_if_exists('Pricing Rule', '_Test Cumulative Pricing Rule') + frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule") test_record = { "doctype": "Pricing Rule", "title": "_Test Cumulative Pricing Rule", "apply_on": "Item Code", "currency": "USD", - "items": [{ - "item_code": "_Test Item", - }], + "items": [ + { + "item_code": "_Test Item", + } + ], "is_cumulative": 1, "selling": 1, "applicable_for": "Customer", @@ -432,24 +455,26 @@ class TestPricingRule(unittest.TestCase): "price_or_product_discount": "Price", "company": "_Test Company", "valid_from": frappe.utils.nowdate(), - "valid_upto": frappe.utils.nowdate() + "valid_upto": frappe.utils.nowdate(), } frappe.get_doc(test_record.copy()).insert() - args = frappe._dict({ - "item_code": "_Test Item", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "name": None, - "transaction_date": frappe.utils.nowdate() - }) + args = frappe._dict( + { + "item_code": "_Test Item", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + "transaction_date": frappe.utils.nowdate(), + } + ) details = get_item_details(args) self.assertTrue(details) @@ -457,8 +482,12 @@ class TestPricingRule(unittest.TestCase): def test_pricing_rule_for_condition(self): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") - make_pricing_rule(selling=1, margin_type="Percentage", \ - condition="customer=='_Test Customer 1' and is_return==0", discount_percentage=10) + make_pricing_rule( + selling=1, + margin_type="Percentage", + condition="customer=='_Test Customer 1' and is_return==0", + discount_percentage=10, + ) # Incorrect Customer and Correct is_return value si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 2", is_return=0) @@ -482,10 +511,20 @@ class TestPricingRule(unittest.TestCase): self.assertEqual(item.rate, 900) def test_multiple_pricing_rules(self): - make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1, - title="_Test Pricing Rule 1") - make_pricing_rule(discount_percentage=10, selling=1, title="_Test Pricing Rule 2", priority=2, - apply_multiple_pricing_rules=1) + make_pricing_rule( + discount_percentage=20, + selling=1, + priority=1, + apply_multiple_pricing_rules=1, + title="_Test Pricing Rule 1", + ) + make_pricing_rule( + discount_percentage=10, + selling=1, + title="_Test Pricing Rule 2", + priority=2, + apply_multiple_pricing_rules=1, + ) si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1) self.assertEqual(si.items[0].discount_percentage, 30) si.delete() @@ -496,10 +535,21 @@ class TestPricingRule(unittest.TestCase): def test_multiple_pricing_rules_with_apply_discount_on_discounted_rate(self): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") - make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1, - title="_Test Pricing Rule 1") - make_pricing_rule(discount_percentage=10, selling=1, priority=2, - apply_discount_on_rate=1, title="_Test Pricing Rule 2", apply_multiple_pricing_rules=1) + make_pricing_rule( + discount_percentage=20, + selling=1, + priority=1, + apply_multiple_pricing_rules=1, + title="_Test Pricing Rule 1", + ) + make_pricing_rule( + discount_percentage=10, + selling=1, + priority=2, + apply_discount_on_rate=1, + title="_Test Pricing Rule 2", + apply_multiple_pricing_rules=1, + ) si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1) self.assertEqual(si.items[0].discount_percentage, 28) @@ -516,16 +566,18 @@ class TestPricingRule(unittest.TestCase): "doctype": "Pricing Rule", "title": "_Test Water Flask Rule", "apply_on": "Item Code", - "items": [{ - "item_code": "Water Flask", - }], + "items": [ + { + "item_code": "Water Flask", + } + ], "selling": 1, "currency": "INR", "rate_or_discount": "Rate", "rate": 0, "margin_type": "Percentage", "margin_rate_or_amount": 2, - "company": "_Test Company" + "company": "_Test Company", } rule = frappe.get_doc(pricing_rule_record) rule.insert() @@ -552,9 +604,11 @@ class TestPricingRule(unittest.TestCase): "doctype": "Pricing Rule", "title": "_Test Sanitizer Rule", "apply_on": "Item Code", - "items": [{ - "item_code": "Test Sanitizer Item", - }], + "items": [ + { + "item_code": "Test Sanitizer Item", + } + ], "selling": 1, "currency": "INR", "rate_or_discount": "Rate", @@ -562,63 +616,73 @@ class TestPricingRule(unittest.TestCase): "priority": 2, "margin_type": "Percentage", "margin_rate_or_amount": 0.0, - "company": "_Test Company" + "company": "_Test Company", } rule = frappe.get_doc(pricing_rule_record) - rule.rate_or_discount = 'Rate' + rule.rate_or_discount = "Rate" rule.rate = 100.0 rule.insert() rule1 = frappe.get_doc(pricing_rule_record) - rule1.currency = 'USD' - rule1.rate_or_discount = 'Rate' + rule1.currency = "USD" + rule1.rate_or_discount = "Rate" rule1.rate = 2.0 rule1.priority = 1 rule1.insert() - args = frappe._dict({ - "item_code": "Test Sanitizer Item", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "USD", - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "name": None, - "transaction_date": frappe.utils.nowdate() - }) + args = frappe._dict( + { + "item_code": "Test Sanitizer Item", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "USD", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + "transaction_date": frappe.utils.nowdate(), + } + ) details = get_item_details(args) self.assertEqual(details.price_list_rate, 2.0) - - args = frappe._dict({ - "item_code": "Test Sanitizer Item", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "INR", - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "name": None, - "transaction_date": frappe.utils.nowdate() - }) + args = frappe._dict( + { + "item_code": "Test Sanitizer Item", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "INR", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + "transaction_date": frappe.utils.nowdate(), + } + ) details = get_item_details(args) self.assertEqual(details.price_list_rate, 100.0) def test_pricing_rule_for_transaction(self): make_item("Water Flask 1") - frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') - make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product", - apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10) + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + make_pricing_rule( + selling=1, + min_qty=5, + price_or_product_discount="Product", + apply_on="Transaction", + free_item="Water Flask 1", + free_qty=1, + free_item_rate=10, + ) si = create_sales_invoice(qty=5, do_not_submit=True) self.assertEqual(len(si.items), 2) @@ -631,10 +695,22 @@ class TestPricingRule(unittest.TestCase): doc.delete() def test_multiple_pricing_rules_with_min_qty(self): - make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, - apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") - make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4, - apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2") + make_pricing_rule( + discount_percentage=20, + selling=1, + priority=1, + min_qty=4, + apply_multiple_pricing_rules=1, + title="_Test Pricing Rule with Min Qty - 1", + ) + make_pricing_rule( + discount_percentage=10, + selling=1, + priority=2, + min_qty=4, + apply_multiple_pricing_rules=1, + title="_Test Pricing Rule with Min Qty - 2", + ) si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD") item = si.items[0] @@ -659,14 +735,16 @@ class TestPricingRule(unittest.TestCase): "title": "_Test Water Flask Rule", "apply_on": "Item Code", "price_or_product_discount": "Price", - "items": [{ - "item_code": "Water Flask", - }], + "items": [ + { + "item_code": "Water Flask", + } + ], "selling": 1, "currency": "INR", "rate_or_discount": "Discount Percentage", "discount_percentage": 20, - "company": "_Test Company" + "company": "_Test Company", } rule = frappe.get_doc(pricing_rule_record) rule.insert() @@ -693,64 +771,75 @@ class TestPricingRule(unittest.TestCase): test_dependencies = ["Campaign"] + def make_pricing_rule(**args): args = frappe._dict(args) - doc = frappe.get_doc({ - "doctype": "Pricing Rule", - "title": args.title or "_Test Pricing Rule", - "company": args.company or "_Test Company", - "apply_on": args.apply_on or "Item Code", - "applicable_for": args.applicable_for, - "selling": args.selling or 0, - "currency": "USD", - "apply_discount_on_rate": args.apply_discount_on_rate or 0, - "buying": args.buying or 0, - "min_qty": args.min_qty or 0.0, - "max_qty": args.max_qty or 0.0, - "rate_or_discount": args.rate_or_discount or "Discount Percentage", - "discount_percentage": args.discount_percentage or 0.0, - "rate": args.rate or 0.0, - "margin_rate_or_amount": args.margin_rate_or_amount or 0.0, - "condition": args.condition or '', - "priority": args.priority or 1, - "discount_amount": args.discount_amount or 0.0, - "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 - }) + doc = frappe.get_doc( + { + "doctype": "Pricing Rule", + "title": args.title or "_Test Pricing Rule", + "company": args.company or "_Test Company", + "apply_on": args.apply_on or "Item Code", + "applicable_for": args.applicable_for, + "selling": args.selling or 0, + "currency": "USD", + "apply_discount_on_rate": args.apply_discount_on_rate or 0, + "buying": args.buying or 0, + "min_qty": args.min_qty or 0.0, + "max_qty": args.max_qty or 0.0, + "rate_or_discount": args.rate_or_discount or "Discount Percentage", + "discount_percentage": args.discount_percentage or 0.0, + "rate": args.rate or 0.0, + "margin_rate_or_amount": args.margin_rate_or_amount or 0.0, + "condition": args.condition or "", + "priority": args.priority or 1, + "discount_amount": args.discount_amount or 0.0, + "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0, + } + ) - for field in ["free_item", "free_qty", "free_item_rate", "priority", - "margin_type", "price_or_product_discount"]: + for field in [ + "free_item", + "free_qty", + "free_item_rate", + "priority", + "margin_type", + "price_or_product_discount", + ]: if args.get(field): doc.set(field, args.get(field)) - apply_on = doc.apply_on.replace(' ', '_').lower() - child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'} + apply_on = doc.apply_on.replace(" ", "_").lower() + child_table = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"} if doc.apply_on != "Transaction": - doc.append(child_table.get(doc.apply_on), { - apply_on: args.get(apply_on) or "_Test Item" - }) + doc.append(child_table.get(doc.apply_on), {apply_on: args.get(apply_on) or "_Test Item"}) doc.insert(ignore_permissions=True) if args.get(apply_on) and apply_on != "item_code": doc.db_set(apply_on, args.get(apply_on)) - applicable_for = doc.applicable_for.replace(' ', '_').lower() + applicable_for = doc.applicable_for.replace(" ", "_").lower() if args.get(applicable_for): doc.db_set(applicable_for, args.get(applicable_for)) return doc + def setup_pricing_rule_data(): - if not frappe.db.exists('Campaign', '_Test Campaign'): - frappe.get_doc({ - 'doctype': 'Campaign', - 'campaign_name': '_Test Campaign', - 'name': '_Test Campaign' - }).insert() + if not frappe.db.exists("Campaign", "_Test Campaign"): + frappe.get_doc( + {"doctype": "Campaign", "campaign_name": "_Test Campaign", "name": "_Test Campaign"} + ).insert() + def delete_existing_pricing_rules(): - for doctype in ["Pricing Rule", "Pricing Rule Item Code", - "Pricing Rule Item Group", "Pricing Rule Brand"]: + for doctype in [ + "Pricing Rule", + "Pricing Rule Item Code", + "Pricing Rule Item Group", + "Pricing Rule Brand", + ]: frappe.db.sql("delete from `tab{0}`".format(doctype)) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 7792590c9c7..70926cfbd72 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -16,22 +16,21 @@ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.get_item_details import get_conversion_factor -class MultiplePricingRuleConflict(frappe.ValidationError): pass +class MultiplePricingRuleConflict(frappe.ValidationError): + pass + + +apply_on_table = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"} -apply_on_table = { - 'Item Code': 'items', - 'Item Group': 'item_groups', - 'Brand': 'brands' -} def get_pricing_rules(args, doc=None): pricing_rules = [] - values = {} + values = {} - if not frappe.db.exists('Pricing Rule', {'disable': 0, args.transaction_type: 1}): + if not frappe.db.exists("Pricing Rule", {"disable": 0, args.transaction_type: 1}): return - for apply_on in ['Item Code', 'Item Group', 'Brand']: + for apply_on in ["Item Code", "Item Group", "Brand"]: pricing_rules.extend(_get_pricing_rules(apply_on, args, values)) if pricing_rules and not apply_multiple_pricing_rules(pricing_rules): break @@ -40,7 +39,8 @@ def get_pricing_rules(args, doc=None): pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc) - if not pricing_rules: return [] + if not pricing_rules: + return [] if apply_multiple_pricing_rules(pricing_rules): pricing_rules = sorted_by_priority(pricing_rules, args, doc) @@ -56,6 +56,7 @@ def get_pricing_rules(args, doc=None): return rules + def sorted_by_priority(pricing_rules, args, doc=None): # If more than one pricing rules, then sort by priority pricing_rules_list = [] @@ -64,10 +65,10 @@ def sorted_by_priority(pricing_rules, args, doc=None): for pricing_rule in pricing_rules: pricing_rule = filter_pricing_rules(args, pricing_rule, doc) if pricing_rule: - if not pricing_rule.get('priority'): - pricing_rule['priority'] = 1 + if not pricing_rule.get("priority"): + pricing_rule["priority"] = 1 - if pricing_rule.get('apply_multiple_pricing_rules'): + if pricing_rule.get("apply_multiple_pricing_rules"): pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule) for key in sorted(pricing_rule_dict): @@ -75,6 +76,7 @@ def sorted_by_priority(pricing_rules, args, doc=None): return pricing_rules_list + def filter_pricing_rule_based_on_condition(pricing_rules, doc=None): filtered_pricing_rules = [] if doc: @@ -92,40 +94,48 @@ def filter_pricing_rule_based_on_condition(pricing_rules, doc=None): return filtered_pricing_rules + def _get_pricing_rules(apply_on, args, values): apply_on_field = frappe.scrub(apply_on) - if not args.get(apply_on_field): return [] + if not args.get(apply_on_field): + return [] - child_doc = '`tabPricing Rule {0}`'.format(apply_on) + child_doc = "`tabPricing Rule {0}`".format(apply_on) conditions = item_variant_condition = item_conditions = "" values[apply_on_field] = args.get(apply_on_field) - if apply_on_field in ['item_code', 'brand']: - item_conditions = "{child_doc}.{apply_on_field}= %({apply_on_field})s".format(child_doc=child_doc, - apply_on_field = apply_on_field) + if apply_on_field in ["item_code", "brand"]: + item_conditions = "{child_doc}.{apply_on_field}= %({apply_on_field})s".format( + child_doc=child_doc, apply_on_field=apply_on_field + ) - if apply_on_field == 'item_code': + if apply_on_field == "item_code": if "variant_of" not in args: args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of") if args.variant_of: - item_variant_condition = ' or {child_doc}.item_code=%(variant_of)s '.format(child_doc=child_doc) - values['variant_of'] = args.variant_of - elif apply_on_field == 'item_group': + item_variant_condition = " or {child_doc}.item_code=%(variant_of)s ".format( + child_doc=child_doc + ) + values["variant_of"] = args.variant_of + elif apply_on_field == "item_group": item_conditions = _get_tree_conditions(args, "Item Group", child_doc, False) conditions += get_other_conditions(conditions, values, args) - warehouse_conditions = _get_tree_conditions(args, "Warehouse", '`tabPricing Rule`') + warehouse_conditions = _get_tree_conditions(args, "Warehouse", "`tabPricing Rule`") if warehouse_conditions: warehouse_conditions = " and {0}".format(warehouse_conditions) - if not args.price_list: args.price_list = None + if not args.price_list: + args.price_list = None conditions += " and ifnull(`tabPricing Rule`.for_price_list, '') in (%(price_list)s, '')" values["price_list"] = args.get("price_list") - pricing_rules = frappe.db.sql("""select `tabPricing Rule`.*, + pricing_rules = ( + frappe.db.sql( + """select `tabPricing Rule`.*, {child_doc}.{apply_on_field}, {child_doc}.uom from `tabPricing Rule`, {child_doc} where ({item_conditions} or (`tabPricing Rule`.apply_rule_on_other is not null @@ -135,25 +145,35 @@ def _get_pricing_rules(apply_on, args, values): `tabPricing Rule`.{transaction_type} = 1 {warehouse_cond} {conditions} order by `tabPricing Rule`.priority desc, `tabPricing Rule`.name desc""".format( - child_doc = child_doc, - apply_on_field = apply_on_field, - item_conditions = item_conditions, - item_variant_condition = item_variant_condition, - transaction_type = args.transaction_type, - warehouse_cond = warehouse_conditions, - apply_on_other_field = "other_{0}".format(apply_on_field), - conditions = conditions), values, as_dict=1) or [] + child_doc=child_doc, + apply_on_field=apply_on_field, + item_conditions=item_conditions, + item_variant_condition=item_variant_condition, + transaction_type=args.transaction_type, + warehouse_cond=warehouse_conditions, + apply_on_other_field="other_{0}".format(apply_on_field), + conditions=conditions, + ), + values, + as_dict=1, + ) + or [] + ) return pricing_rules -def apply_multiple_pricing_rules(pricing_rules): - apply_multiple_rule = [d.apply_multiple_pricing_rules - for d in pricing_rules if d.apply_multiple_pricing_rules] - if not apply_multiple_rule: return False +def apply_multiple_pricing_rules(pricing_rules): + apply_multiple_rule = [ + d.apply_multiple_pricing_rules for d in pricing_rules if d.apply_multiple_pricing_rules + ] + + if not apply_multiple_rule: + return False return True + def _get_tree_conditions(args, parenttype, table, allow_blank=True): field = frappe.scrub(parenttype) condition = "" @@ -169,28 +189,37 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): except TypeError: frappe.throw(_("Invalid {0}").format(args.get(field))) - parent_groups = frappe.db.sql_list("""select name from `tab%s` - where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) + parent_groups = frappe.db.sql_list( + """select name from `tab%s` + where lft<=%s and rgt>=%s""" + % (parenttype, "%s", "%s"), + (lft, rgt), + ) if parenttype in ["Customer Group", "Item Group", "Territory"]: parent_field = "parent_{0}".format(frappe.scrub(parenttype)) - root_name = frappe.db.get_list(parenttype, - {"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1, ignore_permissions=True) + root_name = frappe.db.get_list( + parenttype, + {"is_group": 1, parent_field: ("is", "not set")}, + "name", + as_list=1, + ignore_permissions=True, + ) if root_name and root_name[0][0]: parent_groups.append(root_name[0][0]) if parent_groups: - if allow_blank: parent_groups.append('') + if allow_blank: + parent_groups.append("") condition = "ifnull({table}.{field}, '') in ({parent_groups})".format( - table=table, - field=field, - parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups) + table=table, field=field, parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups) ) frappe.flags.tree_conditions[key] = condition return condition + def get_other_conditions(conditions, values, args): for field in ["company", "customer", "supplier", "campaign", "sales_partner"]: if args.get(field): @@ -200,17 +229,18 @@ def get_other_conditions(conditions, values, args): conditions += " and ifnull(`tabPricing Rule`.{0}, '') = ''".format(field) for parenttype in ["Customer Group", "Territory", "Supplier Group"]: - group_condition = _get_tree_conditions(args, parenttype, '`tabPricing Rule`') + group_condition = _get_tree_conditions(args, parenttype, "`tabPricing Rule`") if group_condition: conditions += " and " + group_condition if args.get("transaction_date"): conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01') and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')""" - values['transaction_date'] = args.get('transaction_date') + values["transaction_date"] = args.get("transaction_date") return conditions + def filter_pricing_rules(args, pricing_rules, doc=None): if not isinstance(pricing_rules, list): pricing_rules = [pricing_rules] @@ -219,15 +249,16 @@ def filter_pricing_rules(args, pricing_rules, doc=None): # filter for qty if pricing_rules: - stock_qty = flt(args.get('stock_qty')) - amount = flt(args.get('price_list_rate')) * flt(args.get('qty')) + stock_qty = flt(args.get("stock_qty")) + amount = flt(args.get("price_list_rate")) * flt(args.get("qty")) if pricing_rules[0].apply_rule_on_other: field = frappe.scrub(pricing_rules[0].apply_rule_on_other) - if (field and pricing_rules[0].get('other_' + field) != args.get(field)): return + if field and pricing_rules[0].get("other_" + field) != args.get(field): + return - pr_doc = frappe.get_cached_doc('Pricing Rule', pricing_rules[0].name) + pr_doc = frappe.get_cached_doc("Pricing Rule", pricing_rules[0].name) if pricing_rules[0].mixed_conditions and doc: stock_qty, amount, items = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args) @@ -235,7 +266,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): pricing_rule_args.apply_rule_on_other_items = items elif pricing_rules[0].is_cumulative: - items = [args.get(frappe.scrub(pr_doc.get('apply_on')))] + items = [args.get(frappe.scrub(pr_doc.get("apply_on")))] data = get_qty_amount_data_for_cumulative(pr_doc, args, items) if data: @@ -249,13 +280,15 @@ def filter_pricing_rules(args, pricing_rules, doc=None): if not pricing_rules: for d in original_pricing_rule: - if not d.threshold_percentage: continue + if not d.threshold_percentage: + continue - msg = validate_quantity_and_amount_for_suggestion(d, stock_qty, - amount, args.get('item_code'), args.get('transaction_type')) + msg = validate_quantity_and_amount_for_suggestion( + d, stock_qty, amount, args.get("item_code"), args.get("transaction_type") + ) if msg: - return {'suggestion': msg, 'item_code': args.get('item_code')} + return {"suggestion": msg, "item_code": args.get("item_code")} # add variant_of property in pricing rule for p in pricing_rules: @@ -265,7 +298,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): p.variant_of = None if len(pricing_rules) > 1: - filtered_rules = list(filter(lambda x: x.currency==args.get('currency'), pricing_rules)) + filtered_rules = list(filter(lambda x: x.currency == args.get("currency"), pricing_rules)) if filtered_rules: pricing_rules = filtered_rules @@ -273,7 +306,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): if pricing_rules: max_priority = max(cint(p.priority) for p in pricing_rules) if max_priority: - pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules)) + pricing_rules = list(filter(lambda x: cint(x.priority) == max_priority, pricing_rules)) if pricing_rules and not isinstance(pricing_rules, list): pricing_rules = list(pricing_rules) @@ -281,42 +314,61 @@ def filter_pricing_rules(args, pricing_rules, doc=None): if len(pricing_rules) > 1: rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules)) if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage": - pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \ - or pricing_rules + pricing_rules = ( + list(filter(lambda x: x.for_price_list == args.price_list, pricing_rules)) or pricing_rules + ) if len(pricing_rules) > 1 and not args.for_shopping_cart: - frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}") - .format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict) + frappe.throw( + _( + "Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}" + ).format("\n".join(d.name for d in pricing_rules)), + MultiplePricingRuleConflict, + ) elif pricing_rules: return pricing_rules[0] -def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, transaction_type): - fieldname, msg = '', '' - type_of_transaction = 'purchase' if transaction_type == 'buying' else 'sale' - for field, value in {'min_qty': qty, 'min_amt': amount}.items(): - if (args.get(field) and value < args.get(field) - and (args.get(field) - cint(args.get(field) * args.threshold_percentage * 0.01)) <= value): +def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, transaction_type): + fieldname, msg = "", "" + type_of_transaction = "purchase" if transaction_type == "buying" else "sale" + + for field, value in {"min_qty": qty, "min_amt": amount}.items(): + if ( + args.get(field) + and value < args.get(field) + and (args.get(field) - cint(args.get(field) * args.threshold_percentage * 0.01)) <= value + ): fieldname = field - for field, value in {'max_qty': qty, 'max_amt': amount}.items(): - if (args.get(field) and value > args.get(field) - and (args.get(field) + cint(args.get(field) * args.threshold_percentage * 0.01)) >= value): + for field, value in {"max_qty": qty, "max_amt": amount}.items(): + if ( + args.get(field) + and value > args.get(field) + and (args.get(field) + cint(args.get(field) * args.threshold_percentage * 0.01)) >= value + ): fieldname = field if fieldname: - msg = (_("If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item.") - .format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description))) + msg = _( + "If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item." + ).format( + type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description) + ) - if fieldname in ['min_amt', 'max_amt']: - msg = (_("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.") - .format(type_of_transaction, fmt_money(args.get(fieldname), currency=args.get("currency")), - bold(item_code), bold(args.rule_description))) + if fieldname in ["min_amt", "max_amt"]: + msg = _("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.").format( + type_of_transaction, + fmt_money(args.get(fieldname), currency=args.get("currency")), + bold(item_code), + bold(args.rule_description), + ) frappe.msgprint(msg) return msg + def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None): rules = [] @@ -327,16 +379,19 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None): if rule.get("uom"): conversion_factor = get_conversion_factor(rule.item_code, rule.uom).get("conversion_factor", 1) - if (flt(qty) >= (flt(rule.min_qty) * conversion_factor) - and (flt(qty)<= (rule.max_qty * conversion_factor) if rule.max_qty else True)): + if flt(qty) >= (flt(rule.min_qty) * conversion_factor) and ( + flt(qty) <= (rule.max_qty * conversion_factor) if rule.max_qty else True + ): status = True # if user has created item price against the transaction UOM if args and rule.get("uom") == args.get("uom"): conversion_factor = 1.0 - if status and (flt(rate) >= (flt(rule.min_amt) * conversion_factor) - and (flt(rate)<= (rule.max_amt * conversion_factor) if rule.max_amt else True)): + if status and ( + flt(rate) >= (flt(rule.min_amt) * conversion_factor) + and (flt(rate) <= (rule.max_amt * conversion_factor) if rule.max_amt else True) + ): status = True else: status = False @@ -346,6 +401,7 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None): return rules + def if_all_rules_same(pricing_rules, fields): all_rules_same = True val = [pricing_rules[0].get(k) for k in fields] @@ -356,30 +412,34 @@ def if_all_rules_same(pricing_rules, fields): return all_rules_same + def apply_internal_priority(pricing_rules, field_set, args): filtered_rules = [] for field in field_set: if args.get(field): # filter function always returns a filter object even if empty # list conversion is necessary to check for an empty result - filtered_rules = list(filter(lambda x: x.get(field)==args.get(field), pricing_rules)) - if filtered_rules: break + filtered_rules = list(filter(lambda x: x.get(field) == args.get(field), pricing_rules)) + if filtered_rules: + break return filtered_rules or pricing_rules + def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): sum_qty, sum_amt = [0, 0] items = get_pricing_rule_items(pr_doc) or [] - apply_on = frappe.scrub(pr_doc.get('apply_on')) + apply_on = frappe.scrub(pr_doc.get("apply_on")) if items and doc.get("items"): - for row in doc.get('items'): - if (row.get(apply_on) or args.get(apply_on)) not in items: continue + for row in doc.get("items"): + if (row.get(apply_on) or args.get(apply_on)) not in items: + continue if pr_doc.mixed_conditions: - amt = args.get('qty') * args.get("price_list_rate") + amt = args.get("qty") * args.get("price_list_rate") if args.get("item_code") != row.get("item_code"): - amt = flt(row.get('qty')) * flt(row.get("price_list_rate") or args.get("rate")) + amt = flt(row.get("qty")) * flt(row.get("price_list_rate") or args.get("rate")) sum_qty += flt(row.get("stock_qty")) or flt(args.get("stock_qty")) or flt(args.get("qty")) sum_amt += amt @@ -393,28 +453,33 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): return sum_qty, sum_amt, items + def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules): items = get_pricing_rule_items(pr_doc) for row in doc.items: if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items: - pricing_rules = filter_pricing_rules_for_qty_amount(row.get("stock_qty"), - row.get("amount"), pricing_rules, row) + pricing_rules = filter_pricing_rules_for_qty_amount( + row.get("stock_qty"), row.get("amount"), pricing_rules, row + ) if pricing_rules and pricing_rules[0]: pricing_rules[0].apply_rule_on_other_items = items return pricing_rules + def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None): if items is None: items = [] sum_qty, sum_amt = [0, 0] - doctype = doc.get('parenttype') or doc.doctype + doctype = doc.get("parenttype") or doc.doctype - date_field = 'transaction_date' if frappe.get_meta(doctype).has_field('transaction_date') else 'posting_date' + date_field = ( + "transaction_date" if frappe.get_meta(doctype).has_field("transaction_date") else "posting_date" + ) - child_doctype = '{0} Item'.format(doctype) - apply_on = frappe.scrub(pr_doc.get('apply_on')) + child_doctype = "{0} Item".format(doctype) + apply_on = frappe.scrub(pr_doc.get("apply_on")) values = [pr_doc.valid_from, pr_doc.valid_upto] condition = "" @@ -423,72 +488,86 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None): warehouses = get_child_warehouses(pr_doc.warehouse) condition += """ and `tab{child_doc}`.warehouse in ({warehouses}) - """.format(child_doc=child_doctype, warehouses = ','.join(['%s'] * len(warehouses))) + """.format( + child_doc=child_doctype, warehouses=",".join(["%s"] * len(warehouses)) + ) values.extend(warehouses) if items: - condition = " and `tab{child_doc}`.{apply_on} in ({items})".format(child_doc = child_doctype, - apply_on = apply_on, items = ','.join(['%s'] * len(items))) + condition = " and `tab{child_doc}`.{apply_on} in ({items})".format( + child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items)) + ) values.extend(items) - data_set = frappe.db.sql(""" SELECT `tab{child_doc}`.stock_qty, + data_set = frappe.db.sql( + """ SELECT `tab{child_doc}`.stock_qty, `tab{child_doc}`.amount FROM `tab{child_doc}`, `tab{parent_doc}` WHERE `tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.{date_field} between %s and %s and `tab{parent_doc}`.docstatus = 1 {condition} group by `tab{child_doc}`.name - """.format(parent_doc = doctype, - child_doc = child_doctype, - condition = condition, - date_field = date_field - ), tuple(values), as_dict=1) + """.format( + parent_doc=doctype, child_doc=child_doctype, condition=condition, date_field=date_field + ), + tuple(values), + as_dict=1, + ) for data in data_set: - sum_qty += data.get('stock_qty') - sum_amt += data.get('amount') + sum_qty += data.get("stock_qty") + sum_amt += data.get("amount") return [sum_qty, sum_amt] + def apply_pricing_rule_on_transaction(doc): conditions = "apply_on = 'Transaction'" values = {} conditions = get_other_conditions(conditions, values, doc) - pricing_rules = frappe.db.sql(""" Select `tabPricing Rule`.* from `tabPricing Rule` + pricing_rules = frappe.db.sql( + """ Select `tabPricing Rule`.* from `tabPricing Rule` where {conditions} and `tabPricing Rule`.disable = 0 - """.format(conditions = conditions), values, as_dict=1) + """.format( + conditions=conditions + ), + values, + as_dict=1, + ) if pricing_rules: - pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, - doc.total, pricing_rules) + pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules) if not pricing_rules: remove_free_item(doc) for d in pricing_rules: - if d.price_or_product_discount == 'Price': + if d.price_or_product_discount == "Price": if d.apply_discount_on: - doc.set('apply_discount_on', d.apply_discount_on) + doc.set("apply_discount_on", d.apply_discount_on) - for field in ['additional_discount_percentage', 'discount_amount']: - pr_field = ('discount_percentage' - if field == 'additional_discount_percentage' else field) + for field in ["additional_discount_percentage", "discount_amount"]: + pr_field = "discount_percentage" if field == "additional_discount_percentage" else field - if not d.get(pr_field): continue + if not d.get(pr_field): + continue - if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field): - frappe.msgprint(_("User has not applied rule on the invoice {0}") - .format(doc.name)) + if ( + d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field) + ): + frappe.msgprint(_("User has not applied rule on the invoice {0}").format(doc.name)) else: if not d.coupon_code_based: doc.set(field, d.get(pr_field)) - elif doc.get('coupon_code'): + elif doc.get("coupon_code"): # coupon code based pricing rule - coupon_code_pricing_rule = frappe.db.get_value('Coupon Code', doc.get('coupon_code'), 'pricing_rule') + coupon_code_pricing_rule = frappe.db.get_value( + "Coupon Code", doc.get("coupon_code"), "pricing_rule" + ) if coupon_code_pricing_rule == d.name: # if selected coupon code is linked with pricing rule doc.set(field, d.get(pr_field)) @@ -500,83 +579,93 @@ def apply_pricing_rule_on_transaction(doc): doc.set(field, 0) doc.calculate_taxes_and_totals() - elif d.price_or_product_discount == 'Product': - item_details = frappe._dict({'parenttype': doc.doctype, 'free_item_data': []}) + elif d.price_or_product_discount == "Product": + item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []}) get_product_discount_rule(d, item_details, doc=doc) apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() doc.calculate_taxes_and_totals() + def remove_free_item(doc): for d in doc.items: if d.is_free_item: doc.remove(d) + def get_applied_pricing_rules(pricing_rules): if pricing_rules: - if pricing_rules.startswith('['): + if pricing_rules.startswith("["): return json.loads(pricing_rules) else: - return pricing_rules.split(',') + return pricing_rules.split(",") return [] + def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): free_item = pricing_rule.free_item - if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction': + if pricing_rule.same_item and pricing_rule.get("apply_on") != "Transaction": free_item = item_details.item_code or args.item_code if not free_item: - frappe.throw(_("Free item not set in the pricing rule {0}") - .format(get_link_to_form("Pricing Rule", pricing_rule.name))) + frappe.throw( + _("Free item not set in the pricing rule {0}").format( + get_link_to_form("Pricing Rule", pricing_rule.name) + ) + ) qty = pricing_rule.free_qty or 1 if pricing_rule.is_recursive: - transaction_qty = args.get('qty') if args else doc.total_qty + transaction_qty = args.get("qty") if args else doc.total_qty if transaction_qty: qty = flt(transaction_qty) * qty free_item_data_args = { - 'item_code': free_item, - 'qty': qty, - 'pricing_rules': pricing_rule.name, - 'rate': pricing_rule.free_item_rate or 0, - 'price_list_rate': pricing_rule.free_item_rate or 0, - 'is_free_item': 1 + "item_code": free_item, + "qty": qty, + "pricing_rules": pricing_rule.name, + "rate": pricing_rule.free_item_rate or 0, + "price_list_rate": pricing_rule.free_item_rate or 0, + "is_free_item": 1, } - item_data = frappe.get_cached_value('Item', free_item, ['item_name', - 'description', 'stock_uom'], as_dict=1) + item_data = frappe.get_cached_value( + "Item", free_item, ["item_name", "description", "stock_uom"], as_dict=1 + ) free_item_data_args.update(item_data) - free_item_data_args['uom'] = pricing_rule.free_item_uom or item_data.stock_uom - free_item_data_args['conversion_factor'] = get_conversion_factor(free_item, - free_item_data_args['uom']).get("conversion_factor", 1) + free_item_data_args["uom"] = pricing_rule.free_item_uom or item_data.stock_uom + free_item_data_args["conversion_factor"] = get_conversion_factor( + free_item, free_item_data_args["uom"] + ).get("conversion_factor", 1) - if item_details.get("parenttype") == 'Purchase Order': - free_item_data_args['schedule_date'] = doc.schedule_date if doc else today() + if item_details.get("parenttype") == "Purchase Order": + free_item_data_args["schedule_date"] = doc.schedule_date if doc else today() - if item_details.get("parenttype") == 'Sales Order': - free_item_data_args['delivery_date'] = doc.delivery_date if doc else today() + if item_details.get("parenttype") == "Sales Order": + free_item_data_args["delivery_date"] = doc.delivery_date if doc else today() item_details.free_item_data.append(free_item_data_args) + def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): if pricing_rule_args: items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item) for args in pricing_rule_args: - if not items or (args.get('item_code'), args.get('pricing_rules')) not in items: - doc.append('items', args) + if not items or (args.get("item_code"), args.get("pricing_rules")) not in items: + doc.append("items", args) + def get_pricing_rule_items(pr_doc): apply_on_data = [] - apply_on = frappe.scrub(pr_doc.get('apply_on')) + apply_on = frappe.scrub(pr_doc.get("apply_on")) - pricing_rule_apply_on = apply_on_table.get(pr_doc.get('apply_on')) + pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on")) for d in pr_doc.get(pricing_rule_apply_on): - if apply_on == 'item_group': + if apply_on == "item_group": apply_on_data.extend(get_child_item_groups(d.get(apply_on))) else: apply_on_data.append(d.get(apply_on)) @@ -587,6 +676,7 @@ def get_pricing_rule_items(pr_doc): return list(set(apply_on_data)) + def validate_coupon_code(coupon_name): coupon = frappe.get_doc("Coupon Code", coupon_name) @@ -599,16 +689,21 @@ def validate_coupon_code(coupon_name): elif coupon.used >= coupon.maximum_use: frappe.throw(_("Sorry, this coupon code is no longer valid")) -def update_coupon_code_count(coupon_name,transaction_type): - coupon=frappe.get_doc("Coupon Code",coupon_name) + +def update_coupon_code_count(coupon_name, transaction_type): + coupon = frappe.get_doc("Coupon Code", coupon_name) if coupon: - if transaction_type=='used': - if coupon.used0: - coupon.used=coupon.used-1 + frappe.throw( + _("{0} Coupon used are {1}. Allowed quantity is exhausted").format( + coupon.coupon_code, coupon.used + ) + ) + elif transaction_type == "cancelled": + if coupon.used > 0: + coupon.used = coupon.used - 1 coupon.save(ignore_permissions=True) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py index d544f976d2a..8ec726b36cd 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py @@ -11,7 +11,7 @@ from erpnext.accounts.deferred_revenue import ( convert_deferred_expense_to_expense, convert_deferred_revenue_to_income, ) -from erpnext.accounts.general_ledger import make_reverse_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries class ProcessDeferredAccounting(Document): @@ -21,17 +21,17 @@ class ProcessDeferredAccounting(Document): def on_submit(self): conditions = build_conditions(self.type, self.account, self.company) - if self.type == 'Income': + if self.type == "Income": convert_deferred_revenue_to_income(self.name, self.start_date, self.end_date, conditions) else: convert_deferred_expense_to_expense(self.name, self.start_date, self.end_date, conditions) def on_cancel(self): - self.ignore_linked_doctypes = ['GL Entry'] - gl_entries = frappe.get_all('GL Entry', fields = ['*'], - filters={ - 'against_voucher_type': self.doctype, - 'against_voucher': self.name - }) + self.ignore_linked_doctypes = ["GL Entry"] + gl_entries = frappe.get_all( + "GL Entry", + fields=["*"], + filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, + ) - make_reverse_gl_entries(gl_entries=gl_entries) + make_gl_entries(gl_entries=gl_entries, cancel=1) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index 757d0fa3d01..164ba6aa348 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -15,9 +15,12 @@ from erpnext.stock.doctype.item.test_item import create_item class TestProcessDeferredAccounting(unittest.TestCase): def test_creation_of_ledger_entry_on_submit(self): - ''' test creation of gl entries on submission of document ''' - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") + """test creation of gl entries on submission of document""" + deferred_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_revenue = 1 @@ -25,7 +28,9 @@ class TestProcessDeferredAccounting(unittest.TestCase): item.no_of_months = 12 item.save() - si = create_sales_invoice(item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True) + si = create_sales_invoice( + item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True + ) si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-10" si.items[0].service_end_date = "2019-03-15" @@ -33,20 +38,22 @@ class TestProcessDeferredAccounting(unittest.TestCase): si.save() si.submit() - process_deferred_accounting = doc = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date="2019-01-01", - start_date="2019-01-01", - end_date="2019-01-31", - type="Income" - )) + process_deferred_accounting = doc = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date="2019-01-01", + start_date="2019-01-01", + end_date="2019-01-31", + type="Income", + ) + ) process_deferred_accounting.insert() process_deferred_accounting.submit() expected_gle = [ [deferred_account, 33.85, 0.0, "2019-01-31"], - ["Sales - _TC", 0.0, 33.85, "2019-01-31"] + ["Sales - _TC", 0.0, 33.85, "2019-01-31"], ] check_gl_entries(self, si.name, expected_gle, "2019-01-10") diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index f8d191cc3f8..82705a9cea4 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -64,10 +64,10 @@ {{ frappe.format(row.account, {fieldtype: "Link"}) or " " }} - {{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} + {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} - {{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} + {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} {% endif %} diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index 088c190f451..29f2e98e779 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -51,6 +51,13 @@ frappe.ui.form.on('Process Statement Of Accounts', { } } }); + frm.set_query("account", function() { + return { + filters: { + 'company': frm.doc.company + } + }; + }); if(frm.doc.__islocal){ frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1)); frm.set_value('to_date', frappe.datetime.get_today()); diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 1b34d6d1f2f..01f716daa21 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -23,130 +23,166 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get class ProcessStatementOfAccounts(Document): def validate(self): if not self.subject: - self.subject = 'Statement Of Accounts for {{ customer.name }}' + self.subject = "Statement Of Accounts for {{ customer.name }}" if not self.body: - self.body = 'Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.' + self.body = "Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." validate_template(self.subject) validate_template(self.body) if not self.customers: - frappe.throw(_('Customers not selected.')) + frappe.throw(_("Customers not selected.")) if self.enable_auto_email: - self.to_date = self.start_date - self.from_date = add_months(self.to_date, -1 * self.filter_duration) + if self.start_date and getdate(self.start_date) >= getdate(today()): + self.to_date = self.start_date + self.from_date = add_months(self.to_date, -1 * self.filter_duration) def get_report_pdf(doc, consolidated=True): statement_dict = {} - ageing = '' + ageing = "" base_template_path = "frappe/www/printview.html" - template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + template_path = ( + "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + ) for entry in doc.customers: if doc.include_ageing: - ageing_filters = frappe._dict({ - 'company': doc.company, - 'report_date': doc.to_date, - 'ageing_based_on': doc.ageing_based_on, - 'range1': 30, - 'range2': 60, - 'range3': 90, - 'range4': 120, - 'customer': entry.customer - }) + ageing_filters = frappe._dict( + { + "company": doc.company, + "report_date": doc.to_date, + "ageing_based_on": doc.ageing_based_on, + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "customer": entry.customer, + } + ) col1, ageing = get_ageing(ageing_filters) if ageing: - ageing[0]['ageing_based_on'] = doc.ageing_based_on + ageing[0]["ageing_based_on"] = doc.ageing_based_on - tax_id = frappe.get_doc('Customer', entry.customer).tax_id - presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \ - or doc.currency or get_company_currency(doc.company) + tax_id = frappe.get_doc("Customer", entry.customer).tax_id + presentation_currency = ( + get_party_account_currency("Customer", entry.customer, doc.company) + or doc.currency + or get_company_currency(doc.company) + ) if doc.letter_head: from frappe.www.printview import get_letter_head + letter_head = get_letter_head(doc, 0) - filters= frappe._dict({ - 'from_date': doc.from_date, - 'to_date': doc.to_date, - 'company': doc.company, - 'finance_book': doc.finance_book if doc.finance_book else None, - 'account': [doc.account] if doc.account else None, - 'party_type': 'Customer', - 'party': [entry.customer], - 'presentation_currency': presentation_currency, - 'group_by': doc.group_by, - 'currency': doc.currency, - 'cost_center': [cc.cost_center_name for cc in doc.cost_center], - 'project': [p.project_name for p in doc.project], - 'show_opening_entries': 0, - 'include_default_book_entries': 0, - 'tax_id': tax_id if tax_id else None - }) + filters = frappe._dict( + { + "from_date": doc.from_date, + "to_date": doc.to_date, + "company": doc.company, + "finance_book": doc.finance_book if doc.finance_book else None, + "account": [doc.account] if doc.account else None, + "party_type": "Customer", + "party": [entry.customer], + "presentation_currency": presentation_currency, + "group_by": doc.group_by, + "currency": doc.currency, + "cost_center": [cc.cost_center_name for cc in doc.cost_center], + "project": [p.project_name for p in doc.project], + "show_opening_entries": 0, + "include_default_book_entries": 0, + "tax_id": tax_id if tax_id else None, + } + ) col, res = get_soa(filters) for x in [0, -2, -1]: - res[x]['account'] = res[x]['account'].replace("'","") + res[x]["account"] = res[x]["account"].replace("'", "") if len(res) == 3: continue - html = frappe.render_template(template_path, \ - {"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None, + html = frappe.render_template( + template_path, + { + "filters": filters, + "data": res, + "ageing": ageing[0] if (doc.include_ageing and ageing) else None, "letter_head": letter_head if doc.letter_head else None, - "terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms') - if doc.terms_and_conditions else None}) + "terms_and_conditions": frappe.db.get_value( + "Terms and Conditions", doc.terms_and_conditions, "terms" + ) + if doc.terms_and_conditions + else None, + }, + ) - html = frappe.render_template(base_template_path, {"body": html, \ - "css": get_print_style(), "title": "Statement For " + entry.customer}) + html = frappe.render_template( + base_template_path, + {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer}, + ) statement_dict[entry.customer] = html if not bool(statement_dict): return False elif consolidated: - result = ''.join(list(statement_dict.values())) - return get_pdf(result, {'orientation': doc.orientation}) + result = "".join(list(statement_dict.values())) + return get_pdf(result, {"orientation": doc.orientation}) else: for customer, statement_html in statement_dict.items(): - statement_dict[customer]=get_pdf(statement_html, {'orientation': doc.orientation}) + statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) return statement_dict + def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name): fields_dict = { - 'Customer Group': 'customer_group', - 'Territory': 'territory', + "Customer Group": "customer_group", + "Territory": "territory", } collection = frappe.get_doc(customer_collection, collection_name) - selected = [customer.name for customer in frappe.get_list(customer_collection, filters=[ - ['lft', '>=', collection.lft], - ['rgt', '<=', collection.rgt] - ], - fields=['name'], - order_by='lft asc, rgt desc' - )] - return frappe.get_list('Customer', fields=['name', 'email_id'], \ - filters=[[fields_dict[customer_collection], 'IN', selected]]) + selected = [ + customer.name + for customer in frappe.get_list( + customer_collection, + filters=[["lft", ">=", collection.lft], ["rgt", "<=", collection.rgt]], + fields=["name"], + order_by="lft asc, rgt desc", + ) + ] + return frappe.get_list( + "Customer", + fields=["name", "email_id"], + filters=[[fields_dict[customer_collection], "IN", selected]], + ) + def get_customers_based_on_sales_person(sales_person): - lft, rgt = frappe.db.get_value("Sales Person", - sales_person, ["lft", "rgt"]) - records = frappe.db.sql(""" + lft, rgt = frappe.db.get_value("Sales Person", sales_person, ["lft", "rgt"]) + records = frappe.db.sql( + """ select distinct parent, parenttype from `tabSales Team` steam where parenttype = 'Customer' and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) - """, (lft, rgt), as_dict=1) + """, + (lft, rgt), + as_dict=1, + ) sales_person_records = frappe._dict() for d in records: sales_person_records.setdefault(d.parenttype, set()).add(d.parent) - if sales_person_records.get('Customer'): - return frappe.get_list('Customer', fields=['name', 'email_id'], \ - filters=[['name', 'in', list(sales_person_records['Customer'])]]) + if sales_person_records.get("Customer"): + return frappe.get_list( + "Customer", + fields=["name", "email_id"], + filters=[["name", "in", list(sales_person_records["Customer"])]], + ) else: return [] + def get_recipients_and_cc(customer, doc): recipients = [] for clist in doc.customers: @@ -155,65 +191,72 @@ def get_recipients_and_cc(customer, doc): if doc.primary_mandatory and clist.primary_email: recipients.append(clist.primary_email) cc = [] - if doc.cc_to != '': + if doc.cc_to != "": try: - cc=[frappe.get_value('User', doc.cc_to, 'email')] + cc = [frappe.get_value("User", doc.cc_to, "email")] except Exception: pass return recipients, cc + def get_context(customer, doc): template_doc = copy.deepcopy(doc) del template_doc.customers template_doc.from_date = format_date(template_doc.from_date) template_doc.to_date = format_date(template_doc.to_date) return { - 'doc': template_doc, - 'customer': frappe.get_doc('Customer', customer), - 'frappe': frappe.utils + "doc": template_doc, + "customer": frappe.get_doc("Customer", customer), + "frappe": frappe.utils, } + @frappe.whitelist() def fetch_customers(customer_collection, collection_name, primary_mandatory): customer_list = [] customers = [] - if customer_collection == 'Sales Person': + if customer_collection == "Sales Person": customers = get_customers_based_on_sales_person(collection_name) if not bool(customers): - frappe.throw(_('No Customers found with selected options.')) + frappe.throw(_("No Customers found with selected options.")) else: - if customer_collection == 'Sales Partner': - customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ - filters=[['default_sales_partner', '=', collection_name]]) + if customer_collection == "Sales Partner": + customers = frappe.get_list( + "Customer", + fields=["name", "email_id"], + filters=[["default_sales_partner", "=", collection_name]], + ) else: - customers = get_customers_based_on_territory_or_customer_group(customer_collection, collection_name) + customers = get_customers_based_on_territory_or_customer_group( + customer_collection, collection_name + ) for customer in customers: - primary_email = customer.get('email_id') or '' + primary_email = customer.get("email_id") or "" billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False) if int(primary_mandatory): - if (primary_email == ''): + if primary_email == "": continue - elif (billing_email == '') and (primary_email == ''): + elif (billing_email == "") and (primary_email == ""): continue - customer_list.append({ - 'name': customer.name, - 'primary_email': primary_email, - 'billing_email': billing_email - }) + customer_list.append( + {"name": customer.name, "primary_email": primary_email, "billing_email": billing_email} + ) return customer_list + @frappe.whitelist() def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): - """ Returns first email from Contact Email table as a Billing email - when Is Billing Contact checked - and Primary email- email with Is Primary checked """ + """Returns first email from Contact Email table as a Billing email + when Is Billing Contact checked + and Primary email- email with Is Primary checked""" - billing_email = frappe.db.sql(""" + billing_email = frappe.db.sql( + """ SELECT email.email_id FROM @@ -231,42 +274,43 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr and link.link_name=%s and contact.is_billing_contact=1 ORDER BY - contact.creation desc""", customer_name) + contact.creation desc""", + customer_name, + ) if len(billing_email) == 0 or (billing_email[0][0] is None): if billing_and_primary: frappe.throw(_("No billing email found for customer: {0}").format(customer_name)) else: - return '' + return "" if billing_and_primary: - primary_email = frappe.get_value('Customer', customer_name, 'email_id') + primary_email = frappe.get_value("Customer", customer_name, "email_id") if primary_email is None and int(primary_mandatory): frappe.throw(_("No primary email found for customer: {0}").format(customer_name)) - return [primary_email or '', billing_email[0][0]] + return [primary_email or "", billing_email[0][0]] else: - return billing_email[0][0] or '' + return billing_email[0][0] or "" + @frappe.whitelist() def download_statements(document_name): - doc = frappe.get_doc('Process Statement Of Accounts', document_name) + doc = frappe.get_doc("Process Statement Of Accounts", document_name) report = get_report_pdf(doc) if report: - frappe.local.response.filename = doc.name + '.pdf' + frappe.local.response.filename = doc.name + ".pdf" frappe.local.response.filecontent = report frappe.local.response.type = "download" + @frappe.whitelist() def send_emails(document_name, from_scheduler=False): - doc = frappe.get_doc('Process Statement Of Accounts', document_name) + doc = frappe.get_doc("Process Statement Of Accounts", document_name) report = get_report_pdf(doc, consolidated=False) if report: for customer, report_pdf in report.items(): - attachments = [{ - 'fname': customer + '.pdf', - 'fcontent': report_pdf - }] + attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}] recipients, cc = get_recipients_and_cc(customer, doc) context = get_context(customer, doc) @@ -274,7 +318,7 @@ def send_emails(document_name, from_scheduler=False): message = frappe.render_template(doc.body, context) frappe.enqueue( - queue='short', + queue="short", method=frappe.sendmail, recipients=recipients, sender=frappe.session.user, @@ -282,28 +326,34 @@ def send_emails(document_name, from_scheduler=False): subject=subject, message=message, now=True, - reference_doctype='Process Statement Of Accounts', + reference_doctype="Process Statement Of Accounts", reference_name=document_name, - attachments=attachments + attachments=attachments, ) if doc.enable_auto_email and from_scheduler: new_to_date = getdate(today()) - if doc.frequency == 'Weekly': + if doc.frequency == "Weekly": new_to_date = add_days(new_to_date, 7) else: - new_to_date = add_months(new_to_date, 1 if doc.frequency == 'Monthly' else 3) + new_to_date = add_months(new_to_date, 1 if doc.frequency == "Monthly" else 3) new_from_date = add_months(new_to_date, -1 * doc.filter_duration) - doc.add_comment('Comment', 'Emails sent on: ' + frappe.utils.format_datetime(frappe.utils.now())) - doc.db_set('to_date', new_to_date, commit=True) - doc.db_set('from_date', new_from_date, commit=True) + doc.add_comment( + "Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()) + ) + doc.db_set("to_date", new_to_date, commit=True) + doc.db_set("from_date", new_from_date, commit=True) return True else: return False + @frappe.whitelist() def send_auto_email(): - selected = frappe.get_list('Process Statement Of Accounts', filters={'to_date': format_date(today()), 'enable_auto_email': 1}) + selected = frappe.get_list( + "Process Statement Of Accounts", + filters={"to_date": format_date(today()), "enable_auto_email": 1}, + ) for entry in selected: send_emails(entry.name, from_scheduler=True) return True diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 5fbe93ee68d..fac9be7bdb1 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -6,29 +6,73 @@ import frappe from frappe import _ from frappe.model.document import Document -pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group', - 'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from', - 'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier', - 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules'] +pricing_rule_fields = [ + "apply_on", + "mixed_conditions", + "is_cumulative", + "other_item_code", + "other_item_group", + "apply_rule_on_other", + "other_brand", + "selling", + "buying", + "applicable_for", + "valid_from", + "valid_upto", + "customer", + "customer_group", + "territory", + "sales_partner", + "campaign", + "supplier", + "supplier_group", + "company", + "currency", + "apply_multiple_pricing_rules", +] -other_fields = ['min_qty', 'max_qty', 'min_amt', - 'max_amt', 'priority','warehouse', 'threshold_percentage', 'rule_description'] +other_fields = [ + "min_qty", + "max_qty", + "min_amt", + "max_amt", + "priority", + "warehouse", + "threshold_percentage", + "rule_description", +] -price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discount_on_rate', - 'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule', 'apply_multiple_pricing_rules'] +price_discount_fields = [ + "rate_or_discount", + "apply_discount_on", + "apply_discount_on_rate", + "rate", + "discount_amount", + "discount_percentage", + "validate_applied_rule", + "apply_multiple_pricing_rules", +] + +product_discount_fields = [ + "free_item", + "free_qty", + "free_item_uom", + "free_item_rate", + "same_item", + "is_recursive", + "apply_multiple_pricing_rules", +] -product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', - 'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules'] class TransactionExists(frappe.ValidationError): pass + class PromotionalScheme(Document): def validate(self): if not self.selling and not self.buying: frappe.throw(_("Either 'Selling' or 'Buying' must be selected"), title=_("Mandatory")) - if not (self.price_discount_slabs - or self.product_discount_slabs): + if not (self.price_discount_slabs or self.product_discount_slabs): frappe.throw(_("Price or product discount slabs are required")) self.validate_applicable_for() @@ -39,7 +83,7 @@ class PromotionalScheme(Document): applicable_for = frappe.scrub(self.applicable_for) if not self.get(applicable_for): - msg = (f'The field {frappe.bold(self.applicable_for)} is required') + msg = f"The field {frappe.bold(self.applicable_for)} is required" frappe.throw(_(msg)) def validate_pricing_rules(self): @@ -53,28 +97,28 @@ class PromotionalScheme(Document): if self._doc_before_save.applicable_for == self.applicable_for: return - docnames = frappe.get_all('Pricing Rule', - filters= {'promotional_scheme': self.name}) + docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name}) for docname in docnames: - if frappe.db.exists('Pricing Rule Detail', - {'pricing_rule': docname.name, 'docstatus': ('<', 2)}): + if frappe.db.exists( + "Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)} + ): raise_for_transaction_exists(self.name) if docnames and not transaction_exists: for docname in docnames: - frappe.delete_doc('Pricing Rule', docname.name) + frappe.delete_doc("Pricing Rule", docname.name) def on_update(self): - pricing_rules = frappe.get_all( - 'Pricing Rule', - fields = ["promotional_scheme_id", "name", "creation"], - filters = { - 'promotional_scheme': self.name, - 'applicable_for': self.applicable_for - }, - order_by = 'creation asc', - ) or {} + pricing_rules = ( + frappe.get_all( + "Pricing Rule", + fields=["promotional_scheme_id", "name", "creation"], + filters={"promotional_scheme": self.name, "applicable_for": self.applicable_for}, + order_by="creation asc", + ) + or {} + ) self.update_pricing_rules(pricing_rules) def update_pricing_rules(self, pricing_rules): @@ -83,7 +127,7 @@ class PromotionalScheme(Document): names = [] for rule in pricing_rules: names.append(rule.name) - rules[rule.get('promotional_scheme_id')] = names + rules[rule.get("promotional_scheme_id")] = names docs = get_pricing_rules(self, rules) @@ -100,34 +144,38 @@ class PromotionalScheme(Document): frappe.msgprint(_("New {0} pricing rules are created").format(count)) def on_trash(self): - for rule in frappe.get_all('Pricing Rule', - {'promotional_scheme': self.name}): - frappe.delete_doc('Pricing Rule', rule.name) + for rule in frappe.get_all("Pricing Rule", {"promotional_scheme": self.name}): + frappe.delete_doc("Pricing Rule", rule.name) + def raise_for_transaction_exists(name): - msg = (f"""You can't change the {frappe.bold(_('Applicable For'))} - because transactions are present against the Promotional Scheme {frappe.bold(name)}. """) - msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.' + msg = f"""You can't change the {frappe.bold(_('Applicable For'))} + because transactions are present against the Promotional Scheme {frappe.bold(name)}. """ + msg += "Kindly disable this Promotional Scheme and create new for new Applicable For." frappe.throw(_(msg), TransactionExists) + def get_pricing_rules(doc, rules=None): if rules is None: rules = {} new_doc = [] - for child_doc, fields in {'price_discount_slabs': price_discount_fields, - 'product_discount_slabs': product_discount_fields}.items(): + for child_doc, fields in { + "price_discount_slabs": price_discount_fields, + "product_discount_slabs": product_discount_fields, + }.items(): if doc.get(child_doc): new_doc.extend(_get_pricing_rules(doc, child_doc, fields, rules)) return new_doc + def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): if rules is None: rules = {} new_doc = [] args = get_args_for_pricing_rule(doc) - applicable_for = frappe.scrub(doc.get('applicable_for')) + applicable_for = frappe.scrub(doc.get("applicable_for")) for idx, d in enumerate(doc.get(child_doc)): if d.name in rules: @@ -138,15 +186,23 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): else: for applicable_for_value in args.get(applicable_for): docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value) - pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, - d, docname, applicable_for, applicable_for_value) + pr = prepare_pricing_rule( + args, doc, child_doc, discount_fields, d, docname, applicable_for, applicable_for_value + ) new_doc.append(pr) elif args.get(applicable_for): applicable_for_values = args.get(applicable_for) or [] for applicable_for_value in applicable_for_values: - pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, - d, applicable_for=applicable_for, value= applicable_for_value) + pr = prepare_pricing_rule( + args, + doc, + child_doc, + discount_fields, + d, + applicable_for=applicable_for, + value=applicable_for_value, + ) new_doc.append(pr) else: @@ -155,20 +211,24 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): return new_doc -def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str: - fields = ['promotional_scheme_id', 'name'] - filters = { - 'promotional_scheme_id': row.name - } + +def get_pricing_rule_docname( + row: dict, applicable_for: str = None, applicable_for_value: str = None +) -> str: + fields = ["promotional_scheme_id", "name"] + filters = {"promotional_scheme_id": row.name} if applicable_for: fields.append(applicable_for) filters[applicable_for] = applicable_for_value - docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters) - return docname[0].name if docname else '' + docname = frappe.get_all("Pricing Rule", fields=fields, filters=filters) + return docname[0].name if docname else "" -def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None): + +def prepare_pricing_rule( + args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None +): if docname: pr = frappe.get_doc("Pricing Rule", docname) else: @@ -182,33 +242,31 @@ def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, return set_args(temp_args, pr, doc, child_doc, discount_fields, d) + def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): pr.update(args) - for field in (other_fields + discount_fields): + for field in other_fields + discount_fields: pr.set(field, child_doc_fields.get(field)) pr.promotional_scheme_id = child_doc_fields.name pr.promotional_scheme = doc.name pr.disable = child_doc_fields.disable if child_doc_fields.disable else doc.disable - pr.price_or_product_discount = ('Price' - if child_doc == 'price_discount_slabs' else 'Product') + pr.price_or_product_discount = "Price" if child_doc == "price_discount_slabs" else "Product" - for field in ['items', 'item_groups', 'brands']: + for field in ["items", "item_groups", "brands"]: if doc.get(field): pr.set(field, []) - apply_on = frappe.scrub(doc.get('apply_on')) + apply_on = frappe.scrub(doc.get("apply_on")) for d in doc.get(field): - pr.append(field, { - apply_on: d.get(apply_on), - 'uom': d.uom - }) + pr.append(field, {apply_on: d.get(apply_on), "uom": d.uom}) return pr + def get_args_for_pricing_rule(doc): - args = { 'promotional_scheme': doc.name } - applicable_for = frappe.scrub(doc.get('applicable_for')) + args = {"promotional_scheme": doc.name} + applicable_for = frappe.scrub(doc.get("applicable_for")) for d in pricing_rule_fields: if d == applicable_for: diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py index 6d079242682..70e87404a35 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py @@ -3,11 +3,6 @@ from frappe import _ def get_data(): return { - 'fieldname': 'promotional_scheme', - 'transactions': [ - { - 'label': _('Reference'), - 'items': ['Pricing Rule'] - } - ] + "fieldname": "promotional_scheme", + "transactions": [{"label": _("Reference"), "items": ["Pricing Rule"]}], } diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 49192a45f87..b3b9d7b2086 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -11,96 +11,111 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde class TestPromotionalScheme(unittest.TestCase): def setUp(self): - if frappe.db.exists('Promotional Scheme', '_Test Scheme'): - frappe.delete_doc('Promotional Scheme', '_Test Scheme') + if frappe.db.exists("Promotional Scheme", "_Test Scheme"): + frappe.delete_doc("Promotional Scheme", "_Test Scheme") def test_promotional_scheme(self): - ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer') - price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], - filters = {'promotional_scheme': ps.name}) - self.assertTrue(len(price_rules),1) - price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) - self.assertTrue(price_doc_details.customer, '_Test Customer') + ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer") + price_rules = frappe.get_all( + "Pricing Rule", + fields=["promotional_scheme_id", "name", "creation"], + filters={"promotional_scheme": ps.name}, + ) + self.assertTrue(len(price_rules), 1) + price_doc_details = frappe.db.get_value( + "Pricing Rule", price_rules[0].name, ["customer", "min_qty", "discount_percentage"], as_dict=1 + ) + self.assertTrue(price_doc_details.customer, "_Test Customer") self.assertTrue(price_doc_details.min_qty, 4) self.assertTrue(price_doc_details.discount_percentage, 20) ps.price_discount_slabs[0].min_qty = 6 - ps.append('customer', { - 'customer': "_Test Customer 2"}) + ps.append("customer", {"customer": "_Test Customer 2"}) ps.save() - price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], - filters = {'promotional_scheme': ps.name}) + price_rules = frappe.get_all( + "Pricing Rule", + fields=["promotional_scheme_id", "name"], + filters={"promotional_scheme": ps.name}, + ) self.assertTrue(len(price_rules), 2) - price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[1].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) - self.assertTrue(price_doc_details.customer, '_Test Customer 2') + price_doc_details = frappe.db.get_value( + "Pricing Rule", price_rules[1].name, ["customer", "min_qty", "discount_percentage"], as_dict=1 + ) + self.assertTrue(price_doc_details.customer, "_Test Customer 2") self.assertTrue(price_doc_details.min_qty, 6) self.assertTrue(price_doc_details.discount_percentage, 20) - price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) - self.assertTrue(price_doc_details.customer, '_Test Customer') + price_doc_details = frappe.db.get_value( + "Pricing Rule", price_rules[0].name, ["customer", "min_qty", "discount_percentage"], as_dict=1 + ) + self.assertTrue(price_doc_details.customer, "_Test Customer") self.assertTrue(price_doc_details.min_qty, 6) - frappe.delete_doc('Promotional Scheme', ps.name) - price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], - filters = {'promotional_scheme': ps.name}) + frappe.delete_doc("Promotional Scheme", ps.name) + price_rules = frappe.get_all( + "Pricing Rule", + fields=["promotional_scheme_id", "name"], + filters={"promotional_scheme": ps.name}, + ) self.assertEqual(price_rules, []) def test_promotional_scheme_without_applicable_for(self): ps = make_promotional_scheme() - price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertTrue(len(price_rules), 1) - frappe.delete_doc('Promotional Scheme', ps.name) + frappe.delete_doc("Promotional Scheme", ps.name) - price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) def test_change_applicable_for_in_promotional_scheme(self): ps = make_promotional_scheme() - price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertTrue(len(price_rules), 1) - so = make_sales_order(qty=5, currency='USD', do_not_save=True) + so = make_sales_order(qty=5, currency="USD", do_not_save=True) so.set_missing_values() so.save() self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule) - ps.applicable_for = 'Customer' - ps.append('customer', { - 'customer': '_Test Customer' - }) + ps.applicable_for = "Customer" + ps.append("customer", {"customer": "_Test Customer"}) self.assertRaises(TransactionExists, ps.save) - frappe.delete_doc('Sales Order', so.name) - frappe.delete_doc('Promotional Scheme', ps.name) - price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + frappe.delete_doc("Sales Order", so.name) + frappe.delete_doc("Promotional Scheme", ps.name) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) + def make_promotional_scheme(**args): args = frappe._dict(args) - ps = frappe.new_doc('Promotional Scheme') - ps.name = '_Test Scheme' - ps.append('items',{ - 'item_code': '_Test Item' - }) + ps = frappe.new_doc("Promotional Scheme") + ps.name = "_Test Scheme" + ps.append("items", {"item_code": "_Test Item"}) ps.selling = 1 - ps.append('price_discount_slabs',{ - 'min_qty': 4, - 'validate_applied_rule': 0, - 'discount_percentage': 20, - 'rule_description': 'Test' - }) + ps.append( + "price_discount_slabs", + { + "min_qty": 4, + "validate_applied_rule": 0, + "discount_percentage": 20, + "rule_description": "Test", + }, + ) - ps.company = '_Test Company' + ps.company = "_Test Company" if args.applicable_for: ps.applicable_for = args.applicable_for - ps.append(frappe.scrub(args.applicable_for), { - frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for)) - }) + ps.append( + frappe.scrub(args.applicable_for), + {frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for))}, + ) ps.save() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 19c0d8aaf3d..4f5640f9cb9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -30,6 +30,9 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ onload: function() { this._super(); + // Ignore linked advances + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry']; + if(!this.frm.doc.__islocal) { // show credit_to in print format if(!this.frm.doc.supplier && this.frm.doc.credit_to) { @@ -276,6 +279,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference) return; + if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return; + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f3452e1cf81..e4719d6b40c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -33,6 +33,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status +from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.buying_controller import BuyingController from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( @@ -40,25 +41,26 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( update_billed_amount_based_on_po, ) -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class PurchaseInvoice(BuyingController): def __init__(self, *args, **kwargs): super(PurchaseInvoice, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Purchase Invoice Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'po_detail', - 'target_field': 'billed_amt', - 'target_parent_dt': 'Purchase Order', - 'target_parent_field': 'per_billed', - 'target_ref_field': 'amount', - 'source_field': 'amount', - 'percent_join_field': 'purchase_order', - 'overflow_type': 'billing' - }] + self.status_updater = [ + { + "source_dt": "Purchase Invoice Item", + "target_dt": "Purchase Order Item", + "join_field": "po_detail", + "target_field": "billed_amt", + "target_parent_dt": "Purchase Order", + "target_parent_field": "per_billed", + "target_ref_field": "amount", + "source_field": "amount", + "percent_join_field": "purchase_order", + "overflow_type": "billing", + } + ] def onload(self): super(PurchaseInvoice, self).onload() @@ -67,15 +69,14 @@ class PurchaseInvoice(BuyingController): def before_save(self): if not self.on_hold: - self.release_date = '' - + self.release_date = "" def invoice_is_blocked(self): return self.on_hold and (not self.release_date or self.release_date > getdate(nowdate())) def validate(self): if not self.is_opening: - self.is_opening = 'No' + self.is_opening = "No" self.validate_posting_time() @@ -87,14 +88,14 @@ class PurchaseInvoice(BuyingController): self.validate_supplier_invoice() # validate cash purchase - if (self.is_paid == 1): + if self.is_paid == 1: self.validate_cash() # validate service stop date to lie in between start and end date validate_service_stop_date(self) - if self._action=="submit" and self.update_stock: - self.make_batches('warehouse') + if self._action == "submit" and self.update_stock: + self.make_batches("warehouse") self.validate_release_date() self.check_conversion_rate() @@ -105,45 +106,53 @@ class PurchaseInvoice(BuyingController): self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("stock_uom", "stock_qty") self.set_expense_account(for_validate=True) + self.validate_expense_account() self.set_against_expense_account() self.validate_write_off_account() self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") self.create_remarks() self.set_status() self.validate_purchase_receipt_if_update_stock() - validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference) + validate_inter_company_party( + self.doctype, self.supplier, self.company, self.inter_company_invoice_reference + ) self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): - frappe.throw(_('Release date must be in the future')) + frappe.throw(_("Release date must be in the future")) def validate_cash(self): if not self.cash_bank_account and flt(self.paid_amount): frappe.throw(_("Cash or Bank Account is mandatory for making payment entry")) - if (flt(self.paid_amount) + flt(self.write_off_amount) - - flt(self.get("rounded_total") or self.grand_total) - > 1/(10**(self.precision("base_grand_total") + 1))): + if flt(self.paid_amount) + flt(self.write_off_amount) - flt( + self.get("rounded_total") or self.grand_total + ) > 1 / (10 ** (self.precision("base_grand_total") + 1)): frappe.throw(_("""Paid amount + Write Off Amount can not be greater than Grand Total""")) def create_remarks(self): if not self.remarks: if self.bill_no and self.bill_date: - self.remarks = _("Against Supplier Invoice {0} dated {1}").format(self.bill_no, - formatdate(self.bill_date)) + self.remarks = _("Against Supplier Invoice {0} dated {1}").format( + self.bill_no, formatdate(self.bill_date) + ) else: self.remarks = _("No Remarks") def set_missing_values(self, for_validate=False): if not self.credit_to: self.credit_to = get_party_account("Supplier", self.supplier, self.company) - self.party_account_currency = frappe.db.get_value("Account", self.credit_to, "account_currency", cache=True) + self.party_account_currency = frappe.db.get_value( + "Account", self.credit_to, "account_currency", cache=True + ) if not self.due_date: - self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date) + self.due_date = get_due_date( + self.posting_date, "Supplier", self.supplier, self.company, self.bill_date + ) tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") if tds_category and not for_validate: @@ -155,8 +164,12 @@ class PurchaseInvoice(BuyingController): def check_conversion_rate(self): default_currency = erpnext.get_company_currency(self.company) if not default_currency: - throw(_('Please enter default currency in Company Master')) - if (self.currency == default_currency and flt(self.conversion_rate) != 1.00) or not self.conversion_rate or (self.currency != default_currency and flt(self.conversion_rate) == 1.00): + throw(_("Please enter default currency in Company Master")) + if ( + (self.currency == default_currency and flt(self.conversion_rate) != 1.00) + or not self.conversion_rate + or (self.currency != default_currency and flt(self.conversion_rate) == 1.00) + ): throw(_("Conversion rate cannot be 0 or 1")) def validate_credit_to_acc(self): @@ -165,19 +178,24 @@ class PurchaseInvoice(BuyingController): if not self.credit_to: self.raise_missing_debit_credit_account_error("Supplier", self.supplier) - account = frappe.db.get_value("Account", self.credit_to, - ["account_type", "report_type", "account_currency"], as_dict=True) + account = frappe.db.get_value( + "Account", self.credit_to, ["account_type", "report_type", "account_currency"], as_dict=True + ) if account.report_type != "Balance Sheet": frappe.throw( - _("Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.") - .format(frappe.bold("Credit To")), title=_("Invalid Account") + _( + "Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." + ).format(frappe.bold("Credit To")), + title=_("Invalid Account"), ) if self.supplier and account.account_type != "Payable": frappe.throw( - _("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.") - .format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account") + _( + "Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account." + ).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), + title=_("Invalid Account"), ) self.party_account_currency = account.account_currency @@ -185,51 +203,61 @@ class PurchaseInvoice(BuyingController): def check_on_hold_or_closed_status(self): check_list = [] - for d in self.get('items'): + for d in self.get("items"): if d.purchase_order and not d.purchase_order in check_list and not d.purchase_receipt: check_list.append(d.purchase_order) - check_on_hold_or_closed_status('Purchase Order', d.purchase_order) + check_on_hold_or_closed_status("Purchase Order", d.purchase_order) def validate_with_previous_doc(self): - super(PurchaseInvoice, self).validate_with_previous_doc({ - "Purchase Order": { - "ref_dn_field": "purchase_order", - "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], - }, - "Purchase Order Item": { - "ref_dn_field": "po_detail", - "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - "Purchase Receipt": { - "ref_dn_field": "purchase_receipt", - "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], - }, - "Purchase Receipt Item": { - "ref_dn_field": "pr_detail", - "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], - "is_child_table": True + super(PurchaseInvoice, self).validate_with_previous_doc( + { + "Purchase Order": { + "ref_dn_field": "purchase_order", + "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], + }, + "Purchase Order Item": { + "ref_dn_field": "po_detail", + "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + "Purchase Receipt": { + "ref_dn_field": "purchase_receipt", + "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], + }, + "Purchase Receipt Item": { + "ref_dn_field": "pr_detail", + "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], + "is_child_table": True, + }, } - }) + ) - if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')) and not self.is_return: - self.validate_rate_with_reference_doc([ - ["Purchase Order", "purchase_order", "po_detail"], - ["Purchase Receipt", "purchase_receipt", "pr_detail"] - ]) + if ( + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return + ): + self.validate_rate_with_reference_doc( + [ + ["Purchase Order", "purchase_order", "po_detail"], + ["Purchase Receipt", "purchase_receipt", "pr_detail"], + ] + ) def validate_warehouse(self, for_validate=True): if self.update_stock and for_validate: - for d in self.get('items'): - if not d.warehouse: - frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}"). - format(d.idx, d.item_code, self.company)) + stock_items = self.get_stock_items() + for d in self.get("items"): + if not d.warehouse and d.item_code in stock_items: + frappe.throw( + _( + "Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}" + ).format(d.idx, d.item_code, self.company) + ) super(PurchaseInvoice, self).validate_warehouse() def validate_item_code(self): - for d in self.get('items'): + for d in self.get("items"): if not d.item_code: frappe.msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True) @@ -257,51 +285,82 @@ class PurchaseInvoice(BuyingController): if item.item_code: asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") - if auto_accounting_for_stock and item.item_code in stock_items \ - and self.is_opening == 'No' and not item.is_fixed_asset \ - and (not item.po_detail or - not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")): + if ( + auto_accounting_for_stock + and item.item_code in stock_items + and self.is_opening == "No" + and not item.is_fixed_asset + and ( + not item.po_detail + or not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier") + ) + ): if self.update_stock and (not item.from_warehouse): - if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]: - msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format( - item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse)) + if ( + for_validate + and item.expense_account + and item.expense_account != warehouse_account[item.warehouse]["account"] + ): + msg = _( + "Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account" + ).format( + item.idx, + frappe.bold(warehouse_account[item.warehouse]["account"]), + frappe.bold(item.expense_account), + frappe.bold(item.warehouse), + ) frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = warehouse_account[item.warehouse]["account"] else: # check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not if item.purchase_receipt: - negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry` + negative_expense_booked_in_pr = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no=%s and account = %s""", - (item.purchase_receipt, stock_not_billed_account)) + (item.purchase_receipt, stock_not_billed_account), + ) if negative_expense_booked_in_pr: - if for_validate and item.expense_account and item.expense_account != stock_not_billed_account: - msg = _("Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}").format( - item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt)) + if ( + for_validate and item.expense_account and item.expense_account != stock_not_billed_account + ): + msg = _( + "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" + ).format( + item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt) + ) frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = stock_not_billed_account else: # If no purchase receipt present then book expense in 'Stock Received But Not Billed' # This is done in cases when Purchase Invoice is created before Purchase Receipt - if for_validate and item.expense_account and item.expense_account != stock_not_billed_account: - msg = _("Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.").format( - item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code)) + if ( + for_validate and item.expense_account and item.expense_account != stock_not_billed_account + ): + msg = _( + "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." + ).format( + item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code) + ) msg += "
" - msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice") + msg += _( + "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" + ) frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = stock_not_billed_account elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): - asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code, - company = self.company) + asset_category_account = get_asset_category_account( + "fixed_asset_account", item=item.item_code, company=self.company + ) if not asset_category_account: - form_link = get_link_to_form('Asset Category', asset_category) + form_link = get_link_to_form("Asset Category", asset_category) throw( _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company), - title=_("Missing Account") + title=_("Missing Account"), ) item.expense_account = asset_category_account elif item.is_fixed_asset and item.pr_detail: @@ -309,6 +368,10 @@ class PurchaseInvoice(BuyingController): elif not item.expense_account and for_validate: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) + def validate_expense_account(self): + for item in self.get("items"): + validate_account_head(item.idx, item.expense_account, self.company, "Expense") + def set_against_expense_account(self): against_accounts = [] for item in self.get("items"): @@ -318,32 +381,44 @@ class PurchaseInvoice(BuyingController): self.against_expense_account = ",".join(against_accounts) def po_required(self): - if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes': + if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": - if frappe.get_value('Supplier', self.supplier, 'allow_purchase_invoice_creation_without_purchase_order'): + if frappe.get_value( + "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" + ): return - for d in self.get('items'): + for d in self.get("items"): if not d.purchase_order: msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) msg += "

" msg += _("To submit the invoice without purchase order please set {0} as {1} in {2}").format( - frappe.bold(_('Purchase Order Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')) + frappe.bold(_("Purchase Order Required")), + frappe.bold("No"), + get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"), + ) throw(msg, title=_("Mandatory Purchase Order")) def pr_required(self): stock_items = self.get_stock_items() - if frappe.db.get_value("Buying Settings", None, "pr_required") == 'Yes': + if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes": - if frappe.get_value('Supplier', self.supplier, 'allow_purchase_invoice_creation_without_purchase_receipt'): + if frappe.get_value( + "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt" + ): return - for d in self.get('items'): + for d in self.get("items"): if not d.purchase_receipt and d.item_code in stock_items: msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code)) msg += "

" - msg += _("To submit the invoice without purchase receipt please set {0} as {1} in {2}").format( - frappe.bold(_('Purchase Receipt Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')) + msg += _( + "To submit the invoice without purchase receipt please set {0} as {1} in {2}" + ).format( + frappe.bold(_("Purchase Receipt Required")), + frappe.bold("No"), + get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"), + ) throw(msg, title=_("Mandatory Purchase Receipt")) def validate_write_off_account(self): @@ -351,56 +426,65 @@ class PurchaseInvoice(BuyingController): throw(_("Please enter Write Off Account")) def check_prev_docstatus(self): - for d in self.get('items'): + for d in self.get("items"): if d.purchase_order: - submitted = frappe.db.sql("select name from `tabPurchase Order` where docstatus = 1 and name = %s", d.purchase_order) + submitted = frappe.db.sql( + "select name from `tabPurchase Order` where docstatus = 1 and name = %s", d.purchase_order + ) if not submitted: frappe.throw(_("Purchase Order {0} is not submitted").format(d.purchase_order)) if d.purchase_receipt: - submitted = frappe.db.sql("select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", d.purchase_receipt) + submitted = frappe.db.sql( + "select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", d.purchase_receipt + ) if not submitted: frappe.throw(_("Purchase Receipt {0} is not submitted").format(d.purchase_receipt)) def update_status_updater_args(self): if cint(self.update_stock): - self.status_updater.append({ - 'source_dt': 'Purchase Invoice Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'po_detail', - 'target_field': 'received_qty', - 'target_parent_dt': 'Purchase Order', - 'target_parent_field': 'per_received', - 'target_ref_field': 'qty', - 'source_field': 'received_qty', - 'second_source_dt': 'Purchase Receipt Item', - 'second_source_field': 'received_qty', - 'second_join_field': 'purchase_order_item', - 'percent_join_field':'purchase_order', - 'overflow_type': 'receipt', - 'extra_cond': """ and exists(select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""" - }) + self.status_updater.append( + { + "source_dt": "Purchase Invoice Item", + "target_dt": "Purchase Order Item", + "join_field": "po_detail", + "target_field": "received_qty", + "target_parent_dt": "Purchase Order", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_field": "received_qty", + "second_source_dt": "Purchase Receipt Item", + "second_source_field": "received_qty", + "second_join_field": "purchase_order_item", + "percent_join_field": "purchase_order", + "overflow_type": "receipt", + "extra_cond": """ and exists(select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""", + } + ) if cint(self.is_return): - self.status_updater.append({ - 'source_dt': 'Purchase Invoice Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'po_detail', - 'target_field': 'returned_qty', - 'source_field': '-1 * qty', - 'second_source_dt': 'Purchase Receipt Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'purchase_order_item', - 'overflow_type': 'receipt', - 'extra_cond': """ and exists (select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)""" - }) + self.status_updater.append( + { + "source_dt": "Purchase Invoice Item", + "target_dt": "Purchase Order Item", + "join_field": "po_detail", + "target_field": "returned_qty", + "source_field": "-1 * qty", + "second_source_dt": "Purchase Receipt Item", + "second_source_field": "-1 * qty", + "second_join_field": "purchase_order_item", + "overflow_type": "receipt", + "extra_cond": """ and exists (select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)""", + } + ) def validate_purchase_receipt_if_update_stock(self): if self.update_stock: for item in self.get("items"): if item.purchase_receipt: - frappe.throw(_("Stock cannot be updated against Purchase Receipt {0}") - .format(item.purchase_receipt)) + frappe.throw( + _("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt) + ) def on_submit(self): super(PurchaseInvoice, self).on_submit() @@ -409,8 +493,9 @@ class PurchaseInvoice(BuyingController): self.update_status_updater_args() self.update_prevdoc_status() - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total + ) if not self.is_return: self.update_against_document_in_jv() @@ -425,6 +510,7 @@ class PurchaseInvoice(BuyingController): self.update_stock_ledger() self.set_consumed_qty_in_po() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") # this sequence because outstanding may get -negative @@ -447,13 +533,23 @@ class PurchaseInvoice(BuyingController): update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" if self.docstatus == 1: - make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost) + make_gl_entries( + gl_entries, + update_outstanding=update_outstanding, + merge_entries=False, + from_repost=from_repost, + ) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": - update_outstanding_amt(self.credit_to, "Supplier", self.supplier, - self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name) + update_outstanding_amt( + self.credit_to, + "Supplier", + self.supplier, + self.doctype, + self.return_against if cint(self.is_return) and self.return_against else self.name, + ) elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -502,28 +598,41 @@ class PurchaseInvoice(BuyingController): def make_supplier_gl_entry(self, gl_entries): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total - grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total) - else self.base_grand_total, self.precision("base_grand_total")) + grand_total = ( + self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + ) + base_grand_total = flt( + self.base_rounded_total + if (self.base_rounding_adjustment and self.base_rounded_total) + else self.base_grand_total, + self.precision("base_grand_total"), + ) if grand_total and not self.is_internal_transfer(): - # Did not use base_grand_total to book rounding loss gle - gl_entries.append( - self.get_gl_dict({ + # Did not use base_grand_total to book rounding loss gle + gl_entries.append( + self.get_gl_dict( + { "account": self.credit_to, "party_type": "Supplier", "party": self.supplier, "due_date": self.due_date, "against": self.against_expense_account, "credit": base_grand_total, - "credit_in_account_currency": base_grand_total \ - if self.party_account_currency==self.company_currency else grand_total, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, + "credit_in_account_currency": base_grand_total + if self.party_account_currency == self.company_currency + else grand_total, + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, "against_voucher_type": self.doctype, "project": self.project, - "cost_center": self.cost_center - }, self.party_account_currency, item=self) + "cost_center": self.cost_center, + }, + self.party_account_currency, + item=self, ) + ) def make_item_gl_entries(self, gl_entries): # item gl entries @@ -535,19 +644,28 @@ class PurchaseInvoice(BuyingController): voucher_wise_stock_value = {} if self.update_stock: - stock_ledger_entries = frappe.get_all("Stock Ledger Entry", - fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], - filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0} + stock_ledger_entries = frappe.get_all( + "Stock Ledger Entry", + fields=["voucher_detail_no", "stock_value_difference", "warehouse"], + filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}, ) for d in stock_ledger_entries: - voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) + voucher_wise_stock_value.setdefault( + (d.voucher_detail_no, d.warehouse), d.stock_value_difference + ) - valuation_tax_accounts = [d.account_head for d in self.get("taxes") - if d.category in ('Valuation', 'Total and Valuation') - and flt(d.base_tax_amount_after_discount_amount)] + valuation_tax_accounts = [ + d.account_head + for d in self.get("taxes") + if d.category in ("Valuation", "Total and Valuation") + and flt(d.base_tax_amount_after_discount_amount) + ] - provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, - 'enable_provisional_accounting_for_non_stock_items')) + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) purchase_receipt_doc_map = {} @@ -559,86 +677,122 @@ class PurchaseInvoice(BuyingController): if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items: # warehouse account - warehouse_debit_amount = self.make_stock_adjustment_entry(gl_entries, - item, voucher_wise_stock_value, account_currency) + warehouse_debit_amount = self.make_stock_adjustment_entry( + gl_entries, item, voucher_wise_stock_value, account_currency + ) if item.from_warehouse: - gl_entries.append(self.get_gl_dict({ - "account": warehouse_account[item.warehouse]['account'], - "against": warehouse_account[item.from_warehouse]["account"], - "cost_center": item.cost_center, - "project": item.project or self.project, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": warehouse_debit_amount, - }, warehouse_account[item.warehouse]["account_currency"], item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": warehouse_account[item.warehouse]["account"], + "against": warehouse_account[item.from_warehouse]["account"], + "cost_center": item.cost_center, + "project": item.project or self.project, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": warehouse_debit_amount, + }, + warehouse_account[item.warehouse]["account_currency"], + item=item, + ) + ) # Intentionally passed negative debit amount to avoid incorrect GL Entry validation - gl_entries.append(self.get_gl_dict({ - "account": warehouse_account[item.from_warehouse]['account'], - "against": warehouse_account[item.warehouse]["account"], - "cost_center": item.cost_center, - "project": item.project or self.project, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), - }, warehouse_account[item.from_warehouse]["account_currency"], item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": warehouse_account[item.from_warehouse]["account"], + "against": warehouse_account[item.warehouse]["account"], + "cost_center": item.cost_center, + "project": item.project or self.project, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), + }, + warehouse_account[item.from_warehouse]["account_currency"], + item=item, + ) + ) # Do not book expense for transfer within same company transfer if not self.is_internal_transfer(): gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": flt(item.base_net_amount, item.precision("base_net_amount")), - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": item.expense_account, + "against": self.supplier, + "debit": flt(item.base_net_amount, item.precision("base_net_amount")), + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project, + }, + account_currency, + item=item, + ) ) else: if not self.is_internal_transfer(): gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": warehouse_debit_amount, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": item.expense_account, + "against": self.supplier, + "debit": warehouse_debit_amount, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) ) # Amount added through landed-cost-voucher if landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(item.item_code, item.name)]): - gl_entries.append(self.get_gl_dict({ - "account": account, - "against": item.expense_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount["base_amount"]), - "credit_in_account_currency": flt(amount["amount"]), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": item.expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), + "project": item.project or self.project, + }, + item=item, + ) + ) # sub-contracting warehouse if flt(item.rm_supp_cost): supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["account"] if not supplier_warehouse_account: - frappe.throw(_("Please set account in Warehouse {0}") - .format(self.supplier_warehouse)) - gl_entries.append(self.get_gl_dict({ - "account": supplier_warehouse_account, - "against": item.expense_account, - "cost_center": item.cost_center, - "project": item.project or self.project, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.rm_supp_cost) - }, warehouse_account[self.supplier_warehouse]["account_currency"], item=item)) + frappe.throw(_("Please set account in Warehouse {0}").format(self.supplier_warehouse)) + gl_entries.append( + self.get_gl_dict( + { + "account": supplier_warehouse_account, + "against": item.expense_account, + "cost_center": item.cost_center, + "project": item.project or self.project, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.rm_supp_cost), + }, + warehouse_account[self.supplier_warehouse]["account_currency"], + item=item, + ) + ) - elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)): - expense_account = (item.expense_account - if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) + elif not item.is_fixed_asset or ( + item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category) + ): + expense_account = ( + item.expense_account + if (not item.enable_deferred_expense or self.is_return) + else item.deferred_expense_account + ) if not item.is_fixed_asset: dummy, amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting) @@ -647,7 +801,9 @@ class PurchaseInvoice(BuyingController): if provisional_accounting_for_non_stock_items: if item.purchase_receipt: - provisional_account = self.get_company_default("default_provisional_account") + provisional_account = frappe.db.get_value( + "Purchase Receipt Item", item.pr_detail, "provisional_expense_account" + ) or self.get_company_default("default_provisional_account") purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) if not purchase_receipt_doc: @@ -655,75 +811,115 @@ class PurchaseInvoice(BuyingController): purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0, - 'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail, - 'account':provisional_account}, ['name']) + expense_booked_in_pr = frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "voucher_type": "Purchase Receipt", + "voucher_no": item.purchase_receipt, + "voucher_detail_no": item.pr_detail, + "account": provisional_account, + }, + ["name"], + ) if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing - purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1) + purchase_receipt_doc.add_provisional_gl_entry( + item, gl_entries, self.posting_date, provisional_account, reverse=1 + ) if not self.is_internal_transfer(): - gl_entries.append(self.get_gl_dict({ - "account": expense_account, - "against": self.supplier, - "debit": amount, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": expense_account, + "against": self.supplier, + "debit": amount, + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) + ) # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: - expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") + expenses_included_in_asset_valuation = self.get_company_default( + "expenses_included_in_asset_valuation" + ) # Amount added through landed-cost-voucher - gl_entries.append(self.get_gl_dict({ - "account": expenses_included_in_asset_valuation, - "against": expense_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": expenses_included_in_asset_valuation, + "against": expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": expense_account, - "against": expenses_included_in_asset_valuation, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": expense_account, + "against": expenses_included_in_asset_valuation, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, + ) + ) # update gross amount of asset bought through this document - assets = frappe.db.get_all('Asset', - filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } + assets = frappe.db.get_all( + "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} ) for asset in assets: frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) + frappe.db.set_value( + "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) + ) - if self.auto_accounting_for_stock and self.is_opening == "No" and \ - item.item_code in stock_items and item.item_tax_amount: - # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - if item.purchase_receipt and valuation_tax_accounts: - negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry` + if ( + self.auto_accounting_for_stock + and self.is_opening == "No" + and item.item_code in stock_items + and item.item_tax_amount + ): + # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt + if item.purchase_receipt and valuation_tax_accounts: + negative_expense_booked_in_pr = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""", - (item.purchase_receipt, valuation_tax_accounts)) + (item.purchase_receipt, valuation_tax_accounts), + ) - if not negative_expense_booked_in_pr: - gl_entries.append( - self.get_gl_dict({ + if not negative_expense_booked_in_pr: + gl_entries.append( + self.get_gl_dict( + { "account": self.stock_received_but_not_billed, "against": self.supplier, "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "remarks": self.remarks or _("Accounting Entry for Stock"), "cost_center": self.cost_center, - "project": item.project or self.project - }, item=item) + "project": item.project or self.project, + }, + item=item, ) + ) - self.negative_expense_to_be_booked += flt(item.item_tax_amount, \ - item.precision("item_tax_amount")) + self.negative_expense_to_be_booked += flt( + item.item_tax_amount, item.precision("item_tax_amount") + ) def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") @@ -731,125 +927,179 @@ class PurchaseInvoice(BuyingController): for item in self.get("items"): if item.is_fixed_asset: - asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) + asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) - item_exp_acc_type = frappe.db.get_value('Account', item.expense_account, 'account_type') - if (not item.expense_account or item_exp_acc_type not in ['Asset Received But Not Billed', 'Fixed Asset']): + item_exp_acc_type = frappe.db.get_value("Account", item.expense_account, "account_type") + if not item.expense_account or item_exp_acc_type not in [ + "Asset Received But Not Billed", + "Fixed Asset", + ]: item.expense_account = arbnb_account if not self.update_stock: arbnb_currency = get_account_currency(item.expense_account) - gl_entries.append(self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": (base_asset_amount - if arbnb_currency == self.company_currency else asset_amount), - "cost_center": item.cost_center, - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": item.expense_account, + "against": self.supplier, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": base_asset_amount, + "debit_in_account_currency": ( + base_asset_amount if arbnb_currency == self.company_currency else asset_amount + ), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + item=item, + ) + ) if item.item_tax_amount: asset_eiiav_currency = get_account_currency(eiiav_account) - gl_entries.append(self.get_gl_dict({ - "account": eiiav_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "cost_center": item.cost_center, - "project": item.project or self.project, - "credit": item.item_tax_amount, - "credit_in_account_currency": (item.item_tax_amount - if asset_eiiav_currency == self.company_currency else - item.item_tax_amount / self.conversion_rate) - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": eiiav_account, + "against": self.supplier, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "cost_center": item.cost_center, + "project": item.project or self.project, + "credit": item.item_tax_amount, + "credit_in_account_currency": ( + item.item_tax_amount + if asset_eiiav_currency == self.company_currency + else item.item_tax_amount / self.conversion_rate + ), + }, + item=item, + ) + ) else: - cwip_account = get_asset_account("capital_work_in_progress_account", - asset_category=item.asset_category,company=self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + ) cwip_account_currency = get_account_currency(cwip_account) - gl_entries.append(self.get_gl_dict({ - "account": cwip_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": (base_asset_amount - if cwip_account_currency == self.company_currency else asset_amount), - "cost_center": self.cost_center, - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": cwip_account, + "against": self.supplier, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": base_asset_amount, + "debit_in_account_currency": ( + base_asset_amount if cwip_account_currency == self.company_currency else asset_amount + ), + "cost_center": self.cost_center, + "project": item.project or self.project, + }, + item=item, + ) + ) if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)): asset_eiiav_currency = get_account_currency(eiiav_account) - gl_entries.append(self.get_gl_dict({ - "account": eiiav_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "cost_center": item.cost_center, - "credit": item.item_tax_amount, - "project": item.project or self.project, - "credit_in_account_currency": (item.item_tax_amount - if asset_eiiav_currency == self.company_currency else - item.item_tax_amount / self.conversion_rate) - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": eiiav_account, + "against": self.supplier, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "cost_center": item.cost_center, + "credit": item.item_tax_amount, + "project": item.project or self.project, + "credit_in_account_currency": ( + item.item_tax_amount + if asset_eiiav_currency == self.company_currency + else item.item_tax_amount / self.conversion_rate + ), + }, + item=item, + ) + ) # When update stock is checked # Assets are bought through this document then it will be linked to this document if self.update_stock: if flt(item.landed_cost_voucher_amount): - gl_entries.append(self.get_gl_dict({ - "account": eiiav_account, - "against": cwip_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": eiiav_account, + "against": cwip_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": cwip_account, - "against": eiiav_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": cwip_account, + "against": eiiav_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, + ) + ) # update gross amount of assets bought through this document - assets = frappe.db.get_all('Asset', - filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } + assets = frappe.db.get_all( + "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} ) for asset in assets: frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) + frappe.db.set_value( + "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) + ) return gl_entries - def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency): + def make_stock_adjustment_entry( + self, gl_entries, item, voucher_wise_stock_value, account_currency + ): net_amt_precision = item.precision("base_net_amount") val_rate_db_precision = 6 if cint(item.precision("valuation_rate")) <= 6 else 9 - warehouse_debit_amount = flt(flt(item.valuation_rate, val_rate_db_precision) - * flt(item.qty) * flt(item.conversion_factor), net_amt_precision) + warehouse_debit_amount = flt( + flt(item.valuation_rate, val_rate_db_precision) * flt(item.qty) * flt(item.conversion_factor), + net_amt_precision, + ) # Stock ledger value is not matching with the warehouse amount - if (self.update_stock and voucher_wise_stock_value.get(item.name) and - warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)): + if ( + self.update_stock + and voucher_wise_stock_value.get(item.name) + and warehouse_debit_amount + != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) + ): cost_of_goods_sold_account = self.get_company_default("default_expense_account") stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) stock_adjustment_amt = warehouse_debit_amount - stock_amount gl_entries.append( - self.get_gl_dict({ - "account": cost_of_goods_sold_account, - "against": item.expense_account, - "debit": stock_adjustment_amt, - "remarks": self.get("remarks") or _("Stock Adjustment"), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": cost_of_goods_sold_account, + "against": item.expense_account, + "debit": stock_adjustment_amt, + "remarks": self.get("remarks") or _("Stock Adjustment"), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) ) warehouse_debit_amount = stock_amount @@ -868,24 +1118,35 @@ class PurchaseInvoice(BuyingController): dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "against": self.supplier, - dr_or_cr: base_amount, - dr_or_cr + "_in_account_currency": base_amount - if account_currency==self.company_currency + self.get_gl_dict( + { + "account": tax.account_head, + "against": self.supplier, + dr_or_cr: base_amount, + dr_or_cr + "_in_account_currency": base_amount + if account_currency == self.company_currency else amount, - "cost_center": tax.cost_center - }, account_currency, item=tax) + "cost_center": tax.cost_center, + }, + account_currency, + item=tax, + ) ) # accumulate valuation tax - if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(base_amount) \ - and not self.is_internal_transfer(): + if ( + self.is_opening == "No" + and tax.category in ("Valuation", "Valuation and Total") + and flt(base_amount) + and not self.is_internal_transfer() + ): if self.auto_accounting_for_stock and not tax.cost_center: - frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) + frappe.throw( + _("Cost Center is required in row {0} in Taxes table for type {1}").format( + tax.idx, _(tax.category) + ) + ) valuation_tax.setdefault(tax.name, 0) - valuation_tax[tax.name] += \ - (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount) + valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount) if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax: # credit valuation tax amount in "Expenses Included In Valuation" @@ -899,17 +1160,22 @@ class PurchaseInvoice(BuyingController): if i == len(valuation_tax): applicable_amount = amount_including_divisional_loss else: - applicable_amount = self.negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) + applicable_amount = self.negative_expense_to_be_booked * ( + valuation_tax[tax.name] / total_valuation_amount + ) amount_including_divisional_loss -= applicable_amount gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "cost_center": tax.cost_center, - "against": self.supplier, - "credit": applicable_amount, - "remarks": self.remarks or _("Accounting Entry for Stock"), - }, item=tax) + self.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "against": self.supplier, + "credit": applicable_amount, + "remarks": self.remarks or _("Accounting Entry for Stock"), + }, + item=tax, + ) ) i += 1 @@ -918,18 +1184,24 @@ class PurchaseInvoice(BuyingController): for tax in self.get("taxes"): if valuation_tax.get(tax.name): gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "cost_center": tax.cost_center, - "against": self.supplier, - "credit": valuation_tax[tax.name], - "remarks": self.remarks or _("Accounting Entry for Stock") - }, item=tax)) + self.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "against": self.supplier, + "credit": valuation_tax[tax.name], + "remarks": self.remarks or _("Accounting Entry for Stock"), + }, + item=tax, + ) + ) @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): - self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + self._enable_discount_accounting = cint( + frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + ) return self._enable_discount_accounting @@ -937,13 +1209,18 @@ class PurchaseInvoice(BuyingController): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) gl_entries.append( - self.get_gl_dict({ - "account": self.unrealized_profit_loss_account, - "against": self.supplier, - "credit": flt(self.total_taxes_and_charges), - "credit_in_account_currency": flt(self.base_total_taxes_and_charges), - "cost_center": self.cost_center - }, account_currency, item=self)) + self.get_gl_dict( + { + "account": self.unrealized_profit_loss_account, + "against": self.supplier, + "credit": flt(self.total_taxes_and_charges), + "credit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center, + }, + account_currency, + item=self, + ) + ) def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries @@ -951,30 +1228,42 @@ class PurchaseInvoice(BuyingController): bank_account_currency = get_account_currency(self.cash_bank_account) # CASH, make payment entries gl_entries.append( - self.get_gl_dict({ - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "against": self.cash_bank_account, - "debit": self.base_paid_amount, - "debit_in_account_currency": self.base_paid_amount \ - if self.party_account_currency==self.company_currency else self.paid_amount, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "against": self.cash_bank_account, + "debit": self.base_paid_amount, + "debit_in_account_currency": self.base_paid_amount + if self.party_account_currency == self.company_currency + else self.paid_amount, + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.cash_bank_account, - "against": self.supplier, - "credit": self.base_paid_amount, - "credit_in_account_currency": self.base_paid_amount \ - if bank_account_currency==self.company_currency else self.paid_amount, - "cost_center": self.cost_center - }, bank_account_currency, item=self) + self.get_gl_dict( + { + "account": self.cash_bank_account, + "against": self.supplier, + "credit": self.base_paid_amount, + "credit_in_account_currency": self.base_paid_amount + if bank_account_currency == self.company_currency + else self.paid_amount, + "cost_center": self.cost_center, + }, + bank_account_currency, + item=self, + ) ) def make_write_off_gl_entry(self, gl_entries): @@ -984,48 +1273,66 @@ class PurchaseInvoice(BuyingController): write_off_account_currency = get_account_currency(self.write_off_account) gl_entries.append( - self.get_gl_dict({ - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "against": self.write_off_account, - "debit": self.base_write_off_amount, - "debit_in_account_currency": self.base_write_off_amount \ - if self.party_account_currency==self.company_currency else self.write_off_amount, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "against": self.write_off_account, + "debit": self.base_write_off_amount, + "debit_in_account_currency": self.base_write_off_amount + if self.party_account_currency == self.company_currency + else self.write_off_amount, + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.write_off_account, - "against": self.supplier, - "credit": flt(self.base_write_off_amount), - "credit_in_account_currency": self.base_write_off_amount \ - if write_off_account_currency==self.company_currency else self.write_off_amount, - "cost_center": self.cost_center or self.write_off_cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.write_off_account, + "against": self.supplier, + "credit": flt(self.base_write_off_amount), + "credit_in_account_currency": self.base_write_off_amount + if write_off_account_currency == self.company_currency + else self.write_off_amount, + "cost_center": self.cost_center or self.write_off_cost_center, + }, + item=self, + ) ) def make_gle_for_rounding_adjustment(self, gl_entries): # if rounding adjustment in small and conversion rate is also small then # base_rounding_adjustment may become zero due to small precision # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 - # then base_rounding_adjustment becomes zero and error is thrown in GL Entry - if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment: - round_off_account, round_off_cost_center = \ - get_round_off_account_and_cost_center(self.company) + # then base_rounding_adjustment becomes zero and error is thrown in GL Entry + if ( + not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment + ): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name + ) gl_entries.append( - self.get_gl_dict({ - "account": round_off_account, - "against": self.supplier, - "debit_in_account_currency": self.rounding_adjustment, - "debit": self.base_rounding_adjustment, - "cost_center": self.cost_center or round_off_cost_center, - }, item=self)) + self.get_gl_dict( + { + "account": round_off_account, + "against": self.supplier, + "debit_in_account_currency": self.rounding_adjustment, + "debit": self.base_rounding_adjustment, + "cost_center": self.cost_center or round_off_cost_center, + }, + item=self, + ) + ) def on_cancel(self): check_if_return_invoice_linked_with_payment_entry(self) @@ -1056,10 +1363,10 @@ class PurchaseInvoice(BuyingController): self.repost_future_sle_and_gle() self.update_project() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.update_advance_tax_references(cancel=1) def update_project(self): @@ -1080,19 +1387,22 @@ class PurchaseInvoice(BuyingController): if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")): fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True) - pi = frappe.db.sql('''select name from `tabPurchase Invoice` + pi = frappe.db.sql( + """select name from `tabPurchase Invoice` where bill_no = %(bill_no)s and supplier = %(supplier)s and name != %(name)s and docstatus < 2 - and posting_date between %(year_start_date)s and %(year_end_date)s''', { - "bill_no": self.bill_no, - "supplier": self.supplier, - "name": self.name, - "year_start_date": fiscal_year.year_start_date, - "year_end_date": fiscal_year.year_end_date - }) + and posting_date between %(year_start_date)s and %(year_end_date)s""", + { + "bill_no": self.bill_no, + "supplier": self.supplier, + "name": self.name, + "year_start_date": fiscal_year.year_start_date, + "year_end_date": fiscal_year.year_end_date, + }, + ) if pi: pi = pi[0][0] @@ -1102,16 +1412,26 @@ class PurchaseInvoice(BuyingController): updated_pr = [] for d in self.get("items"): if d.pr_detail: - billed_amt = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` - where pr_detail=%s and docstatus=1""", d.pr_detail) + billed_amt = frappe.db.sql( + """select sum(amount) from `tabPurchase Invoice Item` + where pr_detail=%s and docstatus=1""", + d.pr_detail, + ) billed_amt = billed_amt and billed_amt[0][0] or 0 - frappe.db.set_value("Purchase Receipt Item", d.pr_detail, "billed_amt", billed_amt, update_modified=update_modified) + frappe.db.set_value( + "Purchase Receipt Item", + d.pr_detail, + "billed_amt", + billed_amt, + update_modified=update_modified, + ) updated_pr.append(d.purchase_receipt) elif d.po_detail: updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified) for pr in set(updated_pr): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage + pr_doc = frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) @@ -1119,25 +1439,29 @@ class PurchaseInvoice(BuyingController): self.due_date = None def block_invoice(self, hold_comment=None, release_date=None): - self.db_set('on_hold', 1) - self.db_set('hold_comment', cstr(hold_comment)) - self.db_set('release_date', release_date) + self.db_set("on_hold", 1) + self.db_set("hold_comment", cstr(hold_comment)) + self.db_set("release_date", release_date) def unblock_invoice(self): - self.db_set('on_hold', 0) - self.db_set('release_date', None) + self.db_set("on_hold", 0) + self.db_set("release_date", None) def set_tax_withholding(self): if not self.apply_tds: return - if self.apply_tds and not self.get('tax_withholding_category'): - self.tax_withholding_category = frappe.db.get_value('Supplier', self.supplier, 'tax_withholding_category') + if self.apply_tds and not self.get("tax_withholding_category"): + self.tax_withholding_category = frappe.db.get_value( + "Supplier", self.supplier, "tax_withholding_category" + ) if not self.tax_withholding_category: return - tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category) + tax_withholding_details, advance_taxes = get_party_tax_withholding_details( + self, self.tax_withholding_category + ) # Adjust TDS paid on advances self.allocate_advance_tds(tax_withholding_details, advance_taxes) @@ -1155,8 +1479,11 @@ class PurchaseInvoice(BuyingController): if not accounts or tax_withholding_details.get("account_head") not in accounts: self.append("taxes", tax_withholding_details) - to_remove = [d for d in self.taxes - if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")] + to_remove = [ + d + for d in self.taxes + if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head") + ] for d in to_remove: self.remove(d) @@ -1165,27 +1492,33 @@ class PurchaseInvoice(BuyingController): self.calculate_taxes_and_totals() def allocate_advance_tds(self, tax_withholding_details, advance_taxes): - self.set('advance_tax', []) + self.set("advance_tax", []) for tax in advance_taxes: allocated_amount = 0 pending_amount = flt(tax.tax_amount - tax.allocated_amount) - if flt(tax_withholding_details.get('tax_amount')) >= pending_amount: - tax_withholding_details['tax_amount'] -= pending_amount + if flt(tax_withholding_details.get("tax_amount")) >= pending_amount: + tax_withholding_details["tax_amount"] -= pending_amount allocated_amount = pending_amount - elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount: - allocated_amount = tax_withholding_details['tax_amount'] - tax_withholding_details['tax_amount'] = 0 + elif ( + flt(tax_withholding_details.get("tax_amount")) + and flt(tax_withholding_details.get("tax_amount")) < pending_amount + ): + allocated_amount = tax_withholding_details["tax_amount"] + tax_withholding_details["tax_amount"] = 0 - self.append('advance_tax', { - 'reference_type': 'Payment Entry', - 'reference_name': tax.parent, - 'reference_detail': tax.name, - 'account_head': tax.account_head, - 'allocated_amount': allocated_amount - }) + self.append( + "advance_tax", + { + "reference_type": "Payment Entry", + "reference_name": tax.parent, + "reference_detail": tax.name, + "account_head": tax.account_head, + "allocated_amount": allocated_amount, + }, + ) def update_advance_tax_references(self, cancel=0): - for tax in self.get('advance_tax'): + for tax in self.get("advance_tax"): at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") if cancel: @@ -1199,8 +1532,8 @@ class PurchaseInvoice(BuyingController): def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): - if self.get('amended_from'): - self.status = 'Draft' + if self.get("amended_from"): + self.status = "Draft" return outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount")) @@ -1211,19 +1544,25 @@ class PurchaseInvoice(BuyingController): status = "Cancelled" elif self.docstatus == 1: if self.is_internal_transfer(): - self.status = 'Internal Transfer' + self.status = "Internal Transfer" elif is_overdue(self, total): self.status = "Overdue" elif 0 < outstanding_amount < total: self.status = "Partly Paid" elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" - #Check if outstanding amount is 0 due to debit note issued against invoice - elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + # Check if outstanding amount is 0 due to debit note issued against invoice + elif ( + outstanding_amount <= 0 + and self.is_return == 0 + and frappe.db.get_value( + "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} + ) + ): self.status = "Debit Note Issued" elif self.is_return == 1: self.status = "Return" - elif outstanding_amount<=0: + elif outstanding_amount <= 0: self.status = "Paid" else: self.status = "Submitted" @@ -1231,76 +1570,86 @@ class PurchaseInvoice(BuyingController): self.status = "Draft" if update: - self.db_set('status', self.status, update_modified = update_modified) + self.db_set("status", self.status, update_modified=update_modified) + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Purchase Invoices'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Purchase Invoices"), + } + ) return list_context + @erpnext.allow_regional def make_regional_gl_entries(gl_entries, doc): return gl_entries + @frappe.whitelist() def make_debit_note(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Purchase Invoice", source_name, target_doc) + @frappe.whitelist() def make_stock_entry(source_name, target_doc=None): - doc = get_mapped_doc("Purchase Invoice", source_name, { - "Purchase Invoice": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1] - } - }, - "Purchase Invoice Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "stock_qty": "transfer_qty", - "batch_no": "batch_no" + doc = get_mapped_doc( + "Purchase Invoice", + source_name, + { + "Purchase Invoice": {"doctype": "Stock Entry", "validation": {"docstatus": ["=", 1]}}, + "Purchase Invoice Item": { + "doctype": "Stock Entry Detail", + "field_map": {"stock_qty": "transfer_qty", "batch_no": "batch_no"}, }, - } - }, target_doc) + }, + target_doc, + ) return doc + @frappe.whitelist() def change_release_date(name, release_date=None): - if frappe.db.exists('Purchase Invoice', name): - pi = frappe.get_doc('Purchase Invoice', name) - pi.db_set('release_date', release_date) + if frappe.db.exists("Purchase Invoice", name): + pi = frappe.get_doc("Purchase Invoice", name) + pi.db_set("release_date", release_date) @frappe.whitelist() def unblock_invoice(name): - if frappe.db.exists('Purchase Invoice', name): - pi = frappe.get_doc('Purchase Invoice', name) + if frappe.db.exists("Purchase Invoice", name): + pi = frappe.get_doc("Purchase Invoice", name) pi.unblock_invoice() @frappe.whitelist() def block_invoice(name, release_date, hold_comment=None): - if frappe.db.exists('Purchase Invoice', name): - pi = frappe.get_doc('Purchase Invoice', name) + if frappe.db.exists("Purchase Invoice", name): + pi = frappe.get_doc("Purchase Invoice", name) pi.block_invoice(hold_comment, release_date) + @frappe.whitelist() def make_inter_company_sales_invoice(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction + return make_inter_company_transaction("Purchase Invoice", source_name, target_doc) + def on_doctype_update(): frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"]) + @frappe.whitelist() def make_purchase_receipt(source_name, target_doc=None): def update_item(obj, target, source_parent): @@ -1308,33 +1657,37 @@ def make_purchase_receipt(source_name, target_doc=None): target.received_qty = flt(obj.qty) - flt(obj.received_qty) target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor) target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) - target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \ - flt(obj.rate) * flt(source_parent.conversion_rate) + target.base_amount = ( + (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate) + ) - doc = get_mapped_doc("Purchase Invoice", source_name, { - "Purchase Invoice": { - "doctype": "Purchase Receipt", - "validation": { - "docstatus": ["=", 1], - } - }, - "Purchase Invoice Item": { - "doctype": "Purchase Receipt Item", - "field_map": { - "name": "purchase_invoice_item", - "parent": "purchase_invoice", - "bom": "bom", - "purchase_order": "purchase_order", - "po_detail": "purchase_order_item", - "material_request": "material_request", - "material_request_item": "material_request_item" + doc = get_mapped_doc( + "Purchase Invoice", + source_name, + { + "Purchase Invoice": { + "doctype": "Purchase Receipt", + "validation": { + "docstatus": ["=", 1], + }, }, - "postprocess": update_item, - "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) + "Purchase Invoice Item": { + "doctype": "Purchase Receipt Item", + "field_map": { + "name": "purchase_invoice_item", + "parent": "purchase_invoice", + "bom": "bom", + "purchase_order": "purchase_order", + "po_detail": "purchase_order_item", + "material_request": "material_request", + "material_request_item": "material_request_item", + }, + "postprocess": update_item, + "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), + }, + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"}, }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges" - } - }, target_doc) + target_doc, + ) return doc diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py index f1878b480ed..10dd0ef6e25 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py @@ -1,38 +1,28 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'purchase_invoice', - 'non_standard_fieldnames': { - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', - 'Payment Request': 'reference_name', - 'Landed Cost Voucher': 'receipt_document', - 'Purchase Invoice': 'return_against', - 'Auto Repeat': 'reference_document' + "fieldname": "purchase_invoice", + "non_standard_fieldnames": { + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", + "Payment Request": "reference_name", + "Landed Cost Voucher": "receipt_document", + "Purchase Invoice": "return_against", + "Auto Repeat": "reference_document", }, - 'internal_links': { - 'Purchase Order': ['items', 'purchase_order'], - 'Purchase Receipt': ['items', 'purchase_receipt'], + "internal_links": { + "Purchase Order": ["items", "purchase_order"], + "Purchase Receipt": ["items", "purchase_receipt"], }, - 'transactions': [ + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]}, { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Payment Request', 'Journal Entry'] + "label": _("Reference"), + "items": ["Purchase Order", "Purchase Receipt", "Asset", "Landed Cost Voucher"], }, - { - 'label': _('Reference'), - 'items': ['Purchase Order', 'Purchase Receipt', 'Asset', 'Landed Cost Voucher'] - }, - { - 'label': _('Returns'), - 'items': ['Purchase Invoice'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + {"label": _("Returns"), "items": ["Purchase Invoice"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 1d2dcdf2776..15803b5bfe1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import unittest import frappe @@ -31,6 +30,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_tra test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"] test_ignore = ["Serial No"] + class TestPurchaseInvoice(unittest.TestCase): @classmethod def setUpClass(self): @@ -43,16 +43,18 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_received_qty(self): """ - 1. Test if received qty is validated against accepted + rejected - 2. Test if received qty is auto set on save + 1. Test if received qty is validated against accepted + rejected + 2. Test if received qty is auto set on save """ pi = make_purchase_invoice( qty=1, rejected_qty=1, received_qty=3, item_code="_Test Item Home Desktop 200", - rejected_warehouse = "_Test Rejected Warehouse - _TC", - update_stock=True, do_not_save=True) + rejected_warehouse="_Test Rejected Warehouse - _TC", + update_stock=True, + do_not_save=True, + ) self.assertRaises(QtyMismatchError, pi.save) pi.items[0].received_qty = 0 @@ -79,18 +81,26 @@ class TestPurchaseInvoice(unittest.TestCase): "_Test Account CST - _TC": [29.88, 0], "_Test Account VAT - _TC": [156.25, 0], "_Test Account Discount - _TC": [0, 168.03], - "Round Off - _TC": [0, 0.3] + "Round Off - _TC": [0, 0.3], } - gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1) + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` + where voucher_type = 'Purchase Invoice' and voucher_no = %s""", + pi.name, + as_dict=1, + ) for d in gl_entries: self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account)) def test_gl_entries_with_perpetual_inventory(self): - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", - warehouse= "Stores - TCP1", cost_center = "Main - TCP1", - expense_account ="_Test Account Cost for Goods Sold - TCP1", - get_taxes_and_charges=True, qty=10) + pi = make_purchase_invoice( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + get_taxes_and_charges=True, + qty=10, + ) self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1) @@ -104,6 +114,7 @@ class TestPurchaseInvoice(unittest.TestCase): def test_payment_entry_unlink_against_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + unlink_payment_on_cancel_of_invoice(0) pi_doc = make_purchase_invoice() @@ -119,7 +130,7 @@ class TestPurchaseInvoice(unittest.TestCase): pe.save(ignore_permissions=True) pe.submit() - pi_doc = frappe.get_doc('Purchase Invoice', pi_doc.name) + pi_doc = frappe.get_doc("Purchase Invoice", pi_doc.name) pi_doc.load_from_db() self.assertTrue(pi_doc.status, "Paid") @@ -127,7 +138,7 @@ class TestPurchaseInvoice(unittest.TestCase): unlink_payment_on_cancel_of_invoice() def test_purchase_invoice_for_blocked_supplier(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 supplier.save() @@ -137,9 +148,9 @@ class TestPurchaseInvoice(unittest.TestCase): supplier.save() def test_purchase_invoice_for_blocked_supplier_invoice(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Invoices' + supplier.hold_type = "Invoices" supplier.save() self.assertRaises(frappe.ValidationError, make_purchase_invoice) @@ -148,31 +159,40 @@ class TestPurchaseInvoice(unittest.TestCase): supplier.save() def test_purchase_invoice_for_blocked_supplier_payment(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' + supplier.hold_type = "Payments" supplier.save() pi = make_purchase_invoice() self.assertRaises( - frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC") + frappe.ValidationError, + get_payment_entry, + dt="Purchase Invoice", + dn=pi.name, + bank_account="_Test Bank - _TC", + ) supplier.on_hold = 0 supplier.save() def test_purchase_invoice_for_blocked_supplier_payment_today_date(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' + supplier.hold_type = "Payments" supplier.release_date = nowdate() supplier.save() pi = make_purchase_invoice() self.assertRaises( - frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, - bank_account="_Test Bank - _TC") + frappe.ValidationError, + get_payment_entry, + dt="Purchase Invoice", + dn=pi.name, + bank_account="_Test Bank - _TC", + ) supplier.on_hold = 0 supplier.save() @@ -181,15 +201,15 @@ class TestPurchaseInvoice(unittest.TestCase): # this test is meant to fail only if something fails in the try block with self.assertRaises(Exception): try: - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' - supplier.release_date = '2018-03-01' + supplier.hold_type = "Payments" + supplier.release_date = "2018-03-01" supplier.save() pi = make_purchase_invoice() - get_payment_entry('Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC") + get_payment_entry("Purchase Invoice", dn=pi.name, bank_account="_Test Bank - _TC") supplier.on_hold = 0 supplier.save() @@ -203,7 +223,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi.release_date = nowdate() self.assertRaises(frappe.ValidationError, pi.save) - pi.release_date = '' + pi.release_date = "" pi.save() def test_purchase_invoice_temporary_blocked(self): @@ -212,7 +232,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi.save() pi.submit() - pe = get_payment_entry('Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", dn=pi.name, bank_account="_Test Bank - _TC") self.assertRaises(frappe.ValidationError, pe.save) @@ -228,9 +248,24 @@ class TestPurchaseInvoice(unittest.TestCase): def test_gl_entries_with_perpetual_inventory_against_pr(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + get_taxes_and_charges=True, + ) - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True") + pi = make_purchase_invoice( + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + get_taxes_and_charges=True, + qty=10, + do_not_save="True", + ) for d in pi.items: d.purchase_receipt = pr.name @@ -243,18 +278,25 @@ class TestPurchaseInvoice(unittest.TestCase): self.check_gle_for_pi(pi.name) def check_gle_for_pi(self, pi): - gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit + gl_entries = frappe.db.sql( + """select account, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - group by account""", pi, as_dict=1) + group by account""", + pi, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - ["Creditors - TCP1", 0, 720], - ["Stock Received But Not Billed - TCP1", 500.0, 0], - ["_Test Account Shipping Charges - TCP1", 100.0, 0.0], - ["_Test Account VAT - TCP1", 120.0, 0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + ["Creditors - TCP1", 0, 720], + ["Stock Received But Not Billed - TCP1", 500.0, 0], + ["_Test Account Shipping Charges - TCP1", 100.0, 0.0], + ["_Test Account VAT - TCP1", 120.0, 0], + ] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) @@ -264,14 +306,17 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_with_discount_accounting_enabled(self): enable_discount_accounting() - discount_account = create_account(account_name="Discount Account", - parent_account="Indirect Expenses - _TC", company="_Test Company") + discount_account = create_account( + account_name="Discount Account", + parent_account="Indirect Expenses - _TC", + company="_Test Company", + ) pi = make_purchase_invoice(discount_account=discount_account, rate=45) expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], ["Creditors - _TC", 0.0, 225.0, nowdate()], - ["Discount Account - _TC", 0.0, 25.0, nowdate()] + ["Discount Account - _TC", 0.0, 25.0, nowdate()], ] check_gl_entries(self, pi.name, expected_gle, nowdate()) @@ -279,28 +324,34 @@ class TestPurchaseInvoice(unittest.TestCase): def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): enable_discount_accounting() - additional_discount_account = create_account(account_name="Discount Account", - parent_account="Indirect Expenses - _TC", company="_Test Company") + additional_discount_account = create_account( + account_name="Discount Account", + parent_account="Indirect Expenses - _TC", + company="_Test Company", + ) pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC") pi.apply_discount_on = "Grand Total" pi.additional_discount_account = additional_discount_account pi.additional_discount_percentage = 10 pi.disable_rounded_total = 1 - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account VAT - _TC", - "cost_center": "Main - _TC", - "description": "Test", - "rate": 10 - }) + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "description": "Test", + "rate": 10, + }, + ) pi.submit() expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], ["_Test Account VAT - _TC", 25.0, 0.0, nowdate()], ["Creditors - _TC", 0.0, 247.5, nowdate()], - ["Discount Account - _TC", 0.0, 27.5, nowdate()] + ["Discount Account - _TC", 0.0, 27.5, nowdate()], ] check_gl_entries(self, pi.name, expected_gle, nowdate()) @@ -308,7 +359,7 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() - pi.naming_series = 'TEST-' + pi.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, pi.save) @@ -317,25 +368,33 @@ class TestPurchaseInvoice(unittest.TestCase): pi.load_from_db() self.assertTrue(pi.status, "Draft") - pi.naming_series = 'TEST-' + pi.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, pi.save) def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self): - pi = make_purchase_invoice(item_code = "_Test Non Stock Item", - company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", - cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + item_code="_Test Non Stock Item", + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) self.assertTrue(pi.status, "Unpaid") - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) expected_values = [ ["_Test Account Cost for Goods Sold - TCP1", 250.0, 0], - ["Creditors - TCP1", 0, 250] + ["Creditors - TCP1", 0, 250], ] for i, gle in enumerate(gl_entries): @@ -350,7 +409,7 @@ class TestPurchaseInvoice(unittest.TestCase): expected_values = [ ["_Test Item Home Desktop 100", 90, 59], - ["_Test Item Home Desktop 200", 135, 177] + ["_Test Item Home Desktop 200", 135, 177], ] for i, item in enumerate(pi.get("items")): self.assertEqual(item.item_code, expected_values[i][0]) @@ -382,10 +441,7 @@ class TestPurchaseInvoice(unittest.TestCase): wrapper.insert() wrapper.load_from_db() - expected_values = [ - ["_Test FG Item", 90, 59], - ["_Test Item Home Desktop 200", 135, 177] - ] + expected_values = [["_Test FG Item", 90, 59], ["_Test Item Home Desktop 200", 135, 177]] for i, item in enumerate(wrapper.get("items")): self.assertEqual(item.item_code, expected_values[i][0]) self.assertEqual(item.item_tax_amount, expected_values[i][1]) @@ -422,14 +478,17 @@ class TestPurchaseInvoice(unittest.TestCase): pi = frappe.copy_doc(test_records[0]) pi.disable_rounded_total = 1 pi.allocate_advances_automatically = 0 - pi.append("advances", { - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + pi.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) pi.insert() self.assertEqual(pi.outstanding_amount, 1212.30) @@ -442,14 +501,24 @@ class TestPurchaseInvoice(unittest.TestCase): pi.submit() pi.load_from_db() - self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` + self.assertTrue( + frappe.db.sql( + """select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' - and reference_name=%s and debit_in_account_currency=300""", pi.name)) + and reference_name=%s and debit_in_account_currency=300""", + pi.name, + ) + ) pi.cancel() - self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_type='Purchase Invoice' and reference_name=%s""", pi.name)) + self.assertFalse( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_type='Purchase Invoice' and reference_name=%s""", + pi.name, + ) + ) def test_invoice_with_advance_and_multi_payment_terms(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( @@ -463,20 +532,26 @@ class TestPurchaseInvoice(unittest.TestCase): pi = frappe.copy_doc(test_records[0]) pi.disable_rounded_total = 1 pi.allocate_advances_automatically = 0 - pi.append("advances", { - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + pi.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) pi.insert() - pi.update({ - "payment_schedule": get_payment_terms("_Test Payment Term Template", - pi.posting_date, pi.grand_total, pi.base_grand_total) - }) + pi.update( + { + "payment_schedule": get_payment_terms( + "_Test Payment Term Template", pi.posting_date, pi.grand_total, pi.base_grand_total + ) + } + ) pi.save() pi.submit() @@ -490,7 +565,9 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertTrue( frappe.db.sql( "select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and " - "reference_name=%s and debit_in_account_currency=300", pi.name) + "reference_name=%s and debit_in_account_currency=300", + pi.name, + ) ) self.assertEqual(pi.outstanding_amount, 1212.30) @@ -500,49 +577,76 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertFalse( frappe.db.sql( "select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and " - "reference_name=%s", pi.name) + "reference_name=%s", + pi.name, + ) ) def test_total_purchase_cost_for_project(self): if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}): - project = make_project({'project_name':'_Test Project for Purchase'}) + project = make_project({"project_name": "_Test Project for Purchase"}) else: project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"}) - existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) + existing_purchase_cost = frappe.db.sql( + """select sum(base_net_amount) from `tabPurchase Invoice Item` where project = '{0}' - and docstatus=1""".format(project.name)) + and docstatus=1""".format( + project.name + ) + ) existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name) - self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), - existing_purchase_cost + 15000) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_purchase_cost"), + existing_purchase_cost + 15000, + ) pi1 = make_purchase_invoice(qty=10, project=project.name) - self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), - existing_purchase_cost + 15500) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_purchase_cost"), + existing_purchase_cost + 15500, + ) pi1.cancel() - self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), - existing_purchase_cost + 15000) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_purchase_cost"), + existing_purchase_cost + 15000, + ) pi.cancel() - self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost + ) def test_return_purchase_invoice_with_perpetual_inventory(self): - pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", - cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") - - return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, - company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", - cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) + return_pi = make_purchase_invoice( + is_return=1, + return_against=pi.name, + qty=-2, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) # check gl entries for return - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account desc""", ("Purchase Invoice", return_pi.name), as_dict=1) + order by account desc""", + ("Purchase Invoice", return_pi.name), + as_dict=1, + ) self.assertTrue(gl_entries) @@ -556,13 +660,21 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.credit) def test_multi_currency_gle(self): - pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", - currency="USD", conversion_rate=50) + pi = make_purchase_invoice( + supplier="_Test Supplier USD", + credit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -572,53 +684,74 @@ class TestPurchaseInvoice(unittest.TestCase): "debit": 0, "debit_in_account_currency": 0, "credit": 12500, - "credit_in_account_currency": 250 + "credit_in_account_currency": 250, }, "_Test Account Cost for Goods Sold - _TC": { "account_currency": "INR", "debit": 12500, "debit_in_account_currency": 12500, "credit": 0, - "credit_in_account_currency": 0 - } + "credit_in_account_currency": 0, + }, } - for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): + for field in ( + "account_currency", + "debit", + "debit_in_account_currency", + "credit", + "credit_in_account_currency", + ): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) - # Check for valid currency - pi1 = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", - do_not_save=True) + pi1 = make_purchase_invoice( + supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", do_not_save=True + ) self.assertRaises(InvalidCurrency, pi1.save) # cancel pi.cancel() - gle = frappe.db.sql("""select name from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", pi.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + pi.name, + ) self.assertFalse(gle) def test_purchase_invoice_update_stock_gl_entry_with_perpetual_inventory(self): - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + cash_bank_account="Cash - TCP1", + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) stock_in_hand_account = get_inventory_account(pi.company, pi.get("items")[0].warehouse) - expected_gl_entries = dict((d[0], d) for d in [ - [pi.credit_to, 0.0, 250.0], - [stock_in_hand_account, 250.0, 0.0] - ]) + expected_gl_entries = dict( + (d[0], d) for d in [[pi.credit_to, 0.0, 250.0], [stock_in_hand_account, 250.0, 0.0]] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gl_entries[gle.account][0], gle.account) @@ -627,22 +760,39 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_for_is_paid_and_update_stock_gl_entry_with_perpetual_inventory(self): - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", is_paid=1, company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + cash_bank_account="Cash - TCP1", + is_paid=1, + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) - gl_entries = frappe.db.sql("""select account, account_currency, sum(debit) as debit, + gl_entries = frappe.db.sql( + """select account, account_currency, sum(debit) as debit, sum(credit) as credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - group by account, voucher_no order by account asc;""", pi.name, as_dict=1) + group by account, voucher_no order by account asc;""", + pi.name, + as_dict=1, + ) stock_in_hand_account = get_inventory_account(pi.company, pi.get("items")[0].warehouse) self.assertTrue(gl_entries) - expected_gl_entries = dict((d[0], d) for d in [ - [pi.credit_to, 250.0, 250.0], - [stock_in_hand_account, 250.0, 0.0], - ["Cash - TCP1", 0.0, 250.0] - ]) + expected_gl_entries = dict( + (d[0], d) + for d in [ + [pi.credit_to, 250.0, 250.0], + [stock_in_hand_account, 250.0, 0.0], + ["Cash - TCP1", 0.0, 250.0], + ] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gl_entries[gle.account][0], gle.account) @@ -650,31 +800,36 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gl_entries[gle.account][2], gle.credit) def test_auto_batch(self): - item_code = frappe.db.get_value('Item', - {'has_batch_no': 1, 'create_new_batch':1}, 'name') + item_code = frappe.db.get_value("Item", {"has_batch_no": 1, "create_new_batch": 1}, "name") if not item_code: - doc = frappe.get_doc({ - 'doctype': 'Item', - 'is_stock_item': 1, - 'item_code': 'test batch item', - 'item_group': 'Products', - 'has_batch_no': 1, - 'create_new_batch': 1 - }).insert(ignore_permissions=True) + doc = frappe.get_doc( + { + "doctype": "Item", + "is_stock_item": 1, + "item_code": "test batch item", + "item_group": "Products", + "has_batch_no": 1, + "create_new_batch": 1, + } + ).insert(ignore_permissions=True) item_code = doc.name - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), item_code=item_code) + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + item_code=item_code, + ) - self.assertTrue(frappe.db.get_value('Batch', - {'item': item_code, 'reference_name': pi.name})) + self.assertTrue(frappe.db.get_value("Batch", {"item": item_code, "reference_name": pi.name})) def test_update_stock_and_purchase_return(self): actual_qty_0 = get_qty_after_transaction() - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime()) + pi = make_purchase_invoice( + update_stock=1, posting_date=frappe.utils.nowdate(), posting_time=frappe.utils.nowtime() + ) actual_qty_1 = get_qty_after_transaction() self.assertEqual(actual_qty_0 + 5, actual_qty_1) @@ -701,13 +856,20 @@ class TestPurchaseInvoice(unittest.TestCase): from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - update_backflush_based_on('BOM') - make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC", - qty=100, basic_rate=100) + update_backflush_based_on("BOM") + make_stock_entry( + item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 + ) + make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse 1 - _TC", + qty=100, + basic_rate=100, + ) - pi = make_purchase_invoice(item_code="_Test FG Item", qty=10, rate=500, - update_stock=1, is_subcontracted="Yes") + pi = make_purchase_invoice( + item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted="Yes" + ) self.assertEqual(len(pi.get("supplied_items")), 2) @@ -715,15 +877,26 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2)) 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", - allow_zero_valuation_rate=1) + 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", + 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) + self.assertEqual( + frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"), + pi.get("items")[0].warehouse, + ) - self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no, - "warehouse"), pi.get("items")[0].rejected_warehouse) + self.assertEqual( + frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no, "warehouse"), + pi.get("items")[0].rejected_warehouse, + ) def test_outstanding_amount_after_advance_jv_cancelation(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( @@ -731,85 +904,95 @@ class TestPurchaseInvoice(unittest.TestCase): ) jv = frappe.copy_doc(jv_test_records[1]) - jv.accounts[0].is_advance = 'Yes' + jv.accounts[0].is_advance = "Yes" jv.insert() jv.submit() pi = frappe.copy_doc(test_records[0]) - pi.append("advances", { - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + pi.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) pi.insert() pi.submit() pi.load_from_db() - #check outstanding after advance allocation + # check outstanding after advance allocation self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total - pi.total_advance)) - #added to avoid Document has been modified exception + # added to avoid Document has been modified exception jv = frappe.get_doc("Journal Entry", jv.name) jv.cancel() pi.load_from_db() - #check outstanding after advance cancellation + # check outstanding after advance cancellation self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total + pi.total_advance)) def test_outstanding_amount_after_advance_payment_entry_cancelation(self): - pe = frappe.get_doc({ - "doctype": "Payment Entry", - "payment_type": "Pay", - "party_type": "Supplier", - "party": "_Test Supplier", - "company": "_Test Company", - "paid_from_account_currency": "INR", - "paid_to_account_currency": "INR", - "source_exchange_rate": 1, - "target_exchange_rate": 1, - "reference_no": "1", - "reference_date": nowdate(), - "received_amount": 300, - "paid_amount": 300, - "paid_from": "_Test Cash - _TC", - "paid_to": "_Test Payable - _TC" - }) + pe = frappe.get_doc( + { + "doctype": "Payment Entry", + "payment_type": "Pay", + "party_type": "Supplier", + "party": "_Test Supplier", + "company": "_Test Company", + "paid_from_account_currency": "INR", + "paid_to_account_currency": "INR", + "source_exchange_rate": 1, + "target_exchange_rate": 1, + "reference_no": "1", + "reference_date": nowdate(), + "received_amount": 300, + "paid_amount": 300, + "paid_from": "_Test Cash - _TC", + "paid_to": "_Test Payable - _TC", + } + ) pe.insert() pe.submit() pi = frappe.copy_doc(test_records[0]) pi.is_pos = 0 - pi.append("advances", { - "doctype": "Purchase Invoice Advance", - "reference_type": "Payment Entry", - "reference_name": pe.name, - "advance_amount": 300, - "allocated_amount": 300, - "remarks": pe.remarks - }) + pi.append( + "advances", + { + "doctype": "Purchase Invoice Advance", + "reference_type": "Payment Entry", + "reference_name": pe.name, + "advance_amount": 300, + "allocated_amount": 300, + "remarks": pe.remarks, + }, + ) pi.insert() pi.submit() pi.load_from_db() - #check outstanding after advance allocation + # check outstanding after advance allocation self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total - pi.total_advance)) - #added to avoid Document has been modified exception + # added to avoid Document has been modified exception pe = frappe.get_doc("Payment Entry", pe.name) pe.cancel() pi.load_from_db() - #check outstanding after advance cancellation + # check outstanding after advance cancellation self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total + pi.total_advance)) def test_purchase_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule - shipping_rule = create_shipping_rule(shipping_rule_type = "Buying", shipping_rule_name = "Shipping Rule - Purchase Invoice Test") + shipping_rule = create_shipping_rule( + shipping_rule_type="Buying", shipping_rule_name="Shipping Rule - Purchase Invoice Test" + ) pi = frappe.copy_doc(test_records[0]) @@ -825,16 +1008,20 @@ class TestPurchaseInvoice(unittest.TestCase): def test_make_pi_without_terms(self): pi = make_purchase_invoice(do_not_save=1) - self.assertFalse(pi.get('payment_schedule')) + self.assertFalse(pi.get("payment_schedule")) pi.insert() - self.assertTrue(pi.get('payment_schedule')) + self.assertTrue(pi.get("payment_schedule")) def test_duplicate_due_date_in_terms(self): pi = make_purchase_invoice(do_not_save=1) - pi.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50)) - pi.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50)) + pi.append( + "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) + ) + pi.append( + "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) + ) self.assertRaises(frappe.ValidationError, pi.insert) @@ -842,12 +1029,13 @@ class TestPurchaseInvoice(unittest.TestCase): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import get_outstanding_amount - pi = make_purchase_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1) + pi = make_purchase_invoice(item_code="_Test Item", qty=(5 * -1), rate=500, is_return=1) pi.load_from_db() self.assertTrue(pi.status, "Return") - outstanding_amount = get_outstanding_amount(pi.doctype, - pi.name, "Creditors - _TC", pi.supplier, "Supplier") + outstanding_amount = get_outstanding_amount( + pi.doctype, pi.name, "Creditors - _TC", pi.supplier, "Supplier" + ) self.assertEqual(pi.outstanding_amount, outstanding_amount) @@ -862,30 +1050,33 @@ class TestPurchaseInvoice(unittest.TestCase): pe.insert() pe.submit() - pi_doc = frappe.get_doc('Purchase Invoice', pi.name) + pi_doc = frappe.get_doc("Purchase Invoice", pi.name) self.assertEqual(pi_doc.outstanding_amount, 0) def test_purchase_invoice_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC") + pi = make_purchase_invoice_against_cost_center( + cost_center=cost_center, credit_to="Creditors - _TC" + ) self.assertEqual(pi.cost_center, cost_center) expected_values = { - "Creditors - _TC": { - "cost_center": cost_center - }, - "_Test Account Cost for Goods Sold - _TC": { - "cost_center": cost_center - } + "Creditors - _TC": {"cost_center": cost_center}, + "_Test Account Cost for Goods Sold - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -894,21 +1085,21 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_without_cost_center(self): cost_center = "_Test Cost Center - _TC" - pi = make_purchase_invoice(credit_to="Creditors - _TC") + pi = make_purchase_invoice(credit_to="Creditors - _TC") expected_values = { - "Creditors - _TC": { - "cost_center": None - }, - "_Test Account Cost for Goods Sold - _TC": { - "cost_center": cost_center - } + "Creditors - _TC": {"cost_center": None}, + "_Test Account Cost for Goods Sold - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -916,36 +1107,40 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) def test_purchase_invoice_with_project_link(self): - project = make_project({ - 'project_name': 'Purchase Invoice Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2020-01-01' - }) - item_project = make_project({ - 'project_name': 'Purchase Invoice Item Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2019-06-01' - }) + project = make_project( + { + "project_name": "Purchase Invoice Project", + "project_template_name": "Test Project Template", + "start_date": "2020-01-01", + } + ) + item_project = make_project( + { + "project_name": "Purchase Invoice Item Project", + "project_template_name": "Test Project Template", + "start_date": "2019-06-01", + } + ) - pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1) + pi = make_purchase_invoice(credit_to="Creditors - _TC", do_not_save=1) pi.items[0].project = item_project.name pi.project = project.name pi.submit() expected_values = { - "Creditors - _TC": { - "project": project.name - }, - "_Test Account Cost for Goods Sold - _TC": { - "project": item_project.name - } + "Creditors - _TC": {"project": project.name}, + "_Test Account Cost for Goods Sold - _TC": {"project": item_project.name}, } - gl_entries = frappe.db.sql("""select account, cost_center, project, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, project, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -953,10 +1148,11 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account]["project"], gle.project) def test_deferred_expense_via_journal_entry(self): - deferred_account = create_account(account_name="Deferred Expense", - parent_account="Current Assets - _TC", company="_Test Company") + deferred_account = create_account( + account_name="Deferred Expense", parent_account="Current Assets - _TC", company="_Test Company" + ) - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 1 acc_settings.submit_journal_entries = 1 acc_settings.save() @@ -968,7 +1164,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True) pi.set_posting_time = 1 - pi.posting_date = '2019-01-10' + pi.posting_date = "2019-01-10" pi.items[0].enable_deferred_expense = 1 pi.items[0].service_start_date = "2019-01-10" pi.items[0].service_end_date = "2019-03-15" @@ -976,14 +1172,16 @@ class TestPurchaseInvoice(unittest.TestCase): pi.save() pi.submit() - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Expense", - company="_Test Company" - )) + pda1 = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Expense", + company="_Test Company", + ) + ) pda1.insert() pda1.submit() @@ -994,13 +1192,17 @@ class TestPurchaseInvoice(unittest.TestCase): ["_Test Account Cost for Goods Sold - _TC", 0.0, 43.08, "2019-02-28"], [deferred_account, 43.08, 0.0, "2019-02-28"], ["_Test Account Cost for Goods Sold - _TC", 0.0, 23.07, "2019-03-15"], - [deferred_account, 23.07, 0.0, "2019-03-15"] + [deferred_account, 23.07, 0.0, "2019-03-15"], ] - gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + gl_entries = gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s - order by posting_date asc, account asc""", (pi.items[0].name, pi.posting_date), as_dict=1) + order by posting_date asc, account asc""", + (pi.items[0].name, pi.posting_date), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1008,108 +1210,139 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() def test_gain_loss_with_advance_entry(self): unlink_enabled = frappe.db.get_value( - "Accounts Settings", "Accounts Settings", - "unlink_payment_on_cancel_of_invoice") + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" + ) frappe.db.set_value( - "Accounts Settings", "Accounts Settings", - "unlink_payment_on_cancel_of_invoice", 1) + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1 + ) original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") - frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC") + frappe.db.set_value( + "Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC" + ) - pay = frappe.get_doc({ - 'doctype': 'Payment Entry', - 'company': '_Test Company', - 'payment_type': 'Pay', - 'party_type': 'Supplier', - 'party': '_Test Supplier USD', - 'paid_to': '_Test Payable USD - _TC', - 'paid_from': 'Cash - _TC', - 'paid_amount': 70000, - 'target_exchange_rate': 70, - 'received_amount': 1000, - }) + pay = frappe.get_doc( + { + "doctype": "Payment Entry", + "company": "_Test Company", + "payment_type": "Pay", + "party_type": "Supplier", + "party": "_Test Supplier USD", + "paid_to": "_Test Payable USD - _TC", + "paid_from": "Cash - _TC", + "paid_amount": 70000, + "target_exchange_rate": 70, + "received_amount": 1000, + } + ) pay.insert() pay.submit() - pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", - conversion_rate=75, rate=500, do_not_save=1, qty=1) + pi = make_purchase_invoice( + supplier="_Test Supplier USD", + currency="USD", + conversion_rate=75, + rate=500, + do_not_save=1, + qty=1, + ) pi.cost_center = "_Test Cost Center - _TC" pi.advances = [] - pi.append("advances", { - "reference_type": "Payment Entry", - "reference_name": pay.name, - "advance_amount": 1000, - "remarks": pay.remarks, - "allocated_amount": 500, - "ref_exchange_rate": 70 - }) + pi.append( + "advances", + { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 1000, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70, + }, + ) pi.save() pi.submit() expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 37500.0], ["_Test Payable USD - _TC", -35000.0], - ["Exchange Gain/Loss - _TC", -2500.0] + ["Exchange Gain/Loss - _TC", -2500.0], ] - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select account, sum(debit - credit) as balance from `tabGL Entry` where voucher_no=%s group by account - order by account asc""", (pi.name), as_dict=1) + order by account asc""", + (pi.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) - pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", - conversion_rate=73, rate=500, do_not_save=1, qty=1) + pi_2 = make_purchase_invoice( + supplier="_Test Supplier USD", + currency="USD", + conversion_rate=73, + rate=500, + do_not_save=1, + qty=1, + ) pi_2.cost_center = "_Test Cost Center - _TC" pi_2.advances = [] - pi_2.append("advances", { - "reference_type": "Payment Entry", - "reference_name": pay.name, - "advance_amount": 500, - "remarks": pay.remarks, - "allocated_amount": 500, - "ref_exchange_rate": 70 - }) + pi_2.append( + "advances", + { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 500, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70, + }, + ) pi_2.save() pi_2.submit() expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 36500.0], ["_Test Payable USD - _TC", -35000.0], - ["Exchange Gain/Loss - _TC", -1500.0] + ["Exchange Gain/Loss - _TC", -1500.0], ] - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select account, sum(debit - credit) as balance from `tabGL Entry` where voucher_no=%s - group by account order by account asc""", (pi_2.name), as_dict=1) + group by account order by account asc""", + (pi_2.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) - expected_gle = [ - ["_Test Payable USD - _TC", 70000.0], - ["Cash - _TC", -70000.0] - ] + expected_gle = [["_Test Payable USD - _TC", 70000.0], ["Cash - _TC", -70000.0]] - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select account, sum(debit - credit) as balance from `tabGL Entry` where voucher_no=%s and is_cancelled=0 - group by account order by account asc""", (pay.name), as_dict=1) + group by account order by account asc""", + (pay.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1124,44 +1357,57 @@ class TestPurchaseInvoice(unittest.TestCase): pay.reload() pay.cancel() - frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled) + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled + ) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) def test_purchase_invoice_advance_taxes(self): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry # create a new supplier to test - supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier', - tax_withholding_category = 'TDS - 194 - Dividends - Individual') + supplier = create_supplier( + supplier_name="_Test TDS Advance Supplier", + tax_withholding_category="TDS - 194 - Dividends - Individual", + ) # Update tax withholding category with current fiscal year and rate details - update_tax_witholding_category('_Test Company', 'TDS Payable - _TC') + update_tax_witholding_category("_Test Company", "TDS Payable - _TC") # Create Purchase Order with TDS applied - po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item', - posting_date='2021-09-15') + po = create_purchase_order( + do_not_save=1, + supplier=supplier.name, + rate=3000, + item="_Test Non Stock Item", + posting_date="2021-09-15", + ) po.save() po.submit() # Create Payment Entry Against the order - payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name) - payment_entry.paid_from = 'Cash - _TC' + payment_entry = get_payment_entry(dt="Purchase Order", dn=po.name) + payment_entry.paid_from = "Cash - _TC" payment_entry.apply_tax_withholding_amount = 1 - payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual' + payment_entry.tax_withholding_category = "TDS - 194 - Dividends - Individual" payment_entry.save() payment_entry.submit() # Check GLE for Payment Entry expected_gle = [ - ['Cash - _TC', 0, 27000], - ['Creditors - _TC', 30000, 0], - ['TDS Payable - _TC', 0, 3000], + ["Cash - _TC", 0, 27000], + ["Creditors - _TC", 30000, 0], + ["TDS Payable - _TC", 0, 3000], ] - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", (payment_entry.name), as_dict=1) + order by account asc""", + (payment_entry.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1171,23 +1417,24 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Invoice against Purchase Order purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice.allocate_advances_automatically = 1 - purchase_invoice.items[0].item_code = '_Test Non Stock Item' - purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC' + purchase_invoice.items[0].item_code = "_Test Non Stock Item" + purchase_invoice.items[0].expense_account = "_Test Account Cost for Goods Sold - _TC" purchase_invoice.save() purchase_invoice.submit() # Check GLE for Purchase Invoice # Zero net effect on final TDS Payable on invoice - expected_gle = [ - ['_Test Account Cost for Goods Sold - _TC', 30000], - ['Creditors - _TC', -30000] - ] + expected_gle = [["_Test Account Cost for Goods Sold - _TC", 30000], ["Creditors - _TC", -30000]] - gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount + gl_entries = frappe.db.sql( + """select account, sum(debit - credit) as amount from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s group by account - order by account asc""", (purchase_invoice.name), as_dict=1) + order by account asc""", + (purchase_invoice.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1202,28 +1449,36 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) def test_provisional_accounting_entry(self): - item = create_item("_Test Non Stock Item", is_stock_item=0) - provisional_account = create_account(account_name="Provision Account", - parent_account="Current Liabilities - _TC", company="_Test Company") + create_item("_Test Non Stock Item", is_stock_item=0) - company = frappe.get_doc('Company', '_Test Company') + provisional_account = create_account( + account_name="Provision Account", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + + company = frappe.get_doc("Company", "_Test Company") company.enable_provisional_accounting_for_non_stock_items = 1 company.default_provisional_account = provisional_account company.save() - pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)) + pr = make_purchase_receipt( + item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2) + ) pi = create_purchase_invoice_from_receipt(pr.name) pi.set_posting_time = 1 pi.posting_date = add_days(pr.posting_date, -1) - pi.items[0].expense_account = 'Cost of Goods Sold - _TC' + pi.items[0].expense_account = "Cost of Goods Sold - _TC" pi.save() pi.submit() + self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC") + # Check GLE for Purchase Invoice expected_gle = [ - ['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)], - ['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)] + ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)], + ["Creditors - _TC", 0, 250, add_days(pr.posting_date, -1)], ] check_gl_entries(self, pi.name, expected_gle, pi.posting_date) @@ -1232,7 +1487,7 @@ class TestPurchaseInvoice(unittest.TestCase): ["Provision Account - _TC", 250, 0, pr.posting_date], ["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date], ["Provision Account - _TC", 0, 250, pi.posting_date], - ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date] + ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date], ] check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) @@ -1240,11 +1495,16 @@ class TestPurchaseInvoice(unittest.TestCase): company.enable_provisional_accounting_for_non_stock_items = 0 company.save() + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s - order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1) + order by posting_date asc, account asc""", + (voucher_no, posting_date), + as_dict=1, + ) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) @@ -1252,45 +1512,55 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + def update_tax_witholding_category(company, account): from erpnext.accounts.utils import get_fiscal_year fiscal_year = get_fiscal_year(date=nowdate()) - if not frappe.db.get_value('Tax Withholding Rate', - {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), - 'to_date': ('<=', fiscal_year[2])}): - tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual') - tds_category.set('rates', []) + if not frappe.db.get_value( + "Tax Withholding Rate", + { + "parent": "TDS - 194 - Dividends - Individual", + "from_date": (">=", fiscal_year[1]), + "to_date": ("<=", fiscal_year[2]), + }, + ): + tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual") + tds_category.set("rates", []) - tds_category.append('rates', { - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 2500, - 'cumulative_threshold': 0 - }) + tds_category.append( + "rates", + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 2500, + "cumulative_threshold": 0, + }, + ) tds_category.save() - if not frappe.db.get_value('Tax Withholding Account', - {'parent': 'TDS - 194 - Dividends - Individual', 'account': account}): - tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual') - tds_category.append('accounts', { - 'company': company, - 'account': account - }) + if not frappe.db.get_value( + "Tax Withholding Account", {"parent": "TDS - 194 - Dividends - Individual", "account": account} + ): + tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual") + tds_category.append("accounts", {"company": company, "account": account}) tds_category.save() + def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.unlink_payment_on_cancellation_of_invoice = enable accounts_settings.save() + def enable_discount_accounting(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.enable_discount_accounting = enable accounts_settings.save() + def make_purchase_invoice(**args): pi = frappe.new_doc("Purchase Invoice") args = frappe._dict(args) @@ -1303,7 +1573,7 @@ def make_purchase_invoice(**args): pi.is_paid = 1 if args.cash_bank_account: - pi.cash_bank_account=args.cash_bank_account + pi.cash_bank_account = args.cash_bank_account pi.company = args.company or "_Test Company" pi.supplier = args.supplier or "_Test Supplier" @@ -1315,27 +1585,30 @@ def make_purchase_invoice(**args): pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" pi.cost_center = args.parent_cost_center - pi.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 5, - "received_qty": args.received_qty or 0, - "rejected_qty": args.rejected_qty or 0, - "rate": args.rate or 50, - "price_list_rate": args.price_list_rate or 50, - "expense_account": args.expense_account or '_Test Account Cost for Goods Sold - _TC', - "discount_account": args.discount_account or None, - "discount_amount": args.discount_amount or 0, - "conversion_factor": 1.0, - "serial_no": args.serial_no, - "stock_uom": args.uom or "_Test UOM", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "project": args.project, - "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "", - "asset_location": args.location or "", - "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0 - }) + pi.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 5, + "received_qty": args.received_qty or 0, + "rejected_qty": args.rejected_qty or 0, + "rate": args.rate or 50, + "price_list_rate": args.price_list_rate or 50, + "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", + "discount_account": args.discount_account or None, + "discount_amount": args.discount_amount or 0, + "conversion_factor": 1.0, + "serial_no": args.serial_no, + "stock_uom": args.uom or "_Test UOM", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "project": args.project, + "rejected_warehouse": args.rejected_warehouse or "", + "rejected_serial_no": args.rejected_serial_no or "", + "asset_location": args.location or "", + "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0, + }, + ) if args.get_taxes_and_charges: taxes = get_taxes() @@ -1348,6 +1621,7 @@ def make_purchase_invoice(**args): pi.submit() return pi + def make_purchase_invoice_against_cost_center(**args): pi = frappe.new_doc("Purchase Invoice") args = frappe._dict(args) @@ -1360,7 +1634,7 @@ def make_purchase_invoice_against_cost_center(**args): pi.is_paid = 1 if args.cash_bank_account: - pi.cash_bank_account=args.cash_bank_account + pi.cash_bank_account = args.cash_bank_account pi.company = args.company or "_Test Company" pi.cost_center = args.cost_center or "_Test Cost Center - _TC" @@ -1374,25 +1648,29 @@ def make_purchase_invoice_against_cost_center(**args): if args.supplier_warehouse: pi.supplier_warehouse = "_Test Warehouse 1 - _TC" - pi.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 5, - "received_qty": args.received_qty or 0, - "rejected_qty": args.rejected_qty or 0, - "rate": args.rate or 50, - "conversion_factor": 1.0, - "serial_no": args.serial_no, - "stock_uom": "_Test UOM", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "project": args.project, - "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "" - }) + pi.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 5, + "received_qty": args.received_qty or 0, + "rejected_qty": args.rejected_qty or 0, + "rate": args.rate or 50, + "conversion_factor": 1.0, + "serial_no": args.serial_no, + "stock_uom": "_Test UOM", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "project": args.project, + "rejected_warehouse": args.rejected_warehouse or "", + "rejected_serial_no": args.rejected_serial_no or "", + }, + ) if not args.do_not_save: pi.insert() if not args.do_not_submit: pi.submit() return pi -test_records = frappe.get_test_records('Purchase Invoice') + +test_records = frappe.get_test_records("Purchase Invoice") diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py index f5eb404d0a4..70d29bfda25 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py @@ -16,5 +16,5 @@ class PurchaseTaxesandChargesTemplate(Document): def autoname(self): if self.company and self.title: - abbr = frappe.get_cached_value('Company', self.company, 'abbr') - self.name = '{0} - {1}'.format(self.title, abbr) + abbr = frappe.get_cached_value("Company", self.company, "abbr") + self.name = "{0} - {1}".format(self.title, abbr) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py index 95a7a1c889b..1f0ea211f20 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py @@ -1,21 +1,17 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'taxes_and_charges', - 'non_standard_fieldnames': { - 'Tax Rule': 'purchase_tax_template', + "fieldname": "taxes_and_charges", + "non_standard_fieldnames": { + "Tax Rule": "purchase_tax_template", }, - 'transactions': [ + "transactions": [ { - 'label': _('Transactions'), - 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] + "label": _("Transactions"), + "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"], }, - { - 'label': _('References'), - 'items': ['Supplier Quotation', 'Tax Rule'] - } - ] + {"label": _("References"), "items": ["Supplier Quotation", "Tax Rule"]}, + ], } diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py index b5b4a67d759..1d02f055048 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Purchase Taxes and Charges Template') + class TestPurchaseTaxesandChargesTemplate(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8a6d3cd5935..c6a110dcab6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte var me = this; this._super(); - this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry']; + this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', + 'POS Closing Entry', 'Journal Entry', 'Payment 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); @@ -281,6 +283,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte } var me = this; if(this.frm.updating_party_details) return; + + if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return; + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 5062c1c807a..80b95db8868 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-05-24 19:29:05", + "creation": "2022-01-25 10:29:57.771398", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -651,7 +651,6 @@ "hide_seconds": 1, "label": "Ignore Pricing Rule", "no_copy": 1, - "permlevel": 0, "print_hide": 1 }, { @@ -1974,9 +1973,10 @@ }, { "default": "0", + "description": "Issue a debit note with 0 qty against an existing Sales Invoice", "fieldname": "is_debit_note", "fieldtype": "Check", - "label": "Is Debit Note" + "label": "Is Rate Adjustment Entry (Debit Note)" }, { "default": "0", @@ -2038,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-12-23 20:19:38.667508", + "modified": "2022-03-08 16:08:53.517903", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2089,8 +2089,9 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 42da6b7708f..e39822e4036 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -38,36 +38,41 @@ from erpnext.assets.doctype.asset.depreciation import ( get_gl_entries_on_asset_regain, make_depreciation_entry, ) +from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController from erpnext.healthcare.utils import manage_invoice_submit_cancel from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.setup.doctype.company.company import update_company_current_month_sales from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so -from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos -from erpnext.stock.utils import calculate_mapped_packed_items_return +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivery_note_serial_no, + get_serial_nos, + update_serial_nos_after_submit, +) + +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} class SalesInvoice(SellingController): def __init__(self, *args, **kwargs): super(SalesInvoice, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Sales Invoice Item', - 'target_field': 'billed_amt', - 'target_ref_field': 'amount', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_parent_dt': 'Sales Order', - 'target_parent_field': 'per_billed', - 'source_field': 'amount', - 'percent_join_field': 'sales_order', - 'status_field': 'billing_status', - 'keyword': 'Billed', - 'overflow_type': 'billing' - }] + self.status_updater = [ + { + "source_dt": "Sales Invoice Item", + "target_field": "billed_amt", + "target_ref_field": "amount", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_parent_dt": "Sales Order", + "target_parent_field": "per_billed", + "source_field": "amount", + "percent_join_field": "sales_order", + "status_field": "billing_status", + "keyword": "Billed", + "overflow_type": "billing", + } + ] def set_indicator(self): """Set indicator for portal""" @@ -110,7 +115,11 @@ class SalesInvoice(SellingController): self.validate_fixed_asset() self.set_income_account_for_fixed_assets() self.validate_item_cost_centers() - validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference) + self.validate_income_account() + + validate_inter_company_party( + self.doctype, self.customer, self.company, self.inter_company_invoice_reference + ) if cint(self.is_pos): self.validate_pos() @@ -126,15 +135,21 @@ class SalesInvoice(SellingController): validate_service_stop_date(self) if not self.is_opening: - self.is_opening = 'No' + self.is_opening = "No" - if self._action != 'submit' and self.update_stock and not self.is_return: - set_batch_nos(self, 'warehouse', True) + if self._action != "submit" and self.update_stock and not self.is_return: + set_batch_nos(self, "warehouse", True) if self.redeem_loyalty_points: - lp = frappe.get_doc('Loyalty Program', self.loyalty_program) - self.loyalty_redemption_account = lp.expense_account if not self.loyalty_redemption_account else self.loyalty_redemption_account - self.loyalty_redemption_cost_center = lp.cost_center if not self.loyalty_redemption_cost_center else self.loyalty_redemption_cost_center + lp = frappe.get_doc("Loyalty Program", self.loyalty_program) + self.loyalty_redemption_account = ( + lp.expense_account if not self.loyalty_redemption_account else self.loyalty_redemption_account + ) + self.loyalty_redemption_cost_center = ( + lp.cost_center + if not self.loyalty_redemption_cost_center + else self.loyalty_redemption_cost_center + ) self.set_against_income_account() self.validate_c_form() @@ -151,11 +166,16 @@ class SalesInvoice(SellingController): if self.is_pos and not self.is_return: self.verify_payment_amount_is_positive() - #validate amount in mode of payments for returned invoices for pos must be negative + # validate amount in mode of payments for returned invoices for pos must be negative if self.is_pos and self.is_return: self.verify_payment_amount_is_negative() - if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated: + if ( + self.redeem_loyalty_points + and self.loyalty_program + and self.loyalty_points + and not self.is_consolidated + ): validate_loyalty_points(self, self.loyalty_points) self.reset_default_field_value("set_warehouse", "items", "warehouse") @@ -168,14 +188,28 @@ class SalesInvoice(SellingController): if self.update_stock: frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return): - frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status)) + elif asset.status in ("Scrapped", "Cancelled") or ( + asset.status == "Sold" and not self.is_return + ): + frappe.throw( + _("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format( + d.idx, d.asset, asset.status + ) + ) def validate_item_cost_centers(self): for item in self.items: cost_center_company = frappe.get_cached_value("Cost Center", item.cost_center, "company") if cost_center_company != self.company: - frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company))) + frappe.throw( + _("Row #{0}: Cost Center {1} does not belong to company {2}").format( + frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company) + ) + ) + + def validate_income_account(self): + for item in self.get("items"): + validate_account_head(item.idx, item.income_account, self.company, "Income") def set_tax_withholding(self): tax_withholding_details = get_party_tax_withholding_details(self) @@ -194,8 +228,11 @@ class SalesInvoice(SellingController): if not accounts or tax_withholding_account not in accounts: self.append("taxes", tax_withholding_details) - to_remove = [d for d in self.taxes - if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account] + to_remove = [ + d + for d in self.taxes + if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account + ] for d in to_remove: self.remove(d) @@ -210,8 +247,9 @@ class SalesInvoice(SellingController): self.validate_pos_paid_amount() if not self.auto_repeat: - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) self.check_prev_docstatus() @@ -228,6 +266,8 @@ class SalesInvoice(SellingController): # because updating reserved qty in bin depends upon updated delivered qty in SO if self.update_stock == 1: self.update_stock_ledger() + if self.is_return and self.update_stock: + update_serial_nos_after_submit(self, "items") # this sequence because outstanding may get -ve self.make_gl_entries() @@ -247,7 +287,9 @@ class SalesInvoice(SellingController): self.update_time_sheet(self.name) - if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": + if ( + frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" + ): update_company_current_month_sales(self.company) self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) @@ -255,7 +297,9 @@ class SalesInvoice(SellingController): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program if not self.is_return and not self.is_consolidated and self.loyalty_program: self.make_loyalty_point_entry() - elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program: + elif ( + self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program + ): against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) against_si_doc.delete_loyalty_point_entry() against_si_doc.make_loyalty_point_entry() @@ -263,7 +307,7 @@ class SalesInvoice(SellingController): self.apply_loyalty_points() # Healthcare Service Invoice. - domain_settings = frappe.get_doc('Domain Settings') + domain_settings = frappe.get_doc("Domain Settings") active_domains = [d.domain for d in domain_settings.active_domains] if "Healthcare" in active_domains: @@ -272,6 +316,9 @@ class SalesInvoice(SellingController): self.process_common_party_accounting() def validate_pos_return(self): + if self.is_consolidated: + # pos return is already validated in pos invoice + return if self.is_pos and self.is_return: total_amount_in_payments = 0 @@ -288,16 +335,16 @@ class SalesInvoice(SellingController): def check_if_consolidated_invoice(self): # since POS Invoice extends Sales Invoice, we explicitly check if doctype is Sales Invoice if self.doctype == "Sales Invoice" and self.is_consolidated: - invoice_or_credit_note = "consolidated_credit_note" if self.is_return else "consolidated_invoice" + invoice_or_credit_note = ( + "consolidated_credit_note" if self.is_return else "consolidated_invoice" + ) pos_closing_entry = frappe.get_all( - "POS Invoice Merge Log", - filters={ invoice_or_credit_note: self.name }, - pluck="pos_closing_entry" + "POS Invoice Merge Log", filters={invoice_or_credit_note: self.name}, pluck="pos_closing_entry" ) if pos_closing_entry and pos_closing_entry[0]: msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( frappe.bold("Consolidated Sales Invoice"), - get_link_to_form("POS Closing Entry", pos_closing_entry[0]) + get_link_to_form("POS Closing Entry", pos_closing_entry[0]), ) frappe.throw(msg, title=_("Not Allowed")) @@ -339,14 +386,18 @@ class SalesInvoice(SellingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") - if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": + if ( + frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" + ): update_company_current_month_sales(self.company) self.update_project() if not self.is_return and not self.is_consolidated and self.loyalty_program: self.delete_loyalty_point_entry() - elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program: + elif ( + self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program + ): against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) against_si_doc.delete_loyalty_point_entry() against_si_doc.make_loyalty_point_entry() @@ -354,56 +405,62 @@ class SalesInvoice(SellingController): unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) # Healthcare Service Invoice. - domain_settings = frappe.get_doc('Domain Settings') + domain_settings = frappe.get_doc("Domain Settings") active_domains = [d.domain for d in domain_settings.active_domains] if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_cancel") self.unlink_sales_invoice_from_timesheets() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") def update_status_updater_args(self): if cint(self.update_stock): - self.status_updater.append({ - 'source_dt':'Sales Invoice Item', - 'target_dt':'Sales Order Item', - 'target_parent_dt':'Sales Order', - 'target_parent_field':'per_delivered', - 'target_field':'delivered_qty', - 'target_ref_field':'qty', - 'source_field':'qty', - 'join_field':'so_detail', - 'percent_join_field':'sales_order', - 'status_field':'delivery_status', - 'keyword':'Delivered', - 'second_source_dt': 'Delivery Note Item', - 'second_source_field': 'qty', - 'second_join_field': 'so_detail', - 'overflow_type': 'delivery', - 'extra_cond': """ and exists(select name from `tabSales Invoice` - where name=`tabSales Invoice Item`.parent and update_stock = 1)""" - }) + self.status_updater.append( + { + "source_dt": "Sales Invoice Item", + "target_dt": "Sales Order Item", + "target_parent_dt": "Sales Order", + "target_parent_field": "per_delivered", + "target_field": "delivered_qty", + "target_ref_field": "qty", + "source_field": "qty", + "join_field": "so_detail", + "percent_join_field": "sales_order", + "status_field": "delivery_status", + "keyword": "Delivered", + "second_source_dt": "Delivery Note Item", + "second_source_field": "qty", + "second_join_field": "so_detail", + "overflow_type": "delivery", + "extra_cond": """ and exists(select name from `tabSales Invoice` + where name=`tabSales Invoice Item`.parent and update_stock = 1)""", + } + ) if cint(self.is_return): - self.status_updater.append({ - 'source_dt': 'Sales Invoice Item', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Sales Order', - 'source_field': '-1 * qty', - 'second_source_dt': 'Delivery Note Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'so_detail', - 'extra_cond': """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and update_stock=1 and is_return=1)""" - }) + self.status_updater.append( + { + "source_dt": "Sales Invoice Item", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_field": "returned_qty", + "target_parent_dt": "Sales Order", + "source_field": "-1 * qty", + "second_source_dt": "Delivery Note Item", + "second_source_field": "-1 * qty", + "second_join_field": "so_detail", + "extra_cond": """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and update_stock=1 and is_return=1)""", + } + ) def check_credit_limit(self): from erpnext.selling.doctype.customer.customer import check_credit_limit validate_against_credit_limit = False - bypass_credit_limit_check_at_sales_order = frappe.db.get_value("Customer Credit Limit", - filters={'parent': self.customer, 'parenttype': 'Customer', 'company': self.company}, - fieldname=["bypass_credit_limit_check"]) + bypass_credit_limit_check_at_sales_order = frappe.db.get_value( + "Customer Credit Limit", + filters={"parent": self.customer, "parenttype": "Customer", "company": self.company}, + fieldname=["bypass_credit_limit_check"], + ) if bypass_credit_limit_check_at_sales_order: validate_against_credit_limit = True @@ -417,7 +474,7 @@ class SalesInvoice(SellingController): def unlink_sales_invoice_from_timesheets(self): for row in self.timesheets: - timesheet = frappe.get_doc('Timesheet', row.time_sheet) + timesheet = frappe.get_doc("Timesheet", row.time_sheet) for time_log in timesheet.time_logs: if time_log.sales_invoice == self.name: time_log.sales_invoice = None @@ -433,15 +490,17 @@ class SalesInvoice(SellingController): if not self.debit_to: self.debit_to = get_party_account("Customer", self.customer, self.company) - self.party_account_currency = frappe.db.get_value("Account", self.debit_to, "account_currency", cache=True) + self.party_account_currency = frappe.db.get_value( + "Account", self.debit_to, "account_currency", cache=True + ) if not self.due_date and self.customer: self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company) super(SalesInvoice, self).set_missing_values(for_validate) print_format = pos.get("print_format") if pos else None - if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')): - print_format = 'POS Invoice' + if not print_format and not cint(frappe.db.get_value("Print Format", "POS Invoice", "disabled")): + print_format = "POS Invoice" if pos: return { @@ -449,7 +508,7 @@ class SalesInvoice(SellingController): "allow_edit_rate": pos.get("allow_user_to_edit_rate"), "allow_edit_discount": pos.get("allow_user_to_edit_discount"), "campaign": pos.get("campaign"), - "allow_print_before_pay": pos.get("allow_print_before_pay") + "allow_print_before_pay": pos.get("allow_print_before_pay"), } def update_time_sheet(self, sales_invoice): @@ -465,9 +524,11 @@ class SalesInvoice(SellingController): def update_time_sheet_detail(self, timesheet, args, sales_invoice): for data in timesheet.time_logs: - if (self.project and args.timesheet_detail == data.name) or \ - (not self.project and not data.sales_invoice) or \ - (not sales_invoice and data.sales_invoice == self.name): + if ( + (self.project and args.timesheet_detail == data.name) + or (not self.project and not data.sales_invoice) + or (not sales_invoice and data.sales_invoice == self.name) + ): data.sales_invoice = sales_invoice def on_update(self): @@ -477,7 +538,7 @@ class SalesInvoice(SellingController): paid_amount = 0.0 base_paid_amount = 0.0 for data in self.payments: - data.base_amount = flt(data.amount*self.conversion_rate, self.precision("base_paid_amount")) + data.base_amount = flt(data.amount * self.conversion_rate, self.precision("base_paid_amount")) paid_amount += data.amount base_paid_amount += data.base_amount @@ -488,7 +549,7 @@ class SalesInvoice(SellingController): for data in self.timesheets: if data.time_sheet: status = frappe.db.get_value("Timesheet", data.time_sheet, "status") - if status not in ['Submitted', 'Payslip']: + if status not in ["Submitted", "Payslip"]: frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet)) def set_pos_fields(self, for_validate=False): @@ -497,20 +558,23 @@ class SalesInvoice(SellingController): return if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + self.account_for_change_amount = frappe.get_cached_value( + "Company", self.company, "default_cash_account" + ) from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details + if not self.pos_profile and not self.flags.ignore_pos_profile: pos_profile = get_pos_profile(self.company) or {} if not pos_profile: return - self.pos_profile = pos_profile.get('name') + self.pos_profile = pos_profile.get("name") pos = {} if self.pos_profile: - pos = frappe.get_doc('POS Profile', self.pos_profile) + pos = frappe.get_doc("POS Profile", self.pos_profile) - if not self.get('payments') and not for_validate: + if not self.get("payments") and not for_validate: update_multi_mode_option(self, pos) if pos: @@ -523,35 +587,52 @@ class SalesInvoice(SellingController): if not for_validate: self.ignore_pricing_rule = pos.ignore_pricing_rule - if pos.get('account_for_change_amount'): - self.account_for_change_amount = pos.get('account_for_change_amount') + if pos.get("account_for_change_amount"): + self.account_for_change_amount = pos.get("account_for_change_amount") - for fieldname in ('currency', 'letter_head', 'tc_name', - 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', - 'write_off_cost_center', 'apply_discount_on', 'cost_center'): - if (not for_validate) or (for_validate and not self.get(fieldname)): - self.set(fieldname, pos.get(fieldname)) + for fieldname in ( + "currency", + "letter_head", + "tc_name", + "company", + "select_print_heading", + "write_off_account", + "taxes_and_charges", + "write_off_cost_center", + "apply_discount_on", + "cost_center", + ): + if (not for_validate) or (for_validate and not self.get(fieldname)): + self.set(fieldname, pos.get(fieldname)) if pos.get("company_address"): self.company_address = pos.get("company_address") if self.customer: - customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) - customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list') - selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') + customer_price_list, customer_group = frappe.get_value( + "Customer", self.customer, ["default_price_list", "customer_group"] + ) + customer_group_price_list = frappe.get_value( + "Customer Group", customer_group, "default_price_list" + ) + selling_price_list = ( + customer_price_list or customer_group_price_list or pos.get("selling_price_list") + ) else: - selling_price_list = pos.get('selling_price_list') + selling_price_list = pos.get("selling_price_list") if selling_price_list: - self.set('selling_price_list', selling_price_list) + self.set("selling_price_list", selling_price_list) if not for_validate: self.update_stock = cint(pos.get("update_stock")) # 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, update_data=True) + if item.get("item_code"): + 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) @@ -575,22 +656,29 @@ class SalesInvoice(SellingController): if not self.debit_to: self.raise_missing_debit_credit_account_error("Customer", self.customer) - account = frappe.get_cached_value("Account", self.debit_to, - ["account_type", "report_type", "account_currency"], as_dict=True) + account = frappe.get_cached_value( + "Account", self.debit_to, ["account_type", "report_type", "account_currency"], as_dict=True + ) if not account: frappe.throw(_("Debit To is required"), title=_("Account Missing")) if account.report_type != "Balance Sheet": - msg = _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " " - msg += _("You can change the parent account to a Balance Sheet account or select a different account.") + msg = ( + _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " " + ) + msg += _( + "You can change the parent account to a Balance Sheet account or select a different account." + ) frappe.throw(msg, title=_("Invalid Account")) if self.customer and account.account_type != "Receivable": - msg = _("Please ensure {} account {} is a Receivable account.").format( - frappe.bold("Debit To"), - frappe.bold(self.debit_to) - ) + " " + msg = ( + _("Please ensure {} account {} is a Receivable account.").format( + frappe.bold("Debit To"), frappe.bold(self.debit_to) + ) + + " " + ) msg += _("Change the account type to Receivable or select a different account.") frappe.throw(msg, title=_("Invalid Account")) @@ -599,52 +687,60 @@ class SalesInvoice(SellingController): def clear_unallocated_mode_of_payments(self): self.set("payments", self.get("payments", {"amount": ["not in", [0, None, ""]]})) - frappe.db.sql("""delete from `tabSales Invoice Payment` where parent = %s - and amount = 0""", self.name) + frappe.db.sql( + """delete from `tabSales Invoice Payment` where parent = %s + and amount = 0""", + self.name, + ) def validate_with_previous_doc(self): - super(SalesInvoice, self).validate_with_previous_doc({ - "Sales Order": { - "ref_dn_field": "sales_order", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Sales Order Item": { - "ref_dn_field": "so_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - "Delivery Note": { - "ref_dn_field": "delivery_note", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Delivery Note Item": { - "ref_dn_field": "dn_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - }) + super(SalesInvoice, self).validate_with_previous_doc( + { + "Sales Order": { + "ref_dn_field": "sales_order", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Sales Order Item": { + "ref_dn_field": "so_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + "Delivery Note": { + "ref_dn_field": "delivery_note", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Delivery Note Item": { + "ref_dn_field": "dn_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } + ) - if cint(frappe.db.get_single_value('Selling Settings', 'maintain_same_sales_rate')) and not self.is_return: - self.validate_rate_with_reference_doc([ - ["Sales Order", "sales_order", "so_detail"], - ["Delivery Note", "delivery_note", "dn_detail"] - ]) + if ( + cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) + and not self.is_return + ): + self.validate_rate_with_reference_doc( + [["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]] + ) def set_against_income_account(self): """Set against account for debit to account""" against_acc = [] - for d in self.get('items'): + for d in self.get("items"): if d.income_account and d.income_account not in against_acc: against_acc.append(d.income_account) - self.against_income_account = ','.join(against_acc) + self.against_income_account = ",".join(against_acc) def add_remarks(self): if not self.remarks: if self.po_no and self.po_date: - self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, - formatdate(self.po_date)) + self.remarks = _("Against Customer Order {0} dated {1}").format( + self.po_no, formatdate(self.po_date) + ) else: self.remarks = _("No Remarks") @@ -660,36 +756,41 @@ class SalesInvoice(SellingController): if self.is_return: return - prev_doc_field_map = {'Sales Order': ['so_required', 'is_pos'],'Delivery Note': ['dn_required', 'update_stock']} + prev_doc_field_map = { + "Sales Order": ["so_required", "is_pos"], + "Delivery Note": ["dn_required", "update_stock"], + } for key, value in iteritems(prev_doc_field_map): - if frappe.db.get_single_value('Selling Settings', value[0]) == 'Yes': + if frappe.db.get_single_value("Selling Settings", value[0]) == "Yes": - if frappe.get_value('Customer', self.customer, value[0]): + if frappe.get_value("Customer", self.customer, value[0]): continue - for d in self.get('items'): - if (d.item_code and not d.get(key.lower().replace(' ', '_')) and not self.get(value[1])): + for d in self.get("items"): + if d.item_code and not d.get(key.lower().replace(" ", "_")) and not self.get(value[1]): msgprint(_("{0} is mandatory for Item {1}").format(key, d.item_code), raise_exception=1) - def validate_proj_cust(self): """check for does customer belong to same project as entered..""" if self.project and self.customer: - res = frappe.db.sql("""select name from `tabProject` + res = frappe.db.sql( + """select name from `tabProject` where name = %s and (customer = %s or customer is null or customer = '')""", - (self.project, self.customer)) + (self.project, self.customer), + ) if not res: - throw(_("Customer {0} does not belong to project {1}").format(self.customer,self.project)) + throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) def validate_pos(self): if self.is_return: invoice_total = self.rounded_total or self.grand_total - if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \ - 1.0/(10.0**(self.precision("grand_total") + 1.0)): - frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) + if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > 1.0 / ( + 10.0 ** (self.precision("grand_total") + 1.0) + ): + frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) def validate_item_code(self): - for d in self.get('items'): + for d in self.get("items"): if not d.item_code and self.is_opening == "No": msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True) @@ -697,17 +798,24 @@ class SalesInvoice(SellingController): super(SalesInvoice, self).validate_warehouse() for d in self.get_item_list(): - if not d.warehouse and d.item_code and frappe.get_cached_value("Item", d.item_code, "is_stock_item"): + if ( + not d.warehouse + and d.item_code + and frappe.get_cached_value("Item", d.item_code, "is_stock_item") + ): frappe.throw(_("Warehouse required for stock Item {0}").format(d.item_code)) def validate_delivery_note(self): for d in self.get("items"): if d.delivery_note: - msgprint(_("Stock cannot be updated against Delivery Note {0}").format(d.delivery_note), raise_exception=1) + msgprint( + _("Stock cannot be updated against Delivery Note {0}").format(d.delivery_note), + raise_exception=1, + ) def validate_write_off_account(self): if flt(self.write_off_amount) and not self.write_off_account: - self.write_off_account = frappe.get_cached_value('Company', self.company, 'write_off_account') + self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account") if flt(self.write_off_amount) and not self.write_off_account: msgprint(_("Please enter Write Off Account"), raise_exception=1) @@ -717,18 +825,23 @@ class SalesInvoice(SellingController): msgprint(_("Please enter Account for Change Amount"), raise_exception=1) def validate_c_form(self): - """ Blank C-form no if C-form applicable marked as 'No'""" - if self.amended_from and self.c_form_applicable == 'No' and self.c_form_no: - frappe.db.sql("""delete from `tabC-Form Invoice Detail` where invoice_no = %s - and parent = %s""", (self.amended_from, self.c_form_no)) + """Blank C-form no if C-form applicable marked as 'No'""" + if self.amended_from and self.c_form_applicable == "No" and self.c_form_no: + frappe.db.sql( + """delete from `tabC-Form Invoice Detail` where invoice_no = %s + and parent = %s""", + (self.amended_from, self.c_form_no), + ) - frappe.db.set(self, 'c_form_no', '') + frappe.db.set(self, "c_form_no", "") def validate_c_form_on_cancel(self): - """ Display message if C-Form no exists on cancellation of Sales Invoice""" - if self.c_form_applicable == 'Yes' and self.c_form_no: - msgprint(_("Please remove this Invoice {0} from C-Form {1}") - .format(self.name, self.c_form_no), raise_exception = 1) + """Display message if C-Form no exists on cancellation of Sales Invoice""" + if self.c_form_applicable == "Yes" and self.c_form_no: + msgprint( + _("Please remove this Invoice {0} from C-Form {1}").format(self.name, self.c_form_no), + raise_exception=1, + ) def validate_dropship_item(self): for item in self.items: @@ -737,30 +850,36 @@ class SalesInvoice(SellingController): frappe.throw(_("Could not update stock, invoice contains drop shipping item.")) def update_current_stock(self): - for d in self.get('items'): + for d in self.get("items"): if d.item_code and d.warehouse: - bin = frappe.db.sql("select actual_qty from `tabBin` where item_code = %s and warehouse = %s", (d.item_code, d.warehouse), as_dict = 1) - d.actual_qty = bin and flt(bin[0]['actual_qty']) or 0 + bin = frappe.db.sql( + "select actual_qty from `tabBin` where item_code = %s and warehouse = %s", + (d.item_code, d.warehouse), + as_dict=1, + ) + d.actual_qty = bin and flt(bin[0]["actual_qty"]) or 0 - for d in self.get('packed_items'): - bin = frappe.db.sql("select actual_qty, projected_qty from `tabBin` where item_code = %s and warehouse = %s", (d.item_code, d.warehouse), as_dict = 1) - d.actual_qty = bin and flt(bin[0]['actual_qty']) or 0 - d.projected_qty = bin and flt(bin[0]['projected_qty']) or 0 + for d in self.get("packed_items"): + bin = frappe.db.sql( + "select actual_qty, projected_qty from `tabBin` where item_code = %s and warehouse = %s", + (d.item_code, d.warehouse), + as_dict=1, + ) + d.actual_qty = bin and flt(bin[0]["actual_qty"]) or 0 + d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0 def update_packing_list(self): if cint(self.update_stock) == 1: - if cint(self.is_return) and self.return_against: - calculate_mapped_packed_items_return(self) - else: - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self) + from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + + make_packing_list(self) else: - self.set('packed_items', []) + self.set("packed_items", []) def set_billing_hours_and_amount(self): if not self.project: for timesheet in self.timesheets: - ts_doc = frappe.get_doc('Timesheet', timesheet.time_sheet) + ts_doc = frappe.get_doc("Timesheet", timesheet.time_sheet) if not timesheet.billing_hours and ts_doc.total_billable_hours: timesheet.billing_hours = ts_doc.total_billable_hours @@ -775,17 +894,20 @@ class SalesInvoice(SellingController): @frappe.whitelist() def add_timesheet_data(self): - self.set('timesheets', []) + self.set("timesheets", []) if self.project: for data in get_projectwise_timesheet_data(self.project): - self.append('timesheets', { - 'time_sheet': data.time_sheet, - 'billing_hours': data.billing_hours, - 'billing_amount': data.billing_amount, - 'timesheet_detail': data.name, - 'activity_type': data.activity_type, - 'description': data.description - }) + self.append( + "timesheets", + { + "time_sheet": data.time_sheet, + "billing_hours": data.billing_hours, + "billing_amount": data.billing_amount, + "timesheet_detail": data.name, + "activity_type": data.activity_type, + "description": data.description, + }, + ) self.calculate_billing_amount_for_timesheet() @@ -797,13 +919,19 @@ class SalesInvoice(SellingController): self.total_billing_hours = timesheet_sum("billing_hours") def get_warehouse(self): - user_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile` - where ifnull(user,'') = %s and company = %s""", (frappe.session['user'], self.company)) + user_pos_profile = frappe.db.sql( + """select name, warehouse from `tabPOS Profile` + where ifnull(user,'') = %s and company = %s""", + (frappe.session["user"], self.company), + ) warehouse = user_pos_profile[0][1] if user_pos_profile else None if not warehouse: - global_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile` - where (user is null or user = '') and company = %s""", self.company) + global_pos_profile = frappe.db.sql( + """select name, warehouse from `tabPOS Profile` + where (user is null or user = '') and company = %s""", + self.company, + ) if global_pos_profile: warehouse = global_pos_profile[0][1] @@ -817,14 +945,16 @@ class SalesInvoice(SellingController): for d in self.get("items"): if d.is_fixed_asset: if not disposal_account: - disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(self.company) + disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center( + self.company + ) d.income_account = disposal_account if not d.cost_center: d.cost_center = depreciation_cost_center def check_prev_docstatus(self): - for d in self.get('items'): + for d in self.get("items"): if d.sales_order and frappe.db.get_value("Sales Order", d.sales_order, "docstatus") != 1: frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order)) @@ -840,22 +970,35 @@ class SalesInvoice(SellingController): if gl_entries: # if POS and amount is written off, updating outstanding amt after posting all gl entries - update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or - cint(self.redeem_loyalty_points)) else "Yes" + update_outstanding = ( + "No" + if (cint(self.is_pos) or self.write_off_account or cint(self.redeem_loyalty_points)) + else "Yes" + ) if self.docstatus == 1: - make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost) + make_gl_entries( + gl_entries, + update_outstanding=update_outstanding, + merge_entries=False, + from_repost=from_repost, + ) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt - update_outstanding_amt(self.debit_to, "Customer", self.customer, - self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name) - elif self.docstatus == 2 and cint(self.update_stock) \ - and cint(auto_accounting_for_stock): - make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + update_outstanding_amt( + self.debit_to, + "Customer", + self.customer, + self.doctype, + self.return_against if cint(self.is_return) and self.return_against else self.name, + ) + + elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock): + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import merge_similar_entries @@ -885,27 +1028,40 @@ class SalesInvoice(SellingController): def make_customer_gl_entry(self, gl_entries): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total - grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total) - else self.base_grand_total, self.precision("base_grand_total")) + grand_total = ( + self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + ) + base_grand_total = flt( + self.base_rounded_total + if (self.base_rounding_adjustment and self.base_rounded_total) + else self.base_grand_total, + self.precision("base_grand_total"), + ) if grand_total and not self.is_internal_transfer(): # Didnot use base_grand_total to book rounding loss gle gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "due_date": self.due_date, - "against": self.against_income_account, - "debit": base_grand_total, - "debit_in_account_currency": base_grand_total \ - if self.party_account_currency==self.company_currency else grand_total, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "due_date": self.due_date, + "against": self.against_income_account, + "debit": base_grand_total, + "debit_in_account_currency": base_grand_total + if self.party_account_currency == self.company_currency + else grand_total, + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) def make_tax_gl_entries(self, gl_entries): @@ -915,29 +1071,39 @@ class SalesInvoice(SellingController): if flt(tax.base_tax_amount_after_discount_amount): account_currency = get_account_currency(tax.account_head) gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "against": self.customer, - "credit": flt(base_amount, - tax.precision("tax_amount_after_discount_amount")), - "credit_in_account_currency": (flt(base_amount, - tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else - flt(amount, tax.precision("tax_amount_after_discount_amount"))), - "cost_center": tax.cost_center - }, account_currency, item=tax) + self.get_gl_dict( + { + "account": tax.account_head, + "against": self.customer, + "credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")), + "credit_in_account_currency": ( + flt(base_amount, tax.precision("base_tax_amount_after_discount_amount")) + if account_currency == self.company_currency + else flt(amount, tax.precision("tax_amount_after_discount_amount")) + ), + "cost_center": tax.cost_center, + }, + account_currency, + item=tax, + ) ) def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) gl_entries.append( - self.get_gl_dict({ - "account": self.unrealized_profit_loss_account, - "against": self.customer, - "debit": flt(self.total_taxes_and_charges), - "debit_in_account_currency": flt(self.base_total_taxes_and_charges), - "cost_center": self.cost_center - }, account_currency, item=self)) + self.get_gl_dict( + { + "account": self.unrealized_profit_loss_account, + "against": self.customer, + "debit": flt(self.total_taxes_and_charges), + "debit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center, + }, + account_currency, + item=self, + ) + ) def make_item_gl_entries(self, gl_entries): # income account gl entries @@ -947,8 +1113,9 @@ class SalesInvoice(SellingController): asset = self.get_asset(item) if self.is_return: - fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset, - item.base_net_amount, item.finance_book) + fixed_asset_gl_entries = get_gl_entries_on_asset_regain( + asset, item.base_net_amount, item.finance_book + ) asset.db_set("disposal_date", None) if asset.calculate_depreciation: @@ -956,8 +1123,9 @@ class SalesInvoice(SellingController): self.reset_depreciation_schedule(asset) else: - fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, - item.base_net_amount, item.finance_book) + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( + asset, item.base_net_amount, item.finance_book + ) asset.db_set("disposal_date", self.posting_date) if asset.calculate_depreciation: @@ -972,47 +1140,57 @@ class SalesInvoice(SellingController): else: # Do not book income for transfer within same company if not self.is_internal_transfer(): - income_account = (item.income_account - if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) + income_account = ( + item.income_account + if (not item.enable_deferred_revenue or self.is_return) + else item.deferred_revenue_account + ) amount, base_amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting) account_currency = get_account_currency(income_account) gl_entries.append( - self.get_gl_dict({ - "account": income_account, - "against": self.customer, - "credit": flt(base_amount, item.precision("base_net_amount")), - "credit_in_account_currency": (flt(base_amount, item.precision("base_net_amount")) - if account_currency==self.company_currency - else flt(amount, item.precision("net_amount"))), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": income_account, + "against": self.customer, + "credit": flt(base_amount, item.precision("base_net_amount")), + "credit_in_account_currency": ( + flt(base_amount, item.precision("base_net_amount")) + if account_currency == self.company_currency + else flt(amount, item.precision("net_amount")) + ), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) ) # expense account gl entries - if cint(self.update_stock) and \ - erpnext.is_perpetual_inventory_enabled(self.company): + if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() def get_asset(self, item): - if item.get('asset'): + if item.get("asset"): asset = frappe.get_doc("Asset", item.asset) else: - frappe.throw(_( - "Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name), - title=_("Missing Asset") + frappe.throw( + _("Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name), + title=_("Missing Asset"), ) self.check_finance_books(item, asset) return asset def check_finance_books(self, item, asset): - if (len(asset.finance_books) > 1 and not item.finance_book - and asset.finance_books[0].finance_book): - frappe.throw(_("Select finance book for the item {0} at row {1}") - .format(item.item_code, item.idx)) + if ( + len(asset.finance_books) > 1 and not item.finance_book and asset.finance_books[0].finance_book + ): + frappe.throw( + _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) + ) def depreciate_asset(self, asset): asset.flags.ignore_validate_update_after_submit = True @@ -1032,14 +1210,12 @@ class SalesInvoice(SellingController): def modify_depreciation_schedule_for_asset_repairs(self, asset): asset_repairs = frappe.get_all( - 'Asset Repair', - filters = {'asset': asset.name}, - fields = ['name', 'increase_in_asset_life'] + "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] ) for repair in asset_repairs: if repair.increase_in_asset_life: - asset_repair = frappe.get_doc('Asset Repair', repair.name) + asset_repair = frappe.get_doc("Asset Repair", repair.name) asset_repair.modify_depreciation_schedule() asset.prepare_depreciation_data() @@ -1049,8 +1225,8 @@ class SalesInvoice(SellingController): posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() row = -1 - finance_book = asset.get('schedules')[0].get('finance_book') - for schedule in asset.get('schedules'): + finance_book = asset.get("schedules")[0].get("finance_book") + for schedule in asset.get("schedules"): if schedule.finance_book != finance_book: row = 0 finance_book = schedule.finance_book @@ -1058,8 +1234,9 @@ class SalesInvoice(SellingController): row += 1 if schedule.schedule_date == posting_date_of_original_invoice: - if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \ - or self.sale_happens_in_the_future(posting_date_of_original_invoice): + if not self.sale_was_made_on_original_schedule_date( + asset, schedule, row, posting_date_of_original_invoice + ) or self.sale_happens_in_the_future(posting_date_of_original_invoice): reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() @@ -1074,14 +1251,17 @@ class SalesInvoice(SellingController): asset.save() def get_posting_date_of_sales_invoice(self): - return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') + return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice): - for finance_book in asset.get('finance_books'): + def sale_was_made_on_original_schedule_date( + self, asset, schedule, row, posting_date_of_original_invoice + ): + for finance_book in asset.get("finance_books"): if schedule.finance_book == finance_book.finance_book: - orginal_schedule_date = add_months(finance_book.depreciation_start_date, - row * cint(finance_book.frequency_of_depreciation)) + orginal_schedule_date = add_months( + finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) + ) if orginal_schedule_date == posting_date_of_original_invoice: return True @@ -1102,7 +1282,9 @@ class SalesInvoice(SellingController): @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): - self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + self._enable_discount_accounting = cint( + frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + ) return self._enable_discount_accounting @@ -1110,36 +1292,46 @@ class SalesInvoice(SellingController): if self.is_return: asset.set_status() else: - asset.set_status("Sold" if self.docstatus==1 else None) + asset.set_status("Sold" if self.docstatus == 1 else None) def make_loyalty_point_redemption_gle(self, gl_entries): if cint(self.redeem_loyalty_points): gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "against": "Expense account - " + cstr(self.loyalty_redemption_account) + " for the Loyalty Program", - "credit": self.loyalty_amount, - "against_voucher": self.return_against if cint(self.is_return) else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": "Expense account - " + + cstr(self.loyalty_redemption_account) + + " for the Loyalty Program", + "credit": self.loyalty_amount, + "against_voucher": self.return_against if cint(self.is_return) else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + }, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.loyalty_redemption_account, - "cost_center": self.cost_center or self.loyalty_redemption_cost_center, - "against": self.customer, - "debit": self.loyalty_amount, - "remark": "Loyalty Points redeemed by the customer" - }, item=self) + self.get_gl_dict( + { + "account": self.loyalty_redemption_account, + "cost_center": self.cost_center or self.loyalty_redemption_cost_center, + "against": self.customer, + "debit": self.loyalty_amount, + "remark": "Loyalty Points redeemed by the customer", + }, + item=self, + ) ) 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')) + 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: @@ -1148,32 +1340,42 @@ class SalesInvoice(SellingController): if payment_mode.amount: # POS, make payment entries gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "against": payment_mode.account, - "credit": payment_mode.base_amount, - "credit_in_account_currency": payment_mode.base_amount \ - if self.party_account_currency==self.company_currency \ + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": payment_mode.account, + "credit": payment_mode.base_amount, + "credit_in_account_currency": payment_mode.base_amount + if self.party_account_currency == self.company_currency else payment_mode.amount, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center - }, self.party_account_currency, item=self) + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + }, + self.party_account_currency, + item=self, + ) ) payment_mode_account_currency = get_account_currency(payment_mode.account) gl_entries.append( - self.get_gl_dict({ - "account": payment_mode.account, - "against": self.customer, - "debit": payment_mode.base_amount, - "debit_in_account_currency": payment_mode.base_amount \ - if payment_mode_account_currency==self.company_currency \ + self.get_gl_dict( + { + "account": payment_mode.account, + "against": self.customer, + "debit": payment_mode.base_amount, + "debit_in_account_currency": payment_mode.base_amount + if payment_mode_account_currency == self.company_currency else payment_mode.amount, - "cost_center": self.cost_center - }, payment_mode_account_currency, item=self) + "cost_center": self.cost_center, + }, + payment_mode_account_currency, + item=self, + ) ) if not skip_change_gl_entries: @@ -1183,28 +1385,38 @@ class SalesInvoice(SellingController): if self.change_amount: if self.account_for_change_amount: gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "against": self.account_for_change_amount, - "debit": flt(self.base_change_amount), - "debit_in_account_currency": flt(self.base_change_amount) \ - if self.party_account_currency==self.company_currency else flt(self.change_amount), - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": self.account_for_change_amount, + "debit": flt(self.base_change_amount), + "debit_in_account_currency": flt(self.base_change_amount) + if self.party_account_currency == self.company_currency + else flt(self.change_amount), + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.account_for_change_amount, - "against": self.customer, - "credit": self.base_change_amount, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.account_for_change_amount, + "against": self.customer, + "credit": self.base_change_amount, + "cost_center": self.cost_center, + }, + item=self, + ) ) else: frappe.throw(_("Select change amount account"), title="Mandatory Field") @@ -1213,61 +1425,86 @@ class SalesInvoice(SellingController): # write off entries, applicable if only pos if self.write_off_account and flt(self.write_off_amount, self.precision("write_off_amount")): write_off_account_currency = get_account_currency(self.write_off_account) - default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') + default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center") gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "against": self.write_off_account, - "credit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), - "credit_in_account_currency": (flt(self.base_write_off_amount, - self.precision("base_write_off_amount")) if self.party_account_currency==self.company_currency - else flt(self.write_off_amount, self.precision("write_off_amount"))), - "against_voucher": self.return_against if cint(self.is_return) else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": self.write_off_account, + "credit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), + "credit_in_account_currency": ( + flt(self.base_write_off_amount, self.precision("base_write_off_amount")) + if self.party_account_currency == self.company_currency + else flt(self.write_off_amount, self.precision("write_off_amount")) + ), + "against_voucher": self.return_against if cint(self.is_return) else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.write_off_account, - "against": self.customer, - "debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), - "debit_in_account_currency": (flt(self.base_write_off_amount, - self.precision("base_write_off_amount")) if write_off_account_currency==self.company_currency - else flt(self.write_off_amount, self.precision("write_off_amount"))), - "cost_center": self.cost_center or self.write_off_cost_center or default_cost_center - }, write_off_account_currency, item=self) + self.get_gl_dict( + { + "account": self.write_off_account, + "against": self.customer, + "debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), + "debit_in_account_currency": ( + flt(self.base_write_off_amount, self.precision("base_write_off_amount")) + if write_off_account_currency == self.company_currency + else flt(self.write_off_amount, self.precision("write_off_amount")) + ), + "cost_center": self.cost_center or self.write_off_cost_center or default_cost_center, + }, + write_off_account_currency, + item=self, + ) ) def make_gle_for_rounding_adjustment(self, gl_entries): - if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment \ - and not self.is_internal_transfer(): - round_off_account, round_off_cost_center = \ - get_round_off_account_and_cost_center(self.company) + if ( + flt(self.rounding_adjustment, self.precision("rounding_adjustment")) + and self.base_rounding_adjustment + and not self.is_internal_transfer() + ): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Sales Invoice", self.name + ) gl_entries.append( - self.get_gl_dict({ - "account": round_off_account, - "against": self.customer, - "credit_in_account_currency": flt(self.rounding_adjustment, - self.precision("rounding_adjustment")), - "credit": flt(self.base_rounding_adjustment, - self.precision("base_rounding_adjustment")), - "cost_center": self.cost_center or round_off_cost_center, - }, item=self)) + self.get_gl_dict( + { + "account": round_off_account, + "against": self.customer, + "credit_in_account_currency": flt( + self.rounding_adjustment, self.precision("rounding_adjustment") + ), + "credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")), + "cost_center": self.cost_center or round_off_cost_center, + }, + item=self, + ) + ) def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] for d in self.get("items"): if d.dn_detail: - billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` - where dn_detail=%s and docstatus=1""", d.dn_detail) + billed_amt = frappe.db.sql( + """select sum(amount) from `tabSales Invoice Item` + where dn_detail=%s and docstatus=1""", + d.dn_detail, + ) billed_amt = billed_amt and billed_amt[0][0] or 0 - frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) + frappe.db.set_value( + "Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified + ) updated_delivery_notes.append(d.delivery_note) elif d.so_detail: updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) @@ -1282,7 +1519,7 @@ class SalesInvoice(SellingController): self.due_date = None def update_serial_no(self, in_cancel=False): - """ update Sales Invoice refrence in Serial No """ + """update Sales Invoice refrence in Serial No""" invoice = None if (in_cancel or self.is_return) else self.name if in_cancel and self.is_return: invoice = self.return_against @@ -1292,26 +1529,25 @@ class SalesInvoice(SellingController): continue for serial_no in get_serial_nos(item.serial_no): - if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code: - frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice) + if serial_no and frappe.db.get_value("Serial No", serial_no, "item_code") == item.item_code: + frappe.db.set_value("Serial No", serial_no, "sales_invoice", invoice) def validate_serial_numbers(self): """ - validate serial number agains Delivery Note and Sales Invoice + validate serial number agains Delivery Note and Sales Invoice """ self.set_serial_no_against_delivery_note() self.validate_serial_against_delivery_note() def set_serial_no_against_delivery_note(self): for item in self.items: - if item.serial_no and item.delivery_note and \ - item.qty != len(get_serial_nos(item.serial_no)): + if item.serial_no and item.delivery_note and item.qty != len(get_serial_nos(item.serial_no)): item.serial_no = get_delivery_note_serial_no(item.item_code, item.qty, item.delivery_note) def validate_serial_against_delivery_note(self): """ - validate if the serial numbers in Sales Invoice Items are same as in - Delivery Note Item + validate if the serial numbers in Sales Invoice Items are same as in + Delivery Note Item """ for item in self.items: @@ -1330,14 +1566,18 @@ class SalesInvoice(SellingController): serial_no_msg = ", ".join(frappe.bold(d) for d in serial_no_diff) msg = _("Row #{0}: The following Serial Nos are not present in Delivery Note {1}:").format( - item.idx, dn_link) + item.idx, dn_link + ) msg += " " + serial_no_msg frappe.throw(msg=msg, title=_("Serial Nos Mismatch")) if item.serial_no and cint(item.qty) != len(si_serial_nos): - frappe.throw(_("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format( - item.idx, item.qty, item.item_code, len(si_serial_nos))) + frappe.throw( + _("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format( + item.idx, item.qty, item.item_code, len(si_serial_nos) + ) + ) def update_project(self): if self.project: @@ -1345,7 +1585,6 @@ class SalesInvoice(SellingController): project.update_billed_amount() project.db_update() - def verify_payment_amount_is_positive(self): for entry in self.payments: if entry.amount < 0: @@ -1361,63 +1600,90 @@ class SalesInvoice(SellingController): returned_amount = self.get_returned_amount() current_amount = flt(self.grand_total) - cint(self.loyalty_amount) eligible_amount = current_amount - returned_amount - lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company, - current_transaction_amount=current_amount, loyalty_program=self.loyalty_program, - expiry_date=self.posting_date, include_expired_entry=True) - if lp_details and getdate(lp_details.from_date) <= getdate(self.posting_date) and \ - (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)): + lp_details = get_loyalty_program_details_with_points( + self.customer, + company=self.company, + current_transaction_amount=current_amount, + loyalty_program=self.loyalty_program, + expiry_date=self.posting_date, + include_expired_entry=True, + ) + if ( + lp_details + and getdate(lp_details.from_date) <= getdate(self.posting_date) + and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)) + ): collection_factor = lp_details.collection_factor if lp_details.collection_factor else 1.0 - points_earned = cint(eligible_amount/collection_factor) + points_earned = cint(eligible_amount / collection_factor) - doc = frappe.get_doc({ - "doctype": "Loyalty Point Entry", - "company": self.company, - "loyalty_program": lp_details.loyalty_program, - "loyalty_program_tier": lp_details.tier_name, - "customer": self.customer, - "invoice_type": self.doctype, - "invoice": self.name, - "loyalty_points": points_earned, - "purchase_amount": eligible_amount, - "expiry_date": add_days(self.posting_date, lp_details.expiry_duration), - "posting_date": self.posting_date - }) + doc = frappe.get_doc( + { + "doctype": "Loyalty Point Entry", + "company": self.company, + "loyalty_program": lp_details.loyalty_program, + "loyalty_program_tier": lp_details.tier_name, + "customer": self.customer, + "invoice_type": self.doctype, + "invoice": self.name, + "loyalty_points": points_earned, + "purchase_amount": eligible_amount, + "expiry_date": add_days(self.posting_date, lp_details.expiry_duration), + "posting_date": self.posting_date, + } + ) doc.flags.ignore_permissions = 1 doc.save() self.set_loyalty_program_tier() # valdite the redemption and then delete the loyalty points earned on cancel of the invoice def delete_loyalty_point_entry(self): - lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where invoice=%s", - (self.name), as_dict=1) + lp_entry = frappe.db.sql( + "select name from `tabLoyalty Point Entry` where invoice=%s", (self.name), as_dict=1 + ) - if not lp_entry: return - against_lp_entry = frappe.db.sql('''select name, invoice from `tabLoyalty Point Entry` - where redeem_against=%s''', (lp_entry[0].name), as_dict=1) + if not lp_entry: + return + against_lp_entry = frappe.db.sql( + """select name, invoice from `tabLoyalty Point Entry` + where redeem_against=%s""", + (lp_entry[0].name), + as_dict=1, + ) if against_lp_entry: invoice_list = ", ".join([d.invoice for d in against_lp_entry]) frappe.throw( - _('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''') - .format(self.doctype, self.doctype, invoice_list) + _( + """{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}""" + ).format(self.doctype, self.doctype, invoice_list) ) else: - frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name)) + frappe.db.sql("""delete from `tabLoyalty Point Entry` where invoice=%s""", (self.name)) # Set loyalty program self.set_loyalty_program_tier() def set_loyalty_program_tier(self): - lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company, - loyalty_program=self.loyalty_program, include_expired_entry=True) + lp_details = get_loyalty_program_details_with_points( + self.customer, + company=self.company, + loyalty_program=self.loyalty_program, + include_expired_entry=True, + ) frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name) def get_returned_amount(self): - returned_amount = frappe.db.sql(""" - select sum(grand_total) - from `tabSales Invoice` - where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s - """, self.name) - return abs(flt(returned_amount[0][0])) if returned_amount else 0 + from frappe.query_builder.functions import Coalesce, Sum + + doc = frappe.qb.DocType(self.doctype) + returned_amount = ( + frappe.qb.from_(doc) + .select(Sum(doc.grand_total)) + .where( + (doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name) + ) + ).run() + + return abs(returned_amount[0][0]) if returned_amount[0][0] else 0 # redeem the loyalty points. def apply_loyalty_points(self): @@ -1425,7 +1691,10 @@ class SalesInvoice(SellingController): get_loyalty_point_entries, get_redemption_details, ) - loyalty_point_entries = get_loyalty_point_entries(self.customer, self.loyalty_program, self.company, self.posting_date) + + loyalty_point_entries = get_loyalty_point_entries( + self.customer, self.loyalty_program, self.company, self.posting_date + ) redemption_details = get_redemption_details(self.customer, self.loyalty_program, self.company) points_to_redeem = self.loyalty_points @@ -1439,24 +1708,26 @@ class SalesInvoice(SellingController): redeemed_points = points_to_redeem else: redeemed_points = available_points - doc = frappe.get_doc({ - "doctype": "Loyalty Point Entry", - "company": self.company, - "loyalty_program": self.loyalty_program, - "loyalty_program_tier": lp_entry.loyalty_program_tier, - "customer": self.customer, - "invoice_type": self.doctype, - "invoice": self.name, - "redeem_against": lp_entry.name, - "loyalty_points": -1*redeemed_points, - "purchase_amount": self.grand_total, - "expiry_date": lp_entry.expiry_date, - "posting_date": self.posting_date - }) + doc = frappe.get_doc( + { + "doctype": "Loyalty Point Entry", + "company": self.company, + "loyalty_program": self.loyalty_program, + "loyalty_program_tier": lp_entry.loyalty_program_tier, + "customer": self.customer, + "invoice_type": self.doctype, + "invoice": self.name, + "redeem_against": lp_entry.name, + "loyalty_points": -1 * redeemed_points, + "purchase_amount": self.grand_total, + "expiry_date": lp_entry.expiry_date, + "posting_date": self.posting_date, + } + ) doc.flags.ignore_permissions = 1 doc.save() points_to_redeem -= redeemed_points - if points_to_redeem < 1: # since points_to_redeem is integer + if points_to_redeem < 1: # since points_to_redeem is integer break # Healthcare @@ -1464,44 +1735,47 @@ class SalesInvoice(SellingController): def set_healthcare_services(self, checked_values): self.set("items", []) from erpnext.stock.get_item_details import get_item_details + for checked_item in checked_values: item_line = self.append("items", {}) - price_list, price_list_currency = frappe.db.get_values("Price List", {"selling": 1}, ['name', 'currency'])[0] + price_list, price_list_currency = frappe.db.get_values( + "Price List", {"selling": 1}, ["name", "currency"] + )[0] args = { - 'doctype': "Sales Invoice", - 'item_code': checked_item['item'], - 'company': self.company, - 'customer': frappe.db.get_value("Patient", self.patient, "customer"), - 'selling_price_list': price_list, - 'price_list_currency': price_list_currency, - 'plc_conversion_rate': 1.0, - 'conversion_rate': 1.0 + "doctype": "Sales Invoice", + "item_code": checked_item["item"], + "company": self.company, + "customer": frappe.db.get_value("Patient", self.patient, "customer"), + "selling_price_list": price_list, + "price_list_currency": price_list_currency, + "plc_conversion_rate": 1.0, + "conversion_rate": 1.0, } item_details = get_item_details(args) - item_line.item_code = checked_item['item'] + item_line.item_code = checked_item["item"] item_line.qty = 1 - if checked_item['qty']: - item_line.qty = checked_item['qty'] - if checked_item['rate']: - item_line.rate = checked_item['rate'] + if checked_item["qty"]: + item_line.qty = checked_item["qty"] + if checked_item["rate"]: + item_line.rate = checked_item["rate"] else: item_line.rate = item_details.price_list_rate item_line.amount = float(item_line.rate) * float(item_line.qty) - if checked_item['income_account']: - item_line.income_account = checked_item['income_account'] - if checked_item['dt']: - item_line.reference_dt = checked_item['dt'] - if checked_item['dn']: - item_line.reference_dn = checked_item['dn'] - if checked_item['description']: - item_line.description = checked_item['description'] + if checked_item["income_account"]: + item_line.income_account = checked_item["income_account"] + if checked_item["dt"]: + item_line.reference_dt = checked_item["dt"] + if checked_item["dn"]: + item_line.reference_dn = checked_item["dn"] + if checked_item["description"]: + item_line.description = checked_item["description"] - self.set_missing_values(for_validate = True) + self.set_missing_values(for_validate=True) def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): - if self.get('amended_from'): - self.status = 'Draft' + if self.get("amended_from"): + self.status = "Draft" return outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount")) @@ -1512,7 +1786,7 @@ class SalesInvoice(SellingController): status = "Cancelled" elif self.docstatus == 1: if self.is_internal_transfer(): - self.status = 'Internal Transfer' + self.status = "Internal Transfer" elif is_overdue(self, total): self.status = "Overdue" elif 0 < outstanding_amount < total: @@ -1520,11 +1794,17 @@ class SalesInvoice(SellingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to credit note issued against invoice - elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + elif ( + outstanding_amount <= 0 + and self.is_return == 0 + and frappe.db.get_value( + "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} + ) + ): self.status = "Credit Note Issued" elif self.is_return == 1: self.status = "Return" - elif outstanding_amount<=0: + elif outstanding_amount <= 0: self.status = "Paid" else: self.status = "Submitted" @@ -1540,34 +1820,29 @@ class SalesInvoice(SellingController): self.status = "Draft" if update: - self.db_set('status', self.status, update_modified = update_modified) + self.db_set("status", self.status, update_modified=update_modified) def get_total_in_party_account_currency(doc): - total_fieldname = ( - "grand_total" - if doc.disable_rounded_total - else "rounded_total" - ) + total_fieldname = "grand_total" if doc.disable_rounded_total else "rounded_total" if doc.party_account_currency != doc.currency: total_fieldname = "base_" + total_fieldname return flt(doc.get(total_fieldname), doc.precision(total_fieldname)) + def is_overdue(doc, total): outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount")) if outstanding_amount <= 0: return today = getdate() - if doc.get('is_pos') or not doc.get('payment_schedule'): + if doc.get("is_pos") or not doc.get("payment_schedule"): return getdate(doc.due_date) < today # calculate payable amount till date payment_amount_field = ( - "base_payment_amount" - if doc.party_account_currency != doc.currency - else "payment_amount" + "base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount" ) payable_amount = sum( @@ -1582,7 +1857,8 @@ def is_overdue(doc, total): def get_discounting_status(sales_invoice): status = None - invoice_discounting_list = frappe.db.sql(""" + invoice_discounting_list = frappe.db.sql( + """ select status from `tabInvoice Discounting` id, `tabDiscounted Invoice` d where @@ -1590,7 +1866,9 @@ def get_discounting_status(sales_invoice): and d.sales_invoice=%s and id.docstatus=1 and status in ('Disbursed', 'Settled') - """, sales_invoice) + """, + sales_invoice, + ) for d in invoice_discounting_list: status = d[0] @@ -1599,6 +1877,7 @@ def get_discounting_status(sales_invoice): return status + def validate_inter_company_party(doctype, party, company, inter_company_reference): if not party: return @@ -1627,10 +1906,19 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc frappe.throw(_("Invalid Company for Inter Company Transaction.")) elif frappe.db.get_value(partytype, {"name": party, internal: 1}, "name") == party: - companies = frappe.get_all("Allowed To Transact With", fields=["company"], filters={"parenttype": partytype, "parent": party}) + companies = frappe.get_all( + "Allowed To Transact With", + fields=["company"], + filters={"parenttype": partytype, "parent": party}, + ) companies = [d.company for d in companies] if not company in companies: - frappe.throw(_("{0} not allowed to transact with {1}. Please change the Company.").format(partytype, company)) + frappe.throw( + _("{0} not allowed to transact with {1}. Please change the Company.").format( + partytype, company + ) + ) + def update_linked_doc(doctype, name, inter_company_reference): @@ -1640,8 +1928,8 @@ def update_linked_doc(doctype, name, inter_company_reference): ref_field = "inter_company_order_reference" if inter_company_reference: - frappe.db.set_value(doctype, inter_company_reference,\ - ref_field, name) + frappe.db.set_value(doctype, inter_company_reference, ref_field, name) + def unlink_inter_company_doc(doctype, name, inter_company_reference): @@ -1656,48 +1944,57 @@ def unlink_inter_company_doc(doctype, name, inter_company_reference): frappe.db.set_value(doctype, name, ref_field, "") frappe.db.set_value(ref_doc, inter_company_reference, ref_field, "") + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Invoices'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Invoices"), + } + ) return list_context + @frappe.whitelist() def get_bank_cash_account(mode_of_payment, company): - account = frappe.db.get_value("Mode of Payment Account", - {"parent": mode_of_payment, "company": company}, "default_account") + account = frappe.db.get_value( + "Mode of Payment Account", {"parent": mode_of_payment, "company": company}, "default_account" + ) if not account: - frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") - .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) - return { - "account": account - } + frappe.throw( + _("Please set default Cash or Bank account in Mode of Payment {0}").format( + get_link_to_form("Mode of Payment", mode_of_payment) + ), + title=_("Missing Account"), + ) + return {"account": account} + @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Maintenance Schedule", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Invoice", + source_name, + { + "Sales Invoice": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, + "Sales Invoice Item": { + "doctype": "Maintenance Schedule Item", + }, }, - "Sales Invoice Item": { - "doctype": "Maintenance Schedule Item", - }, - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_delivery_note(source_name, target_doc=None): def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") @@ -1709,82 +2006,104 @@ def make_delivery_note(source_name, target_doc=None): target_doc.base_amount = target_doc.qty * flt(source_doc.base_rate) target_doc.amount = target_doc.qty * flt(source_doc.rate) - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Delivery Note", - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Invoice Item": { - "doctype": "Delivery Note Item", - "field_map": { - "name": "si_detail", - "parent": "against_sales_invoice", - "serial_no": "serial_no", - "sales_order": "against_sales_order", - "so_detail": "so_detail", - "cost_center": "cost_center" + doclist = get_mapped_doc( + "Sales Invoice", + source_name, + { + "Sales Invoice": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}}, + "Sales Invoice Item": { + "doctype": "Delivery Note Item", + "field_map": { + "name": "si_detail", + "parent": "against_sales_invoice", + "serial_no": "serial_no", + "sales_order": "against_sales_order", + "so_detail": "so_detail", + "cost_center": "cost_center", + }, + "postprocess": update_item, + "condition": lambda doc: doc.delivered_by_supplier != 1, }, - "postprocess": update_item, - "condition": lambda doc: doc.delivered_by_supplier!=1 - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "field_map": { - "incentives": "incentives" + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": { + "doctype": "Sales Team", + "field_map": {"incentives": "incentives"}, + "add_if_empty": True, }, - "add_if_empty": True - } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) + doclist.set_onload("ignore_price_list", True) return doclist + @frappe.whitelist() def make_sales_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Sales Invoice", source_name, target_doc) + def set_account_for_mode_of_payment(self): for data in self.payments: if not data.account: data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account") + def get_inter_company_details(doc, doctype): if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]: - parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}) + parties = frappe.db.get_all( + "Supplier", + fields=["name"], + filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}, + ) company = frappe.get_cached_value("Customer", doc.customer, "represents_company") if not parties: - frappe.throw(_('No Supplier found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company))) + frappe.throw( + _("No Supplier found for Inter Company Transactions which represents company {0}").format( + frappe.bold(doc.company) + ) + ) party = get_internal_party(parties, "Supplier", doc) else: - parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}) + parties = frappe.db.get_all( + "Customer", + fields=["name"], + filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}, + ) company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company") if not parties: - frappe.throw(_('No Customer found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company))) + frappe.throw( + _("No Customer found for Inter Company Transactions which represents company {0}").format( + frappe.bold(doc.company) + ) + ) party = get_internal_party(parties, "Customer", doc) - return { - "party": party, - "company": company - } + return {"party": party, "company": company} + def get_internal_party(parties, link_doctype, doc): if len(parties) == 1: - party = parties[0].name + party = parties[0].name else: # If more than one Internal Supplier/Customer, get supplier/customer on basis of address - if doc.get('company_address') or doc.get('shipping_address'): - party = frappe.db.get_value("Dynamic Link", {"parent": doc.get('company_address') or doc.get('shipping_address'), - "parenttype": "Address", "link_doctype": link_doctype}, "link_name") + if doc.get("company_address") or doc.get("shipping_address"): + party = frappe.db.get_value( + "Dynamic Link", + { + "parent": doc.get("company_address") or doc.get("shipping_address"), + "parenttype": "Address", + "link_doctype": link_doctype, + }, + "link_name", + ) if not party: party = parties[0].name @@ -1793,11 +2112,18 @@ def get_internal_party(parties, link_doctype, doc): return party + def validate_inter_company_transaction(doc, doctype): details = get_inter_company_details(doc, doctype) - price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list - valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1}) + price_list = ( + doc.selling_price_list + if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] + else doc.buying_price_list + ) + valid_price_list = frappe.db.get_value( + "Price List", {"name": price_list, "buying": 1, "selling": 1} + ) if not valid_price_list and not doc.is_internal_transfer(): frappe.throw(_("Selected Price List should have buying and selling fields checked.")) @@ -1807,28 +2133,32 @@ def validate_inter_company_transaction(doc, doctype): frappe.throw(_("No {0} found for Inter Company Transactions.").format(partytype)) company = details.get("company") - default_currency = frappe.get_cached_value('Company', company, "default_currency") + default_currency = frappe.get_cached_value("Company", company, "default_currency") if default_currency != doc.currency: - frappe.throw(_("Company currencies of both the companies should match for Inter Company Transactions.")) + frappe.throw( + _("Company currencies of both the companies should match for Inter Company Transactions.") + ) return + @frappe.whitelist() def make_inter_company_purchase_invoice(source_name, target_doc=None): return make_inter_company_transaction("Sales Invoice", source_name, target_doc) + def make_inter_company_transaction(doctype, source_name, target_doc=None): if doctype in ["Sales Invoice", "Sales Order"]: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item" - source_document_warehouse_field = 'target_warehouse' - target_document_warehouse_field = 'from_warehouse' + source_document_warehouse_field = "target_warehouse" + target_document_warehouse_field = "from_warehouse" else: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order" - source_document_warehouse_field = 'from_warehouse' - target_document_warehouse_field = 'target_warehouse' + source_document_warehouse_field = "from_warehouse" + target_document_warehouse_field = "target_warehouse" validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -1840,7 +2170,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]: - currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency') + currency = frappe.db.get_value("Supplier", details.get("party"), "default_currency") target_doc.company = details.get("company") target_doc.supplier = details.get("party") target_doc.is_internal_supplier = 1 @@ -1848,130 +2178,176 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc.buying_price_list = source_doc.selling_price_list # Invert Addresses - update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) - update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + ) if currency: target_doc.currency = currency - update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.supplier_address, - company_address=target_doc.shipping_address) + update_taxes( + target_doc, + party=target_doc.supplier, + party_type="Supplier", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address, + ) else: - currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency') + currency = frappe.db.get_value("Customer", details.get("party"), "default_currency") target_doc.company = details.get("company") target_doc.customer = details.get("party") target_doc.selling_price_list = source_doc.buying_price_list - update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) - update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) - update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + update_address( + target_doc, "company_address", "company_address_display", source_doc.supplier_address + ) + update_address( + target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address + ) + update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address) if currency: target_doc.currency = currency - update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.customer_address, - company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + update_taxes( + target_doc, + party=target_doc.customer, + party_type="Customer", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.customer_address, + company_address=target_doc.company_address, + shipping_address_name=target_doc.shipping_address_name, + ) item_field_map = { "doctype": target_doctype + " Item", - "field_no_map": [ - "income_account", - "expense_account", - "cost_center", - "warehouse" - ], + "field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"], "field_map": { - 'rate': 'rate', - } + "rate": "rate", + }, } if doctype in ["Sales Invoice", "Sales Order"]: - item_field_map["field_map"].update({ - "name": target_detail_field, - }) + item_field_map["field_map"].update( + { + "name": target_detail_field, + } + ) - if source_doc.get('update_stock'): - item_field_map["field_map"].update({ - source_document_warehouse_field: target_document_warehouse_field, - 'batch_no': 'batch_no', - 'serial_no': 'serial_no' - }) + if source_doc.get("update_stock"): + item_field_map["field_map"].update( + { + source_document_warehouse_field: target_document_warehouse_field, + "batch_no": "batch_no", + "serial_no": "serial_no", + } + ) - doclist = get_mapped_doc(doctype, source_name, { - doctype: { - "doctype": target_doctype, - "postprocess": update_details, - "set_target_warehouse": "set_from_warehouse", - "field_no_map": [ - "taxes_and_charges", - "set_warehouse", - "shipping_address" - ] + doclist = get_mapped_doc( + doctype, + source_name, + { + doctype: { + "doctype": target_doctype, + "postprocess": update_details, + "set_target_warehouse": "set_from_warehouse", + "field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"], + }, + doctype + " Item": item_field_map, }, - doctype +" Item": item_field_map - - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + def set_purchase_references(doc): # add internal PO or PR links if any if doc.is_internal_transfer(): - if doc.doctype == 'Purchase Receipt': + if doc.doctype == "Purchase Receipt": so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) if so_item_map: - pd_item_map, parent_child_map, warehouse_map = \ - get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + pd_item_map, parent_child_map, warehouse_map = get_pd_details( + "Purchase Order Item", so_item_map, "sales_order_item" + ) update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map) - elif doc.doctype == 'Purchase Invoice': + elif doc.doctype == "Purchase Invoice": dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference) # First check for Purchase receipt if list(dn_item_map.values()): - pd_item_map, parent_child_map, warehouse_map = \ - get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item') + pd_item_map, parent_child_map, warehouse_map = get_pd_details( + "Purchase Receipt Item", dn_item_map, "delivery_note_item" + ) - update_pi_items(doc, 'pr_detail', 'purchase_receipt', - dn_item_map, pd_item_map, parent_child_map, warehouse_map) + update_pi_items( + doc, + "pr_detail", + "purchase_receipt", + dn_item_map, + pd_item_map, + parent_child_map, + warehouse_map, + ) if list(so_item_map.values()): - pd_item_map, parent_child_map, warehouse_map = \ - get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + pd_item_map, parent_child_map, warehouse_map = get_pd_details( + "Purchase Order Item", so_item_map, "sales_order_item" + ) - update_pi_items(doc, 'po_detail', 'purchase_order', - so_item_map, pd_item_map, parent_child_map, warehouse_map) + update_pi_items( + doc, "po_detail", "purchase_order", so_item_map, pd_item_map, parent_child_map, warehouse_map + ) -def update_pi_items(doc, detail_field, parent_field, sales_item_map, - purchase_item_map, parent_child_map, warehouse_map): - for item in doc.get('items'): + +def update_pi_items( + doc, + detail_field, + parent_field, + sales_item_map, + purchase_item_map, + parent_child_map, + warehouse_map, +): + for item in doc.get("items"): item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item))) item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) if doc.update_stock: item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): - for item in doc.get('items'): + for item in doc.get("items"): item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + def get_delivery_note_details(internal_reference): - si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'], - filters={'parent': internal_reference}) + si_item_details = frappe.get_all( + "Delivery Note Item", fields=["name", "so_detail"], filters={"parent": internal_reference} + ) return {d.name: d.so_detail for d in si_item_details if d.so_detail} + def get_sales_invoice_details(internal_reference): dn_item_map = {} so_item_map = {} - si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail', - 'dn_detail'], filters={'parent': internal_reference}) + si_item_details = frappe.get_all( + "Sales Invoice Item", + fields=["name", "so_detail", "dn_detail"], + filters={"parent": internal_reference}, + ) for d in si_item_details: if d.dn_detail: @@ -1981,13 +2357,17 @@ def get_sales_invoice_details(internal_reference): return dn_item_map, so_item_map + def get_pd_details(doctype, sd_detail_map, sd_detail_field): pd_item_map = {} accepted_warehouse_map = {} parent_child_map = {} - pd_item_details = frappe.get_all(doctype, - fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))}) + pd_item_details = frappe.get_all( + doctype, + fields=[sd_detail_field, "name", "warehouse", "parent"], + filters={sd_detail_field: ("in", list(sd_detail_map.values()))}, + ) for d in pd_item_details: pd_item_map.setdefault(d.get(sd_detail_field), d.name) @@ -1996,16 +2376,33 @@ def get_pd_details(doctype, sd_detail_map, sd_detail_field): return pd_item_map, parent_child_map, accepted_warehouse_map -def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None, - company_address=None, shipping_address_name=None, master_doctype=None): + +def update_taxes( + doc, + party=None, + party_type=None, + company=None, + doctype=None, + party_address=None, + company_address=None, + shipping_address_name=None, + master_doctype=None, +): # Update Party Details - party_details = get_party_details(party=party, party_type=party_type, company=company, - doctype=doctype, party_address=party_address, company_address=company_address, - shipping_address=shipping_address_name) + party_details = get_party_details( + party=party, + party_type=party_type, + company=company, + doctype=doctype, + party_address=party_address, + company_address=company_address, + shipping_address=shipping_address_name, + ) # Update taxes and charges if any - doc.taxes_and_charges = party_details.get('taxes_and_charges') - doc.set('taxes', party_details.get('taxes')) + doc.taxes_and_charges = party_details.get("taxes_and_charges") + doc.set("taxes", party_details.get("taxes")) + def update_address(doc, address_field, address_display_field, address_name): doc.set(address_field, address_name) @@ -2016,53 +2413,61 @@ def update_address(doc, address_field, address_display_field, address_name): doc.set(address_display_field, get_address_display(doc.get(address_field))) + @frappe.whitelist() def get_loyalty_programs(customer): - ''' sets applicable loyalty program to the customer or returns a list of applicable programs ''' + """sets applicable loyalty program to the customer or returns a list of applicable programs""" from erpnext.selling.doctype.customer.customer import get_loyalty_programs - customer = frappe.get_doc('Customer', customer) - if customer.loyalty_program: return [customer.loyalty_program] + customer = frappe.get_doc("Customer", customer) + if customer.loyalty_program: + return [customer.loyalty_program] lp_details = get_loyalty_programs(customer) if len(lp_details) == 1: - frappe.db.set(customer, 'loyalty_program', lp_details[0]) + frappe.db.set(customer, "loyalty_program", lp_details[0]) return lp_details else: return lp_details + def on_doctype_update(): frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"]) + @frappe.whitelist() def create_invoice_discounting(source_name, target_doc=None): invoice = frappe.get_doc("Sales Invoice", source_name) invoice_discounting = frappe.new_doc("Invoice Discounting") invoice_discounting.company = invoice.company - invoice_discounting.append("invoices", { - "sales_invoice": source_name, - "customer": invoice.customer, - "posting_date": invoice.posting_date, - "outstanding_amount": invoice.outstanding_amount - }) + invoice_discounting.append( + "invoices", + { + "sales_invoice": source_name, + "customer": invoice.customer, + "posting_date": invoice.posting_date, + "outstanding_amount": invoice.outstanding_amount, + }, + ) return invoice_discounting + def update_multi_mode_option(doc, pos_profile): def append_payment(payment_mode): - payment = doc.append('payments', {}) + payment = doc.append("payments", {}) payment.default = payment_mode.default payment.mode_of_payment = payment_mode.mop payment.account = payment_mode.default_account payment.type = payment_mode.type - doc.set('payments', []) + doc.set("payments", []) invalid_modes = [] - mode_of_payments = [d.mode_of_payment for d in pos_profile.get('payments')] + mode_of_payments = [d.mode_of_payment for d in pos_profile.get("payments")] mode_of_payments_info = get_mode_of_payments_info(mode_of_payments, doc.company) - for row in pos_profile.get('payments'): + for row in pos_profile.get("payments"): payment_mode = mode_of_payments_info.get(row.mode_of_payment) if not payment_mode: invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment)) @@ -2078,12 +2483,17 @@ def update_multi_mode_option(doc, pos_profile): msg = _("Please set default Cash or Bank account in Mode of Payments {}") frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account")) + def get_all_mode_of_payments(doc): - return frappe.db.sql(""" + return frappe.db.sql( + """ select mpa.default_account, mpa.parent, mp.type as type from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", - {'company': doc.company}, as_dict=1) + {"company": doc.company}, + as_dict=1, + ) + def get_mode_of_payments_info(mode_of_payments, company): data = frappe.db.sql( @@ -2099,16 +2509,24 @@ def get_mode_of_payments_info(mode_of_payments, company): mp.name in %s group by mp.name - """, (company, mode_of_payments), as_dict=1) + """, + (company, mode_of_payments), + as_dict=1, + ) + + return {row.get("mop"): row for row in data} - return {row.get('mop'): row for row in data} def get_mode_of_payment_info(mode_of_payment, company): - return frappe.db.sql(""" + return frappe.db.sql( + """ select mpa.default_account, mpa.parent, mp.type as type from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", - (company, mode_of_payment), as_dict=1) + (company, mode_of_payment), + as_dict=1, + ) + @frappe.whitelist() def create_dunning(source_name, target_doc=None): @@ -2118,41 +2536,58 @@ def create_dunning(source_name, target_doc=None): calculate_interest_and_amount, get_dunning_letter_text, ) + def set_missing_values(source, target): target.sales_invoice = source_name target.outstanding_amount = source.outstanding_amount overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days target.overdue_days = overdue_days - if frappe.db.exists('Dunning Type', {'start_day': [ - '<', overdue_days], 'end_day': ['>=', overdue_days]}): - dunning_type = frappe.get_doc('Dunning Type', {'start_day': [ - '<', overdue_days], 'end_day': ['>=', overdue_days]}) + if frappe.db.exists( + "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} + ): + dunning_type = frappe.get_doc( + "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} + ) target.dunning_type = dunning_type.name target.rate_of_interest = dunning_type.rate_of_interest target.dunning_fee = dunning_type.dunning_fee - letter_text = get_dunning_letter_text(dunning_type = dunning_type.name, doc = target.as_dict()) + letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict()) if letter_text: - target.body_text = letter_text.get('body_text') - target.closing_text = letter_text.get('closing_text') - target.language = letter_text.get('language') - amounts = calculate_interest_and_amount(target.posting_date, target.outstanding_amount, - target.rate_of_interest, target.dunning_fee, target.overdue_days) - target.interest_amount = amounts.get('interest_amount') - target.dunning_amount = amounts.get('dunning_amount') - target.grand_total = amounts.get('grand_total') + target.body_text = letter_text.get("body_text") + target.closing_text = letter_text.get("closing_text") + target.language = letter_text.get("language") + amounts = calculate_interest_and_amount( + target.posting_date, + target.outstanding_amount, + target.rate_of_interest, + target.dunning_fee, + target.overdue_days, + ) + target.interest_amount = amounts.get("interest_amount") + target.dunning_amount = amounts.get("dunning_amount") + target.grand_total = amounts.get("grand_total") - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Dunning", - } - }, target_doc, set_missing_values) + doclist = get_mapped_doc( + "Sales Invoice", + source_name, + { + "Sales Invoice": { + "doctype": "Dunning", + } + }, + target_doc, + set_missing_values, + ) return doclist + def check_if_return_invoice_linked_with_payment_entry(self): # If a Return invoice is linked with payment entry along with other invoices, # the cancellation of the Return causes allocated amount to be greater than paid - if not frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): + if not frappe.db.get_single_value( + "Accounts Settings", "unlink_payment_on_cancellation_of_invoice" + ): return payment_entries = [] @@ -2161,7 +2596,8 @@ def check_if_return_invoice_linked_with_payment_entry(self): else: invoice = self.name - payment_entries = frappe.db.sql_list(""" + payment_entries = frappe.db.sql_list( + """ SELECT t1.name FROM @@ -2171,7 +2607,9 @@ def check_if_return_invoice_linked_with_payment_entry(self): and t1.docstatus = 1 and t2.reference_name = %s and t2.allocated_amount < 0 - """, invoice) + """, + invoice, + ) links_to_pe = [] if payment_entries: @@ -2180,7 +2618,9 @@ def check_if_return_invoice_linked_with_payment_entry(self): if len(payment_entry.references) > 1: links_to_pe.append(payment_entry.name) if links_to_pe: - payment_entries_link = [get_link_to_form('Payment Entry', name, label=name) for name in links_to_pe] + payment_entries_link = [ + get_link_to_form("Payment Entry", name, label=name) for name in links_to_pe + ] message = _("Please cancel and amend the Payment Entry") message += " " + ", ".join(payment_entries_link) + " " message += _("to unallocate the amount of this Return Invoice before cancelling it.") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 104d4f9b8aa..c0005f78cfd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -1,37 +1,34 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'sales_invoice', - 'non_standard_fieldnames': { - 'Delivery Note': 'against_sales_invoice', - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', - 'Payment Request': 'reference_name', - 'Sales Invoice': 'return_against', - 'Auto Repeat': 'reference_document', + "fieldname": "sales_invoice", + "non_standard_fieldnames": { + "Delivery Note": "against_sales_invoice", + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", + "Payment Request": "reference_name", + "Sales Invoice": "return_against", + "Auto Repeat": "reference_document", }, - 'internal_links': { - 'Sales Order': ['items', 'sales_order'] + "internal_links": { + "Sales Order": ["items", "sales_order"], + "Timesheet": ["timesheets", "time_sheet"], }, - 'transactions': [ + "transactions": [ { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Payment Request', 'Journal Entry', 'Invoice Discounting', 'Dunning'] + "label": _("Payment"), + "items": [ + "Payment Entry", + "Payment Request", + "Journal Entry", + "Invoice Discounting", + "Dunning", + ], }, - { - 'label': _('Reference'), - 'items': ['Timesheet', 'Delivery Note', 'Sales Order'] - }, - { - 'label': _('Returns'), - 'items': ['Sales Invoice'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + {"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]}, + {"label": _("Returns"), "items": ["Sales Invoice"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 69ab1738bc6..bd79f7997d6 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -58,23 +58,25 @@ class TestSalesInvoice(unittest.TestCase): w2 = frappe.get_doc(w.doctype, w.name) import time + time.sleep(1) w.save() import time + time.sleep(1) self.assertRaises(frappe.TimestampMismatchError, w2.save) def test_sales_invoice_change_naming_series(self): si = frappe.copy_doc(test_records[2]) si.insert() - si.naming_series = 'TEST-' + si.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, si.save) si = frappe.copy_doc(test_records[1]) si.insert() - si.naming_series = 'TEST-' + si.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, si.save) @@ -90,15 +92,21 @@ class TestSalesInvoice(unittest.TestCase): si.insert() expected_values = { - "keys": ["price_list_rate", "discount_percentage", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount"], + "keys": [ + "price_list_rate", + "discount_percentage", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + ], "_Test Item Home Desktop 100": [50, 0, 50, 500, 50, 50, 500], "_Test Item Home Desktop 200": [150, 0, 150, 750, 150, 150, 750], } # check if children are saved - self.assertEqual(len(si.get("items")), - len(expected_values)-1) + self.assertEqual(len(si.get("items")), len(expected_values) - 1) # check if item values are calculated for d in si.get("items"): @@ -119,7 +127,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account S&H Education Cess - _TC": [1.4, 1619.2], "_Test Account CST - _TC": [32.38, 1651.58], "_Test Account VAT - _TC": [156.25, 1807.83], - "_Test Account Discount - _TC": [-180.78, 1627.05] + "_Test Account Discount - _TC": [-180.78, 1627.05], } for d in si.get("taxes"): @@ -149,7 +157,7 @@ class TestSalesInvoice(unittest.TestCase): pe.submit() unlink_payment_on_cancel_of_invoice(0) - si = frappe.get_doc('Sales Invoice', si.name) + si = frappe.get_doc("Sales Invoice", si.name) self.assertRaises(frappe.LinkExistsError, si.cancel) unlink_payment_on_cancel_of_invoice() @@ -160,25 +168,30 @@ class TestSalesInvoice(unittest.TestCase): si2 = create_sales_invoice(rate=300) si3 = create_sales_invoice(qty=-1, rate=300, is_return=1) - pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC") - pe.append('references', { - 'reference_doctype': 'Sales Invoice', - 'reference_name': si2.name, - 'total_amount': si2.grand_total, - 'outstanding_amount': si2.outstanding_amount, - 'allocated_amount': si2.outstanding_amount - }) + pe.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": si2.name, + "total_amount": si2.grand_total, + "outstanding_amount": si2.outstanding_amount, + "allocated_amount": si2.outstanding_amount, + }, + ) - pe.append('references', { - 'reference_doctype': 'Sales Invoice', - 'reference_name': si3.name, - 'total_amount': si3.grand_total, - 'outstanding_amount': si3.outstanding_amount, - 'allocated_amount': si3.outstanding_amount - }) + pe.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": si3.name, + "total_amount": si3.grand_total, + "outstanding_amount": si3.outstanding_amount, + "allocated_amount": si3.outstanding_amount, + }, + ) - pe.reference_no = 'Test001' + pe.reference_no = "Test001" pe.reference_date = nowdate() pe.save() pe.submit() @@ -189,7 +202,6 @@ class TestSalesInvoice(unittest.TestCase): si1.load_from_db() self.assertRaises(PaymentEntryUnlinkError, si1.cancel) - def test_sales_invoice_calculation_export_currency(self): si = frappe.copy_doc(test_records[2]) si.currency = "USD" @@ -204,14 +216,21 @@ class TestSalesInvoice(unittest.TestCase): si.insert() expected_values = { - "keys": ["price_list_rate", "discount_percentage", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount"], + "keys": [ + "price_list_rate", + "discount_percentage", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + ], "_Test Item Home Desktop 100": [1, 0, 1, 10, 50, 50, 500], "_Test Item Home Desktop 200": [3, 0, 3, 15, 150, 150, 750], } # check if children are saved - self.assertEqual(len(si.get("items")), len(expected_values)-1) + self.assertEqual(len(si.get("items")), len(expected_values) - 1) # check if item values are calculated for d in si.get("items"): @@ -234,7 +253,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39], "_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04], "_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17], - "_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55] + "_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55], } for d in si.get("taxes"): @@ -246,22 +265,28 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_with_discount_and_inclusive_tax(self): si = create_sales_invoice(qty=100, rate=50, do_not_save=True) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) - si.append("taxes", { - "charge_type": "On Item Quantity", - "account_head": "_Test Account Education Cess - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "CESS", - "rate": 5, - 'included_in_print_rate': 1 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) + si.append( + "taxes", + { + "charge_type": "On Item Quantity", + "account_head": "_Test Account Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "CESS", + "rate": 5, + "included_in_print_rate": 1, + }, + ) si.insert() # with inclusive tax @@ -273,7 +298,7 @@ class TestSalesInvoice(unittest.TestCase): # additional discount si.discount_amount = 100 - si.apply_discount_on = 'Net Total' + si.apply_discount_on = "Net Total" si.payment_schedule = [] si.save() @@ -286,7 +311,7 @@ class TestSalesInvoice(unittest.TestCase): # additional discount on grand total si.discount_amount = 100 - si.apply_discount_on = 'Grand Total' + si.apply_discount_on = "Grand Total" si.payment_schedule = [] si.save() @@ -298,14 +323,17 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_discount_amount(self): si = frappe.copy_doc(test_records[3]) si.discount_amount = 104.94 - si.append("taxes", { - "charge_type": "On Previous Row Amount", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 10, - "row_id": 8, - }) + si.append( + "taxes", + { + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 10, + "row_id": 8, + }, + ) si.insert() expected_values = [ @@ -321,7 +349,7 @@ class TestSalesInvoice(unittest.TestCase): "net_rate": 46.54, "net_amount": 465.37, "base_net_rate": 46.54, - "base_net_amount": 465.37 + "base_net_amount": 465.37, }, { "item_code": "_Test Item Home Desktop 200", @@ -335,12 +363,12 @@ class TestSalesInvoice(unittest.TestCase): "net_rate": 139.62, "net_amount": 698.08, "base_net_rate": 139.62, - "base_net_amount": 698.08 - } + "base_net_amount": 698.08, + }, ] # check if children are saved - self.assertEqual(len(si.get("items")), len(expected_values)) + self.assertEqual(len(si.get("items")), len(expected_values)) # check if item values are calculated for i, d in enumerate(si.get("items")): @@ -362,7 +390,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account Customs Duty - _TC": [125, 116.35, 1585.40], "_Test Account Shipping Charges - _TC": [100, 100, 1685.40], "_Test Account Discount - _TC": [-180.33, -168.54, 1516.86], - "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01] + "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01], } for d in si.get("taxes"): @@ -377,38 +405,48 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") si = frappe.copy_doc(test_records[3]) si.discount_amount = 104.94 - si.append("taxes", { - "doctype": "Sales Taxes and Charges", - "charge_type": "On Previous Row Amount", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 10, - "row_id": 8 - }) + si.append( + "taxes", + { + "doctype": "Sales Taxes and Charges", + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 10, + "row_id": 8, + }, + ) si.insert() si.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 1500, 0.0], - [test_records[3]["items"][0]["income_account"], 0.0, 1163.45], - [test_records[3]["taxes"][0]["account_head"], 0.0, 130.31], - [test_records[3]["taxes"][1]["account_head"], 0.0, 2.61], - [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30], - [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95], - [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43], - [test_records[3]["taxes"][5]["account_head"], 0.0, 116.35], - [test_records[3]["taxes"][6]["account_head"], 0.0, 100], - [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0], - ["_Test Account Service Tax - _TC", 16.85, 0.0], - ["Round Off - _TC", 0.01, 0.0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 1500, 0.0], + [test_records[3]["items"][0]["income_account"], 0.0, 1163.45], + [test_records[3]["taxes"][0]["account_head"], 0.0, 130.31], + [test_records[3]["taxes"][1]["account_head"], 0.0, 2.61], + [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30], + [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95], + [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43], + [test_records[3]["taxes"][5]["account_head"], 0.0, 116.35], + [test_records[3]["taxes"][6]["account_head"], 0.0, 100], + [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0], + ["_Test Account Service Tax - _TC", 16.85, 0.0], + ["Round Off - _TC", 0.01, 0.0], + ] + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) @@ -418,8 +456,11 @@ class TestSalesInvoice(unittest.TestCase): # cancel si.cancel() - gle = frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) + gle = frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + si.name, + ) self.assertTrue(gle) @@ -431,14 +472,17 @@ class TestSalesInvoice(unittest.TestCase): item_row_copy.qty = qty si.append("items", item_row_copy) - si.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 19 - }) + si.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 19, + }, + ) si.insert() self.assertEqual(si.net_total, 4600) @@ -453,10 +497,10 @@ class TestSalesInvoice(unittest.TestCase): item_row = si.get("items")[0] add_items = [ - (54, '_Test Account Excise Duty @ 12 - _TC'), - (288, '_Test Account Excise Duty @ 15 - _TC'), - (144, '_Test Account Excise Duty @ 20 - _TC'), - (430, '_Test Item Tax Template 1 - _TC') + (54, "_Test Account Excise Duty @ 12 - _TC"), + (288, "_Test Account Excise Duty @ 15 - _TC"), + (144, "_Test Account Excise Duty @ 20 - _TC"), + (430, "_Test Item Tax Template 1 - _TC"), ] for qty, item_tax_template in add_items: item_row_copy = copy.deepcopy(item_row) @@ -464,30 +508,39 @@ class TestSalesInvoice(unittest.TestCase): item_row_copy.item_tax_template = item_tax_template si.append("items", item_row_copy) - si.append("taxes", { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 11 - }) - si.append("taxes", { - "account_head": "_Test Account Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 0 - }) - si.append("taxes", { - "account_head": "_Test Account S&H Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "S&H Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 3 - }) + si.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 11, + }, + ) + si.append( + "taxes", + { + "account_head": "_Test Account Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 0, + }, + ) + si.append( + "taxes", + { + "account_head": "_Test Account S&H Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "S&H Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 3, + }, + ) si.insert() self.assertEqual(si.net_total, 4600) @@ -517,14 +570,17 @@ class TestSalesInvoice(unittest.TestCase): si.apply_discount_on = "Net Total" si.discount_amount = 75.0 - si.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 24 - }) + si.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 24, + }, + ) si.insert() self.assertEqual(si.total, 975) @@ -538,7 +594,7 @@ class TestSalesInvoice(unittest.TestCase): def test_inclusive_rate_validations(self): si = frappe.copy_doc(test_records[2]) for i, tax in enumerate(si.get("taxes")): - tax.idx = i+1 + tax.idx = i + 1 si.get("items")[0].price_list_rate = 62.5 si.get("items")[0].price_list_rate = 191 @@ -558,14 +614,43 @@ class TestSalesInvoice(unittest.TestCase): si.insert() expected_values = { - "keys": ["price_list_rate", "discount_percentage", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount", "net_rate", "net_amount"], - "_Test Item Home Desktop 100": [62.5, 0, 62.5, 625.0, 62.5, 62.5, 625.0, 50, 499.97600115194473], - "_Test Item Home Desktop 200": [190.66, 0, 190.66, 953.3, 190.66, 190.66, 953.3, 150, 749.9968530500239], + "keys": [ + "price_list_rate", + "discount_percentage", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + "net_rate", + "net_amount", + ], + "_Test Item Home Desktop 100": [ + 62.5, + 0, + 62.5, + 625.0, + 62.5, + 62.5, + 625.0, + 50, + 499.97600115194473, + ], + "_Test Item Home Desktop 200": [ + 190.66, + 0, + 190.66, + 953.3, + 190.66, + 190.66, + 953.3, + 150, + 749.9968530500239, + ], } # check if children are saved - self.assertEqual(len(si.get("items")), len(expected_values)-1) + self.assertEqual(len(si.get("items")), len(expected_values) - 1) # check if item values are calculated for d in si.get("items"): @@ -586,7 +671,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account VAT - _TC": [156.25, 1578.30], "_Test Account Customs Duty - _TC": [125, 1703.30], "_Test Account Shipping Charges - _TC": [100, 1803.30], - "_Test Account Discount - _TC": [-180.33, 1622.97] + "_Test Account Discount - _TC": [-180.33, 1622.97], } for d in si.get("taxes"): @@ -624,7 +709,7 @@ class TestSalesInvoice(unittest.TestCase): "net_rate": 40, "net_amount": 399.9808009215558, "base_net_rate": 2000, - "base_net_amount": 19999 + "base_net_amount": 19999, }, { "item_code": "_Test Item Home Desktop 200", @@ -638,8 +723,8 @@ class TestSalesInvoice(unittest.TestCase): "net_rate": 118.01, "net_amount": 590.0531205155963, "base_net_rate": 5900.5, - "base_net_amount": 29502.5 - } + "base_net_amount": 29502.5, + }, ] # check if children are saved @@ -664,8 +749,8 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account CST - _TC": [1104, 56312.0, 22.08, 1126.24], "_Test Account VAT - _TC": [6187.5, 62499.5, 123.75, 1249.99], "_Test Account Customs Duty - _TC": [4950.0, 67449.5, 99.0, 1348.99], - "_Test Account Shipping Charges - _TC": [ 100, 67549.5, 2, 1350.99], - "_Test Account Discount - _TC": [ -6755, 60794.5, -135.10, 1215.89] + "_Test Account Shipping Charges - _TC": [100, 67549.5, 2, 1350.99], + "_Test Account Discount - _TC": [-6755, 60794.5, -135.10, 1215.89], } for d in si.get("taxes"): @@ -677,7 +762,6 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.rounding_adjustment, 0.01) self.assertEqual(si.base_rounding_adjustment, 0.50) - def test_outstanding(self): w = self.make() self.assertEqual(w.outstanding_amount, w.base_rounded_total) @@ -697,11 +781,11 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 162.0) - link_data = get_dynamic_link_map().get('Sales Invoice', []) + link_data = get_dynamic_link_map().get("Sales Invoice", []) link_doctypes = [d.parent for d in link_data] # test case for dynamic link order - self.assertTrue(link_doctypes.index('GL Entry') > link_doctypes.index('Journal Entry Account')) + self.assertTrue(link_doctypes.index("GL Entry") > link_doctypes.index("Journal Entry Account")) jv.cancel() self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 562.0) @@ -711,18 +795,25 @@ class TestSalesInvoice(unittest.TestCase): si.insert() si.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 630.0, 0.0], - [test_records[1]["items"][0]["income_account"], 0.0, 500.0], - [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0], - [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0], - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 630.0, 0.0], + [test_records[1]["items"][0]["income_account"], 0.0, 500.0], + [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0], + [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0], + ] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) @@ -732,25 +823,49 @@ class TestSalesInvoice(unittest.TestCase): # cancel si.cancel() - gle = frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) + gle = frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + si.name, + ) self.assertTrue(gle) def test_pos_gl_entry_with_perpetual_inventory(self): - 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_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", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") + pr = 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 = 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 - 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': 50}) + 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": 50}) taxes = get_taxes_and_charges() pos.taxes = [] @@ -770,20 +885,19 @@ class TestSalesInvoice(unittest.TestCase): pos_profile = make_pos_profile() pos_profile.payments = [] - pos_profile.append('payments', { - 'default': 1, - 'mode_of_payment': 'Cash' - }) + pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"}) pos_profile.save() - pos = create_sales_invoice(qty = 10, do_not_save=True) + pos = create_sales_invoice(qty=10, do_not_save=True) pos.is_pos = 1 pos.pos_profile = pos_profile.name - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500} + ) + pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500}) pos.insert() pos.submit() @@ -792,46 +906,123 @@ class TestSalesInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - self.assertEqual(pos_return.get('payments')[0].amount, -1000) + self.assertEqual(pos_return.get("payments")[0].amount, -1000) def test_pos_change_amount(self): - 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_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") + 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 = 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 - 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.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.change_amount = 5.0 + pos.write_off_outstanding_amount_automatically = 1 pos.insert() pos.submit() self.assertEqual(pos.grand_total, 100.0) - self.assertEqual(pos.write_off_amount, -5) + self.assertEqual(pos.write_off_amount, 0) + + def test_auto_write_off_amount(self): + 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 + + 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": 40}) + + pos.write_off_outstanding_amount_automatically = 1 + pos.insert() + pos.submit() + + self.assertEqual(pos.grand_total, 100.0) + self.assertEqual(pos.write_off_amount, 10) def test_pos_with_no_gl_entry_for_change_amount(self): - frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0) + 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_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") + 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 = 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 @@ -841,8 +1032,10 @@ class TestSalesInvoice(unittest.TestCase): 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.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() @@ -852,40 +1045,50 @@ class TestSalesInvoice(unittest.TestCase): 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) + 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` + sle = frappe.db.sql( + """select * from `tabStock Ledger Entry` where voucher_type = 'Sales Invoice' and voucher_no = %s""", - si.name, as_dict=1)[0] + si.name, + as_dict=1, + )[0] self.assertTrue(sle) - self.assertEqual([sle.item_code, sle.warehouse, sle.actual_qty], - ['_Test FG Item', 'Stores - TCP1', -1.0]) + self.assertEqual( + [sle.item_code, sle.warehouse, sle.actual_qty], ["_Test FG Item", "Stores - TCP1", -1.0] + ) # check gl entries - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc, debit asc, credit asc""", si.name, as_dict=1) + order by account asc, debit asc, credit asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - stock_in_hand = get_inventory_account('_Test Company with perpetual inventory') - expected_gl_entries = sorted([ - [si.debit_to, 100.0, 0.0], - [pos.items[0].income_account, 0.0, 89.09], - ['Round Off - TCP1', 0.0, 0.01], - [pos.taxes[0].account_head, 0.0, 10.69], - [pos.taxes[1].account_head, 0.0, 0.21], - [stock_in_hand, 0.0, abs(sle.stock_value_difference)], - [pos.items[0].expense_account, abs(sle.stock_value_difference), 0.0], - [si.debit_to, 0.0, 50.0], - [si.debit_to, 0.0, cash_amount], - ["_Test Bank - TCP1", 50, 0.0], - ["Cash - TCP1", cash_amount, 0.0] - ]) + stock_in_hand = get_inventory_account("_Test Company with perpetual inventory") + expected_gl_entries = sorted( + [ + [si.debit_to, 100.0, 0.0], + [pos.items[0].income_account, 0.0, 89.09], + ["Round Off - TCP1", 0.0, 0.01], + [pos.taxes[0].account_head, 0.0, 10.69], + [pos.taxes[1].account_head, 0.0, 0.21], + [stock_in_hand, 0.0, abs(sle.stock_value_difference)], + [pos.items[0].expense_account, abs(sle.stock_value_difference), 0.0], + [si.debit_to, 0.0, 50.0], + [si.debit_to, 0.0, cash_amount], + ["_Test Bank - TCP1", 50, 0.0], + ["Cash - TCP1", cash_amount, 0.0], + ] + ) for i, gle in enumerate(sorted(gl_entries, key=lambda gle: gle.account)): self.assertEqual(expected_gl_entries[i][0], gle.account) @@ -893,8 +1096,11 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_gl_entries[i][2], gle.credit) si.cancel() - gle = frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) + gle = frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + si.name, + ) self.assertTrue(gle) @@ -914,21 +1120,29 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(frappe.ValidationError, si.submit) def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self): - si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1", - income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True) + si = create_sales_invoice( + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + cost_center="Main - TCP1", + do_not_save=True, + ) si.get("items")[0].item_code = None si.insert() si.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - ["Debtors - TCP1", 100.0, 0.0], - ["Sales - TCP1", 0.0, 100.0] - ]) + expected_values = dict( + (d[0], d) for d in [["Debtors - TCP1", 100.0, 0.0], ["Sales - TCP1", 0.0, 100.0]] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) @@ -937,25 +1151,32 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self): si = create_sales_invoice(item="_Test Non Stock Item") - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 100.0, 0.0], - [test_records[1]["items"][0]["income_account"], 0.0, 100.0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 100.0, 0.0], + [test_records[1]["items"][0]["income_account"], 0.0, 100.0], + ] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) - def _insert_purchase_receipt(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( test_records as pr_test_records, ) + pr = frappe.copy_doc(pr_test_records[0]) pr.naming_series = "_T-Purchase Receipt-" pr.insert() @@ -965,6 +1186,7 @@ class TestSalesInvoice(unittest.TestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import ( test_records as dn_test_records, ) + dn = frappe.copy_doc(dn_test_records[0]) dn.naming_series = "_T-Delivery Note-" dn.insert() @@ -982,24 +1204,37 @@ class TestSalesInvoice(unittest.TestCase): si = frappe.copy_doc(test_records[0]) si.allocate_advances_automatically = 0 - si.append("advances", { - "doctype": "Sales Invoice Advance", - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) si.insert() si.submit() si.load_from_db() - self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_name=%s""", si.name)) + self.assertTrue( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_name=%s""", + si.name, + ) + ) - self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_name=%s and credit_in_account_currency=300""", si.name)) + self.assertTrue( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_name=%s and credit_in_account_currency=300""", + si.name, + ) + ) self.assertEqual(si.outstanding_amount, 262.0) @@ -1021,29 +1256,34 @@ class TestSalesInvoice(unittest.TestCase): si.submit() self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) - self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], - "delivery_document_no"), si.name) + self.assertEqual( + frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name + ) return si def test_serialized_cancel(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + si = self.test_serialized() si.cancel() serial_nos = get_serial_nos(si.get("items")[0].serial_no) - self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC") - self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], - "delivery_document_no")) + self.assertEqual( + frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC" + ) + self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no")) self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice")) def test_serialize_status(self): - serial_no = frappe.get_doc({ - "doctype": "Serial No", - "item_code": "_Test Serialized Item With Series", - "serial_no": make_autoname("SR", "Serial No") - }) + serial_no = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": "_Test Serialized Item With Series", + "serial_no": make_autoname("SR", "Serial No"), + } + ) serial_no.save() si = frappe.copy_doc(test_records[0]) @@ -1057,8 +1297,8 @@ class TestSalesInvoice(unittest.TestCase): def test_serial_numbers_against_delivery_note(self): """ - check if the sales invoice item serial numbers and the delivery note items - serial numbers are same + check if the sales invoice item serial numbers and the delivery note items + serial numbers are same """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -1078,40 +1318,84 @@ class TestSalesInvoice(unittest.TestCase): def test_return_sales_invoice(self): make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) - actual_qty_0 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1") + actual_qty_0 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1") - si = create_sales_invoice(qty = 5, rate=500, update_stock=1, company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1") + si = create_sales_invoice( + qty=5, + rate=500, + update_stock=1, + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + item_code="_Test Item", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - - actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1") + actual_qty_1 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1") self.assertEqual(actual_qty_0 - 5, actual_qty_1) # outgoing_rate - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Sales Invoice", - "voucher_no": si.name}, "stock_value_difference") / 5 + outgoing_rate = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si.name}, + "stock_value_difference", + ) + / 5 + ) # return entry - si1 = create_sales_invoice(is_return=1, return_against=si.name, qty=-2, rate=500, update_stock=1, company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1") + si1 = create_sales_invoice( + is_return=1, + return_against=si.name, + qty=-2, + rate=500, + update_stock=1, + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + item_code="_Test Item", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - actual_qty_2 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1") + actual_qty_2 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1") self.assertEqual(actual_qty_1 + 2, actual_qty_2) - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Sales Invoice", "voucher_no": si1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory', si1.items[0].warehouse) + stock_in_hand_account = get_inventory_account( + "_Test Company with perpetual inventory", si1.items[0].warehouse + ) # Check gl entry - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice", - "voucher_no": si1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, stock_value_difference) - party_credited = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice", - "voucher_no": si1.name, "account": "Debtors - TCP1", "party": "_Test Customer"}, "credit") + party_credited = frappe.db.get_value( + "GL Entry", + { + "voucher_type": "Sales Invoice", + "voucher_no": si1.name, + "account": "Debtors - TCP1", + "party": "_Test Customer", + }, + "credit", + ) self.assertEqual(party_credited, 1000) @@ -1122,51 +1406,87 @@ class TestSalesInvoice(unittest.TestCase): def test_gle_made_when_asset_is_returned(self): create_asset_data() - pi = frappe.new_doc('Purchase Invoice') - pi.supplier = '_Test Supplier' - pi.append('items', { - 'item_code': 'Macbook Pro', - 'qty': 1 - }) + pi = frappe.new_doc("Purchase Invoice") + pi.supplier = "_Test Supplier" + pi.append("items", {"item_code": "Macbook Pro", "qty": 1}) pi.set_missing_values() asset = create_asset(item_code="Macbook Pro") si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000) - return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000) + return_si = create_sales_invoice( + is_return=1, + return_against=si.name, + item_code="Macbook Pro", + asset=asset.name, + qty=-1, + rate=90000, + ) disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account") # Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000 loss_for_si = frappe.get_all( "GL Entry", - filters = { - "voucher_no": si.name, - "account": disposal_account - }, - fields = ["credit", "debit"] + filters={"voucher_no": si.name, "account": disposal_account}, + fields=["credit", "debit"], )[0] loss_for_return_si = frappe.get_all( "GL Entry", - filters = { - "voucher_no": return_si.name, - "account": disposal_account - }, - fields = ["credit", "debit"] + filters={"voucher_no": return_si.name, "account": disposal_account}, + fields=["credit", "debit"], )[0] - self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit']) - self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit']) + self.assertEqual(loss_for_si["credit"], loss_for_return_si["debit"]) + self.assertEqual(loss_for_si["debit"], loss_for_return_si["credit"]) def test_incoming_rate_for_stand_alone_credit_note(self): - return_si = create_sales_invoice(is_return=1, update_stock=1, qty=-1, rate=90000, incoming_rate=10, - company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', debit_to='Debtors - TCP1', - income_account='Sales - TCP1', expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1') + return_si = create_sales_invoice( + is_return=1, + update_stock=1, + qty=-1, + rate=90000, + incoming_rate=10, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - incoming_rate = frappe.db.get_value('Stock Ledger Entry', {'voucher_no': return_si.name}, 'incoming_rate') - debit_amount = frappe.db.get_value('GL Entry', - {'voucher_no': return_si.name, 'account': 'Stock In Hand - TCP1'}, 'debit') + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": return_si.name}, "incoming_rate" + ) + debit_amount = frappe.db.get_value( + "GL Entry", {"voucher_no": return_si.name, "account": "Stock In Hand - TCP1"}, "debit" + ) + + self.assertEqual(debit_amount, 10.0) + self.assertEqual(incoming_rate, 10.0) + + def test_incoming_rate_for_stand_alone_credit_note(self): + return_si = create_sales_invoice( + is_return=1, + update_stock=1, + qty=-1, + rate=90000, + incoming_rate=10, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": return_si.name}, "incoming_rate" + ) + debit_amount = frappe.db.get_value( + "GL Entry", {"voucher_no": return_si.name, "account": "Stock In Hand - TCP1"}, "debit" + ) self.assertEqual(debit_amount, 10.0) self.assertEqual(incoming_rate, 10.0) @@ -1178,16 +1498,25 @@ class TestSalesInvoice(unittest.TestCase): si.insert() expected_values = { - "keys": ["price_list_rate", "discount_percentage", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount", - "net_rate", "base_net_rate", "net_amount", "base_net_amount"], + "keys": [ + "price_list_rate", + "discount_percentage", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + "net_rate", + "base_net_rate", + "net_amount", + "base_net_amount", + ], "_Test Item Home Desktop 100": [50, 0, 50, 500, 50, 50, 500, 25, 25, 250, 250], "_Test Item Home Desktop 200": [150, 0, 150, 750, 150, 150, 750, 75, 75, 375, 375], } # check if children are saved - self.assertEqual(len(si.get("items")), - len(expected_values)-1) + self.assertEqual(len(si.get("items")), len(expected_values) - 1) # check if item values are calculated for d in si.get("items"): @@ -1202,16 +1531,19 @@ class TestSalesInvoice(unittest.TestCase): # check tax calculation expected_values = { - "keys": ["tax_amount", "tax_amount_after_discount_amount", - "base_tax_amount_after_discount_amount"], + "keys": [ + "tax_amount", + "tax_amount_after_discount_amount", + "base_tax_amount_after_discount_amount", + ], "_Test Account Shipping Charges - _TC": [100, 100, 100], "_Test Account Customs Duty - _TC": [62.5, 62.5, 62.5], "_Test Account Excise Duty - _TC": [70, 70, 70], "_Test Account Education Cess - _TC": [1.4, 1.4, 1.4], - "_Test Account S&H Education Cess - _TC": [.7, 0.7, 0.7], + "_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7], "_Test Account CST - _TC": [17.19, 17.19, 17.19], "_Test Account VAT - _TC": [78.13, 78.13, 78.13], - "_Test Account Discount - _TC": [-95.49, -95.49, -95.49] + "_Test Account Discount - _TC": [-95.49, -95.49, -95.49], } for d in si.get("taxes"): @@ -1219,19 +1551,26 @@ class TestSalesInvoice(unittest.TestCase): if expected_values.get(d.account_head): self.assertEqual(d.get(k), expected_values[d.account_head][i]) - self.assertEqual(si.total_taxes_and_charges, 234.43) self.assertEqual(si.base_grand_total, 859.43) self.assertEqual(si.grand_total, 859.43) def test_multi_currency_gle(self): - si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -1241,26 +1580,35 @@ class TestSalesInvoice(unittest.TestCase): "debit": 5000, "debit_in_account_currency": 100, "credit": 0, - "credit_in_account_currency": 0 + "credit_in_account_currency": 0, }, "Sales - _TC": { "account_currency": "INR", "debit": 0, "debit_in_account_currency": 0, "credit": 5000, - "credit_in_account_currency": 5000 - } + "credit_in_account_currency": 5000, + }, } - for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): + for field in ( + "account_currency", + "debit", + "debit_in_account_currency", + "credit", + "credit_in_account_currency", + ): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) # cancel si.cancel() - gle = frappe.db.sql("""select name from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + si.name, + ) self.assertTrue(gle) @@ -1268,32 +1616,52 @@ class TestSalesInvoice(unittest.TestCase): # Customer currency = USD # Transaction currency cannot be INR - si1 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - do_not_save=True) + si1 = create_sales_invoice( + customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", do_not_save=True + ) self.assertRaises(InvalidCurrency, si1.save) # Transaction currency cannot be EUR - si2 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="EUR", conversion_rate=80, do_not_save=True) + si2 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="EUR", + conversion_rate=80, + do_not_save=True, + ) self.assertRaises(InvalidCurrency, si2.save) # Transaction currency only allowed in USD - si3 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si3 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) # Party Account currency must be in USD, as there is existing GLE with USD - si4 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC", - currency="USD", conversion_rate=50, do_not_submit=True) + si4 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable - _TC", + currency="USD", + conversion_rate=50, + do_not_submit=True, + ) self.assertRaises(InvalidAccountCurrency, si4.submit) # Party Account currency must be in USD, force customer currency as there is no GLE si3.cancel() - si5 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC", - currency="USD", conversion_rate=50, do_not_submit=True) + si5 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable - _TC", + currency="USD", + conversion_rate=50, + do_not_submit=True, + ) self.assertRaises(InvalidAccountCurrency, si5.submit) @@ -1301,12 +1669,12 @@ class TestSalesInvoice(unittest.TestCase): si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True) price_list_rate = flt(100) * flt(si.plc_conversion_rate) si.items[0].price_list_rate = price_list_rate - si.items[0].margin_type = 'Percentage' + si.items[0].margin_type = "Percentage" si.items[0].margin_rate_or_amount = 25 si.items[0].discount_amount = 0.0 si.items[0].discount_percentage = 0.0 si.save() - self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) + self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate)) def test_outstanding_amount_after_advance_jv_cancelation(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( @@ -1314,89 +1682,107 @@ class TestSalesInvoice(unittest.TestCase): ) jv = frappe.copy_doc(jv_test_records[0]) - jv.accounts[0].is_advance = 'Yes' + jv.accounts[0].is_advance = "Yes" jv.insert() jv.submit() si = frappe.copy_doc(test_records[0]) - si.append("advances", { - "doctype": "Sales Invoice Advance", - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) si.insert() si.submit() si.load_from_db() - #check outstanding after advance allocation - self.assertEqual(flt(si.outstanding_amount), - flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount"))) + # check outstanding after advance allocation + self.assertEqual( + flt(si.outstanding_amount), + flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")), + ) - #added to avoid Document has been modified exception + # added to avoid Document has been modified exception jv = frappe.get_doc("Journal Entry", jv.name) jv.cancel() si.load_from_db() - #check outstanding after advance cancellation - self.assertEqual(flt(si.outstanding_amount), - flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount"))) + # check outstanding after advance cancellation + self.assertEqual( + flt(si.outstanding_amount), + flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")), + ) def test_outstanding_amount_after_advance_payment_entry_cancelation(self): - pe = frappe.get_doc({ - "doctype": "Payment Entry", - "payment_type": "Receive", - "party_type": "Customer", - "party": "_Test Customer", - "company": "_Test Company", - "paid_from_account_currency": "INR", - "paid_to_account_currency": "INR", - "source_exchange_rate": 1, - "target_exchange_rate": 1, - "reference_no": "1", - "reference_date": nowdate(), - "received_amount": 300, - "paid_amount": 300, - "paid_from": "_Test Receivable - _TC", - "paid_to": "_Test Cash - _TC" - }) + pe = frappe.get_doc( + { + "doctype": "Payment Entry", + "payment_type": "Receive", + "party_type": "Customer", + "party": "_Test Customer", + "company": "_Test Company", + "paid_from_account_currency": "INR", + "paid_to_account_currency": "INR", + "source_exchange_rate": 1, + "target_exchange_rate": 1, + "reference_no": "1", + "reference_date": nowdate(), + "received_amount": 300, + "paid_amount": 300, + "paid_from": "_Test Receivable - _TC", + "paid_to": "_Test Cash - _TC", + } + ) pe.insert() pe.submit() si = frappe.copy_doc(test_records[0]) si.is_pos = 0 - si.append("advances", { - "doctype": "Sales Invoice Advance", - "reference_type": "Payment Entry", - "reference_name": pe.name, - "advance_amount": 300, - "allocated_amount": 300, - "remarks": pe.remarks - }) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": "Payment Entry", + "reference_name": pe.name, + "advance_amount": 300, + "allocated_amount": 300, + "remarks": pe.remarks, + }, + ) si.insert() si.submit() si.load_from_db() - #check outstanding after advance allocation - self.assertEqual(flt(si.outstanding_amount), - flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount"))) + # check outstanding after advance allocation + self.assertEqual( + flt(si.outstanding_amount), + flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")), + ) - #added to avoid Document has been modified exception + # added to avoid Document has been modified exception pe = frappe.get_doc("Payment Entry", pe.name) pe.cancel() si.load_from_db() - #check outstanding after advance cancellation - self.assertEqual(flt(si.outstanding_amount), - flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount"))) + # check outstanding after advance cancellation + self.assertEqual( + flt(si.outstanding_amount), + flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")), + ) def test_multiple_uom_in_selling(self): - frappe.db.sql("""delete from `tabItem Price` - where price_list='_Test Price List' and item_code='_Test Item'""") + frappe.db.sql( + """delete from `tabItem Price` + where price_list='_Test Price List' and item_code='_Test Item'""" + ) item_price = frappe.new_doc("Item Price") item_price.price_list = "_Test Price List" item_price.item_code = "_Test Item" @@ -1410,9 +1796,18 @@ class TestSalesInvoice(unittest.TestCase): si.save() expected_values = { - "keys": ["price_list_rate", "stock_uom", "uom", "conversion_factor", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount"], - "_Test Item": [1000, "_Test UOM", "_Test UOM 1", 10.0, 1000, 1000, 1000, 1000, 1000] + "keys": [ + "price_list_rate", + "stock_uom", + "uom", + "conversion_factor", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + ], + "_Test Item": [1000, "_Test UOM", "_Test UOM 1", 10.0, 1000, 1000, 1000, 1000, 1000], } # check if the conversion_factor and price_list_rate is calculated according to uom @@ -1427,23 +1822,10 @@ class TestSalesInvoice(unittest.TestCase): itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si) expected_itemised_tax = { - "_Test Item": { - "Service Tax": { - "tax_rate": 10.0, - "tax_amount": 1000.0 - } - }, - "_Test Item 2": { - "Service Tax": { - "tax_rate": 10.0, - "tax_amount": 500.0 - } - } - } - expected_itemised_taxable_amount = { - "_Test Item": 10000.0, - "_Test Item 2": 5000.0 + "_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}}, + "_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}}, } + expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0} self.assertEqual(itemised_tax, expected_itemised_tax) self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount) @@ -1458,23 +1840,10 @@ class TestSalesInvoice(unittest.TestCase): itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si) expected_itemised_tax = { - "_Test Item": { - "Service Tax": { - "tax_rate": 10.0, - "tax_amount": 1000.0 - } - }, - "_Test Item 2": { - "Service Tax": { - "tax_rate": 10.0, - "tax_amount": 500.0 - } - } - } - expected_itemised_taxable_amount = { - "_Test Item": 10000.0, - "_Test Item 2": 5000.0 + "_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}}, + "_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}}, } + expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0} self.assertEqual(itemised_tax, expected_itemised_tax) self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount) @@ -1483,59 +1852,73 @@ class TestSalesInvoice(unittest.TestCase): def create_si_to_test_tax_breakup(self): si = create_sales_invoice(qty=100, rate=50, do_not_save=True) - si.append("items", { - "item_code": "_Test Item", - "gst_hsn_code": "999800", - "warehouse": "_Test Warehouse - _TC", - "qty": 100, - "rate": 50, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC" - }) - si.append("items", { - "item_code": "_Test Item 2", - "gst_hsn_code": "999800", - "warehouse": "_Test Warehouse - _TC", - "qty": 100, - "rate": 50, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC" - }) + si.append( + "items", + { + "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": 100, + "rate": 50, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) + si.append( + "items", + { + "item_code": "_Test Item 2", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": 100, + "rate": 50, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 10 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 10, + }, + ) si.insert() return si def test_company_monthly_sales(self): - existing_current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales") + existing_current_month_sales = frappe.get_cached_value( + "Company", "_Test Company", "total_monthly_sales" + ) si = create_sales_invoice() - current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales") + current_month_sales = frappe.get_cached_value("Company", "_Test Company", "total_monthly_sales") self.assertEqual(current_month_sales, existing_current_month_sales + si.base_grand_total) si.cancel() - current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales") + current_month_sales = frappe.get_cached_value("Company", "_Test Company", "total_monthly_sales") self.assertEqual(current_month_sales, existing_current_month_sales) def test_rounding_adjustment(self): si = create_sales_invoice(rate=24900, do_not_save=True) for tax in ["Tax 1", "Tax2"]: - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "description": tax, - "rate": 14, - "cost_center": "_Test Cost Center - _TC", - "included_in_print_rate": 1 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "description": tax, + "rate": 14, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1, + }, + ) si.save() si.submit() self.assertEqual(si.net_total, 19453.13) @@ -1543,16 +1926,23 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.total_taxes_and_charges, 5446.88) self.assertEqual(si.rounding_adjustment, -0.01) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 24900, 0.0], - ["_Test Account Service Tax - _TC", 0.0, 5446.88], - ["Sales - _TC", 0.0, 19453.13], - ["Round Off - _TC", 0.01, 0.0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 24900, 0.0], + ["_Test Account Service Tax - _TC", 0.0, 5446.88], + ["Sales - _TC", 0.0, 19453.13], + ["Round Off - _TC", 0.01, 0.0], + ] + ) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) @@ -1562,24 +1952,30 @@ class TestSalesInvoice(unittest.TestCase): def test_rounding_adjustment_2(self): si = create_sales_invoice(rate=400, do_not_save=True) for rate in [400, 600, 100]: - si.append("items", { - "item_code": "_Test Item", - "gst_hsn_code": "999800", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "rate": rate, - "income_account": "Sales - _TC", - "cost_center": "_Test Cost Center - _TC" - }) + si.append( + "items", + { + "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": rate, + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]: - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": tax_account, - "description": tax_account, - "rate": 9, - "cost_center": "_Test Cost Center - _TC", - "included_in_print_rate": 1 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": tax_account, + "rate": 9, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1, + }, + ) si.save() si.submit() self.assertEqual(si.net_total, 1271.19) @@ -1587,16 +1983,23 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.total_taxes_and_charges, 228.82) self.assertEqual(si.rounding_adjustment, -0.01) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 1500, 0.0], - ["_Test Account Service Tax - _TC", 0.0, 114.41], - ["_Test Account VAT - _TC", 0.0, 114.41], - ["Sales - _TC", 0.0, 1271.18] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 1500, 0.0], + ["_Test Account Service Tax - _TC", 0.0, 114.41], + ["_Test Account VAT - _TC", 0.0, 114.41], + ["Sales - _TC", 0.0, 1271.18], + ] + ) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) @@ -1604,27 +2007,44 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_rounding_adjustment_3(self): + from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( + create_dimension, + disable_dimension, + ) + + create_dimension() + si = create_sales_invoice(do_not_save=True) si.items = [] for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: - si.append("items", { - "item_code": "_Test Item", - "gst_hsn_code": "999800", - "warehouse": "_Test Warehouse - _TC", - "qty": d[1], - "rate": d[0], - "income_account": "Sales - _TC", - "cost_center": "_Test Cost Center - _TC" - }) + si.append( + "items", + { + "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": d[1], + "rate": d[0], + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]: - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": tax_account, - "description": tax_account, - "rate": 6, - "cost_center": "_Test Cost Center - _TC", - "included_in_print_rate": 1 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": tax_account, + "rate": 6, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1, + }, + ) + + si.cost_center = "_Test Cost Center 2 - _TC" + si.location = "Block 1" + si.save() si.submit() self.assertEqual(si.net_total, 4007.16) @@ -1632,31 +2052,52 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.total_taxes_and_charges, 480.86) self.assertEqual(si.rounding_adjustment, -0.02) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 4488.0, 0.0], - ["_Test Account Service Tax - _TC", 0.0, 240.43], - ["_Test Account VAT - _TC", 0.0, 240.43], - ["Sales - _TC", 0.0, 4007.15], - ["Round Off - _TC", 0.01, 0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 4488.0, 0.0], + ["_Test Account Service Tax - _TC", 0.0, 240.43], + ["_Test Account VAT - _TC", 0.0, 240.43], + ["Sales - _TC", 0.0, 4007.15], + ["Round Off - _TC", 0.01, 0], + ] + ) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) debit_credit_diff = 0 for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) - debit_credit_diff += (gle.debit - gle.credit) + debit_credit_diff += gle.debit - gle.credit self.assertEqual(debit_credit_diff, 0) + round_off_gle = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"}, + ["cost_center", "location"], + as_dict=1, + ) + + self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC") + self.assertEqual(round_off_gle.location, "Block 1") + + disable_dimension() + def test_sales_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule - shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test") + shipping_rule = create_shipping_rule( + shipping_rule_type="Selling", shipping_rule_name="Shipping Rule - Sales Invoice Test" + ) si = frappe.copy_doc(test_records[2]) @@ -1669,29 +2110,32 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.total_taxes_and_charges, 468.85) self.assertEqual(si.grand_total, 1718.85) - - def test_create_invoice_without_terms(self): si = create_sales_invoice(do_not_save=1) - self.assertFalse(si.get('payment_schedule')) + self.assertFalse(si.get("payment_schedule")) si.insert() - self.assertTrue(si.get('payment_schedule')) + self.assertTrue(si.get("payment_schedule")) def test_duplicate_due_date_in_terms(self): si = create_sales_invoice(do_not_save=1) - si.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50)) - si.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50)) + si.append( + "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) + ) + si.append( + "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) + ) self.assertRaises(frappe.ValidationError, si.insert) def test_credit_note(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1) + si = create_sales_invoice(item_code="_Test Item", qty=(5 * -1), rate=500, is_return=1) - outstanding_amount = get_outstanding_amount(si.doctype, - si.name, "Debtors - _TC", si.customer, "Customer") + outstanding_amount = get_outstanding_amount( + si.doctype, si.name, "Debtors - _TC", si.customer, "Customer" + ) self.assertEqual(si.outstanding_amount, outstanding_amount) @@ -1706,30 +2150,31 @@ class TestSalesInvoice(unittest.TestCase): pe.insert() pe.submit() - si_doc = frappe.get_doc('Sales Invoice', si.name) + si_doc = frappe.get_doc("Sales Invoice", si.name) self.assertEqual(si_doc.outstanding_amount, 0) def test_sales_invoice_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") + si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") self.assertEqual(si.cost_center, cost_center) expected_values = { - "Debtors - _TC": { - "cost_center": cost_center - }, - "Sales - _TC": { - "cost_center": cost_center - } + "Debtors - _TC": {"cost_center": cost_center}, + "Sales - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -1739,16 +2184,20 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_with_project_link(self): from erpnext.projects.doctype.project.test_project import make_project - project = make_project({ - 'project_name': 'Sales Invoice Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2020-01-01' - }) - item_project = make_project({ - 'project_name': 'Sales Invoice Item Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2019-06-01' - }) + project = make_project( + { + "project_name": "Sales Invoice Project", + "project_template_name": "Test Project Template", + "start_date": "2020-01-01", + } + ) + item_project = make_project( + { + "project_name": "Sales Invoice Item Project", + "project_template_name": "Test Project Template", + "start_date": "2019-06-01", + } + ) sales_invoice = create_sales_invoice(do_not_save=1) sales_invoice.items[0].project = item_project.name @@ -1757,18 +2206,18 @@ class TestSalesInvoice(unittest.TestCase): sales_invoice.submit() expected_values = { - "Debtors - _TC": { - "project": project.name - }, - "Sales - _TC": { - "project": item_project.name - } + "Debtors - _TC": {"project": project.name}, + "Sales - _TC": {"project": item_project.name}, } - gl_entries = frappe.db.sql("""select account, cost_center, project, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, project, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", sales_invoice.name, as_dict=1) + order by account asc""", + sales_invoice.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -1777,21 +2226,21 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_without_cost_center(self): cost_center = "_Test Cost Center - _TC" - si = create_sales_invoice(debit_to="Debtors - _TC") + si = create_sales_invoice(debit_to="Debtors - _TC") expected_values = { - "Debtors - _TC": { - "cost_center": None - }, - "Sales - _TC": { - "cost_center": cost_center - } + "Debtors - _TC": {"cost_center": None}, + "Sales - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -1799,8 +2248,11 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) def test_deferred_revenue(self): - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") + deferred_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_revenue = 1 @@ -1816,14 +2268,16 @@ class TestSalesInvoice(unittest.TestCase): si.save() si.submit() - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) + pda1 = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", + ) + ) pda1.insert() pda1.submit() @@ -1834,17 +2288,28 @@ class TestSalesInvoice(unittest.TestCase): [deferred_account, 43.08, 0.0, "2019-02-28"], ["Sales - _TC", 0.0, 43.08, "2019-02-28"], [deferred_account, 23.07, 0.0, "2019-03-15"], - ["Sales - _TC", 0.0, 23.07, "2019-03-15"] + ["Sales - _TC", 0.0, 23.07, "2019-03-15"], ] check_gl_entries(self, si.name, expected_gle, "2019-01-30") - def test_fixed_deferred_revenue(self): - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") + def test_deferred_revenue_missing_account(self): + si = create_sales_invoice(posting_date="2019-01-10", do_not_submit=True) + si.items[0].enable_deferred_revenue = 1 + si.items[0].service_start_date = "2019-01-10" + si.items[0].service_end_date = "2019-03-15" - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - acc_settings.book_deferred_entries_based_on = 'Months' + self.assertRaises(frappe.ValidationError, si.save) + + def test_fixed_deferred_revenue(self): + deferred_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + acc_settings.book_deferred_entries_based_on = "Months" acc_settings.save() item = create_item("_Test Item for Deferred Accounting") @@ -1853,7 +2318,9 @@ class TestSalesInvoice(unittest.TestCase): item.no_of_months = 12 item.save() - si = create_sales_invoice(item=item.name, posting_date="2019-01-16", rate=50000, do_not_submit=True) + si = create_sales_invoice( + item=item.name, posting_date="2019-01-16", rate=50000, do_not_submit=True + ) si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-16" si.items[0].service_end_date = "2019-03-31" @@ -1861,14 +2328,16 @@ class TestSalesInvoice(unittest.TestCase): si.save() si.submit() - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date='2019-03-31', - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) + pda1 = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date="2019-03-31", + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", + ) + ) pda1.insert() pda1.submit() @@ -1879,13 +2348,13 @@ class TestSalesInvoice(unittest.TestCase): [deferred_account, 20000.0, 0.0, "2019-02-28"], ["Sales - _TC", 0.0, 20000.0, "2019-02-28"], [deferred_account, 20000.0, 0.0, "2019-03-31"], - ["Sales - _TC", 0.0, 20000.0, "2019-03-31"] + ["Sales - _TC", 0.0, 20000.0, "2019-03-31"], ] check_gl_entries(self, si.name, expected_gle, "2019-01-30") - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - acc_settings.book_deferred_entries_based_on = 'Days' + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + acc_settings.book_deferred_entries_based_on = "Days" acc_settings.save() def test_inter_company_transaction(self): @@ -1894,45 +2363,47 @@ class TestSalesInvoice(unittest.TestCase): create_internal_customer( customer_name="_Test Internal Customer", represents_company="_Test Company 1", - allowed_to_interact_with="Wind Power LLC" + allowed_to_interact_with="Wind Power LLC", ) if not frappe.db.exists("Supplier", "_Test Internal Supplier"): - supplier = frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Internal Supplier", - "doctype": "Supplier", - "is_internal_supplier": 1, - "represents_company": "Wind Power LLC" - }) + supplier = frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Internal Supplier", + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": "Wind Power LLC", + } + ) - supplier.append("companies", { - "company": "_Test Company 1" - }) + supplier.append("companies", {"company": "_Test Company 1"}) supplier.insert() si = create_sales_invoice( - company = "Wind Power LLC", - customer = "_Test Internal Customer", - debit_to = "Debtors - WP", - warehouse = "Stores - WP", - income_account = "Sales - WP", - expense_account = "Cost of Goods Sold - WP", - cost_center = "Main - WP", - currency = "USD", - do_not_save = 1 + company="Wind Power LLC", + customer="_Test Internal Customer", + debit_to="Debtors - WP", + warehouse="Stores - WP", + income_account="Sales - WP", + expense_account="Cost of Goods Sold - WP", + cost_center="Main - WP", + currency="USD", + do_not_save=1, ) si.selling_price_list = "_Test Price List Rest of the World" si.submit() target_doc = make_inter_company_transaction("Sales Invoice", si.name) - target_doc.items[0].update({ - "expense_account": "Cost of Goods Sold - _TC1", - "cost_center": "Main - _TC1", - "warehouse": "Stores - _TC1" - }) + target_doc.items[0].update( + { + "expense_account": "Cost of Goods Sold - _TC1", + "cost_center": "Main - _TC1", + "warehouse": "Stores - _TC1", + } + ) target_doc.submit() self.assertEqual(target_doc.company, "_Test Company 1") @@ -1942,9 +2413,9 @@ class TestSalesInvoice(unittest.TestCase): se = make_stock_entry( item_code="138-CMS Shoe", target="Finished Goods - _TC", - company = "_Test Company", + company="_Test Company", qty=1, - basic_rate=500 + basic_rate=500, ) si = frappe.copy_doc(test_records[0]) @@ -1956,8 +2427,9 @@ class TestSalesInvoice(unittest.TestCase): si.insert() si.submit() - sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name}, - fields=["name", "actual_qty"]) + sles = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": si.name}, fields=["name", "actual_qty"] + ) # check if both SLEs are created self.assertEqual(len(sles), 2) @@ -1971,82 +2443,92 @@ class TestSalesInvoice(unittest.TestCase): ## Create internal transfer account from erpnext.selling.doctype.customer.test_customer import create_internal_customer - account = create_account(account_name="Unrealized Profit", - parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") + account = create_account( + account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", + company="_Test Company with perpetual inventory", + ) - frappe.db.set_value('Company', '_Test Company with perpetual inventory', - 'unrealized_profit_loss_account', account) + frappe.db.set_value( + "Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account + ) - customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", - "_Test Company with perpetual inventory") + customer = create_internal_customer( + "_Test Internal Customer 2", + "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory", + ) - create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", - "_Test Company with perpetual inventory") + create_internal_supplier( + "_Test Internal Supplier 2", + "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory", + ) si = create_sales_invoice( - company = "_Test Company with perpetual inventory", - customer = customer, - debit_to = "Debtors - TCP1", - warehouse = "Stores - TCP1", - income_account = "Sales - TCP1", - expense_account = "Cost of Goods Sold - TCP1", - cost_center = "Main - TCP1", - currency = "INR", - do_not_save = 1 + company="_Test Company with perpetual inventory", + customer=customer, + debit_to="Debtors - TCP1", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + currency="INR", + do_not_save=1, ) si.selling_price_list = "_Test Price List Rest of the World" si.update_stock = 1 - si.items[0].target_warehouse = 'Work In Progress - TCP1' + si.items[0].target_warehouse = "Work In Progress - TCP1" # Add stock to stores for succesful stock transfer make_stock_entry( - target="Stores - TCP1", - company = "_Test Company with perpetual inventory", - qty=1, - basic_rate=100 + target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=1, basic_rate=100 ) add_taxes(si) si.save() rate = 0.0 - for d in si.get('items'): - rate = get_incoming_rate({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": si.posting_date, - "posting_time": si.posting_time, - "qty": -1 * flt(d.get('stock_qty')), - "serial_no": d.serial_no, - "company": si.company, - "voucher_type": 'Sales Invoice', - "voucher_no": si.name, - "allow_zero_valuation": d.get("allow_zero_valuation") - }, raise_error_if_no_rate=False) + for d in si.get("items"): + rate = get_incoming_rate( + { + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": si.posting_date, + "posting_time": si.posting_time, + "qty": -1 * flt(d.get("stock_qty")), + "serial_no": d.serial_no, + "company": si.company, + "voucher_type": "Sales Invoice", + "voucher_no": si.name, + "allow_zero_valuation": d.get("allow_zero_valuation"), + }, + raise_error_if_no_rate=False, + ) rate = flt(rate, 2) si.submit() target_doc = make_inter_company_transaction("Sales Invoice", si.name) - target_doc.company = '_Test Company with perpetual inventory' - target_doc.items[0].warehouse = 'Finished Goods - TCP1' + target_doc.company = "_Test Company with perpetual inventory" + target_doc.items[0].warehouse = "Finished Goods - TCP1" add_taxes(target_doc) target_doc.save() target_doc.submit() - tax_amount = flt(rate * (12/100), 2) + tax_amount = flt(rate * (12 / 100), 2) si_gl_entries = [ ["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()], - ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()] + ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()], ] check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) pi_gl_entries = [ - ["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()], - ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()] + ["_Test Account Excise Duty - TCP1", tax_amount, 0.0, nowdate()], + ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()], ] # Sale and Purchase both should be at valuation rate @@ -2062,43 +2544,46 @@ class TestSalesInvoice(unittest.TestCase): data = get_ewb_data("Sales Invoice", [si.name]) - self.assertEqual(data['version'], '1.0.0421') - self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR') - self.assertEqual(data['billLists'][0]['fromTrdName'], '_Test Company') - self.assertEqual(data['billLists'][0]['toTrdName'], '_Test Customer') - self.assertEqual(data['billLists'][0]['vehicleType'], 'R') - self.assertEqual(data['billLists'][0]['totalValue'], 60000) - self.assertEqual(data['billLists'][0]['cgstValue'], 5400) - self.assertEqual(data['billLists'][0]['sgstValue'], 5400) - self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') - self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) - self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) - self.assertEqual(data['billLists'][0]['fromStateCode'],27) + self.assertEqual(data["version"], "1.0.0421") + self.assertEqual(data["billLists"][0]["fromGstin"], "27AAECE4835E1ZR") + self.assertEqual(data["billLists"][0]["fromTrdName"], "_Test Company") + self.assertEqual(data["billLists"][0]["toTrdName"], "_Test Customer") + self.assertEqual(data["billLists"][0]["vehicleType"], "R") + self.assertEqual(data["billLists"][0]["totalValue"], 60000) + self.assertEqual(data["billLists"][0]["cgstValue"], 5400) + self.assertEqual(data["billLists"][0]["sgstValue"], 5400) + self.assertEqual(data["billLists"][0]["vehicleNo"], "KA12KA1234") + self.assertEqual(data["billLists"][0]["itemList"][0]["taxableAmount"], 60000) + self.assertEqual(data["billLists"][0]["actualFromStateCode"], 7) + self.assertEqual(data["billLists"][0]["fromStateCode"], 27) def test_einvoice_submission_without_irn(self): # init - einvoice_settings = frappe.get_doc('E Invoice Settings') + einvoice_settings = frappe.get_doc("E Invoice Settings") einvoice_settings.enable = 1 einvoice_settings.applicable_from = nowdate() - einvoice_settings.append('credentials', { - 'company': '_Test Company', - 'gstin': '27AAECE4835E1ZR', - 'username': 'test', - 'password': 'test' - }) + einvoice_settings.append( + "credentials", + { + "company": "_Test Company", + "gstin": "27AAECE4835E1ZR", + "username": "test", + "password": "test", + }, + ) einvoice_settings.save() country = frappe.flags.country - frappe.flags.country = 'India' + frappe.flags.country = "India" si = make_sales_invoice_for_ewaybill() self.assertRaises(frappe.ValidationError, si.submit) - si.irn = 'test_irn' + si.irn = "test_irn" si.submit() # reset - einvoice_settings = frappe.get_doc('E Invoice Settings') + einvoice_settings = frappe.get_doc("E Invoice Settings") einvoice_settings.enable = 0 frappe.flags.country = country @@ -2110,15 +2595,15 @@ class TestSalesInvoice(unittest.TestCase): si.save() einvoice = make_einvoice(si) - self.assertTrue(einvoice['EwbDtls']) + self.assertTrue(einvoice["EwbDtls"]) validate_totals(einvoice) - si.apply_discount_on = 'Net Total' + si.apply_discount_on = "Net Total" si.save() einvoice = make_einvoice(si) validate_totals(einvoice) - [d.set('included_in_print_rate', 1) for d in si.taxes] + [d.set("included_in_print_rate", 1) for d in si.taxes] si.save() einvoice = make_einvoice(si) validate_totals(einvoice) @@ -2126,29 +2611,39 @@ class TestSalesInvoice(unittest.TestCase): def test_item_tax_net_range(self): item = create_item("T Shirt") - item.set('taxes', []) - item.append("taxes", { - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", - "minimum_net_rate": 0, - "maximum_net_rate": 500 - }) + item.set("taxes", []) + item.append( + "taxes", + { + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + "minimum_net_rate": 0, + "maximum_net_rate": 500, + }, + ) - item.append("taxes", { - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", - "minimum_net_rate": 501, - "maximum_net_rate": 1000 - }) + item.append( + "taxes", + { + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + "minimum_net_rate": 501, + "maximum_net_rate": 1000, + }, + ) item.save() - sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True) - self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC") + sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True) + self.assertEqual( + sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC" + ) # Apply discount - sales_invoice.apply_discount_on = 'Net Total' + sales_invoice.apply_discount_on = "Net Total" sales_invoice.discount_amount = 300 sales_invoice.save() - self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") + self.assertEqual( + sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC" + ) def test_sales_invoice_with_discount_accounting_enabled(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( @@ -2157,14 +2652,17 @@ class TestSalesInvoice(unittest.TestCase): enable_discount_accounting() - discount_account = create_account(account_name="Discount Account", - parent_account="Indirect Expenses - _TC", company="_Test Company") + discount_account = create_account( + account_name="Discount Account", + parent_account="Indirect Expenses - _TC", + company="_Test Company", + ) si = create_sales_invoice(discount_account=discount_account, discount_percentage=10, rate=90) expected_gle = [ ["Debtors - _TC", 90.0, 0.0, nowdate()], ["Discount Account - _TC", 10.0, 0.0, nowdate()], - ["Sales - _TC", 0.0, 100.0, nowdate()] + ["Sales - _TC", 0.0, 100.0, nowdate()], ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) @@ -2176,27 +2674,33 @@ class TestSalesInvoice(unittest.TestCase): ) enable_discount_accounting() - additional_discount_account = create_account(account_name="Discount Account", - parent_account="Indirect Expenses - _TC", company="_Test Company") + additional_discount_account = create_account( + account_name="Discount Account", + parent_account="Indirect Expenses - _TC", + company="_Test Company", + ) - si = create_sales_invoice(parent_cost_center='Main - _TC', do_not_save=1) + si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1) si.apply_discount_on = "Grand Total" si.additional_discount_account = additional_discount_account si.additional_discount_percentage = 20 - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account VAT - _TC", - "cost_center": "Main - _TC", - "description": "Test", - "rate": 10 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "description": "Test", + "rate": 10, + }, + ) si.submit() expected_gle = [ ["_Test Account VAT - _TC", 0.0, 10.0, nowdate()], ["Debtors - _TC", 88, 0.0, nowdate()], ["Discount Account - _TC", 22.0, 0.0, nowdate()], - ["Sales - _TC", 0.0, 100.0, nowdate()] + ["Sales - _TC", 0.0, 100.0, nowdate()], ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) @@ -2204,20 +2708,22 @@ class TestSalesInvoice(unittest.TestCase): def test_asset_depreciation_on_sale_with_pro_rata(self): """ - Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. + Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. """ create_asset_data() asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) post_depreciation_entries(getdate("2021-09-30")) - create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")) + create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30") + ) asset.load_from_db() expected_values = [ ["2020-06-30", 1366.12, 1366.12], ["2021-06-30", 20000.0, 21366.12], - ["2021-09-30", 5041.1, 26407.22] + ["2021-09-30", 5041.1, 26407.22], ] for i, schedule in enumerate(asset.schedules): @@ -2228,23 +2734,28 @@ class TestSalesInvoice(unittest.TestCase): def test_asset_depreciation_on_sale_without_pro_rata(self): """ - Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale. + Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale. """ create_asset_data() - asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, - available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3, - expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1) + asset = create_asset( + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date=getdate("2019-12-31"), + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date=getdate("2020-12-31"), + submit=1, + ) post_depreciation_entries(getdate("2021-09-30")) - create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31")) + create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31") + ) asset.load_from_db() - expected_values = [ - ["2020-12-31", 30000, 30000], - ["2021-12-31", 30000, 60000] - ] + expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]] for i, schedule in enumerate(asset.schedules): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) @@ -2259,7 +2770,9 @@ class TestSalesInvoice(unittest.TestCase): asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) post_depreciation_entries(getdate("2021-09-30")) - si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")) + si = create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30") + ) return_si = make_return_doc("Sales Invoice", si.name) return_si.submit() asset.load_from_db() @@ -2269,8 +2782,8 @@ class TestSalesInvoice(unittest.TestCase): ["2021-06-30", 20000.0, 21366.12, True], ["2022-06-30", 20000.0, 41366.12, False], ["2023-06-30", 20000.0, 61366.12, False], - ["2024-06-30", 20000.0, 81366.12, False], - ["2025-06-06", 18633.88, 100000.0, False] + ["2024-06-30", 20000.0, 81366.12, False], + ["2025-06-06", 18633.88, 100000.0, False], ] for i, schedule in enumerate(asset.schedules): @@ -2295,30 +2808,34 @@ class TestSalesInvoice(unittest.TestCase): party_link = create_party_link("Supplier", supplier, customer) # enable common party accounting - frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) + frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 1) # create a sales invoice si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC") # check outstanding of sales invoice si.reload() - self.assertEqual(si.status, 'Paid') + self.assertEqual(si.status, "Paid") self.assertEqual(flt(si.outstanding_amount), 0.0) # check creation of journal entry - jv = frappe.get_all('Journal Entry Account', { - 'account': si.debit_to, - 'party_type': 'Customer', - 'party': si.customer, - 'reference_type': si.doctype, - 'reference_name': si.name - }, pluck='credit_in_account_currency') + jv = frappe.get_all( + "Journal Entry Account", + { + "account": si.debit_to, + "party_type": "Customer", + "party": si.customer, + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="credit_in_account_currency", + ) self.assertTrue(jv) self.assertEqual(jv[0], si.grand_total) party_link.delete() - frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0) + frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 0) def test_payment_statuses(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -2328,16 +2845,14 @@ class TestSalesInvoice(unittest.TestCase): # Test Overdue si = create_sales_invoice(do_not_submit=True) si.payment_schedule = [] - si.append("payment_schedule", { - "due_date": add_days(today, -5), - "invoice_portion": 50, - "payment_amount": si.grand_total / 2 - }) - si.append("payment_schedule", { - "due_date": add_days(today, 5), - "invoice_portion": 50, - "payment_amount": si.grand_total / 2 - }) + si.append( + "payment_schedule", + {"due_date": add_days(today, -5), "invoice_portion": 50, "payment_amount": si.grand_total / 2}, + ) + si.append( + "payment_schedule", + {"due_date": add_days(today, 5), "invoice_portion": 50, "payment_amount": si.grand_total / 2}, + ) si.submit() self.assertEqual(si.status, "Overdue") @@ -2376,21 +2891,23 @@ class TestSalesInvoice(unittest.TestCase): # Sales Invoice with Payment Schedule si_with_payment_schedule = create_sales_invoice(do_not_submit=True) - si_with_payment_schedule.extend("payment_schedule", [ - { - "due_date": add_days(today, -5), - "invoice_portion": 50, - "payment_amount": si_with_payment_schedule.grand_total / 2 - }, - { - "due_date": add_days(today, 5), - "invoice_portion": 50, - "payment_amount": si_with_payment_schedule.grand_total / 2 - } - ]) + si_with_payment_schedule.extend( + "payment_schedule", + [ + { + "due_date": add_days(today, -5), + "invoice_portion": 50, + "payment_amount": si_with_payment_schedule.grand_total / 2, + }, + { + "due_date": add_days(today, 5), + "invoice_portion": 50, + "payment_amount": si_with_payment_schedule.grand_total / 2, + }, + ], + ) si_with_payment_schedule.submit() - for invoice in (si, si_with_payment_schedule): invoice.db_set("status", "Unpaid") update_invoice_status() @@ -2402,24 +2919,27 @@ class TestSalesInvoice(unittest.TestCase): invoice.reload() self.assertEqual(invoice.status, "Overdue and Discounted") - def test_sales_commission(self): si = frappe.copy_doc(test_records[2]) - frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1) - frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0) + frappe.db.set_value("Item", si.get("items")[0].item_code, "grant_commission", 1) + frappe.db.set_value("Item", si.get("items")[1].item_code, "grant_commission", 0) - item = copy.deepcopy(si.get('items')[0]) - item.update({ - "qty": 1, - "rate": 500, - }) + item = copy.deepcopy(si.get("items")[0]) + item.update( + { + "qty": 1, + "rate": 500, + } + ) - item = copy.deepcopy(si.get('items')[1]) - item.update({ - "qty": 1, - "rate": 500, - }) + item = copy.deepcopy(si.get("items")[1]) + item.update( + { + "qty": 1, + "rate": 500, + } + ) # Test valid values for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): @@ -2435,7 +2955,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(frappe.ValidationError, si.save) def test_sales_invoice_submission_post_account_freezing_date(self): - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1)) si = create_sales_invoice(do_not_save=True) si.posting_date = add_days(getdate(), 1) si.save() @@ -2444,17 +2964,19 @@ class TestSalesInvoice(unittest.TestCase): si.posting_date = getdate() si.submit() - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) def test_over_billing_case_against_delivery_note(self): - ''' - Test a case where duplicating the item with qty = 1 in the invoice - allows overbilling even if it is disabled - ''' + """ + Test a case where duplicating the item with qty = 1 in the invoice + allows overbilling even if it is disabled + """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance') - frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0) + over_billing_allowance = frappe.db.get_single_value( + "Accounts Settings", "over_billing_allowance" + ) + frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) dn = create_delivery_note() dn.submit() @@ -2462,7 +2984,7 @@ class TestSalesInvoice(unittest.TestCase): si = make_sales_invoice(dn.name) # make a copy of first item and add it to invoice item_copy = frappe.copy_doc(si.items[0]) - si.append('items', item_copy) + si.append("items", item_copy) si.save() with self.assertRaises(frappe.ValidationError) as err: @@ -2470,13 +2992,16 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue("cannot overbill" in str(err.exception).lower()) - frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance) + frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance) def test_multi_currency_deferred_revenue_via_journal_entry(self): - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") + deferred_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) - acc_settings = frappe.get_single('Accounts Settings') + acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 1 acc_settings.submit_journal_entries = 1 acc_settings.save() @@ -2486,12 +3011,19 @@ class TestSalesInvoice(unittest.TestCase): item.deferred_revenue_account = deferred_account item.save() - si = create_sales_invoice(customer='_Test Customer USD', currency='USD', - item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True) + si = create_sales_invoice( + customer="_Test Customer USD", + currency="USD", + item=item.name, + qty=1, + rate=100, + conversion_rate=60, + do_not_save=True, + ) si.set_posting_time = 1 - si.posting_date = '2019-01-01' - si.debit_to = '_Test Receivable USD - _TC' + si.posting_date = "2019-01-01" + si.debit_to = "_Test Receivable USD - _TC" si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-01" si.items[0].service_end_date = "2019-03-30" @@ -2499,16 +3031,18 @@ class TestSalesInvoice(unittest.TestCase): si.save() si.submit() - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31')) + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", getdate("2019-01-31")) - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) + pda1 = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", + ) + ) pda1.insert() pda1.submit() @@ -2519,13 +3053,17 @@ class TestSalesInvoice(unittest.TestCase): ["Sales - _TC", 0.0, 1887.64, "2019-02-28"], [deferred_account, 1887.64, 0.0, "2019-02-28"], ["Sales - _TC", 0.0, 2022.47, "2019-03-15"], - [deferred_account, 2022.47, 0.0, "2019-03-15"] + [deferred_account, 2022.47, 0.0, "2019-03-15"], ] - gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + gl_entries = gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s - order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1) + order by posting_date asc, account asc""", + (si.items[0].name, si.posting_date), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -2533,125 +3071,196 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - acc_settings = frappe.get_single('Accounts Settings') + acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 - acc_settings.submit_journal_entriessubmit_journal_entries = 0 + acc_settings.submit_journal_entries = 0 acc_settings.save() - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) + + def test_standalone_serial_no_return(self): + si = create_sales_invoice( + item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 + ) + si.reload() + self.assertTrue(si.items[0].serial_no) + + def test_gain_loss_with_advance_entry(self): + from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry + + unlink_enabled = frappe.db.get_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" + ) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1 + ) + + jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False) + + jv.accounts[0].exchange_rate = 70 + jv.accounts[0].credit_in_account_currency = 100 + jv.accounts[0].party_type = "Customer" + jv.accounts[0].party = "_Test Customer USD" + + jv.save() + jv.submit() + + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=75, + do_not_save=1, + rate=100, + ) + + si.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.accounts[0].name, + "advance_amount": 100, + "allocated_amount": 100, + "ref_exchange_rate": 70, + }, + ) + si.save() + si.submit() + + expected_gle = [ + ["_Test Receivable USD - _TC", 7500.0, 500], + ["Exchange Gain/Loss - _TC", 500.0, 0.0], + ["Sales - _TC", 0.0, 7500.0], + ] + + check_gl_entries(self, si.name, expected_gle, nowdate()) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled + ) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() - si.naming_series = 'INV-2020-.#####' + si.naming_series = "INV-2020-.#####" si.items = [] - si.append("items", { - "item_code": "_Test Item", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 2000, - "rate": 12, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) + si.append( + "items", + { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 2000, + "rate": 12, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) - si.append("items", { - "item_code": "_Test Item 2", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 420, - "rate": 15, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) + si.append( + "items", + { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 420, + "rate": 15, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) return si def make_test_address_for_ewaybill(): - if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Address for Eway bill", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+910000000000", - "gstin": "27AAECE4835E1ZR", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "401108" - }).insert() + if not frappe.db.exists("Address", "_Test Address for Eway bill-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Address for Eway bill", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AAECE4835E1ZR", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "401108", + } + ).insert() - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company" - }) + address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"}) address.save() - if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Customer-Address for Eway bill", - "address_type": "Shipping", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+910000000000", - "gstin": "27AACCM7806M1Z3", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "410038" - }).insert() + if not frappe.db.exists("Address", "_Test Customer-Address for Eway bill-Shipping"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Customer-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AACCM7806M1Z3", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "410038", + } + ).insert() - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Customer" - }) + address.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"}) address.save() - if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'): - address = frappe.get_doc({ - "address_line1": "_Test Dispatch Address Line 1", - "address_title": "_Test Dispatch-Address for Eway bill", - "address_type": "Shipping", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 0, - "phone": "+910000000000", - "gstin": "07AAACC1206D1ZI", - "gst_state": "Delhi", - "gst_state_number": "07", - "pincode": "1100101" - }).insert() + if not frappe.db.exists("Address", "_Test Dispatch-Address for Eway bill-Shipping"): + address = frappe.get_doc( + { + "address_line1": "_Test Dispatch Address Line 1", + "address_title": "_Test Dispatch-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 0, + "phone": "+910000000000", + "gstin": "07AAACC1206D1ZI", + "gst_state": "Delhi", + "gst_state_number": "07", + "pincode": "1100101", + } + ).insert() - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company" - }) + address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"}) address.save() + def make_test_transporter_for_ewaybill(): - if not frappe.db.exists('Supplier', '_Test Transporter'): - frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": "_Test Transporter", - "country": "India", - "supplier_group": "_Test Supplier Group", - "supplier_type": "Company", - "is_transporter": 1 - }).insert() + if not frappe.db.exists("Supplier", "_Test Transporter"): + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": "_Test Transporter", + "country": "India", + "supplier_group": "_Test Supplier Group", + "supplier_type": "Company", + "is_transporter": 1, + } + ).insert() + def make_sales_invoice_for_ewaybill(): make_test_address_for_ewaybill() @@ -2662,20 +3271,23 @@ def make_sales_invoice_for_ewaybill(): gst_account = frappe.get_all( "GST Account", fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"} + filters={"company": "_Test Company"}, ) if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company", - "cgst_account": "Output Tax CGST - _TC", - "sgst_account": "Output Tax SGST - _TC", - "igst_account": "Output Tax IGST - _TC", - }) + gst_settings.append( + "gst_accounts", + { + "company": "_Test Company", + "cgst_account": "Output Tax CGST - _TC", + "sgst_account": "Output Tax SGST - _TC", + "igst_account": "Output Tax IGST - _TC", + }, + ) gst_settings.save() - si = create_sales_invoice(do_not_save=1, rate='60000') + si = create_sales_invoice(do_not_save=1, rate="60000") si.distance = 2000 si.company_address = "_Test Address for Eway bill-Billing" @@ -2683,32 +3295,43 @@ def make_sales_invoice_for_ewaybill(): si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping" si.vehicle_no = "KA12KA1234" si.gst_category = "Registered Regular" - si.mode_of_transport = 'Road' - si.transporter = '_Test Transporter' + si.mode_of_transport = "Road" + si.transporter = "_Test Transporter" - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax CGST - _TC", - "cost_center": "Main - _TC", - "description": "CGST @ 9.0", - "rate": 9 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax CGST - _TC", + "cost_center": "Main - _TC", + "description": "CGST @ 9.0", + "rate": 9, + }, + ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax SGST - _TC", - "cost_center": "Main - _TC", - "description": "SGST @ 9.0", - "rate": 9 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax SGST - _TC", + "cost_center": "Main - _TC", + "description": "SGST @ 9.0", + "rate": 9, + }, + ) return si + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s - order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1) + order by posting_date asc, account asc""", + (voucher_no, posting_date), + as_dict=1, + ) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) @@ -2716,6 +3339,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + def create_sales_invoice(**args): si = frappe.new_doc("Sales Invoice") args = frappe._dict(args) @@ -2730,32 +3354,35 @@ def create_sales_invoice(**args): si.is_pos = args.is_pos si.is_return = args.is_return si.return_against = args.return_against - si.currency=args.currency or "INR" + si.currency = args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 si.naming_series = args.naming_series or "T-SINV-" si.cost_center = args.parent_cost_center - si.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "item_name": args.item_name or "_Test Item", - "description": args.description or "_Test Item", - "gst_hsn_code": "999800", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "uom": args.uom or "Nos", - "stock_uom": args.uom or "Nos", - "rate": args.rate if args.get("rate") is not None else 100, - "price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100, - "income_account": args.income_account or "Sales - _TC", - "expense_account": args.expense_account or "Cost of Goods Sold - _TC", - "asset": args.asset or None, - "discount_account": args.discount_account or None, - "discount_amount": args.discount_amount or 0, - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no, - "conversion_factor": 1, - "incoming_rate": args.incoming_rate or 0 - }) + si.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "item_name": args.item_name or "_Test Item", + "description": args.description or "_Test Item", + "gst_hsn_code": "999800", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "uom": args.uom or "Nos", + "stock_uom": args.uom or "Nos", + "rate": args.rate if args.get("rate") is not None else 100, + "price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100, + "income_account": args.income_account or "Sales - _TC", + "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "asset": args.asset or None, + "discount_account": args.discount_account or None, + "discount_amount": args.discount_amount or 0, + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "serial_no": args.serial_no, + "conversion_factor": 1, + "incoming_rate": args.incoming_rate or 0, + }, + ) if not args.do_not_save: si.insert() @@ -2768,6 +3395,7 @@ def create_sales_invoice(**args): return si + def create_sales_invoice_against_cost_center(**args): si = frappe.new_doc("Sales Invoice") args = frappe._dict(args) @@ -2783,20 +3411,23 @@ def create_sales_invoice_against_cost_center(**args): si.is_pos = args.is_pos si.is_return = args.is_return si.return_against = args.return_against - si.currency=args.currency or "INR" + si.currency = args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 - si.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "gst_hsn_code": "999800", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "rate": args.rate or 100, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no - }) + si.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "gst_hsn_code": "999800", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "rate": args.rate or 100, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "serial_no": args.serial_no, + }, + ) if not args.do_not_save: si.insert() @@ -2811,59 +3442,69 @@ def create_sales_invoice_against_cost_center(**args): test_dependencies = ["Journal Entry", "Contact", "Address"] -test_records = frappe.get_test_records('Sales Invoice') +test_records = frappe.get_test_records("Sales Invoice") + def get_outstanding_amount(against_voucher_type, against_voucher, account, party, party_type): - bal = flt(frappe.db.sql(""" + bal = flt( + frappe.db.sql( + """ select sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where against_voucher_type=%s and against_voucher=%s and account = %s and party = %s and party_type = %s""", - (against_voucher_type, against_voucher, account, party, party_type))[0][0] or 0.0) + (against_voucher_type, against_voucher, account, party, party_type), + )[0][0] + or 0.0 + ) - if against_voucher_type == 'Purchase Invoice': + if against_voucher_type == "Purchase Invoice": bal = bal * -1 return bal + def get_taxes_and_charges(): - return [{ - "account_head": "_Test Account Excise Duty - TCP1", - "charge_type": "On Net Total", - "cost_center": "Main - TCP1", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "idx": 1, - "included_in_print_rate": 1, - "parentfield": "taxes", - "rate": 12 - }, - { - "account_head": "_Test Account Education Cess - TCP1", - "charge_type": "On Previous Row Amount", - "cost_center": "Main - TCP1", - "description": "Education Cess", - "doctype": "Sales Taxes and Charges", - "idx": 2, - "included_in_print_rate": 1, - "parentfield": "taxes", - "rate": 2, - "row_id": 1 - }] + return [ + { + "account_head": "_Test Account Excise Duty - TCP1", + "charge_type": "On Net Total", + "cost_center": "Main - TCP1", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "idx": 1, + "included_in_print_rate": 1, + "parentfield": "taxes", + "rate": 12, + }, + { + "account_head": "_Test Account Education Cess - TCP1", + "charge_type": "On Previous Row Amount", + "cost_center": "Main - TCP1", + "description": "Education Cess", + "doctype": "Sales Taxes and Charges", + "idx": 2, + "included_in_print_rate": 1, + "parentfield": "taxes", + "rate": 2, + "row_id": 1, + }, + ] + def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Supplier", supplier_name): - supplier = frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": supplier_name, - "doctype": "Supplier", - "is_internal_supplier": 1, - "represents_company": represents_company - }) + supplier = frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": supplier_name, + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": represents_company, + } + ) - supplier.append("companies", { - "company": allowed_to_interact_with - }) + supplier.append("companies", {"company": allowed_to_interact_with}) supplier.insert() supplier_name = supplier.name @@ -2872,11 +3513,15 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter return supplier_name + def add_taxes(doc): - doc.append('taxes', { - 'account_head': '_Test Account Excise Duty - TCP1', - "charge_type": "On Net Total", - "cost_center": "Main - TCP1", - "description": "Excise Duty", - "rate": 12 - }) + doc.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - TCP1", + "charge_type": "On Net Total", + "cost_center": "Main - TCP1", + "description": "Excise Duty", + "rate": 12, + }, + ) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index 8043a1b66f2..d9009bae4c0 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -21,13 +21,14 @@ class SalesTaxesandChargesTemplate(Document): def autoname(self): if self.company and self.title: - abbr = frappe.get_cached_value('Company', self.company, 'abbr') - self.name = '{0} - {1}'.format(self.title, abbr) + abbr = frappe.get_cached_value("Company", self.company, "abbr") + self.name = "{0} - {1}".format(self.title, abbr) def set_missing_values(self): for data in self.taxes: - if data.charge_type == 'On Net Total' and flt(data.rate) == 0.0: - data.rate = frappe.db.get_value('Account', data.account_head, 'tax_rate') + if data.charge_type == "On Net Total" and flt(data.rate) == 0.0: + data.rate = frappe.db.get_value("Account", data.account_head, "tax_rate") + def valdiate_taxes_and_charges_template(doc): # default should not be disabled @@ -35,9 +36,13 @@ def valdiate_taxes_and_charges_template(doc): # doc.is_default = 1 if doc.is_default == 1: - frappe.db.sql("""update `tab{0}` set is_default = 0 - where is_default = 1 and name != %s and company = %s""".format(doc.doctype), - (doc.name, doc.company)) + frappe.db.sql( + """update `tab{0}` set is_default = 0 + where is_default = 1 and name != %s and company = %s""".format( + doc.doctype + ), + (doc.name, doc.company), + ) validate_disabled(doc) @@ -50,13 +55,27 @@ def valdiate_taxes_and_charges_template(doc): validate_cost_center(tax, doc) validate_inclusive_tax(tax, doc) + def validate_disabled(doc): if doc.is_default and doc.disabled: frappe.throw(_("Disabled template must not be default template")) + def validate_for_tax_category(doc): if not doc.tax_category: return - if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}): - frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) + if frappe.db.exists( + doc.doctype, + { + "company": doc.company, + "tax_category": doc.tax_category, + "disabled": 0, + "name": ["!=", doc.name], + }, + ): + frappe.throw( + _( + "A template with tax category {0} already exists. Only one template is allowed with each tax category" + ).format(frappe.bold(doc.tax_category)) + ) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py index 5b9fbafed8d..6432acaae93 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py @@ -1,23 +1,16 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'taxes_and_charges', - 'non_standard_fieldnames': { - 'Tax Rule': 'sales_tax_template', - 'Subscription': 'sales_tax_template', - 'Restaurant': 'default_tax_template' + "fieldname": "taxes_and_charges", + "non_standard_fieldnames": { + "Tax Rule": "sales_tax_template", + "Subscription": "sales_tax_template", + "Restaurant": "default_tax_template", }, - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Sales Invoice', 'Sales Order', 'Delivery Note'] - }, - { - 'label': _('References'), - 'items': ['POS Profile', 'Subscription', 'Restaurant', 'Tax Rule'] - } - ] + "transactions": [ + {"label": _("Transactions"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]}, + {"label": _("References"), "items": ["POS Profile", "Subscription", "Restaurant", "Tax Rule"]}, + ], } diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py index 7b13c6c6925..972b773501a 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py @@ -5,7 +5,8 @@ import unittest import frappe -test_records = frappe.get_test_records('Sales Taxes and Charges Template') +test_records = frappe.get_test_records("Sales Taxes and Charges Template") + class TestSalesTaxesandChargesTemplate(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py index b543ad8204d..4f49843c1eb 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py @@ -10,95 +10,115 @@ from frappe.model.naming import make_autoname from frappe.utils import nowdate -class ShareDontExists(ValidationError): pass +class ShareDontExists(ValidationError): + pass + class ShareTransfer(Document): def on_submit(self): - if self.transfer_type == 'Issue': + if self.transfer_type == "Issue": shareholder = self.get_company_shareholder() - shareholder.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares, - 'is_company': 1, - 'current_state': 'Issued' - }) + shareholder.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + "is_company": 1, + "current_state": "Issued", + }, + ) shareholder.save() doc = self.get_shareholder_doc(self.to_shareholder) - doc.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + doc.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) doc.save() - elif self.transfer_type == 'Purchase': + elif self.transfer_type == "Purchase": self.remove_shares(self.from_shareholder) self.remove_shares(self.get_company_shareholder().name) - elif self.transfer_type == 'Transfer': + elif self.transfer_type == "Transfer": self.remove_shares(self.from_shareholder) doc = self.get_shareholder_doc(self.to_shareholder) - doc.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + doc.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) doc.save() def on_cancel(self): - if self.transfer_type == 'Issue': + if self.transfer_type == "Issue": compnay_shareholder = self.get_company_shareholder() self.remove_shares(compnay_shareholder.name) self.remove_shares(self.to_shareholder) - elif self.transfer_type == 'Purchase': + elif self.transfer_type == "Purchase": compnay_shareholder = self.get_company_shareholder() from_shareholder = self.get_shareholder_doc(self.from_shareholder) - from_shareholder.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + from_shareholder.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) from_shareholder.save() - compnay_shareholder.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + compnay_shareholder.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) compnay_shareholder.save() - elif self.transfer_type == 'Transfer': + elif self.transfer_type == "Transfer": self.remove_shares(self.to_shareholder) from_shareholder = self.get_shareholder_doc(self.from_shareholder) - from_shareholder.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + from_shareholder.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) from_shareholder.save() def validate(self): @@ -106,90 +126,96 @@ class ShareTransfer(Document): self.basic_validations() self.folio_no_validation() - if self.transfer_type == 'Issue': + if self.transfer_type == "Issue": # validate share doesn't exist in company ret_val = self.share_exists(self.get_company_shareholder().name) - if ret_val in ('Complete', 'Partial'): - frappe.throw(_('The shares already exist'), frappe.DuplicateEntryError) + if ret_val in ("Complete", "Partial"): + frappe.throw(_("The shares already exist"), frappe.DuplicateEntryError) else: # validate share exists with from_shareholder ret_val = self.share_exists(self.from_shareholder) - if ret_val in ('Outside', 'Partial'): - frappe.throw(_("The shares don't exist with the {0}") - .format(self.from_shareholder), ShareDontExists) + if ret_val in ("Outside", "Partial"): + frappe.throw( + _("The shares don't exist with the {0}").format(self.from_shareholder), ShareDontExists + ) def basic_validations(self): - if self.transfer_type == 'Purchase': - self.to_shareholder = '' + if self.transfer_type == "Purchase": + self.to_shareholder = "" if not self.from_shareholder: - frappe.throw(_('The field From Shareholder cannot be blank')) + frappe.throw(_("The field From Shareholder cannot be blank")) if not self.from_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) if not self.asset_account: - frappe.throw(_('The field Asset Account cannot be blank')) - elif (self.transfer_type == 'Issue'): - self.from_shareholder = '' + frappe.throw(_("The field Asset Account cannot be blank")) + elif self.transfer_type == "Issue": + self.from_shareholder = "" if not self.to_shareholder: - frappe.throw(_('The field To Shareholder cannot be blank')) + frappe.throw(_("The field To Shareholder cannot be blank")) if not self.to_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) if not self.asset_account: - frappe.throw(_('The field Asset Account cannot be blank')) + frappe.throw(_("The field Asset Account cannot be blank")) else: if not self.from_shareholder or not self.to_shareholder: - frappe.throw(_('The fields From Shareholder and To Shareholder cannot be blank')) + frappe.throw(_("The fields From Shareholder and To Shareholder cannot be blank")) if not self.to_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) if not self.equity_or_liability_account: - frappe.throw(_('The field Equity/Liability Account cannot be blank')) + frappe.throw(_("The field Equity/Liability Account cannot be blank")) if self.from_shareholder == self.to_shareholder: - frappe.throw(_('The seller and the buyer cannot be the same')) + frappe.throw(_("The seller and the buyer cannot be the same")) if self.no_of_shares != self.to_no - self.from_no + 1: - frappe.throw(_('The number of shares and the share numbers are inconsistent')) + frappe.throw(_("The number of shares and the share numbers are inconsistent")) if not self.amount: self.amount = self.rate * self.no_of_shares if self.amount != self.rate * self.no_of_shares: - frappe.throw(_('There are inconsistencies between the rate, no of shares and the amount calculated')) + frappe.throw( + _("There are inconsistencies between the rate, no of shares and the amount calculated") + ) def share_exists(self, shareholder): doc = self.get_shareholder_doc(shareholder) for entry in doc.share_balance: - if entry.share_type != self.share_type or \ - entry.from_no > self.to_no or \ - entry.to_no < self.from_no: - continue # since query lies outside bounds - elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: #both inside - return 'Complete' # absolute truth! + if ( + entry.share_type != self.share_type or entry.from_no > self.to_no or entry.to_no < self.from_no + ): + continue # since query lies outside bounds + elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: # both inside + return "Complete" # absolute truth! elif entry.from_no <= self.from_no <= self.to_no: - return 'Partial' + return "Partial" elif entry.from_no <= self.to_no <= entry.to_no: - return 'Partial' + return "Partial" - return 'Outside' + return "Outside" def folio_no_validation(self): - shareholder_fields = ['from_shareholder', 'to_shareholder'] + shareholder_fields = ["from_shareholder", "to_shareholder"] for shareholder_field in shareholder_fields: shareholder_name = self.get(shareholder_field) if not shareholder_name: continue doc = self.get_shareholder_doc(shareholder_name) if doc.company != self.company: - frappe.throw(_('The shareholder does not belong to this company')) + frappe.throw(_("The shareholder does not belong to this company")) if not doc.folio_no: - doc.folio_no = self.from_folio_no \ - if (shareholder_field == 'from_shareholder') else self.to_folio_no + doc.folio_no = ( + self.from_folio_no if (shareholder_field == "from_shareholder") else self.to_folio_no + ) doc.save() else: - if doc.folio_no and doc.folio_no != (self.from_folio_no if (shareholder_field == 'from_shareholder') else self.to_folio_no): - frappe.throw(_('The folio numbers are not matching')) + if doc.folio_no and doc.folio_no != ( + self.from_folio_no if (shareholder_field == "from_shareholder") else self.to_folio_no + ): + frappe.throw(_("The folio numbers are not matching")) def autoname_folio(self, shareholder, is_company=False): if is_company: doc = self.get_company_shareholder() else: doc = self.get_shareholder_doc(shareholder) - doc.folio_no = make_autoname('FN.#####') + doc.folio_no = make_autoname("FN.#####") doc.save() return doc.folio_no @@ -197,106 +223,120 @@ class ShareTransfer(Document): # query = {'from_no': share_starting_no, 'to_no': share_ending_no} # Shares exist for sure # Iterate over all entries and modify entry if in entry - doc = frappe.get_doc('Shareholder', shareholder) + doc = frappe.get_doc("Shareholder", shareholder) current_entries = doc.share_balance new_entries = [] for entry in current_entries: # use spaceage logic here - if entry.share_type != self.share_type or \ - entry.from_no > self.to_no or \ - entry.to_no < self.from_no: + if ( + entry.share_type != self.share_type or entry.from_no > self.to_no or entry.to_no < self.from_no + ): new_entries.append(entry) - continue # since query lies outside bounds + continue # since query lies outside bounds elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: - #split + # split if entry.from_no == self.from_no: if entry.to_no == self.to_no: - pass #nothing to append + pass # nothing to append else: - new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) + new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate)) else: if entry.to_no == self.to_no: - new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) + new_entries.append( + self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate) + ) else: - new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) - new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) + new_entries.append( + self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate) + ) + new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate)) elif entry.from_no >= self.from_no and entry.to_no <= self.to_no: # split and check - pass #nothing to append + pass # nothing to append elif self.from_no <= entry.from_no <= self.to_no and entry.to_no >= self.to_no: - new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) + new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate)) elif self.from_no <= entry.to_no <= self.to_no and entry.from_no <= self.from_no: - new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) + new_entries.append( + self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate) + ) else: new_entries.append(entry) doc.share_balance = [] for entry in new_entries: - doc.append('share_balance', entry) + doc.append("share_balance", entry) doc.save() def return_share_balance_entry(self, from_no, to_no, rate): # return an entry as a dict return { - 'share_type' : self.share_type, - 'from_no' : from_no, - 'to_no' : to_no, - 'rate' : rate, - 'amount' : self.rate * (to_no - from_no + 1), - 'no_of_shares' : to_no - from_no + 1 + "share_type": self.share_type, + "from_no": from_no, + "to_no": to_no, + "rate": rate, + "amount": self.rate * (to_no - from_no + 1), + "no_of_shares": to_no - from_no + 1, } def get_shareholder_doc(self, shareholder): # Get Shareholder doc based on the Shareholder name if shareholder: - query_filters = {'name': shareholder} + query_filters = {"name": shareholder} - name = frappe.db.get_value('Shareholder', {'name': shareholder}, 'name') + name = frappe.db.get_value("Shareholder", {"name": shareholder}, "name") - return frappe.get_doc('Shareholder', name) + return frappe.get_doc("Shareholder", name) def get_company_shareholder(self): # Get company doc or create one if not present - company_shareholder = frappe.db.get_value('Shareholder', - { - 'company': self.company, - 'is_company': 1 - }, 'name') + company_shareholder = frappe.db.get_value( + "Shareholder", {"company": self.company, "is_company": 1}, "name" + ) if company_shareholder: - return frappe.get_doc('Shareholder', company_shareholder) + return frappe.get_doc("Shareholder", company_shareholder) else: - shareholder = frappe.get_doc({ - 'doctype': 'Shareholder', - 'title': self.company, - 'company': self.company, - 'is_company': 1 - }) + shareholder = frappe.get_doc( + {"doctype": "Shareholder", "title": self.company, "company": self.company, "is_company": 1} + ) shareholder.insert() return shareholder + @frappe.whitelist() -def make_jv_entry( company, account, amount, payment_account,\ - credit_applicant_type, credit_applicant, debit_applicant_type, debit_applicant): - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Journal Entry' +def make_jv_entry( + company, + account, + amount, + payment_account, + credit_applicant_type, + credit_applicant, + debit_applicant_type, + debit_applicant, +): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Journal Entry" journal_entry.company = company journal_entry.posting_date = nowdate() account_amt_list = [] - account_amt_list.append({ - "account": account, - "debit_in_account_currency": amount, - "party_type": debit_applicant_type, - "party": debit_applicant, - }) - account_amt_list.append({ - "account": payment_account, - "credit_in_account_currency": amount, - "party_type": credit_applicant_type, - "party": credit_applicant, - }) + account_amt_list.append( + { + "account": account, + "debit_in_account_currency": amount, + "party_type": debit_applicant_type, + "party": debit_applicant, + } + ) + account_amt_list.append( + { + "account": payment_account, + "credit_in_account_currency": amount, + "party_type": credit_applicant_type, + "party": credit_applicant, + } + ) journal_entry.set("accounts", account_amt_list) return journal_entry.as_dict() diff --git a/erpnext/accounts/doctype/share_transfer/test_share_transfer.py b/erpnext/accounts/doctype/share_transfer/test_share_transfer.py index bc3a52167db..97310743605 100644 --- a/erpnext/accounts/doctype/share_transfer/test_share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/test_share_transfer.py @@ -9,6 +9,7 @@ from erpnext.accounts.doctype.share_transfer.share_transfer import ShareDontExis test_dependencies = ["Share Type", "Shareholder"] + class TestShareTransfer(unittest.TestCase): def setUp(self): frappe.db.sql("delete from `tabShare Transfer`") @@ -26,7 +27,7 @@ class TestShareTransfer(unittest.TestCase): "rate": 10, "company": "_Test Company", "asset_account": "Cash - _TC", - "equity_or_liability_account": "Creditors - _TC" + "equity_or_liability_account": "Creditors - _TC", }, { "doctype": "Share Transfer", @@ -40,7 +41,7 @@ class TestShareTransfer(unittest.TestCase): "no_of_shares": 100, "rate": 15, "company": "_Test Company", - "equity_or_liability_account": "Creditors - _TC" + "equity_or_liability_account": "Creditors - _TC", }, { "doctype": "Share Transfer", @@ -54,7 +55,7 @@ class TestShareTransfer(unittest.TestCase): "no_of_shares": 300, "rate": 20, "company": "_Test Company", - "equity_or_liability_account": "Creditors - _TC" + "equity_or_liability_account": "Creditors - _TC", }, { "doctype": "Share Transfer", @@ -68,7 +69,7 @@ class TestShareTransfer(unittest.TestCase): "no_of_shares": 200, "rate": 15, "company": "_Test Company", - "equity_or_liability_account": "Creditors - _TC" + "equity_or_liability_account": "Creditors - _TC", }, { "doctype": "Share Transfer", @@ -82,42 +83,46 @@ class TestShareTransfer(unittest.TestCase): "rate": 25, "company": "_Test Company", "asset_account": "Cash - _TC", - "equity_or_liability_account": "Creditors - _TC" - } + "equity_or_liability_account": "Creditors - _TC", + }, ] for d in share_transfers: st = frappe.get_doc(d) st.submit() def test_invalid_share_transfer(self): - doc = frappe.get_doc({ - "doctype": "Share Transfer", - "transfer_type": "Transfer", - "date": "2018-01-05", - "from_shareholder": "SH-00003", - "to_shareholder": "SH-00002", - "share_type": "Equity", - "from_no": 1, - "to_no": 100, - "no_of_shares": 100, - "rate": 15, - "company": "_Test Company", - "equity_or_liability_account": "Creditors - _TC" - }) + doc = frappe.get_doc( + { + "doctype": "Share Transfer", + "transfer_type": "Transfer", + "date": "2018-01-05", + "from_shareholder": "SH-00003", + "to_shareholder": "SH-00002", + "share_type": "Equity", + "from_no": 1, + "to_no": 100, + "no_of_shares": 100, + "rate": 15, + "company": "_Test Company", + "equity_or_liability_account": "Creditors - _TC", + } + ) self.assertRaises(ShareDontExists, doc.insert) - doc = frappe.get_doc({ - "doctype": "Share Transfer", - "transfer_type": "Purchase", - "date": "2018-01-02", - "from_shareholder": "SH-00001", - "share_type": "Equity", - "from_no": 1, - "to_no": 200, - "no_of_shares": 200, - "rate": 15, - "company": "_Test Company", - "asset_account": "Cash - _TC", - "equity_or_liability_account": "Creditors - _TC" - }) + doc = frappe.get_doc( + { + "doctype": "Share Transfer", + "transfer_type": "Purchase", + "date": "2018-01-02", + "from_shareholder": "SH-00001", + "share_type": "Equity", + "from_no": 1, + "to_no": 200, + "no_of_shares": 200, + "rate": 15, + "company": "_Test Company", + "asset_account": "Cash - _TC", + "equity_or_liability_account": "Creditors - _TC", + } + ) self.assertRaises(ShareDontExists, doc.insert) diff --git a/erpnext/accounts/doctype/share_type/share_type_dashboard.py b/erpnext/accounts/doctype/share_type/share_type_dashboard.py index fdb417ed6d0..19604b332a3 100644 --- a/erpnext/accounts/doctype/share_type/share_type_dashboard.py +++ b/erpnext/accounts/doctype/share_type/share_type_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'share_type', - 'transactions': [ - { - 'label': _('References'), - 'items': ['Share Transfer', 'Shareholder'] - } - ] + "fieldname": "share_type", + "transactions": [{"label": _("References"), "items": ["Share Transfer", "Shareholder"]}], } diff --git a/erpnext/accounts/doctype/shareholder/shareholder.py b/erpnext/accounts/doctype/shareholder/shareholder.py index 8a0fa85a692..b0e2493f7a6 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder.py +++ b/erpnext/accounts/doctype/shareholder/shareholder.py @@ -15,7 +15,7 @@ class Shareholder(Document): load_address_and_contact(self) def on_trash(self): - delete_contact_and_address('Shareholder', self.name) + delete_contact_and_address("Shareholder", self.name) def before_save(self): for entry in self.share_balance: diff --git a/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py b/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py index 44d5ec684fb..fa9d431c19e 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py +++ b/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py @@ -1,14 +1,6 @@ - - def get_data(): return { - 'fieldname': 'shareholder', - 'non_standard_fieldnames': { - 'Share Transfer': 'to_shareholder' - }, - 'transactions': [ - { - 'items': ['Share Transfer'] - } - ] + "fieldname": "shareholder", + "non_standard_fieldnames": {"Share Transfer": "to_shareholder"}, + "transactions": [{"items": ["Share Transfer"]}], } diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index 792e7d21a78..1d79503a05e 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -12,9 +12,17 @@ from frappe.utils import flt, fmt_money import erpnext -class OverlappingConditionError(frappe.ValidationError): pass -class FromGreaterThanToError(frappe.ValidationError): pass -class ManyBlankToValuesError(frappe.ValidationError): pass +class OverlappingConditionError(frappe.ValidationError): + pass + + +class FromGreaterThanToError(frappe.ValidationError): + pass + + +class ManyBlankToValuesError(frappe.ValidationError): + pass + class ShippingRule(Document): def validate(self): @@ -35,15 +43,19 @@ class ShippingRule(Document): if not d.to_value: zero_to_values.append(d) elif d.from_value >= d.to_value: - throw(_("From value must be less than to value in row {0}").format(d.idx), FromGreaterThanToError) + throw( + _("From value must be less than to value in row {0}").format(d.idx), FromGreaterThanToError + ) # check if more than two or more rows has To Value = 0 if len(zero_to_values) >= 2: - throw(_('There can only be one Shipping Rule Condition with 0 or blank value for "To Value"'), - ManyBlankToValuesError) + throw( + _('There can only be one Shipping Rule Condition with 0 or blank value for "To Value"'), + ManyBlankToValuesError, + ) def apply(self, doc): - '''Apply shipping rule on given doc. Called from accounts controller''' + """Apply shipping rule on given doc. Called from accounts controller""" shipping_amount = 0.0 by_value = False @@ -52,15 +64,15 @@ class ShippingRule(Document): # validate country only if there is address self.validate_countries(doc) - if self.calculate_based_on == 'Net Total': + if self.calculate_based_on == "Net Total": value = doc.base_net_total by_value = True - elif self.calculate_based_on == 'Net Weight': + elif self.calculate_based_on == "Net Weight": value = doc.total_net_weight by_value = True - elif self.calculate_based_on == 'Fixed': + elif self.calculate_based_on == "Fixed": shipping_amount = self.shipping_amount # shipping amount by value, apply conditions @@ -71,12 +83,13 @@ class ShippingRule(Document): if doc.currency != doc.company_currency: shipping_amount = flt(shipping_amount / doc.conversion_rate, 2) - if shipping_amount: - self.add_shipping_rule_to_tax_table(doc, shipping_amount) + self.add_shipping_rule_to_tax_table(doc, shipping_amount) def get_shipping_amount_from_rules(self, value): for condition in self.get("conditions"): - if not condition.to_value or (flt(condition.from_value) <= flt(value) <= flt(condition.to_value)): + if not condition.to_value or ( + flt(condition.from_value) <= flt(value) <= flt(condition.to_value) + ): return condition.shipping_amount return 0.0 @@ -84,27 +97,31 @@ class ShippingRule(Document): def validate_countries(self, doc): # validate applicable countries if self.countries: - shipping_country = doc.get_shipping_address().get('country') + shipping_country = doc.get_shipping_address().get("country") if not shipping_country: - frappe.throw(_('Shipping Address does not have country, which is required for this Shipping Rule')) + frappe.throw( + _("Shipping Address does not have country, which is required for this Shipping Rule") + ) if shipping_country not in [d.country for d in self.countries]: - frappe.throw(_('Shipping rule not applicable for country {0} in Shipping Address').format(shipping_country)) + frappe.throw( + _("Shipping rule not applicable for country {0} in Shipping Address").format(shipping_country) + ) def add_shipping_rule_to_tax_table(self, doc, shipping_amount): shipping_charge = { "charge_type": "Actual", "account_head": self.account, - "cost_center": self.cost_center + "cost_center": self.cost_center, } if self.shipping_rule_type == "Selling": # check if not applied on purchase - if not doc.meta.get_field('taxes').options == 'Sales Taxes and Charges': - frappe.throw(_('Shipping rule only applicable for Selling')) + if not doc.meta.get_field("taxes").options == "Sales Taxes and Charges": + frappe.throw(_("Shipping rule only applicable for Selling")) shipping_charge["doctype"] = "Sales Taxes and Charges" else: # check if not applied on sales - if not doc.meta.get_field('taxes').options == 'Purchase Taxes and Charges': - frappe.throw(_('Shipping rule only applicable for Buying')) + if not doc.meta.get_field("taxes").options == "Purchase Taxes and Charges": + frappe.throw(_("Shipping rule only applicable for Buying")) shipping_charge["doctype"] = "Purchase Taxes and Charges" shipping_charge["category"] = "Valuation and Total" @@ -128,19 +145,19 @@ class ShippingRule(Document): def validate_overlapping_shipping_rule_conditions(self): def overlap_exists_between(num_range1, num_range2): """ - num_range1 and num_range2 are two ranges - ranges are represented as a tuple e.g. range 100 to 300 is represented as (100, 300) - if condition num_range1 = 100 to 300 - then condition num_range2 can only be like 50 to 99 or 301 to 400 - hence, non-overlapping condition = (x1 <= x2 < y1 <= y2) or (y1 <= y2 < x1 <= x2) + num_range1 and num_range2 are two ranges + ranges are represented as a tuple e.g. range 100 to 300 is represented as (100, 300) + if condition num_range1 = 100 to 300 + then condition num_range2 can only be like 50 to 99 or 301 to 400 + hence, non-overlapping condition = (x1 <= x2 < y1 <= y2) or (y1 <= y2 < x1 <= x2) """ (x1, x2), (y1, y2) = num_range1, num_range2 separate = (x1 <= x2 <= y1 <= y2) or (y1 <= y2 <= x1 <= x2) - return (not separate) + return not separate overlaps = [] for i in range(0, len(self.conditions)): - for j in range(i+1, len(self.conditions)): + for j in range(i + 1, len(self.conditions)): d1, d2 = self.conditions[i], self.conditions[j] if d1.as_dict() != d2.as_dict(): # in our case, to_value can be zero, hence pass the from_value if so @@ -154,7 +171,12 @@ class ShippingRule(Document): msgprint(_("Overlapping conditions found between:")) messages = [] for d1, d2 in overlaps: - messages.append("%s-%s = %s " % (d1.from_value, d1.to_value, fmt_money(d1.shipping_amount, currency=company_currency)) + - _("and") + " %s-%s = %s" % (d2.from_value, d2.to_value, fmt_money(d2.shipping_amount, currency=company_currency))) + messages.append( + "%s-%s = %s " + % (d1.from_value, d1.to_value, fmt_money(d1.shipping_amount, currency=company_currency)) + + _("and") + + " %s-%s = %s" + % (d2.from_value, d2.to_value, fmt_money(d2.shipping_amount, currency=company_currency)) + ) msgprint("\n".join(messages), raise_exception=OverlappingConditionError) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py index ef2a053227b..60ce120c54f 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py @@ -1,25 +1,13 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'shipping_rule', - 'non_standard_fieldnames': { - 'Payment Entry': 'party_name' - }, - 'transactions': [ - { - 'label': _('Pre Sales'), - 'items': ['Quotation', 'Supplier Quotation'] - }, - { - 'label': _('Sales'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] - } - ] + "fieldname": "shipping_rule", + "non_standard_fieldnames": {"Payment Entry": "party_name"}, + "transactions": [ + {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]}, + {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]}, + ], } diff --git a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py index c06dae09701..a24e834c572 100644 --- a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py @@ -11,18 +11,19 @@ from erpnext.accounts.doctype.shipping_rule.shipping_rule import ( OverlappingConditionError, ) -test_records = frappe.get_test_records('Shipping Rule') +test_records = frappe.get_test_records("Shipping Rule") + class TestShippingRule(unittest.TestCase): def test_from_greater_than_to(self): shipping_rule = frappe.copy_doc(test_records[0]) - shipping_rule.name = test_records[0].get('name') + shipping_rule.name = test_records[0].get("name") shipping_rule.get("conditions")[0].from_value = 101 self.assertRaises(FromGreaterThanToError, shipping_rule.insert) def test_many_zero_to_values(self): shipping_rule = frappe.copy_doc(test_records[0]) - shipping_rule.name = test_records[0].get('name') + shipping_rule.name = test_records[0].get("name") shipping_rule.get("conditions")[0].to_value = 0 self.assertRaises(ManyBlankToValuesError, shipping_rule.insert) @@ -35,48 +36,58 @@ class TestShippingRule(unittest.TestCase): ((50, 150), (50, 150)), ]: shipping_rule = frappe.copy_doc(test_records[0]) - shipping_rule.name = test_records[0].get('name') + shipping_rule.name = test_records[0].get("name") shipping_rule.get("conditions")[0].from_value = range_a[0] shipping_rule.get("conditions")[0].to_value = range_a[1] shipping_rule.get("conditions")[1].from_value = range_b[0] shipping_rule.get("conditions")[1].to_value = range_b[1] self.assertRaises(OverlappingConditionError, shipping_rule.insert) + def create_shipping_rule(shipping_rule_type, shipping_rule_name): if frappe.db.exists("Shipping Rule", shipping_rule_name): return frappe.get_doc("Shipping Rule", shipping_rule_name) sr = frappe.new_doc("Shipping Rule") - sr.account = "_Test Account Shipping Charges - _TC" - sr.calculate_based_on = "Net Total" + sr.account = "_Test Account Shipping Charges - _TC" + sr.calculate_based_on = "Net Total" sr.company = "_Test Company" sr.cost_center = "_Test Cost Center - _TC" sr.label = shipping_rule_name sr.name = shipping_rule_name sr.shipping_rule_type = shipping_rule_type - sr.append("conditions", { + sr.append( + "conditions", + { "doctype": "Shipping Rule Condition", "from_value": 0, "parentfield": "conditions", "shipping_amount": 50.0, - "to_value": 100 - }) - sr.append("conditions", { + "to_value": 100, + }, + ) + sr.append( + "conditions", + { "doctype": "Shipping Rule Condition", "from_value": 101, "parentfield": "conditions", "shipping_amount": 100.0, - "to_value": 200 - }) - sr.append("conditions", { + "to_value": 200, + }, + ) + sr.append( + "conditions", + { "doctype": "Shipping Rule Condition", "from_value": 201, "parentfield": "conditions", "shipping_amount": 200.0, - "to_value": 2000 - }) + "to_value": 2000, + }, + ) sr.insert(ignore_permissions=True) sr.submit() return sr diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 744b99807fc..62ffc79fee8 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -1,4 +1,3 @@ - # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt @@ -61,7 +60,11 @@ class Subscription(Document): """ _current_invoice_start = None - if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date): + if ( + self.is_new_subscription() + and self.trial_period_end + and getdate(self.trial_period_end) > getdate(self.start_date) + ): _current_invoice_start = add_days(self.trial_period_end, 1) elif self.trial_period_start and self.is_trialling(): _current_invoice_start = self.trial_period_start @@ -103,7 +106,7 @@ class Subscription(Document): if self.follow_calendar_months: billing_info = self.get_billing_cycle_and_interval() - billing_interval_count = billing_info[0]['billing_interval_count'] + billing_interval_count = billing_info[0]["billing_interval_count"] calendar_months = get_calendar_months(billing_interval_count) calendar_month = 0 current_invoice_end_month = getdate(_current_invoice_end).month @@ -113,12 +116,13 @@ class Subscription(Document): if month <= current_invoice_end_month: calendar_month = month - if cint(calendar_month - billing_interval_count) <= 0 and \ - getdate(date).month != 1: + if cint(calendar_month - billing_interval_count) <= 0 and getdate(date).month != 1: calendar_month = 12 current_invoice_end_year -= 1 - _current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' + cstr(calendar_month) + '-01') + _current_invoice_end = get_last_day( + cstr(current_invoice_end_year) + "-" + cstr(calendar_month) + "-01" + ) if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date): _current_invoice_end = self.end_date @@ -132,7 +136,7 @@ class Subscription(Document): same billing interval """ if billing_cycle_data and len(billing_cycle_data) != 1: - frappe.throw(_('You can only have Plans with the same billing cycle in a Subscription')) + frappe.throw(_("You can only have Plans with the same billing cycle in a Subscription")) def get_billing_cycle_and_interval(self): """ @@ -142,10 +146,11 @@ class Subscription(Document): """ plan_names = [plan.plan for plan in self.plans] billing_info = frappe.db.sql( - 'select distinct `billing_interval`, `billing_interval_count` ' - 'from `tabSubscription Plan` ' - 'where name in %s', - (plan_names,), as_dict=1 + "select distinct `billing_interval`, `billing_interval_count` " + "from `tabSubscription Plan` " + "where name in %s", + (plan_names,), + as_dict=1, ) return billing_info @@ -162,19 +167,19 @@ class Subscription(Document): if billing_info: data = dict() - interval = billing_info[0]['billing_interval'] - interval_count = billing_info[0]['billing_interval_count'] - if interval not in ['Day', 'Week']: - data['days'] = -1 - if interval == 'Day': - data['days'] = interval_count - 1 - elif interval == 'Month': - data['months'] = interval_count - elif interval == 'Year': - data['years'] = interval_count + interval = billing_info[0]["billing_interval"] + interval_count = billing_info[0]["billing_interval_count"] + if interval not in ["Day", "Week"]: + data["days"] = -1 + if interval == "Day": + data["days"] = interval_count - 1 + elif interval == "Month": + data["months"] = interval_count + elif interval == "Year": + data["years"] = interval_count # todo: test week - elif interval == 'Week': - data['days'] = interval_count * 7 - 1 + elif interval == "Week": + data["days"] = interval_count * 7 - 1 return data @@ -185,27 +190,27 @@ class Subscription(Document): Used when the `Subscription` needs to decide what to do after the current generated invoice is past it's due date and grace period. """ - subscription_settings = frappe.get_single('Subscription Settings') - if self.status == 'Past Due Date' and self.is_past_grace_period(): - self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid' + subscription_settings = frappe.get_single("Subscription Settings") + if self.status == "Past Due Date" and self.is_past_grace_period(): + self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid" def set_subscription_status(self): """ Sets the status of the `Subscription` """ if self.is_trialling(): - self.status = 'Trialling' - elif self.status == 'Active' and self.end_date and getdate() > getdate(self.end_date): - self.status = 'Completed' + self.status = "Trialling" + elif self.status == "Active" and self.end_date and getdate() > getdate(self.end_date): + self.status = "Completed" elif self.is_past_grace_period(): - subscription_settings = frappe.get_single('Subscription Settings') - self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid' + subscription_settings = frappe.get_single("Subscription Settings") + self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid" elif self.current_invoice_is_past_due() and not self.is_past_grace_period(): - self.status = 'Past Due Date' + self.status = "Past Due Date" elif not self.has_outstanding_invoice(): - self.status = 'Active' + self.status = "Active" elif self.is_new_subscription(): - self.status = 'Active' + self.status = "Active" self.save() def is_trialling(self): @@ -232,7 +237,7 @@ class Subscription(Document): """ current_invoice = self.get_current_invoice() if self.current_invoice_is_past_due(current_invoice): - subscription_settings = frappe.get_single('Subscription Settings') + subscription_settings = frappe.get_single("Subscription Settings") grace_period = cint(subscription_settings.grace_period) return getdate() > add_days(current_invoice.due_date, grace_period) @@ -253,15 +258,15 @@ class Subscription(Document): """ Returns the most recent generated invoice. """ - doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' + doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" if len(self.invoices): current = self.invoices[-1] - if frappe.db.exists(doctype, current.get('invoice')): - doc = frappe.get_doc(doctype, current.get('invoice')) + if frappe.db.exists(doctype, current.get("invoice")): + doc = frappe.get_doc(doctype, current.get("invoice")) return doc else: - frappe.throw(_('Invoice {0} no longer exists').format(current.get('invoice'))) + frappe.throw(_("Invoice {0} no longer exists").format(current.get("invoice"))) def is_new_subscription(self): """ @@ -274,7 +279,7 @@ class Subscription(Document): self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) self.validate_end_date() self.validate_to_follow_calendar_months() - self.cost_center = erpnext.get_default_cost_center(self.get('company')) + self.cost_center = erpnext.get_default_cost_center(self.get("company")) def validate_trial_period(self): """ @@ -282,30 +287,34 @@ class Subscription(Document): """ if self.trial_period_start and self.trial_period_end: if getdate(self.trial_period_end) < getdate(self.trial_period_start): - frappe.throw(_('Trial Period End Date Cannot be before Trial Period Start Date')) + frappe.throw(_("Trial Period End Date Cannot be before Trial Period Start Date")) if self.trial_period_start and not self.trial_period_end: - frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set')) + frappe.throw(_("Both Trial Period Start Date and Trial Period End Date must be set")) if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date): - frappe.throw(_('Trial Period Start date cannot be after Subscription Start Date')) + frappe.throw(_("Trial Period Start date cannot be after Subscription Start Date")) def validate_end_date(self): billing_cycle_info = self.get_billing_cycle_data() end_date = add_to_date(self.start_date, **billing_cycle_info) if self.end_date and getdate(self.end_date) <= getdate(end_date): - frappe.throw(_('Subscription End Date must be after {0} as per the subscription plan').format(end_date)) + frappe.throw( + _("Subscription End Date must be after {0} as per the subscription plan").format(end_date) + ) def validate_to_follow_calendar_months(self): if self.follow_calendar_months: billing_info = self.get_billing_cycle_and_interval() if not self.end_date: - frappe.throw(_('Subscription End Date is mandatory to follow calendar months')) + frappe.throw(_("Subscription End Date is mandatory to follow calendar months")) - if billing_info[0]['billing_interval'] != 'Month': - frappe.throw(_('Billing Interval in Subscription Plan must be Month to follow calendar months')) + if billing_info[0]["billing_interval"] != "Month": + frappe.throw( + _("Billing Interval in Subscription Plan must be Month to follow calendar months") + ) def after_insert(self): # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype? @@ -317,13 +326,10 @@ class Subscription(Document): saves the `Subscription`. """ - doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' + doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" invoice = self.create_invoice(prorate) - self.append('invoices', { - 'document_type': doctype, - 'invoice': invoice.name - }) + self.append("invoices", {"document_type": doctype, "invoice": invoice.name}) self.save() @@ -333,55 +339,58 @@ class Subscription(Document): """ Creates a `Invoice`, submits it and returns it """ - doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' + doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" invoice = frappe.new_doc(doctype) # For backward compatibility # Earlier subscription didn't had any company field - company = self.get('company') or get_default_company() + company = self.get("company") or get_default_company() if not company: - frappe.throw(_("Company is mandatory was generating invoice. Please set default company in Global Defaults")) + frappe.throw( + _("Company is mandatory was generating invoice. Please set default company in Global Defaults") + ) invoice.company = company invoice.set_posting_time = 1 - invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \ + invoice.posting_date = ( + self.current_invoice_start + if self.generate_invoice_at_period_start else self.current_invoice_end + ) invoice.cost_center = self.cost_center - if doctype == 'Sales Invoice': + if doctype == "Sales Invoice": invoice.customer = self.party else: invoice.supplier = self.party - if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'): + if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"): invoice.apply_tds = 1 - ### Add party currency to invoice + # Add party currency to invoice invoice.currency = get_party_account_currency(self.party_type, self.party, self.company) - ## Add dimensions in invoice for subscription: + # Add dimensions in invoice for subscription: accounting_dimensions = get_accounting_dimensions() for dimension in accounting_dimensions: if self.get(dimension): - invoice.update({ - dimension: self.get(dimension) - }) + invoice.update({dimension: self.get(dimension)}) # Subscription is better suited for service items. I won't update `update_stock` # for that reason items_list = self.get_items_from_plans(self.plans, prorate) for item in items_list: - item['cost_center'] = self.cost_center - invoice.append('items', item) + item["cost_center"] = self.cost_center + invoice.append("items", item) # Taxes - tax_template = '' + tax_template = "" - if doctype == 'Sales Invoice' and self.sales_tax_template: + if doctype == "Sales Invoice" and self.sales_tax_template: tax_template = self.sales_tax_template - if doctype == 'Purchase Invoice' and self.purchase_tax_template: + if doctype == "Purchase Invoice" and self.purchase_tax_template: tax_template = self.purchase_tax_template if tax_template: @@ -391,11 +400,11 @@ class Subscription(Document): # Due date if self.days_until_due: invoice.append( - 'payment_schedule', + "payment_schedule", { - 'due_date': add_days(invoice.posting_date, cint(self.days_until_due)), - 'invoice_portion': 100 - } + "due_date": add_days(invoice.posting_date, cint(self.days_until_due)), + "invoice_portion": 100, + }, ) # Discounts @@ -407,7 +416,7 @@ class Subscription(Document): if self.additional_discount_percentage or self.additional_discount_amount: discount_on = self.apply_additional_discount - invoice.apply_discount_on = discount_on if discount_on else 'Grand Total' + invoice.apply_discount_on = discount_on if discount_on else "Grand Total" # Subscription period invoice.from_date = self.current_invoice_start @@ -427,44 +436,62 @@ class Subscription(Document): Returns the `Item`s linked to `Subscription Plan` """ if prorate: - prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start, - self.generate_invoice_at_period_start) + prorate_factor = get_prorata_factor( + self.current_invoice_end, self.current_invoice_start, self.generate_invoice_at_period_start + ) items = [] party = self.party for plan in plans: - plan_doc = frappe.get_doc('Subscription Plan', plan.plan) + plan_doc = frappe.get_doc("Subscription Plan", plan.plan) item_code = plan_doc.item - if self.party == 'Customer': - deferred_field = 'enable_deferred_revenue' + if self.party == "Customer": + deferred_field = "enable_deferred_revenue" else: - deferred_field = 'enable_deferred_expense' + deferred_field = "enable_deferred_expense" - deferred = frappe.db.get_value('Item', item_code, deferred_field) + deferred = frappe.db.get_value("Item", item_code, deferred_field) if not prorate: - item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party, - self.current_invoice_start, self.current_invoice_end), 'cost_center': plan_doc.cost_center} + item = { + "item_code": item_code, + "qty": plan.qty, + "rate": get_plan_rate( + plan.plan, plan.qty, party, self.current_invoice_start, self.current_invoice_end + ), + "cost_center": plan_doc.cost_center, + } else: - item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party, - self.current_invoice_start, self.current_invoice_end, prorate_factor), 'cost_center': plan_doc.cost_center} + item = { + "item_code": item_code, + "qty": plan.qty, + "rate": get_plan_rate( + plan.plan, + plan.qty, + party, + self.current_invoice_start, + self.current_invoice_end, + prorate_factor, + ), + "cost_center": plan_doc.cost_center, + } if deferred: - item.update({ - deferred_field: deferred, - 'service_start_date': self.current_invoice_start, - 'service_end_date': self.current_invoice_end - }) + item.update( + { + deferred_field: deferred, + "service_start_date": self.current_invoice_start, + "service_end_date": self.current_invoice_end, + } + ) accounting_dimensions = get_accounting_dimensions() for dimension in accounting_dimensions: if plan_doc.get(dimension): - item.update({ - dimension: plan_doc.get(dimension) - }) + item.update({dimension: plan_doc.get(dimension)}) items.append(item) @@ -477,9 +504,9 @@ class Subscription(Document): 1. `process_for_active` 2. `process_for_past_due` """ - if self.status == 'Active': + if self.status == "Active": self.process_for_active() - elif self.status in ['Past Due Date', 'Unpaid']: + elif self.status in ["Past Due Date", "Unpaid"]: self.process_for_past_due_date() self.set_subscription_status() @@ -487,8 +514,10 @@ class Subscription(Document): self.save() def is_postpaid_to_invoice(self): - return getdate() > getdate(self.current_invoice_end) or \ - (getdate() >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) + return getdate() > getdate(self.current_invoice_end) or ( + getdate() >= getdate(self.current_invoice_end) + and getdate(self.current_invoice_end) == getdate(self.current_invoice_start) + ) def is_prepaid_to_invoice(self): if not self.generate_invoice_at_period_start: @@ -504,9 +533,13 @@ class Subscription(Document): invoice = self.get_current_invoice() if not (_current_start_date and _current_end_date): - _current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True) + _current_start_date, _current_end_date = self.update_subscription_period( + date=add_days(self.current_invoice_end, 1), return_date=True + ) - if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date): + if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate( + _current_end_date + ): return True return False @@ -521,10 +554,11 @@ class Subscription(Document): 3. Change the `Subscription` status to 'Cancelled' """ - if not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \ - and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): + if not self.is_current_invoice_generated( + self.current_invoice_start, self.current_invoice_end + ) and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): - prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') + prorate = frappe.db.get_single_value("Subscription Settings", "prorate") self.generate_invoice(prorate) if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice(): @@ -540,7 +574,7 @@ class Subscription(Document): if self.end_date and getdate() < getdate(self.end_date): return - self.status = 'Cancelled' + self.status = "Cancelled" if not self.cancelation_date: self.cancelation_date = nowdate() @@ -555,19 +589,21 @@ class Subscription(Document): """ current_invoice = self.get_current_invoice() if not current_invoice: - frappe.throw(_('Current invoice {0} is missing').format(current_invoice.invoice)) + frappe.throw(_("Current invoice {0} is missing").format(current_invoice.invoice)) else: if not self.has_outstanding_invoice(): - self.status = 'Active' + self.status = "Active" else: self.set_status_grace_period() # Generate invoices periodically even if current invoice are unpaid - if self.generate_new_invoices_past_due_date and not \ - self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \ - and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): + if ( + self.generate_new_invoices_past_due_date + and not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) + and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()) + ): - prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') + prorate = frappe.db.get_single_value("Subscription Settings", "prorate") self.generate_invoice(prorate) if getdate() > getdate(self.current_invoice_end): @@ -578,18 +614,19 @@ class Subscription(Document): """ Return `True` if the given invoice is paid """ - return invoice.status == 'Paid' + return invoice.status == "Paid" def has_outstanding_invoice(self): """ Returns `True` if the most recent invoice for the `Subscription` is not paid """ - doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' + doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" current_invoice = self.get_current_invoice() invoice_list = [d.invoice for d in self.invoices] - outstanding_invoices = frappe.get_all(doctype, fields=['name'], - filters={'status': ('!=', 'Paid'), 'name': ('in', invoice_list)}) + outstanding_invoices = frappe.get_all( + doctype, fields=["name"], filters={"status": ("!=", "Paid"), "name": ("in", invoice_list)} + ) if outstanding_invoices: return True @@ -601,10 +638,12 @@ class Subscription(Document): This sets the subscription as cancelled. It will stop invoices from being generated but it will not affect already created invoices. """ - if self.status != 'Cancelled': - to_generate_invoice = True if self.status == 'Active' and not self.generate_invoice_at_period_start else False - to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') - self.status = 'Cancelled' + if self.status != "Cancelled": + to_generate_invoice = ( + True if self.status == "Active" and not self.generate_invoice_at_period_start else False + ) + to_prorate = frappe.db.get_single_value("Subscription Settings", "prorate") + self.status = "Cancelled" self.cancelation_date = nowdate() if to_generate_invoice: self.generate_invoice(prorate=to_prorate) @@ -616,19 +655,20 @@ class Subscription(Document): subscription and the `Subscription` will lose all the history of generated invoices it has. """ - if self.status == 'Cancelled': - self.status = 'Active' - self.db_set('start_date', nowdate()) + if self.status == "Cancelled": + self.status = "Active" + self.db_set("start_date", nowdate()) self.update_subscription_period(nowdate()) self.invoices = [] self.save() else: - frappe.throw(_('You cannot restart a Subscription that is not cancelled.')) + frappe.throw(_("You cannot restart a Subscription that is not cancelled.")) def get_precision(self): invoice = self.get_current_invoice() if invoice: - return invoice.precision('grand_total') + return invoice.precision("grand_total") + def get_calendar_months(billing_interval): calendar_months = [] @@ -639,6 +679,7 @@ def get_calendar_months(billing_interval): return calendar_months + def get_prorata_factor(period_end, period_start, is_prepaid): if is_prepaid: prorate_factor = 1 @@ -663,7 +704,7 @@ def get_all_subscriptions(): """ Returns all `Subscription` documents """ - return frappe.db.get_all('Subscription', {'status': ('!=','Cancelled')}) + return frappe.db.get_all("Subscription", {"status": ("!=", "Cancelled")}) def process(data): @@ -672,7 +713,7 @@ def process(data): """ if data: try: - subscription = frappe.get_doc('Subscription', data['name']) + subscription = frappe.get_doc("Subscription", data["name"]) subscription.process() frappe.db.commit() except frappe.ValidationError: @@ -688,7 +729,7 @@ def cancel_subscription(name): Cancels a `Subscription`. This will stop the `Subscription` from further invoicing the `Subscriber` but all already outstanding invoices will not be affected. """ - subscription = frappe.get_doc('Subscription', name) + subscription = frappe.get_doc("Subscription", name) subscription.cancel_subscription() @@ -698,7 +739,7 @@ def restart_subscription(name): Restarts a cancelled `Subscription`. The `Subscription` will 'forget' the history of all invoices it has generated """ - subscription = frappe.get_doc('Subscription', name) + subscription = frappe.get_doc("Subscription", name) subscription.restart_subscription() @@ -707,5 +748,5 @@ def get_subscription_updates(name): """ Use this to get the latest state of the given `Subscription` """ - subscription = frappe.get_doc('Subscription', name) + subscription = frappe.get_doc("Subscription", name) subscription.process() diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 6f67bc5128b..eb17daa282f 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -18,104 +18,111 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto test_dependencies = ("UOM", "Item Group", "Item") + def create_plan(): - if not frappe.db.exists('Subscription Plan', '_Test Plan Name'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Name' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Name"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Name" + plan.item = "_Test Non Stock Item" plan.price_determination = "Fixed Rate" plan.cost = 900 - plan.billing_interval = 'Month' + plan.billing_interval = "Month" plan.billing_interval_count = 1 plan.insert() - if not frappe.db.exists('Subscription Plan', '_Test Plan Name 2'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Name 2' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Name 2"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Name 2" + plan.item = "_Test Non Stock Item" plan.price_determination = "Fixed Rate" plan.cost = 1999 - plan.billing_interval = 'Month' + plan.billing_interval = "Month" plan.billing_interval_count = 1 plan.insert() - if not frappe.db.exists('Subscription Plan', '_Test Plan Name 3'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Name 3' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Name 3"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Name 3" + plan.item = "_Test Non Stock Item" plan.price_determination = "Fixed Rate" plan.cost = 1999 - plan.billing_interval = 'Day' + plan.billing_interval = "Day" plan.billing_interval_count = 14 plan.insert() # Defined a quarterly Subscription Plan - if not frappe.db.exists('Subscription Plan', '_Test Plan Name 4'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Name 4' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Name 4"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Name 4" + plan.item = "_Test Non Stock Item" plan.price_determination = "Monthly Rate" plan.cost = 20000 - plan.billing_interval = 'Month' + plan.billing_interval = "Month" plan.billing_interval_count = 3 plan.insert() - if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Multicurrency' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Multicurrency"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Multicurrency" + plan.item = "_Test Non Stock Item" plan.price_determination = "Fixed Rate" plan.cost = 50 - plan.currency = 'USD' - plan.billing_interval = 'Month' + plan.currency = "USD" + plan.billing_interval = "Month" plan.billing_interval_count = 1 plan.insert() + def create_parties(): - if not frappe.db.exists('Supplier', '_Test Supplier'): - supplier = frappe.new_doc('Supplier') - supplier.supplier_name = '_Test Supplier' - supplier.supplier_group = 'All Supplier Groups' + if not frappe.db.exists("Supplier", "_Test Supplier"): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = "_Test Supplier" + supplier.supplier_group = "All Supplier Groups" supplier.insert() - if not frappe.db.exists('Customer', '_Test Subscription Customer'): - customer = frappe.new_doc('Customer') - customer.customer_name = '_Test Subscription Customer' - customer.billing_currency = 'USD' - customer.append('accounts', { - 'company': '_Test Company', - 'account': '_Test Receivable USD - _TC' - }) + if not frappe.db.exists("Customer", "_Test Subscription Customer"): + customer = frappe.new_doc("Customer") + customer.customer_name = "_Test Subscription Customer" + customer.billing_currency = "USD" + customer.append( + "accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"} + ) customer.insert() + class TestSubscription(unittest.TestCase): def setUp(self): create_plan() create_parties() def test_create_subscription_with_trial_with_correct_period(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.trial_period_start = nowdate() subscription.trial_period_end = add_months(nowdate(), 1) - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() self.assertEqual(subscription.trial_period_start, nowdate()) self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) - self.assertEqual(add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start)) - self.assertEqual(add_to_date(subscription.current_invoice_start, months=1, days=-1), get_date_str(subscription.current_invoice_end)) + self.assertEqual( + add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start) + ) + self.assertEqual( + add_to_date(subscription.current_invoice_start, months=1, days=-1), + get_date_str(subscription.current_invoice_end), + ) self.assertEqual(subscription.invoices, []) - self.assertEqual(subscription.status, 'Trialling') + self.assertEqual(subscription.status, "Trialling") subscription.delete() def test_create_subscription_without_trial_with_correct_period(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() self.assertEqual(subscription.trial_period_start, None) @@ -124,190 +131,190 @@ class TestSubscription(unittest.TestCase): self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) # No invoice is created self.assertEqual(len(subscription.invoices), 0) - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") subscription.delete() def test_create_subscription_trial_with_wrong_dates(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.trial_period_end = nowdate() subscription.trial_period_start = add_days(nowdate(), 30) - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) self.assertRaises(frappe.ValidationError, subscription.save) subscription.delete() def test_create_subscription_multi_with_different_billing_fails(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.trial_period_end = nowdate() subscription.trial_period_start = add_days(nowdate(), 30) - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.append('plans', {'plan': '_Test Plan Name 3', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1}) self.assertRaises(frappe.ValidationError, subscription.save) subscription.delete() def test_invoice_is_generated_at_end_of_billing_period(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.start_date = '2018-01-01' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.start_date = "2018-01-01" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.insert() - self.assertEqual(subscription.status, 'Active') - self.assertEqual(subscription.current_invoice_start, '2018-01-01') - self.assertEqual(subscription.current_invoice_end, '2018-01-31') + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, "2018-01-01") + self.assertEqual(subscription.current_invoice_end, "2018-01-31") subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.current_invoice_start, '2018-01-01') + self.assertEqual(subscription.current_invoice_start, "2018-01-01") subscription.process() - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") subscription.delete() def test_status_goes_back_to_active_after_invoice_is_paid(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(len(subscription.invoices), 1) # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") subscription.get_current_invoice() current_invoice = subscription.get_current_invoice() self.assertIsNotNone(current_invoice) - current_invoice.db_set('outstanding_amount', 0) - current_invoice.db_set('status', 'Paid') + current_invoice.db_set("outstanding_amount", 0) + current_invoice.db_set("status", "Paid") subscription.process() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1)) self.assertEqual(len(subscription.invoices), 1) subscription.delete() def test_subscription_cancel_after_grace_period(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.cancel_after_grace = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") - subscription.process() # generate first invoice + subscription.process() # generate first invoice # This should change status to Cancelled since grace period is 0 # And is backdated subscription so subscription will be cancelled after processing - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") settings.cancel_after_grace = default_grace_period_action settings.save() subscription.delete() def test_subscription_unpaid_after_grace_period(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.cancel_after_grace = 0 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") settings.cancel_after_grace = default_grace_period_action settings.save() subscription.delete() def test_subscription_invoice_days_until_due(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.days_until_due = 10 subscription.start_date = add_months(nowdate(), -1) subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") subscription.delete() def test_subscription_is_past_due_doesnt_change_within_grace_period(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") grace_period = settings.grace_period settings.grace_period = 1000 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.start_date = add_days(nowdate(), -1000) subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice - self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(subscription.status, "Past Due Date") subscription.process() # Grace period is 1000 days so status should remain as Past Due Date - self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(subscription.status, "Past Due Date") subscription.process() - self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(subscription.status, "Past Due Date") subscription.process() - self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(subscription.status, "Past Due Date") settings.grace_period = grace_period settings.save() subscription.delete() def test_subscription_remains_active_during_invoice_period(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() - subscription.process() # no changes expected + subscription.process() # no changes expected - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.current_invoice_start, nowdate()) self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) self.assertEqual(len(subscription.invoices), 0) - subscription.process() # no changes expected still - self.assertEqual(subscription.status, 'Active') + subscription.process() # no changes expected still + self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.current_invoice_start, nowdate()) self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) self.assertEqual(len(subscription.invoices), 0) - subscription.process() # no changes expected yet still - self.assertEqual(subscription.status, 'Active') + subscription.process() # no changes expected yet still + self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.current_invoice_start, nowdate()) self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) self.assertEqual(len(subscription.invoices), 0) @@ -315,30 +322,30 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_cancelation(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") subscription.delete() def test_subscription_cancellation_invoices(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") to_prorate = settings.prorate settings.prorate = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") subscription.cancel_subscription() # Invoice must have been generated @@ -346,33 +353,39 @@ class TestSubscription(unittest.TestCase): invoice = subscription.get_current_invoice() diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) - prorate_factor = flt(diff/plan_days) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) + prorate_factor = flt(diff / plan_days) self.assertEqual( flt( - get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start, - subscription.generate_invoice_at_period_start), - 2), - flt(prorate_factor, 2) + get_prorata_factor( + subscription.current_invoice_end, + subscription.current_invoice_start, + subscription.generate_invoice_at_period_start, + ), + 2, + ), + flt(prorate_factor, 2), ) self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") subscription.delete() settings.prorate = to_prorate settings.save() def test_subscription_cancellation_invoices_with_prorata_false(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") to_prorate = settings.prorate settings.prorate = 0 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() invoice = subscription.get_current_invoice() @@ -385,21 +398,23 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_cancellation_invoices_with_prorata_true(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") to_prorate = settings.prorate settings.prorate = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() invoice = subscription.get_current_invoice() diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) prorate_factor = flt(diff / plan_days) self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) @@ -410,30 +425,30 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subcription_cancellation_and_process(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.cancel_after_grace = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice invoices = len(subscription.invoices) subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") self.assertEqual(len(subscription.invoices), invoices) subscription.process() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") self.assertEqual(len(subscription.invoices), invoices) subscription.process() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") self.assertEqual(len(subscription.invoices), invoices) settings.cancel_after_grace = default_grace_period_action @@ -441,36 +456,36 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_restart_and_process(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.grace_period = 0 settings.cancel_after_grace = 0 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") subscription.restart_subscription() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(len(subscription.invoices), 0) subscription.process() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(len(subscription.invoices), 0) subscription.process() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(len(subscription.invoices), 0) settings.cancel_after_grace = default_grace_period_action @@ -478,42 +493,42 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_unpaid_back_to_active(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.cancel_after_grace = 0 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice # This should change status to Unpaid since grace period is 0 - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") invoice = subscription.get_current_invoice() - invoice.db_set('outstanding_amount', 0) - invoice.db_set('status', 'Paid') + invoice.db_set("outstanding_amount", 0) + invoice.db_set("status", "Paid") subscription.process() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") # A new invoice is generated subscription.process() - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") settings.cancel_after_grace = default_grace_period_action settings.save() subscription.delete() def test_restart_active_subscription(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() self.assertRaises(frappe.ValidationError, subscription.restart_subscription) @@ -521,44 +536,44 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_invoice_discount_percentage(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.additional_discount_percentage = 10 - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() invoice = subscription.get_current_invoice() self.assertEqual(invoice.additional_discount_percentage, 10) - self.assertEqual(invoice.apply_discount_on, 'Grand Total') + self.assertEqual(invoice.apply_discount_on, "Grand Total") subscription.delete() def test_subscription_invoice_discount_amount(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.additional_discount_amount = 11 - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() invoice = subscription.get_current_invoice() self.assertEqual(invoice.discount_amount, 11) - self.assertEqual(invoice.apply_discount_on, 'Grand Total') + self.assertEqual(invoice.apply_discount_on, "Grand Total") subscription.delete() def test_prepaid_subscriptions(self): # Create a non pre-billed subscription, processing should not create # invoices. - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.process() @@ -573,16 +588,16 @@ class TestSubscription(unittest.TestCase): self.assertEqual(len(subscription.invoices), 1) def test_prepaid_subscriptions_with_prorate_true(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") to_prorate = settings.prorate settings.prorate = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.generate_invoice_at_period_start = True - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.process() subscription.cancel_subscription() @@ -602,38 +617,38 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_with_follow_calendar_months(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Supplier' - subscription.party = '_Test Supplier' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" subscription.generate_invoice_at_period_start = 1 subscription.follow_calendar_months = 1 # select subscription start date as '2018-01-15' - subscription.start_date = '2018-01-15' - subscription.end_date = '2018-07-15' - subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) + subscription.start_date = "2018-01-15" + subscription.end_date = "2018-07-15" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) subscription.save() # even though subscription starts at '2018-01-15' and Billing interval is Month and count 3 # First invoice will end at '2018-03-31' instead of '2018-04-14' - self.assertEqual(get_date_str(subscription.current_invoice_end), '2018-03-31') + self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31") def test_subscription_generate_invoice_past_due(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Supplier' - subscription.party = '_Test Supplier' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" subscription.generate_invoice_at_period_start = 1 subscription.generate_new_invoices_past_due_date = 1 # select subscription start date as '2018-01-15' - subscription.start_date = '2018-01-01' - subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) + subscription.start_date = "2018-01-01" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) subscription.save() # Process subscription and create first invoice # Subscription status will be unpaid since due date has already passed subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") # Now the Subscription is unpaid # Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in @@ -643,39 +658,39 @@ class TestSubscription(unittest.TestCase): self.assertEqual(len(subscription.invoices), 2) def test_subscription_without_generate_invoice_past_due(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Supplier' - subscription.party = '_Test Supplier' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" subscription.generate_invoice_at_period_start = 1 # select subscription start date as '2018-01-15' - subscription.start_date = '2018-01-01' - subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) + subscription.start_date = "2018-01-01" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) subscription.save() # Process subscription and create first invoice # Subscription status will be unpaid since due date has already passed subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") subscription.process() self.assertEqual(len(subscription.invoices), 1) def test_multicurrency_subscription(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Subscription Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Subscription Customer" subscription.generate_invoice_at_period_start = 1 - subscription.company = '_Test Company' + subscription.company = "_Test Company" # select subscription start date as '2018-01-15' - subscription.start_date = '2018-01-01' - subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1}) + subscription.start_date = "2018-01-01" + subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1}) subscription.save() subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") # Check the currency of the created invoice - currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency') - self.assertEqual(currency, 'USD') \ No newline at end of file + currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency") + self.assertEqual(currency, "USD") diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 1285343d196..a95e0a9c2da 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -16,10 +16,13 @@ class SubscriptionPlan(Document): def validate_interval_count(self): if self.billing_interval_count < 1: - frappe.throw(_('Billing Interval Count cannot be less than 1')) + frappe.throw(_("Billing Interval Count cannot be less than 1")) + @frappe.whitelist() -def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1): +def get_plan_rate( + plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1 +): plan = frappe.get_doc("Subscription Plan", plan) if plan.price_determination == "Fixed Rate": return plan.cost * prorate_factor @@ -30,13 +33,19 @@ def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=Non else: customer_group = None - price = get_price(item_code=plan.item, price_list=plan.price_list, customer_group=customer_group, company=None, qty=quantity) + price = get_price( + item_code=plan.item, + price_list=plan.price_list, + customer_group=customer_group, + company=None, + qty=quantity, + ) if not price: return 0 else: return price.price_list_rate * prorate_factor - elif plan.price_determination == 'Monthly Rate': + elif plan.price_determination == "Monthly Rate": start_date = getdate(start_date) end_date = getdate(end_date) @@ -44,15 +53,21 @@ def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=Non cost = plan.cost * no_of_months # Adjust cost if start or end date is not month start or end - prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') + prorate = frappe.db.get_single_value("Subscription Settings", "prorate") if prorate: - prorate_factor = flt(date_diff(start_date, get_first_day(start_date)) / date_diff( - get_last_day(start_date), get_first_day(start_date)), 1) + prorate_factor = flt( + date_diff(start_date, get_first_day(start_date)) + / date_diff(get_last_day(start_date), get_first_day(start_date)), + 1, + ) - prorate_factor += flt(date_diff(get_last_day(end_date), end_date) / date_diff( - get_last_day(end_date), get_first_day(end_date)), 1) + prorate_factor += flt( + date_diff(get_last_day(end_date), end_date) + / date_diff(get_last_day(end_date), get_first_day(end_date)), + 1, + ) - cost -= (plan.cost * prorate_factor) + cost -= plan.cost * prorate_factor return cost diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py index 15df62daa23..7df76cde80c 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py @@ -1,18 +1,9 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'subscription_plan', - 'non_standard_fieldnames': { - 'Payment Request': 'plan', - 'Subscription': 'plan' - }, - 'transactions': [ - { - 'label': _('References'), - 'items': ['Payment Request', 'Subscription'] - } - ] + "fieldname": "subscription_plan", + "non_standard_fieldnames": {"Payment Request": "plan", "Subscription": "plan"}, + "transactions": [{"label": _("References"), "items": ["Payment Request", "Subscription"]}], } diff --git a/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py b/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py index c9d52da78ad..17a275ebc34 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py +++ b/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py @@ -1,30 +1,14 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'tax_category', - 'transactions': [ - { - 'label': _('Pre Sales'), - 'items': ['Quotation', 'Supplier Quotation'] - }, - { - 'label': _('Sales'), - 'items': ['Sales Invoice', 'Delivery Note', 'Sales Order'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Receipt'] - }, - { - 'label': _('Party'), - 'items': ['Customer', 'Supplier'] - }, - { - 'label': _('Taxes'), - 'items': ['Item', 'Tax Rule'] - } - ] + "fieldname": "tax_category", + "transactions": [ + {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]}, + {"label": _("Sales"), "items": ["Sales Invoice", "Delivery Note", "Sales Order"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Receipt"]}, + {"label": _("Party"), "items": ["Customer", "Supplier"]}, + {"label": _("Taxes"), "items": ["Item", "Tax Rule"]}, + ], } diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index ce64d222856..a21d48415fd 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -16,9 +16,17 @@ from six import iteritems from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups -class IncorrectCustomerGroup(frappe.ValidationError): pass -class IncorrectSupplierType(frappe.ValidationError): pass -class ConflictingTaxRule(frappe.ValidationError): pass +class IncorrectCustomerGroup(frappe.ValidationError): + pass + + +class IncorrectSupplierType(frappe.ValidationError): + pass + + +class ConflictingTaxRule(frappe.ValidationError): + pass + class TaxRule(Document): def __setup__(self): @@ -31,7 +39,7 @@ class TaxRule(Document): self.validate_use_for_shopping_cart() def validate_tax_template(self): - if self.tax_type== "Sales": + if self.tax_type == "Sales": self.purchase_tax_template = self.supplier = self.supplier_group = None if self.customer: self.customer_group = None @@ -51,28 +59,28 @@ class TaxRule(Document): def validate_filters(self): filters = { - "tax_type": self.tax_type, - "customer": self.customer, - "customer_group": self.customer_group, - "supplier": self.supplier, - "supplier_group": self.supplier_group, - "item": self.item, - "item_group": self.item_group, - "billing_city": self.billing_city, - "billing_county": self.billing_county, - "billing_state": self.billing_state, - "billing_zipcode": self.billing_zipcode, - "billing_country": self.billing_country, - "shipping_city": self.shipping_city, - "shipping_county": self.shipping_county, - "shipping_state": self.shipping_state, - "shipping_zipcode": self.shipping_zipcode, - "shipping_country": self.shipping_country, - "tax_category": self.tax_category, - "company": self.company + "tax_type": self.tax_type, + "customer": self.customer, + "customer_group": self.customer_group, + "supplier": self.supplier, + "supplier_group": self.supplier_group, + "item": self.item, + "item_group": self.item_group, + "billing_city": self.billing_city, + "billing_county": self.billing_county, + "billing_state": self.billing_state, + "billing_zipcode": self.billing_zipcode, + "billing_country": self.billing_country, + "shipping_city": self.shipping_city, + "shipping_county": self.shipping_county, + "shipping_state": self.shipping_state, + "shipping_zipcode": self.shipping_zipcode, + "shipping_country": self.shipping_country, + "tax_category": self.tax_category, + "company": self.company, } - conds="" + conds = "" for d in filters: if conds: conds += " and " @@ -82,85 +90,112 @@ class TaxRule(Document): conds += """ and ((from_date > '{from_date}' and from_date < '{to_date}') or (to_date > '{from_date}' and to_date < '{to_date}') or ('{from_date}' > from_date and '{from_date}' < to_date) or - ('{from_date}' = from_date and '{to_date}' = to_date))""".format(from_date=self.from_date, to_date=self.to_date) + ('{from_date}' = from_date and '{to_date}' = to_date))""".format( + from_date=self.from_date, to_date=self.to_date + ) elif self.from_date and not self.to_date: - conds += """ and to_date > '{from_date}'""".format(from_date = self.from_date) + conds += """ and to_date > '{from_date}'""".format(from_date=self.from_date) elif self.to_date and not self.from_date: - conds += """ and from_date < '{to_date}'""".format(to_date = self.to_date) + conds += """ and from_date < '{to_date}'""".format(to_date=self.to_date) - tax_rule = frappe.db.sql("select name, priority \ - from `tabTax Rule` where {0} and name != '{1}'".format(conds, self.name), as_dict=1) + tax_rule = frappe.db.sql( + "select name, priority \ + from `tabTax Rule` where {0} and name != '{1}'".format( + conds, self.name + ), + as_dict=1, + ) if tax_rule: if tax_rule[0].priority == self.priority: frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule) def validate_use_for_shopping_cart(self): - '''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one''' - if (not self.use_for_shopping_cart - and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled')) - and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})): + """If shopping cart is enabled and no tax rule exists for shopping cart, enable this one""" + if ( + not self.use_for_shopping_cart + and cint(frappe.db.get_single_value("E Commerce Settings", "enabled")) + and not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1, "name": ["!=", self.name]}) + ): self.use_for_shopping_cart = 1 - frappe.msgprint(_("Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart")) + frappe.msgprint( + _( + "Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart" + ) + ) + @frappe.whitelist() def get_party_details(party, party_type, args=None): out = {} billing_address, shipping_address = None, None if args: - if args.get('billing_address'): - billing_address = frappe.get_doc('Address', args.get('billing_address')) - if args.get('shipping_address'): - shipping_address = frappe.get_doc('Address', args.get('shipping_address')) + if args.get("billing_address"): + billing_address = frappe.get_doc("Address", args.get("billing_address")) + if args.get("shipping_address"): + shipping_address = frappe.get_doc("Address", args.get("shipping_address")) else: billing_address_name = get_default_address(party_type, party) - shipping_address_name = get_default_address(party_type, party, 'is_shipping_address') + shipping_address_name = get_default_address(party_type, party, "is_shipping_address") if billing_address_name: - billing_address = frappe.get_doc('Address', billing_address_name) + billing_address = frappe.get_doc("Address", billing_address_name) if shipping_address_name: - shipping_address = frappe.get_doc('Address', shipping_address_name) + shipping_address = frappe.get_doc("Address", shipping_address_name) if billing_address: - out["billing_city"]= billing_address.city - out["billing_county"]= billing_address.county - out["billing_state"]= billing_address.state - out["billing_zipcode"]= billing_address.pincode - out["billing_country"]= billing_address.country + out["billing_city"] = billing_address.city + out["billing_county"] = billing_address.county + out["billing_state"] = billing_address.state + out["billing_zipcode"] = billing_address.pincode + out["billing_country"] = billing_address.country if shipping_address: - out["shipping_city"]= shipping_address.city - out["shipping_county"]= shipping_address.county - out["shipping_state"]= shipping_address.state - out["shipping_zipcode"]= shipping_address.pincode - out["shipping_country"]= shipping_address.country + out["shipping_city"] = shipping_address.city + out["shipping_county"] = shipping_address.county + out["shipping_state"] = shipping_address.state + out["shipping_zipcode"] = shipping_address.pincode + out["shipping_country"] = shipping_address.country return out + def get_tax_template(posting_date, args): """Get matching tax rule""" args = frappe._dict(args) - conditions = ["""(from_date is null or from_date <= '{0}') - and (to_date is null or to_date >= '{0}')""".format(posting_date)] + conditions = [ + """(from_date is null or from_date <= '{0}') + and (to_date is null or to_date >= '{0}')""".format( + posting_date + ) + ] - conditions.append("ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))) - if 'tax_category' in args.keys(): - del args['tax_category'] + conditions.append( + "ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category")))) + ) + if "tax_category" in args.keys(): + del args["tax_category"] for key, value in iteritems(args): - if key=="use_for_shopping_cart": + if key == "use_for_shopping_cart": conditions.append("use_for_shopping_cart = {0}".format(1 if value else 0)) - elif key == 'customer_group': - if not value: value = get_root_of("Customer Group") + elif key == "customer_group": + if not value: + value = get_root_of("Customer Group") customer_group_condition = get_customer_group_condition(value) conditions.append("ifnull({0}, '') in ('', {1})".format(key, customer_group_condition)) else: conditions.append("ifnull({0}, '') in ('', {1})".format(key, frappe.db.escape(cstr(value)))) - tax_rule = frappe.db.sql("""select * from `tabTax Rule` - where {0}""".format(" and ".join(conditions)), as_dict = True) + tax_rule = frappe.db.sql( + """select * from `tabTax Rule` + where {0}""".format( + " and ".join(conditions) + ), + as_dict=True, + ) if not tax_rule: return None @@ -168,24 +203,30 @@ def get_tax_template(posting_date, args): for rule in tax_rule: rule.no_of_keys_matched = 0 for key in args: - if rule.get(key): rule.no_of_keys_matched += 1 + if rule.get(key): + rule.no_of_keys_matched += 1 - rule = sorted(tax_rule, - key = functools.cmp_to_key(lambda b, a: - cmp(a.no_of_keys_matched, b.no_of_keys_matched) or - cmp(a.priority, b.priority)))[0] + rule = sorted( + tax_rule, + key=functools.cmp_to_key( + lambda b, a: cmp(a.no_of_keys_matched, b.no_of_keys_matched) or cmp(a.priority, b.priority) + ), + )[0] tax_template = rule.sales_tax_template or rule.purchase_tax_template doctype = "{0} Taxes and Charges Template".format(rule.tax_type) - if frappe.db.get_value(doctype, tax_template, 'disabled')==1: + if frappe.db.get_value(doctype, tax_template, "disabled") == 1: return None return tax_template + def get_customer_group_condition(customer_group): condition = "" - customer_groups = ["%s"%(frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)] + customer_groups = [ + "%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group) + ] if customer_groups: - condition = ",".join(['%s'] * len(customer_groups))%(tuple(customer_groups)) + condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups)) return condition diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py index 44344bb7635..2cb9564875f 100644 --- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py @@ -9,7 +9,7 @@ from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule, get_t from erpnext.crm.doctype.opportunity.opportunity import make_quotation from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity -test_records = frappe.get_test_records('Tax Rule') +test_records = frappe.get_test_records("Tax Rule") from six import iteritems @@ -27,40 +27,70 @@ class TestTaxRule(unittest.TestCase): frappe.db.sql("delete from `tabTax Rule`") def test_conflict(self): - tax_rule1 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1) + tax_rule1 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + ) tax_rule1.save() - tax_rule2 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1) + tax_rule2 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + ) self.assertRaises(ConflictingTaxRule, tax_rule2.save) def test_conflict_with_non_overlapping_dates(self): - tax_rule1 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01") + tax_rule1 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + from_date="2015-01-01", + ) tax_rule1.save() - tax_rule2 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, to_date = "2013-01-01") + tax_rule2 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + to_date="2013-01-01", + ) tax_rule2.save() self.assertTrue(tax_rule2.name) def test_for_parent_customer_group(self): - tax_rule1 = make_tax_rule(customer_group= "All Customer Groups", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01") + tax_rule1 = make_tax_rule( + customer_group="All Customer Groups", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + from_date="2015-01-01", + ) tax_rule1.save() - self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}), - "_Test Sales Taxes and Charges Template - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer_group": "Commercial", "use_for_shopping_cart": 1}), + "_Test Sales Taxes and Charges Template - _TC", + ) def test_conflict_with_overlapping_dates(self): - tax_rule1 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01", to_date = "2015-01-05") + tax_rule1 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + from_date="2015-01-01", + to_date="2015-01-05", + ) tax_rule1.save() - tax_rule2 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-03", to_date = "2015-01-09") + tax_rule2 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + from_date="2015-01-03", + to_date="2015-01-09", + ) self.assertRaises(ConflictingTaxRule, tax_rule2.save) @@ -68,93 +98,186 @@ class TestTaxRule(unittest.TestCase): tax_rule = make_tax_rule() self.assertEqual(tax_rule.purchase_tax_template, None) - def test_select_tax_rule_based_on_customer(self): - make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer 1", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + customer="_Test Customer 1", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer 2", - sales_tax_template = "_Test Sales Taxes and Charges Template 2 - _TC", save=1) + make_tax_rule( + customer="_Test Customer 2", + sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer 2"}), - "_Test Sales Taxes and Charges Template 2 - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer 2"}), + "_Test Sales Taxes and Charges Template 2 - _TC", + ) def test_select_tax_rule_based_on_tax_category(self): - make_tax_rule(customer="_Test Customer", tax_category="_Test Tax Category 1", - sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + tax_category="_Test Tax Category 1", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - make_tax_rule(customer="_Test Customer", tax_category="_Test Tax Category 2", - sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + tax_category="_Test Tax Category 2", + sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC", + save=1, + ) self.assertFalse(get_tax_template("2015-01-01", {"customer": "_Test Customer"})) - self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 1"}), - "_Test Sales Taxes and Charges Template 1 - _TC") - self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 2"}), - "_Test Sales Taxes and Charges Template 2 - _TC") + self.assertEqual( + get_tax_template( + "2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 1"} + ), + "_Test Sales Taxes and Charges Template 1 - _TC", + ) + self.assertEqual( + get_tax_template( + "2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 2"} + ), + "_Test Sales Taxes and Charges Template 2 - _TC", + ) - make_tax_rule(customer="_Test Customer", tax_category="", - sales_tax_template="_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + tax_category="", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer"}), - "_Test Sales Taxes and Charges Template - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer"}), + "_Test Sales Taxes and Charges Template - _TC", + ) def test_select_tax_rule_based_on_better_match(self): - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", billing_state = "Test State", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + billing_state="Test State", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer", billing_city = "Test City1", billing_state = "Test State", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City1", + billing_state="Test State", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City", "billing_state": "Test State"}), - "_Test Sales Taxes and Charges Template - _TC") + self.assertEqual( + get_tax_template( + "2015-01-01", + {"customer": "_Test Customer", "billing_city": "Test City", "billing_state": "Test State"}, + ), + "_Test Sales Taxes and Charges Template - _TC", + ) def test_select_tax_rule_based_on_state_match(self): - make_tax_rule(customer= "_Test Customer", shipping_state = "Test State", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + shipping_state="Test State", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer", shipping_state = "Test State12", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", priority=2, save=1) + make_tax_rule( + customer="_Test Customer", + shipping_state="Test State12", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + priority=2, + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "shipping_state": "Test State"}), - "_Test Sales Taxes and Charges Template - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer", "shipping_state": "Test State"}), + "_Test Sales Taxes and Charges Template - _TC", + ) def test_select_tax_rule_based_on_better_priority(self): - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority=1, save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + save=1, + ) - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", priority=2, save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + priority=2, + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City"}), - "_Test Sales Taxes and Charges Template 1 - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City"}), + "_Test Sales Taxes and Charges Template 1 - _TC", + ) def test_select_tax_rule_based_cross_matching_keys(self): - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer 1", billing_city = "Test City 1", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + customer="_Test Customer 1", + billing_city="Test City 1", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}), - None) + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City 1"}), + None, + ) def test_select_tax_rule_based_cross_partially_keys(self): - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(billing_city = "Test City 1", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + billing_city="Test City 1", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}), - "_Test Sales Taxes and Charges Template 1 - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City 1"}), + "_Test Sales Taxes and Charges Template 1 - _TC", + ) def test_taxes_fetch_via_tax_rule(self): - make_tax_rule(customer= "_Test Customer", billing_city = "_Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="_Test City", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) # create opportunity for customer opportunity = make_opportunity(with_items=1) @@ -169,7 +292,6 @@ class TestTaxRule(unittest.TestCase): self.assertTrue(len(quotation.taxes) > 0) - def make_tax_rule(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index f33aa31e161..63698439be1 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -15,7 +15,7 @@ class TaxWithholdingCategory(Document): def validate_dates(self): last_date = None - for d in self.get('rates'): + for d in self.get("rates"): if getdate(d.from_date) >= getdate(d.to_date): frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx)) @@ -24,18 +24,25 @@ class TaxWithholdingCategory(Document): frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx)) def validate_thresholds(self): - for d in self.get('rates'): - if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold: - frappe.throw(_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(d.idx)) + for d in self.get("rates"): + if ( + d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold + ): + frappe.throw( + _("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format( + d.idx + ) + ) + def get_party_details(inv): - party_type, party = '', '' + party_type, party = "", "" - if inv.doctype == 'Sales Invoice': - party_type = 'Customer' + if inv.doctype == "Sales Invoice": + party_type = "Customer" party = inv.customer else: - party_type = 'Supplier' + party_type = "Supplier" party = inv.supplier if not party: @@ -43,65 +50,71 @@ def get_party_details(inv): return party_type, party + def get_party_tax_withholding_details(inv, tax_withholding_category=None): - pan_no = '' + pan_no = "" parties = [] party_type, party = get_party_details(inv) has_pan_field = frappe.get_meta(party_type).has_field("pan") if not tax_withholding_category: if has_pan_field: - fields = ['tax_withholding_category', 'pan'] + fields = ["tax_withholding_category", "pan"] else: - fields = ['tax_withholding_category'] + fields = ["tax_withholding_category"] tax_withholding_details = frappe.db.get_value(party_type, party, fields, as_dict=1) - tax_withholding_category = tax_withholding_details.get('tax_withholding_category') - pan_no = tax_withholding_details.get('pan') + tax_withholding_category = tax_withholding_details.get("tax_withholding_category") + pan_no = tax_withholding_details.get("pan") if not tax_withholding_category: return # if tax_withholding_category passed as an argument but not pan_no if not pan_no and has_pan_field: - pan_no = frappe.db.get_value(party_type, party, 'pan') + pan_no = frappe.db.get_value(party_type, party, "pan") # Get others suppliers with the same PAN No if pan_no: - parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name') + parties = frappe.get_all(party_type, filters={"pan": pan_no}, pluck="name") if not parties: parties.append(party) - posting_date = inv.get('posting_date') or inv.get('transaction_date') + posting_date = inv.get("posting_date") or inv.get("transaction_date") tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company) if not tax_details: - frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') - .format(tax_withholding_category, inv.company)) + frappe.throw( + _("Please set associated account in Tax Withholding Category {0} against Company {1}").format( + tax_withholding_category, inv.company + ) + ) - if party_type == 'Customer' and not tax_details.cumulative_threshold: + if party_type == "Customer" and not tax_details.cumulative_threshold: # TCS is only chargeable on sum of invoiced value - frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.') - .format(tax_withholding_category, inv.company, party)) + frappe.throw( + _( + "Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value." + ).format(tax_withholding_category, inv.company, party) + ) tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount( - party_type, parties, - inv, tax_details, - posting_date, pan_no + party_type, parties, inv, tax_details, posting_date, pan_no ) - if party_type == 'Supplier': + if party_type == "Supplier": tax_row = get_tax_row_for_tds(tax_details, tax_amount) else: tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) - if inv.doctype == 'Purchase Invoice': + if inv.doctype == "Purchase Invoice": return tax_row, tax_deducted_on_advances else: return tax_row + def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) @@ -109,19 +122,24 @@ def get_tax_withholding_details(tax_withholding_category, posting_date, company) for account_detail in tax_withholding.accounts: if company == account_detail.company: - return frappe._dict({ - "tax_withholding_category": tax_withholding_category, - "account_head": account_detail.account, - "rate": tax_rate_detail.tax_withholding_rate, - "from_date": tax_rate_detail.from_date, - "to_date": tax_rate_detail.to_date, - "threshold": tax_rate_detail.single_threshold, - "cumulative_threshold": tax_rate_detail.cumulative_threshold, - "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category, - "consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount, - "tax_on_excess_amount": tax_withholding.tax_on_excess_amount, - "round_off_tax_amount": tax_withholding.round_off_tax_amount - }) + return frappe._dict( + { + "tax_withholding_category": tax_withholding_category, + "account_head": account_detail.account, + "rate": tax_rate_detail.tax_withholding_rate, + "from_date": tax_rate_detail.from_date, + "to_date": tax_rate_detail.to_date, + "threshold": tax_rate_detail.single_threshold, + "cumulative_threshold": tax_rate_detail.cumulative_threshold, + "description": tax_withholding.category_name + if tax_withholding.category_name + else tax_withholding_category, + "consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount, + "tax_on_excess_amount": tax_withholding.tax_on_excess_amount, + "round_off_tax_amount": tax_withholding.round_off_tax_amount, + } + ) + def get_tax_withholding_rates(tax_withholding, posting_date): # returns the row that matches with the fiscal year from posting date @@ -131,13 +149,14 @@ def get_tax_withholding_rates(tax_withholding, posting_date): frappe.throw(_("No Tax Withholding data found for the current posting date.")) + def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted): row = { "category": "Total", "charge_type": "Actual", "tax_amount": tax_amount, "description": tax_details.description, - "account_head": tax_details.account_head + "account_head": tax_details.account_head, } if tax_deducted: @@ -147,20 +166,20 @@ def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted): taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head] if taxes_excluding_tcs: # chargeable amount is the total amount after other charges are applied - row.update({ - "charge_type": "On Previous Row Total", - "row_id": len(taxes_excluding_tcs), - "rate": tax_details.rate - }) + row.update( + { + "charge_type": "On Previous Row Total", + "row_id": len(taxes_excluding_tcs), + "rate": tax_details.rate, + } + ) else: # if only TCS is to be charged, then net total is chargeable amount - row.update({ - "charge_type": "On Net Total", - "rate": tax_details.rate - }) + row.update({"charge_type": "On Net Total", "rate": tax_details.rate}) return row + def get_tax_row_for_tds(tax_details, tax_amount): return { "category": "Total", @@ -168,29 +187,39 @@ def get_tax_row_for_tds(tax_details, tax_amount): "tax_amount": tax_amount, "add_deduct_tax": "Deduct", "description": tax_details.description, - "account_head": tax_details.account_head + "account_head": tax_details.account_head, } + def get_lower_deduction_certificate(tax_details, pan_no): - ldc_name = frappe.db.get_value('Lower Deduction Certificate', + ldc_name = frappe.db.get_value( + "Lower Deduction Certificate", { - 'pan_no': pan_no, - 'tax_withholding_category': tax_details.tax_withholding_category, - 'valid_from': ('>=', tax_details.from_date), - 'valid_upto': ('<=', tax_details.to_date) - }, 'name') + "pan_no": pan_no, + "tax_withholding_category": tax_details.tax_withholding_category, + "valid_from": (">=", tax_details.from_date), + "valid_upto": ("<=", tax_details.to_date), + }, + "name", + ) if ldc_name: - return frappe.get_doc('Lower Deduction Certificate', ldc_name) + return frappe.get_doc("Lower Deduction Certificate", ldc_name) + def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None): vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type) - advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date, - to_date=tax_details.to_date, party_type=party_type) + advance_vouchers = get_advance_vouchers( + parties, + company=inv.company, + from_date=tax_details.from_date, + to_date=tax_details.to_date, + party_type=party_type, + ) taxable_vouchers = vouchers + advance_vouchers tax_deducted_on_advances = 0 - if inv.doctype == 'Purchase Invoice': + if inv.doctype == "Purchase Invoice": tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) tax_deducted = 0 @@ -198,18 +227,20 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_deducted = get_deducted_tax(taxable_vouchers, tax_details) tax_amount = 0 - if party_type == 'Supplier': + if party_type == "Supplier": ldc = get_lower_deduction_certificate(tax_details, pan_no) if tax_deducted: net_total = inv.net_total if ldc: - tax_amount = get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total) + tax_amount = get_tds_amount_from_ldc( + ldc, parties, pan_no, tax_details, posting_date, net_total + ) else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 else: tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers) - elif party_type == 'Customer': + elif party_type == "Customer": if tax_deducted: # if already TCS is charged, then amount will be calculated based on 'Previous Row Total' tax_amount = 0 @@ -223,27 +254,28 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N return tax_amount, tax_deducted, tax_deducted_on_advances -def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'): - dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' - doctype = 'Purchase Invoice' if party_type == 'Supplier' else 'Sales Invoice' + +def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): + dr_or_cr = "credit" if party_type == "Supplier" else "debit" + doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" filters = { - 'company': company, - frappe.scrub(party_type): ['in', parties], - 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], - 'is_opening': 'No', - 'docstatus': 1 + "company": company, + frappe.scrub(party_type): ["in", parties], + "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], + "is_opening": "No", + "docstatus": 1, } - if not tax_details.get('consider_party_ledger_amount') and doctype != "Sales Invoice": - filters.update({ - 'apply_tds': 1, - 'tax_withholding_category': tax_details.get('tax_withholding_category') - }) + if not tax_details.get("consider_party_ledger_amount") and doctype != "Sales Invoice": + filters.update( + {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} + ) invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""] - journal_entries = frappe.db.sql(""" + journal_entries = frappe.db.sql( + """ SELECT j.name FROM `tabJournal Entry` j, `tabJournal Entry Account` ja WHERE @@ -252,52 +284,60 @@ def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'): AND j.posting_date between %s and %s AND ja.{dr_or_cr} > 0 AND ja.party in %s - """.format(dr_or_cr=dr_or_cr), (tax_details.from_date, tax_details.to_date, tuple(parties)), as_list=1) + """.format( + dr_or_cr=dr_or_cr + ), + (tax_details.from_date, tax_details.to_date, tuple(parties)), + as_list=1, + ) if journal_entries: journal_entries = journal_entries[0] return invoices + journal_entries -def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'): + +def get_advance_vouchers( + parties, company=None, from_date=None, to_date=None, party_type="Supplier" +): # for advance vouchers, debit and credit is reversed - dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit' + dr_or_cr = "debit" if party_type == "Supplier" else "credit" filters = { - dr_or_cr: ['>', 0], - 'is_opening': 'No', - 'is_cancelled': 0, - 'party_type': party_type, - 'party': ['in', parties], - 'against_voucher': ['is', 'not set'] + dr_or_cr: [">", 0], + "is_opening": "No", + "is_cancelled": 0, + "party_type": party_type, + "party": ["in", parties], + "against_voucher": ["is", "not set"], } if company: - filters['company'] = company + filters["company"] = company if from_date and to_date: - filters['posting_date'] = ['between', (from_date, to_date)] + filters["posting_date"] = ["between", (from_date, to_date)] + + return frappe.get_all("GL Entry", filters=filters, distinct=1, pluck="voucher_no") or [""] - return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] def get_taxes_deducted_on_advances_allocated(inv, tax_details): - advances = [d.reference_name for d in inv.get('advances')] + advances = [d.reference_name for d in inv.get("advances")] tax_info = [] if advances: pe = frappe.qb.DocType("Payment Entry").as_("pe") at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") - tax_info = frappe.qb.from_(at).inner_join(pe).on( - pe.name == at.parent - ).select( - at.parent, at.name, at.tax_amount, at.allocated_amount - ).where( - pe.tax_withholding_category == tax_details.get('tax_withholding_category') - ).where( - at.parent.isin(advances) - ).where( - at.account_head == tax_details.account_head - ).run(as_dict=True) + tax_info = ( + frappe.qb.from_(at) + .inner_join(pe) + .on(pe.name == at.parent) + .select(at.parent, at.name, at.tax_amount, at.allocated_amount) + .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category")) + .where(at.parent.isin(advances)) + .where(at.account_head == tax_details.account_head) + .run(as_dict=True) + ) return tax_info @@ -305,59 +345,74 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details): def get_deducted_tax(taxable_vouchers, tax_details): # check if TDS / TCS account is already charged on taxable vouchers filters = { - 'is_cancelled': 0, - 'credit': ['>', 0], - 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], - 'account': tax_details.account_head, - 'voucher_no': ['in', taxable_vouchers], + "is_cancelled": 0, + "credit": [">", 0], + "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], + "account": tax_details.account_head, + "voucher_no": ["in", taxable_vouchers], } field = "credit" - entries = frappe.db.get_all('GL Entry', filters, pluck=field) + entries = frappe.db.get_all("GL Entry", filters, pluck=field) return sum(entries) + def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): tds_amount = 0 - invoice_filters = { - 'name': ('in', vouchers), - 'docstatus': 1, - 'apply_tds': 1 - } + invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} - field = 'sum(net_total)' + field = "sum(net_total)" if cint(tax_details.consider_party_ledger_amount): - invoice_filters.pop('apply_tds', None) - field = 'sum(grand_total)' + invoice_filters.pop("apply_tds", None) + field = "sum(grand_total)" - supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0 + supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 - supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { - 'parent': ('in', vouchers), 'docstatus': 1, - 'party': ('in', parties), 'reference_type': ('!=', 'Purchase Invoice') - }, 'sum(credit_in_account_currency)') or 0.0 + supp_jv_credit_amt = ( + frappe.db.get_value( + "Journal Entry Account", + { + "parent": ("in", vouchers), + "docstatus": 1, + "party": ("in", parties), + "reference_type": ("!=", "Purchase Invoice"), + }, + "sum(credit_in_account_currency)", + ) + or 0.0 + ) supp_credit_amt += supp_jv_credit_amt supp_credit_amt += inv.net_total - debit_note_amount = get_debit_note_amount(parties, tax_details.from_date, tax_details.to_date, inv.company) + debit_note_amount = get_debit_note_amount( + parties, tax_details.from_date, tax_details.to_date, inv.company + ) supp_credit_amt -= debit_note_amount - threshold = tax_details.get('threshold', 0) - cumulative_threshold = tax_details.get('cumulative_threshold', 0) + threshold = tax_details.get("threshold", 0) + cumulative_threshold = tax_details.get("cumulative_threshold", 0) - if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): - if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount): + if (threshold and inv.net_total >= threshold) or ( + cumulative_threshold and supp_credit_amt >= cumulative_threshold + ): + if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint( + tax_details.tax_on_excess_amount + ): # Get net total again as TDS is calculated on net total # Grand is used to just check for threshold breach - net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0 + net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0 net_total += inv.net_total supp_credit_amt = net_total - cumulative_threshold if ldc and is_valid_certificate( - ldc.valid_from, ldc.valid_upto, - inv.get('posting_date') or inv.get('transaction_date'), tax_deducted, - inv.net_total, ldc.certificate_limit + ldc.valid_from, + ldc.valid_upto, + inv.get("posting_date") or inv.get("transaction_date"), + tax_deducted, + inv.net_total, + ldc.certificate_limit, ): tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) else: @@ -365,98 +420,127 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): return tds_amount + def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): tcs_amount = 0 # sum of debit entries made from sales invoices - invoiced_amt = frappe.db.get_value('GL Entry', { - 'is_cancelled': 0, - 'party': ['in', parties], - 'company': inv.company, - 'voucher_no': ['in', vouchers], - }, 'sum(debit)') or 0.0 + invoiced_amt = ( + frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "party": ["in", parties], + "company": inv.company, + "voucher_no": ["in", vouchers], + }, + "sum(debit)", + ) + or 0.0 + ) # sum of credit entries made from PE / JV with unset 'against voucher' - advance_amt = frappe.db.get_value('GL Entry', { - 'is_cancelled': 0, - 'party': ['in', parties], - 'company': inv.company, - 'voucher_no': ['in', adv_vouchers], - }, 'sum(credit)') or 0.0 + advance_amt = ( + frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "party": ["in", parties], + "company": inv.company, + "voucher_no": ["in", adv_vouchers], + }, + "sum(credit)", + ) + or 0.0 + ) # sum of credit entries made from sales invoice - credit_note_amt = sum(frappe.db.get_all('GL Entry', { - 'is_cancelled': 0, - 'credit': ['>', 0], - 'party': ['in', parties], - 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], - 'company': inv.company, - 'voucher_type': 'Sales Invoice', - }, pluck='credit')) + credit_note_amt = sum( + frappe.db.get_all( + "GL Entry", + { + "is_cancelled": 0, + "credit": [">", 0], + "party": ["in", parties], + "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], + "company": inv.company, + "voucher_type": "Sales Invoice", + }, + pluck="credit", + ) + ) - cumulative_threshold = tax_details.get('cumulative_threshold', 0) + cumulative_threshold = tax_details.get("cumulative_threshold", 0) current_invoice_total = get_invoice_total_without_tcs(inv, tax_details) total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)): + if cumulative_threshold and total_invoiced_amt >= cumulative_threshold: chargeable_amt = total_invoiced_amt - cumulative_threshold tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0 return tcs_amount + def get_invoice_total_without_tcs(inv, tax_details): tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head] tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0 return inv.grand_total - tcs_tax_row_amount + def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total): tds_amount = 0 - limit_consumed = frappe.db.get_value('Purchase Invoice', { - 'supplier': ('in', parties), - 'apply_tds': 1, - 'docstatus': 1 - }, 'sum(net_total)') + limit_consumed = frappe.db.get_value( + "Purchase Invoice", + {"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1}, + "sum(net_total)", + ) if is_valid_certificate( - ldc.valid_from, ldc.valid_upto, - posting_date, limit_consumed, - net_total, ldc.certificate_limit + ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, ldc.certificate_limit ): - tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) + tds_amount = get_ltds_amount( + net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details + ) return tds_amount + def get_debit_note_amount(suppliers, from_date, to_date, company=None): filters = { - 'supplier': ['in', suppliers], - 'is_return': 1, - 'docstatus': 1, - 'posting_date': ['between', (from_date, to_date)] + "supplier": ["in", suppliers], + "is_return": 1, + "docstatus": 1, + "posting_date": ["between", (from_date, to_date)], } - fields = ['abs(sum(net_total)) as net_total'] + fields = ["abs(sum(net_total)) as net_total"] if company: - filters['company'] = company + filters["company"] = company + + return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0 - return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0 def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): if current_amount < (certificate_limit - deducted_amount): - return current_amount * rate/100 + return current_amount * rate / 100 else: - ltds_amount = (certificate_limit - deducted_amount) + ltds_amount = certificate_limit - deducted_amount tds_amount = current_amount - ltds_amount - return ltds_amount * rate/100 + tds_amount * tax_details.rate/100 + return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100 -def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit): + +def is_valid_certificate( + valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit +): valid = False - if ((getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and - certificate_limit > deducted_amount): + if ( + getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto) + ) and certificate_limit > deducted_amount: valid = True return valid diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py index 46d0c2e487e..8a510ea023f 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'tax_withholding_category', - 'transactions': [ - { - 'items': ['Supplier'] - } - ] - } + return {"fieldname": "tax_withholding_category", "transactions": [{"items": ["Supplier"]}]} diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index a3fcf7da7a5..3059f8d64b8 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -10,6 +10,7 @@ from erpnext.accounts.utils import get_fiscal_year test_dependencies = ["Supplier Group", "Customer Group"] + class TestTaxWithholdingCategory(unittest.TestCase): @classmethod def setUpClass(self): @@ -21,18 +22,20 @@ class TestTaxWithholdingCategory(unittest.TestCase): cancel_invoices() def test_cumulative_threshold_tds(self): - frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS") + frappe.db.set_value( + "Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS" + ) invoices = [] # create invoices for lower than single threshold tax rate for _ in range(2): - pi = create_purchase_invoice(supplier = "Test TDS Supplier") + pi = create_purchase_invoice(supplier="Test TDS Supplier") pi.submit() invoices.append(pi) # create another invoice whose total when added to previously created invoice, # surpasses cumulative threshhold - pi = create_purchase_invoice(supplier = "Test TDS Supplier") + pi = create_purchase_invoice(supplier="Test TDS Supplier") pi.submit() # assert equal tax deduction on total invoice amount uptil now @@ -41,21 +44,23 @@ class TestTaxWithholdingCategory(unittest.TestCase): invoices.append(pi) # TDS is already deducted, so from onward system will deduct the TDS on every invoice - pi = create_purchase_invoice(supplier = "Test TDS Supplier", rate=5000) + pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000) pi.submit() # assert equal tax deduction on total invoice amount uptil now self.assertEqual(pi.taxes_and_charges_deducted, 500) invoices.append(pi) - #delete invoices to avoid clashing + # delete invoices to avoid clashing for d in invoices: d.cancel() def test_single_threshold_tds(self): invoices = [] - frappe.db.set_value("Supplier", "Test TDS Supplier1", "tax_withholding_category", "Single Threshold TDS") - pi = create_purchase_invoice(supplier = "Test TDS Supplier1", rate = 20000) + frappe.db.set_value( + "Supplier", "Test TDS Supplier1", "tax_withholding_category", "Single Threshold TDS" + ) + pi = create_purchase_invoice(supplier="Test TDS Supplier1", rate=20000) pi.submit() invoices.append(pi) @@ -63,7 +68,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi.grand_total, 18000) # check gl entry for the purchase invoice - gl_entries = frappe.db.get_all('GL Entry', filters={'voucher_no': pi.name}, fields=["*"]) + gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"]) self.assertEqual(len(gl_entries), 3) for d in gl_entries: if d.account == pi.credit_to: @@ -75,7 +80,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): else: raise ValueError("Account head does not match.") - pi = create_purchase_invoice(supplier = "Test TDS Supplier1") + pi = create_purchase_invoice(supplier="Test TDS Supplier1") pi.submit() invoices.append(pi) @@ -88,17 +93,19 @@ class TestTaxWithholdingCategory(unittest.TestCase): def test_tax_withholding_category_checks(self): invoices = [] - frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category") + frappe.db.set_value( + "Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category" + ) # First Invoice with no tds check - pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True) + pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000, do_not_save=True) pi.apply_tds = 0 pi.save() pi.submit() invoices.append(pi) # Second Invoice will apply TDS checked - pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000) + pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000) pi1.submit() invoices.append(pi1) @@ -110,82 +117,89 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() - def test_cumulative_threshold_tcs(self): - frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") + frappe.db.set_value( + "Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS" + ) invoices = [] # create invoices for lower than single threshold tax rate for _ in range(2): - si = create_sales_invoice(customer = "Test TCS Customer") + si = create_sales_invoice(customer="Test TCS Customer") si.submit() invoices.append(si) # create another invoice whose total when added to previously created invoice, # surpasses cumulative threshhold - si = create_sales_invoice(customer = "Test TCS Customer", rate=12000) + si = create_sales_invoice(customer="Test TCS Customer", rate=12000) si.submit() # assert tax collection on total invoice amount created until now - tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) + tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"]) self.assertEqual(tcs_charged, 200) self.assertEqual(si.grand_total, 12200) invoices.append(si) # TCS is already collected once, so going forward system will collect TCS on every invoice - si = create_sales_invoice(customer = "Test TCS Customer", rate=5000) + si = create_sales_invoice(customer="Test TCS Customer", rate=5000) si.submit() - tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC') + tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC") self.assertEqual(tcs_charged, 500) invoices.append(si) - #delete invoices to avoid clashing + # delete invoices to avoid clashing for d in invoices: d.cancel() def test_tds_calculation_on_net_total(self): - frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS") + frappe.db.set_value( + "Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS" + ) invoices = [] - pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True) - pi.append('taxes', { - "category": "Total", - "charge_type": "Actual", - "account_head": '_Test Account VAT - _TC', - "cost_center": 'Main - _TC', - "tax_amount": 1000, - "description": "Test", - "add_deduct_tax": "Add" - - }) + pi = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000, do_not_save=True) + pi.append( + "taxes", + { + "category": "Total", + "charge_type": "Actual", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "tax_amount": 1000, + "description": "Test", + "add_deduct_tax": "Add", + }, + ) pi.save() pi.submit() invoices.append(pi) # Second Invoice will apply TDS checked - pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000) + pi1 = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000) pi1.submit() invoices.append(pi1) self.assertEqual(pi1.taxes[0].tax_amount, 4000) - #delete invoices to avoid clashing + # delete invoices to avoid clashing for d in invoices: d.cancel() def test_multi_category_single_supplier(self): - frappe.db.set_value("Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category") + frappe.db.set_value( + "Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category" + ) invoices = [] - pi = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 500, do_not_save=True) + pi = create_purchase_invoice(supplier="Test TDS Supplier5", rate=500, do_not_save=True) pi.tax_withholding_category = "Test Service Category" pi.save() pi.submit() invoices.append(pi) # Second Invoice will apply TDS checked - pi1 = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 2500, do_not_save=True) + pi1 = create_purchase_invoice(supplier="Test TDS Supplier5", rate=2500, do_not_save=True) pi1.tax_withholding_category = "Test Goods Category" pi1.save() pi1.submit() @@ -193,258 +207,294 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 250) - #delete invoices to avoid clashing + # delete invoices to avoid clashing for d in invoices: d.cancel() -def cancel_invoices(): - purchase_invoices = frappe.get_all("Purchase Invoice", { - 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']], - 'docstatus': 1 - }, pluck="name") - sales_invoices = frappe.get_all("Sales Invoice", { - 'customer': 'Test TCS Customer', - 'docstatus': 1 - }, pluck="name") +def cancel_invoices(): + purchase_invoices = frappe.get_all( + "Purchase Invoice", + { + "supplier": ["in", ["Test TDS Supplier", "Test TDS Supplier1", "Test TDS Supplier2"]], + "docstatus": 1, + }, + pluck="name", + ) + + sales_invoices = frappe.get_all( + "Sales Invoice", {"customer": "Test TCS Customer", "docstatus": 1}, pluck="name" + ) for d in purchase_invoices: - frappe.get_doc('Purchase Invoice', d).cancel() + frappe.get_doc("Purchase Invoice", d).cancel() for d in sales_invoices: - frappe.get_doc('Sales Invoice', d).cancel() + frappe.get_doc("Sales Invoice", d).cancel() + def create_purchase_invoice(**args): # return sales invoice doc object - item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name") + item = frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name") args = frappe._dict(args) - pi = frappe.get_doc({ - "doctype": "Purchase Invoice", - "posting_date": today(), - "apply_tds": 0 if args.do_not_apply_tds else 1, - "supplier": args.supplier, - "company": '_Test Company', - "taxes_and_charges": "", - "currency": "INR", - "credit_to": "Creditors - _TC", - "taxes": [], - "items": [{ - 'doctype': 'Purchase Invoice Item', - 'item_code': item, - 'qty': args.qty or 1, - 'rate': args.rate or 10000, - 'cost_center': 'Main - _TC', - 'expense_account': 'Stock Received But Not Billed - _TC' - }] - }) + pi = frappe.get_doc( + { + "doctype": "Purchase Invoice", + "posting_date": today(), + "apply_tds": 0 if args.do_not_apply_tds else 1, + "supplier": args.supplier, + "company": "_Test Company", + "taxes_and_charges": "", + "currency": "INR", + "credit_to": "Creditors - _TC", + "taxes": [], + "items": [ + { + "doctype": "Purchase Invoice Item", + "item_code": item, + "qty": args.qty or 1, + "rate": args.rate or 10000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + } + ], + } + ) pi.save() return pi + def create_sales_invoice(**args): # return sales invoice doc object - item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name") + item = frappe.db.get_value("Item", {"item_name": "TCS Item"}, "name") args = frappe._dict(args) - si = frappe.get_doc({ - "doctype": "Sales Invoice", - "posting_date": today(), - "customer": args.customer, - "company": '_Test Company', - "taxes_and_charges": "", - "currency": "INR", - "debit_to": "Debtors - _TC", - "taxes": [], - "items": [{ - 'doctype': 'Sales Invoice Item', - 'item_code': item, - 'qty': args.qty or 1, - 'rate': args.rate or 10000, - 'cost_center': 'Main - _TC', - 'expense_account': 'Cost of Goods Sold - _TC', - 'warehouse': args.warehouse or '_Test Warehouse - _TC' - }] - }) + si = frappe.get_doc( + { + "doctype": "Sales Invoice", + "posting_date": today(), + "customer": args.customer, + "company": "_Test Company", + "taxes_and_charges": "", + "currency": "INR", + "debit_to": "Debtors - _TC", + "taxes": [], + "items": [ + { + "doctype": "Sales Invoice Item", + "item_code": item, + "qty": args.qty or 1, + "rate": args.rate or 10000, + "cost_center": "Main - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + } + ], + } + ) si.save() return si + def create_records(): # create a new suppliers - for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', - 'Test TDS Supplier4', 'Test TDS Supplier5']: - if frappe.db.exists('Supplier', name): + for name in [ + "Test TDS Supplier", + "Test TDS Supplier1", + "Test TDS Supplier2", + "Test TDS Supplier3", + "Test TDS Supplier4", + "Test TDS Supplier5", + ]: + if frappe.db.exists("Supplier", name): continue - frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": name, - "doctype": "Supplier", - }).insert() + frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": name, + "doctype": "Supplier", + } + ).insert() - for name in ['Test TCS Customer']: - if frappe.db.exists('Customer', name): + for name in ["Test TCS Customer"]: + if frappe.db.exists("Customer", name): continue - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": name, - "doctype": "Customer" - }).insert() + frappe.get_doc( + {"customer_group": "_Test Customer Group", "customer_name": name, "doctype": "Customer"} + ).insert() # create item - if not frappe.db.exists('Item', "TDS Item"): - frappe.get_doc({ - "doctype": "Item", - "item_code": "TDS Item", - "item_name": "TDS Item", - "item_group": "All Item Groups", - "is_stock_item": 0, - }).insert() + if not frappe.db.exists("Item", "TDS Item"): + frappe.get_doc( + { + "doctype": "Item", + "item_code": "TDS Item", + "item_name": "TDS Item", + "item_group": "All Item Groups", + "is_stock_item": 0, + } + ).insert() - if not frappe.db.exists('Item', "TCS Item"): - frappe.get_doc({ - "doctype": "Item", - "item_code": "TCS Item", - "item_name": "TCS Item", - "item_group": "All Item Groups", - "is_stock_item": 1 - }).insert() + if not frappe.db.exists("Item", "TCS Item"): + frappe.get_doc( + { + "doctype": "Item", + "item_code": "TCS Item", + "item_name": "TCS Item", + "item_group": "All Item Groups", + "is_stock_item": 1, + } + ).insert() # create tds account if not frappe.db.exists("Account", "TDS - _TC"): - frappe.get_doc({ - 'doctype': 'Account', - 'company': '_Test Company', - 'account_name': 'TDS', - 'parent_account': 'Tax Assets - _TC', - 'report_type': 'Balance Sheet', - 'root_type': 'Asset' - }).insert() + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "TDS", + "parent_account": "Tax Assets - _TC", + "report_type": "Balance Sheet", + "root_type": "Asset", + } + ).insert() # create tcs account if not frappe.db.exists("Account", "TCS - _TC"): - frappe.get_doc({ - 'doctype': 'Account', - 'company': '_Test Company', - 'account_name': 'TCS', - 'parent_account': 'Duties and Taxes - _TC', - 'report_type': 'Balance Sheet', - 'root_type': 'Liability' - }).insert() + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "TCS", + "parent_account": "Duties and Taxes - _TC", + "report_type": "Balance Sheet", + "root_type": "Liability", + } + ).insert() + def create_tax_with_holding_category(): fiscal_year = get_fiscal_year(today(), company="_Test Company") # Cumulative threshold if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Cumulative Threshold TDS", - "category_name": "10% TDS", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 0, - 'cumulative_threshold': 30000.00 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Cumulative Threshold TDS", + "category_name": "10% TDS", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 30000.00, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Cumulative Threshold TCS", - "category_name": "10% TCS", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 0, - 'cumulative_threshold': 30000.00 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TCS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Cumulative Threshold TCS", + "category_name": "10% TCS", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 30000.00, + } + ], + "accounts": [{"company": "_Test Company", "account": "TCS - _TC"}], + } + ).insert() # Single thresold if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Single Threshold TDS", - "category_name": "10% TDS", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 20000.00, - 'cumulative_threshold': 0 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Single Threshold TDS", + "category_name": "10% TDS", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 20000.00, + "cumulative_threshold": 0, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() if not frappe.db.exists("Tax Withholding Category", "New TDS Category"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "New TDS Category", - "category_name": "New TDS Category", - "round_off_tax_amount": 1, - "consider_party_ledger_amount": 1, - "tax_on_excess_amount": 1, - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 0, - 'cumulative_threshold': 30000 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "New TDS Category", + "category_name": "New TDS Category", + "round_off_tax_amount": 1, + "consider_party_ledger_amount": 1, + "tax_on_excess_amount": 1, + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 30000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() if not frappe.db.exists("Tax Withholding Category", "Test Service Category"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Test Service Category", - "category_name": "Test Service Category", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 2000, - 'cumulative_threshold': 2000 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Test Service Category", + "category_name": "Test Service Category", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 2000, + "cumulative_threshold": 2000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Test Goods Category", - "category_name": "Test Goods Category", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 2000, - 'cumulative_threshold': 2000 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Test Goods Category", + "category_name": "Test Goods Category", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 2000, + "cumulative_threshold": 2000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index fd5173fd659..47e2e0761b6 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -14,9 +14,18 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget -class ClosedAccountingPeriod(frappe.ValidationError): pass +class ClosedAccountingPeriod(frappe.ValidationError): + pass -def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): + +def make_gl_entries( + gl_map, + cancel=False, + adv_adj=False, + merge_entries=True, + update_outstanding="Yes", + from_repost=False, +): if gl_map: if not cancel: validate_accounting_period(gl_map) @@ -25,12 +34,18 @@ def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, upd save_entries(gl_map, adv_adj, update_outstanding, from_repost) # Post GL Map proccess there may no be any GL Entries elif gl_map: - frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.")) + frappe.throw( + _( + "Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction." + ) + ) else: make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) + def validate_accounting_period(gl_map): - accounting_periods = frappe.db.sql(""" SELECT + accounting_periods = frappe.db.sql( + """ SELECT ap.name as name FROM `tabAccounting Period` ap, `tabClosed Document` cd @@ -40,15 +55,23 @@ def validate_accounting_period(gl_map): AND cd.closed = 1 AND cd.document_type = %(voucher_type)s AND %(date)s between ap.start_date and ap.end_date - """, { - 'date': gl_map[0].posting_date, - 'company': gl_map[0].company, - 'voucher_type': gl_map[0].voucher_type - }, as_dict=1) + """, + { + "date": gl_map[0].posting_date, + "company": gl_map[0].company, + "voucher_type": gl_map[0].voucher_type, + }, + as_dict=1, + ) if accounting_periods: - frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") - .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) + frappe.throw( + _( + "You cannot create or cancel any accounting entries with in the closed Accounting Period {0}" + ).format(frappe.bold(accounting_periods[0].name)), + ClosedAccountingPeriod, + ) + def process_gl_map(gl_map, merge_entries=True, precision=None): if merge_entries: @@ -60,8 +83,9 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): entry.debit = 0.0 if flt(entry.debit_in_account_currency) < 0: - entry.credit_in_account_currency = \ - flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency) + entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt( + entry.debit_in_account_currency + ) entry.debit_in_account_currency = 0.0 if flt(entry.credit) < 0: @@ -69,32 +93,37 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): entry.credit = 0.0 if flt(entry.credit_in_account_currency) < 0: - entry.debit_in_account_currency = \ - flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency) + entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt( + entry.credit_in_account_currency + ) entry.credit_in_account_currency = 0.0 update_net_values(entry) return gl_map + def update_net_values(entry): # In some scenarios net value needs to be shown in the ledger # This method updates net values as debit or credit if entry.post_net_value and entry.debit and entry.credit: if entry.debit > entry.credit: entry.debit = entry.debit - entry.credit - entry.debit_in_account_currency = entry.debit_in_account_currency \ - - entry.credit_in_account_currency + entry.debit_in_account_currency = ( + entry.debit_in_account_currency - entry.credit_in_account_currency + ) entry.credit = 0 entry.credit_in_account_currency = 0 else: entry.credit = entry.credit - entry.debit - entry.credit_in_account_currency = entry.credit_in_account_currency \ - - entry.debit_in_account_currency + entry.credit_in_account_currency = ( + entry.credit_in_account_currency - entry.debit_in_account_currency + ) entry.debit = 0 entry.debit_in_account_currency = 0 + def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() @@ -103,12 +132,14 @@ def merge_similar_entries(gl_map, precision=None): # to that entry same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions) if same_head: - same_head.debit = flt(same_head.debit) + flt(entry.debit) - same_head.debit_in_account_currency = \ - flt(same_head.debit_in_account_currency) + flt(entry.debit_in_account_currency) + same_head.debit = flt(same_head.debit) + flt(entry.debit) + same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt( + entry.debit_in_account_currency + ) same_head.credit = flt(same_head.credit) + flt(entry.credit) - same_head.credit_in_account_currency = \ - flt(same_head.credit_in_account_currency) + flt(entry.credit_in_account_currency) + same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt( + entry.credit_in_account_currency + ) else: merged_gl_map.append(entry) @@ -119,14 +150,25 @@ def merge_similar_entries(gl_map, precision=None): precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency) # filter zero debit and credit entries - merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map) + merged_gl_map = filter( + lambda x: flt(x.debit, precision) != 0 or flt(x.credit, precision) != 0, merged_gl_map + ) merged_gl_map = list(merged_gl_map) return merged_gl_map + def check_if_in_list(gle, gl_map, dimensions=None): - account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher', - 'cost_center', 'against_voucher_type', 'party_type', 'project', 'finance_book'] + account_head_fieldnames = [ + "voucher_detail_no", + "party", + "against_voucher", + "cost_center", + "against_voucher_type", + "party_type", + "project", + "finance_book", + ] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions @@ -145,6 +187,7 @@ def check_if_in_list(gle, gl_map, dimensions=None): if same_head: return e + def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): if not from_repost: validate_cwip_accounts(gl_map) @@ -157,36 +200,52 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): for entry in gl_map: make_entry(entry, adv_adj, update_outstanding, from_repost) + def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle = frappe.new_doc("GL Entry") gle.update(args) gle.flags.ignore_permissions = 1 gle.flags.from_repost = from_repost gle.flags.adv_adj = adv_adj - gle.flags.update_outstanding = update_outstanding or 'Yes' + gle.flags.update_outstanding = update_outstanding or "Yes" gle.submit() if not from_repost: validate_expense_against_budget(args) + def validate_cwip_accounts(gl_map): """Validate that CWIP account are not used in Journal Entry""" if gl_map and gl_map[0].voucher_type != "Journal Entry": return - cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting")) + cwip_enabled = any( + cint(ac.enable_cwip_accounting) + for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting") + ) if cwip_enabled: - cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount - where account_type = 'Capital Work in Progress' and is_group=0""")] + cwip_accounts = [ + d[0] + for d in frappe.db.sql( + """select name from tabAccount + where account_type = 'Capital Work in Progress' and is_group=0""" + ) + ] for entry in gl_map: if entry.account in cwip_accounts: frappe.throw( - _("Account: {0} is capital Work in progress and can not be updated by Journal Entry").format(entry.account)) + _( + "Account: {0} is capital Work in progress and can not be updated by Journal Entry" + ).format(entry.account) + ) + def round_off_debit_credit(gl_map): - precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), - currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) + precision = get_field_precision( + frappe.get_meta("GL Entry").get_field("debit"), + currency=frappe.get_cached_value("Company", gl_map[0].company, "default_currency"), + ) debit_credit_diff = 0.0 for entry in gl_map: @@ -199,17 +258,23 @@ def round_off_debit_credit(gl_map): if gl_map[0]["voucher_type"] in ("Journal Entry", "Payment Entry"): allowance = 5.0 / (10**precision) else: - allowance = .5 + allowance = 0.5 if abs(debit_credit_diff) > allowance: - frappe.throw(_("Debit and Credit not equal for {0} #{1}. Difference is {2}.") - .format(gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff)) + frappe.throw( + _("Debit and Credit not equal for {0} #{1}. Difference is {2}.").format( + gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff + ) + ) elif abs(debit_credit_diff) >= (1.0 / (10**precision)): make_round_off_gle(gl_map, debit_credit_diff, precision) + def make_round_off_gle(gl_map, debit_credit_diff, precision): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(gl_map[0].company) + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no + ) round_off_account_exists = False round_off_gle = frappe._dict() for d in gl_map: @@ -226,30 +291,62 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): return if not round_off_gle: - for k in ["voucher_type", "voucher_no", "company", - "posting_date", "remarks"]: - round_off_gle[k] = gl_map[0][k] + for k in ["voucher_type", "voucher_no", "company", "posting_date", "remarks"]: + round_off_gle[k] = gl_map[0][k] - round_off_gle.update({ - "account": round_off_account, - "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, - "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0, - "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, - "credit": debit_credit_diff if debit_credit_diff > 0 else 0, - "cost_center": round_off_cost_center, - "party_type": None, - "party": None, - "is_opening": "No", - "against_voucher_type": None, - "against_voucher": None - }) + round_off_gle.update( + { + "account": round_off_account, + "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, + "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0, + "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, + "credit": debit_credit_diff if debit_credit_diff > 0 else 0, + "cost_center": round_off_cost_center, + "party_type": None, + "party": None, + "is_opening": "No", + "against_voucher_type": None, + "against_voucher": None, + } + ) + + update_accounting_dimensions(round_off_gle) if not round_off_account_exists: gl_map.append(round_off_gle) -def get_round_off_account_and_cost_center(company): - round_off_account, round_off_cost_center = frappe.get_cached_value('Company', company, - ["round_off_account", "round_off_cost_center"]) or [None, None] + +def update_accounting_dimensions(round_off_gle): + dimensions = get_accounting_dimensions() + meta = frappe.get_meta(round_off_gle["voucher_type"]) + has_all_dimensions = True + + for dimension in dimensions: + if not meta.has_field(dimension): + has_all_dimensions = False + + if dimensions and has_all_dimensions: + dimension_values = frappe.db.get_value( + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1 + ) + + for dimension in dimensions: + round_off_gle[dimension] = dimension_values.get(dimension) + + +def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): + round_off_account, round_off_cost_center = frappe.get_cached_value( + "Company", company, ["round_off_account", "round_off_cost_center"] + ) or [None, None] + + meta = frappe.get_meta(voucher_type) + + # Give first preference to parent cost center for round off GLE + if meta.has_field("cost_center"): + parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center") + if parent_cost_center: + round_off_cost_center = parent_cost_center + if not round_off_account: frappe.throw(_("Please mention Round Off Account in Company")) @@ -258,68 +355,78 @@ def get_round_off_account_and_cost_center(company): return round_off_account, round_off_cost_center -def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, - adv_adj=False, update_outstanding="Yes"): + +def make_reverse_gl_entries( + gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes" +): """ - Get original gl entries of the voucher - and make reverse gl entries by swapping debit and credit + Get original gl entries of the voucher + and make reverse gl entries by swapping debit and credit """ if not gl_entries: - gl_entries = frappe.get_all("GL Entry", - fields = ["*"], - filters = { - "voucher_type": voucher_type, - "voucher_no": voucher_no, - "is_cancelled": 0 - }) + gl_entries = frappe.get_all( + "GL Entry", + fields=["*"], + filters={"voucher_type": voucher_type, "voucher_no": voucher_no, "is_cancelled": 0}, + ) if gl_entries: validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) - set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) + set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) for entry in gl_entries: - entry['name'] = None - debit = entry.get('debit', 0) - credit = entry.get('credit', 0) + entry["name"] = None + debit = entry.get("debit", 0) + credit = entry.get("credit", 0) - debit_in_account_currency = entry.get('debit_in_account_currency', 0) - credit_in_account_currency = entry.get('credit_in_account_currency', 0) + debit_in_account_currency = entry.get("debit_in_account_currency", 0) + credit_in_account_currency = entry.get("credit_in_account_currency", 0) - entry['debit'] = credit - entry['credit'] = debit - entry['debit_in_account_currency'] = credit_in_account_currency - entry['credit_in_account_currency'] = debit_in_account_currency + entry["debit"] = credit + entry["credit"] = debit + entry["debit_in_account_currency"] = credit_in_account_currency + entry["credit_in_account_currency"] = debit_in_account_currency - entry['remarks'] = "On cancellation of " + entry['voucher_no'] - entry['is_cancelled'] = 1 + entry["remarks"] = "On cancellation of " + entry["voucher_no"] + entry["is_cancelled"] = 1 - if entry['debit'] or entry['credit']: + if entry["debit"] or entry["credit"]: make_entry(entry, adv_adj, "Yes") def check_freezing_date(posting_date, adv_adj=False): """ - Nobody can do GL Entries where posting date is before freezing date - except authorized person + Nobody can do GL Entries where posting date is before freezing date + except authorized person - Administrator has all the roles so this check will be bypassed if any role is allowed to post - Hence stop admin to bypass if accounts are freezed + Administrator has all the roles so this check will be bypassed if any role is allowed to post + Hence stop admin to bypass if accounts are freezed """ if not adv_adj: - acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto') + acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto") if acc_frozen_upto: - frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier') - if getdate(posting_date) <= getdate(acc_frozen_upto) \ - and (frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == 'Administrator'): - frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto))) + frozen_accounts_modifier = frappe.db.get_value( + "Accounts Settings", None, "frozen_accounts_modifier" + ) + if getdate(posting_date) <= getdate(acc_frozen_upto) and ( + frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator" + ): + frappe.throw( + _("You are not authorized to add or update entries before {0}").format( + formatdate(acc_frozen_upto) + ) + ) + def set_as_cancel(voucher_type, voucher_no): """ - Set is_cancelled=1 in all original gl entries for the voucher + Set is_cancelled=1 in all original gl entries for the voucher """ - frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1, + frappe.db.sql( + """UPDATE `tabGL Entry` SET is_cancelled = 1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", - (now(), frappe.session.user, voucher_type, voucher_no)) + (now(), frappe.session.user, voucher_type, voucher_no), + ) diff --git a/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py b/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py +++ b/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 907964720ff..b76ce29b505 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -34,125 +34,230 @@ from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen -class DuplicatePartyAccountError(frappe.ValidationError): pass +class DuplicatePartyAccountError(frappe.ValidationError): + pass + @frappe.whitelist() -def get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, - bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, fetch_payment_terms_template=True, - party_address=None, company_address=None, shipping_address=None, pos_profile=None): +def get_party_details( + party=None, + account=None, + party_type="Customer", + company=None, + posting_date=None, + bill_date=None, + price_list=None, + currency=None, + doctype=None, + ignore_permissions=False, + fetch_payment_terms_template=True, + party_address=None, + company_address=None, + shipping_address=None, + pos_profile=None, +): if not party: return {} if not frappe.db.exists(party_type, party): frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) - return _get_party_details(party, account, party_type, - company, posting_date, bill_date, price_list, currency, doctype, ignore_permissions, - fetch_payment_terms_template, party_address, company_address, shipping_address, pos_profile) + return _get_party_details( + party, + account, + party_type, + company, + posting_date, + bill_date, + price_list, + currency, + doctype, + ignore_permissions, + fetch_payment_terms_template, + party_address, + company_address, + shipping_address, + pos_profile, + ) -def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, - bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, - fetch_payment_terms_template=True, party_address=None, company_address=None, shipping_address=None, pos_profile=None): - party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) + +def _get_party_details( + party=None, + account=None, + party_type="Customer", + company=None, + posting_date=None, + bill_date=None, + price_list=None, + currency=None, + doctype=None, + ignore_permissions=False, + fetch_payment_terms_template=True, + party_address=None, + company_address=None, + shipping_address=None, + pos_profile=None, +): + party_details = frappe._dict( + set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype) + ) party = party_details[party_type.lower()] - if not ignore_permissions and not (frappe.has_permission(party_type, "read", party) or frappe.has_permission(party_type, "select", party)): + if not ignore_permissions and not ( + frappe.has_permission(party_type, "read", party) + or frappe.has_permission(party_type, "select", party) + ): frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) party = frappe.get_doc(party_type, party) currency = party.get("default_currency") or currency or get_company_currency(company) - party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address) + party_address, shipping_address = set_address_details( + party_details, + party, + party_type, + doctype, + company, + party_address, + company_address, + shipping_address, + ) set_contact_details(party_details, party, party_type) set_other_values(party_details, party, party_type) set_price_list(party_details, party, party_type, price_list, pos_profile) - party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), - party_address, shipping_address if party_type != "Supplier" else party_address) + party_details["tax_category"] = get_address_tax_category( + party.get("tax_category"), + party_address, + shipping_address if party_type != "Supplier" else party_address, + ) - tax_template = set_taxes(party.name, party_type, posting_date, company, - customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, - billing_address=party_address, shipping_address=shipping_address) + tax_template = set_taxes( + party.name, + party_type, + posting_date, + company, + customer_group=party_details.customer_group, + supplier_group=party_details.supplier_group, + tax_category=party_details.tax_category, + billing_address=party_address, + shipping_address=shipping_address, + ) if tax_template: - party_details['taxes_and_charges'] = tax_template + party_details["taxes_and_charges"] = tax_template if cint(fetch_payment_terms_template): - party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) + party_details["payment_terms_template"] = get_payment_terms_template( + party.name, party_type, company + ) if not party_details.get("currency"): party_details["currency"] = currency # sales team - if party_type=="Customer": - party_details["sales_team"] = [{ - "sales_person": d.sales_person, - "allocated_percentage": d.allocated_percentage or None, - "commission_rate": d.commission_rate - } for d in party.get("sales_team")] + if party_type == "Customer": + party_details["sales_team"] = [ + { + "sales_person": d.sales_person, + "allocated_percentage": d.allocated_percentage or None, + "commission_rate": d.commission_rate, + } + for d in party.get("sales_team") + ] # supplier tax withholding category if party_type == "Supplier" and party: - party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") + party_details["supplier_tds"] = frappe.get_value( + party_type, party.name, "tax_withholding_category" + ) return party_details -def set_address_details(party_details, party, party_type, doctype=None, company=None, party_address=None, company_address=None, shipping_address=None): - billing_address_field = "customer_address" if party_type == "Lead" \ - else party_type.lower() + "_address" - party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) + +def set_address_details( + party_details, + party, + party_type, + doctype=None, + company=None, + party_address=None, + company_address=None, + shipping_address=None, +): + billing_address_field = ( + "customer_address" if party_type == "Lead" else party_type.lower() + "_address" + ) + party_details[billing_address_field] = party_address or get_default_address( + party_type, party.name + ) if doctype: - party_details.update(get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])) + party_details.update( + get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) + ) # address display party_details.address_display = get_address_display(party_details[billing_address_field]) # shipping address if party_type in ["Customer", "Lead"]: - party_details.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) + party_details.shipping_address_name = shipping_address or get_party_shipping_address( + party_type, party.name + ) party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) if doctype: - party_details.update(get_fetch_values(doctype, 'shipping_address_name', party_details.shipping_address_name)) + party_details.update( + get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) + ) if company_address: - party_details.update({'company_address': company_address}) + party_details.update({"company_address": company_address}) else: party_details.update(get_company_address(company)) - if doctype and doctype in ['Delivery Note', 'Sales Invoice', 'Sales Order']: + if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order"]: if party_details.company_address: - party_details.update(get_fetch_values(doctype, 'company_address', party_details.company_address)) + party_details.update( + get_fetch_values(doctype, "company_address", party_details.company_address) + ) get_regional_address_details(party_details, doctype, company) elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: if party_details.company_address: party_details["shipping_address"] = shipping_address or party_details["company_address"] party_details.shipping_address_display = get_address_display(party_details["shipping_address"]) - party_details.update(get_fetch_values(doctype, 'shipping_address', party_details.shipping_address)) + party_details.update( + get_fetch_values(doctype, "shipping_address", party_details.shipping_address) + ) get_regional_address_details(party_details, doctype, company) return party_details.get(billing_address_field), party_details.shipping_address_name + @erpnext.allow_regional def get_regional_address_details(party_details, doctype, company): pass + def set_contact_details(party_details, party, party_type): party_details.contact_person = get_default_contact(party_type, party.name) if not party_details.contact_person: - party_details.update({ - "contact_person": None, - "contact_display": None, - "contact_email": None, - "contact_mobile": None, - "contact_phone": None, - "contact_designation": None, - "contact_department": None - }) + party_details.update( + { + "contact_person": None, + "contact_display": None, + "contact_email": None, + "contact_mobile": None, + "contact_phone": None, + "contact_designation": None, + "contact_department": None, + } + ) else: party_details.update(get_contact_details(party_details.contact_person)) + def set_other_values(party_details, party, party_type): # copy - if party_type=="Customer": + if party_type == "Customer": to_copy = ["customer_name", "customer_group", "territory", "language"] else: to_copy = ["supplier_name", "supplier_group", "language"] @@ -160,112 +265,121 @@ def set_other_values(party_details, party, party_type): party_details[f] = party.get(f) # fields prepended with default in Customer doctype - for f in ['currency'] \ - + (['sales_partner', 'commission_rate'] if party_type=="Customer" else []): + for f in ["currency"] + ( + ["sales_partner", "commission_rate"] if party_type == "Customer" else [] + ): if party.get("default_" + f): party_details[f] = party.get("default_" + f) + def get_default_price_list(party): """Return default price list for party (Document object)""" if party.get("default_price_list"): return party.default_price_list if party.doctype == "Customer": - price_list = frappe.get_cached_value("Customer Group", - party.customer_group, "default_price_list") - if price_list: - return price_list + return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list") - return None def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list - price_list = get_permitted_documents('Price List') + price_list = get_permitted_documents("Price List") # if there is only one permitted document based on user permissions, set it if price_list and len(price_list) == 1: price_list = price_list[0] - elif pos and party_type == 'Customer': - customer_price_list = frappe.get_value('Customer', party.name, 'default_price_list') + elif pos and party_type == "Customer": + customer_price_list = frappe.get_value("Customer", party.name, "default_price_list") if customer_price_list: price_list = customer_price_list else: - pos_price_list = frappe.get_value('POS Profile', pos, 'selling_price_list') + pos_price_list = frappe.get_value("POS Profile", pos, "selling_price_list") price_list = pos_price_list or given_price_list else: price_list = get_default_price_list(party) or given_price_list if price_list: - party_details.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) + party_details.price_list_currency = frappe.db.get_value( + "Price List", price_list, "currency", cache=True + ) - party_details["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list + party_details[ + "selling_price_list" if party.doctype == "Customer" else "buying_price_list" + ] = price_list -def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype): +def set_account_and_due_date( + party, account, party_type, company, posting_date, bill_date, doctype +): if doctype not in ["POS Invoice", "Sales Invoice", "Purchase Invoice"]: # not an invoice - return { - party_type.lower(): party - } + return {party_type.lower(): party} if party: account = get_party_account(party_type, party, company) - account_fieldname = "debit_to" if party_type=="Customer" else "credit_to" + account_fieldname = "debit_to" if party_type == "Customer" else "credit_to" out = { party_type.lower(): party, - account_fieldname : account, - "due_date": get_due_date(posting_date, party_type, party, company, bill_date) + account_fieldname: account, + "due_date": get_due_date(posting_date, party_type, party, company, bill_date), } return out + @frappe.whitelist() def get_party_account(party_type, party=None, company=None): """Returns the account for the given `party`. - Will first search in party (Customer / Supplier) record, if not found, - will search in group (Customer Group / Supplier Group), - finally will return default.""" + Will first search in party (Customer / Supplier) record, if not found, + will search in group (Customer Group / Supplier Group), + finally will return default.""" if not company: frappe.throw(_("Please select a Company")) - if not party and party_type in ['Customer', 'Supplier']: - default_account_name = "default_receivable_account" \ - if party_type=="Customer" else "default_payable_account" + if not party and party_type in ["Customer", "Supplier"]: + default_account_name = ( + "default_receivable_account" if party_type == "Customer" else "default_payable_account" + ) - return frappe.get_cached_value('Company', company, default_account_name) + return frappe.get_cached_value("Company", company, default_account_name) - account = frappe.db.get_value("Party Account", - {"parenttype": party_type, "parent": party, "company": company}, "account") + account = frappe.db.get_value( + "Party Account", {"parenttype": party_type, "parent": party, "company": company}, "account" + ) - if not account and party_type in ['Customer', 'Supplier']: - party_group_doctype = "Customer Group" if party_type=="Customer" else "Supplier Group" + if not account and party_type in ["Customer", "Supplier"]: + party_group_doctype = "Customer Group" if party_type == "Customer" else "Supplier Group" group = frappe.get_cached_value(party_type, party, scrub(party_group_doctype)) - account = frappe.db.get_value("Party Account", - {"parenttype": party_group_doctype, "parent": group, "company": company}, "account") + account = frappe.db.get_value( + "Party Account", + {"parenttype": party_group_doctype, "parent": group, "company": company}, + "account", + ) - if not account and party_type in ['Customer', 'Supplier']: - default_account_name = "default_receivable_account" \ - if party_type=="Customer" else "default_payable_account" - account = frappe.get_cached_value('Company', company, default_account_name) + if not account and party_type in ["Customer", "Supplier"]: + default_account_name = ( + "default_receivable_account" if party_type == "Customer" else "default_payable_account" + ) + account = frappe.get_cached_value("Company", company, default_account_name) existing_gle_currency = get_party_gle_currency(party_type, party, company) if existing_gle_currency: if account: account_currency = frappe.db.get_value("Account", account, "account_currency", cache=True) if (account and account_currency != existing_gle_currency) or not account: - account = get_party_gle_account(party_type, party, company) + account = get_party_gle_account(party_type, party, company) return account + @frappe.whitelist() def get_party_bank_account(party_type, party): - return frappe.db.get_value('Bank Account', { - 'party_type': party_type, - 'party': party, - 'is_default': 1 - }) + return frappe.db.get_value( + "Bank Account", {"party_type": party_type, "party": party, "is_default": 1} + ) + def get_party_account_currency(party_type, party, company): def generator(): @@ -274,27 +388,38 @@ def get_party_account_currency(party_type, party, company): return frappe.local_cache("party_account_currency", (party_type, party, company), generator) + def get_party_gle_currency(party_type, party, company): def generator(): - existing_gle_currency = frappe.db.sql("""select account_currency from `tabGL Entry` + existing_gle_currency = frappe.db.sql( + """select account_currency from `tabGL Entry` where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s - limit 1""", { "company": company, "party_type": party_type, "party": party }) + limit 1""", + {"company": company, "party_type": party_type, "party": party}, + ) return existing_gle_currency[0][0] if existing_gle_currency else None - return frappe.local_cache("party_gle_currency", (party_type, party, company), generator, - regenerate_if_none=True) + return frappe.local_cache( + "party_gle_currency", (party_type, party, company), generator, regenerate_if_none=True + ) + def get_party_gle_account(party_type, party, company): def generator(): - existing_gle_account = frappe.db.sql("""select account from `tabGL Entry` + existing_gle_account = frappe.db.sql( + """select account from `tabGL Entry` where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s - limit 1""", { "company": company, "party_type": party_type, "party": party }) + limit 1""", + {"company": company, "party_type": party_type, "party": party}, + ) return existing_gle_account[0][0] if existing_gle_account else None - return frappe.local_cache("party_gle_account", (party_type, party, company), generator, - regenerate_if_none=True) + return frappe.local_cache( + "party_gle_account", (party_type, party, company), generator, regenerate_if_none=True + ) + def validate_party_gle_currency(party_type, party, company, party_account_currency=None): """Validate party account currency with existing GL Entry's currency""" @@ -304,32 +429,55 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren existing_gle_currency = get_party_gle_currency(party_type, party, company) if existing_gle_currency and party_account_currency != existing_gle_currency: - frappe.throw(_("{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.") - .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) + frappe.throw( + _( + "{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}." + ).format( + frappe.bold(party_type), + frappe.bold(party), + frappe.bold(existing_gle_currency), + frappe.bold(company), + ), + InvalidAccountCurrency, + ) + def validate_party_accounts(doc): from erpnext.controllers.accounts_controller import validate_account_head + companies = [] for account in doc.get("accounts"): if account.company in companies: - frappe.throw(_("There can only be 1 Account per Company in {0} {1}") - .format(doc.doctype, doc.name), DuplicatePartyAccountError) + frappe.throw( + _("There can only be 1 Account per Company in {0} {1}").format(doc.doctype, doc.name), + DuplicatePartyAccountError, + ) else: companies.append(account.company) - party_account_currency = frappe.db.get_value("Account", account.account, "account_currency", cache=True) + party_account_currency = frappe.db.get_value( + "Account", account.account, "account_currency", cache=True + ) if frappe.db.get_default("Company"): - company_default_currency = frappe.get_cached_value('Company', - frappe.db.get_default("Company"), "default_currency") + company_default_currency = frappe.get_cached_value( + "Company", frappe.db.get_default("Company"), "default_currency" + ) else: - company_default_currency = frappe.db.get_value('Company', account.company, "default_currency") + company_default_currency = frappe.db.get_value("Company", account.company, "default_currency") validate_party_gle_currency(doc.doctype, doc.name, account.company, party_account_currency) if doc.get("default_currency") and party_account_currency and company_default_currency: - if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: - frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency")) + if ( + doc.default_currency != party_account_currency + and doc.default_currency != company_default_currency + ): + frappe.throw( + _( + "Billing currency must be equal to either default company's currency or party account currency" + ) + ) # validate if account is mapped for same company validate_account_head(account.idx, account.account, account.company) @@ -344,18 +492,23 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None): template_name = get_payment_terms_template(party, party_type, company) if template_name: - due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") + due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( + "%Y-%m-%d" + ) else: if party_type == "Supplier": supplier_group = frappe.get_cached_value(party_type, party, "supplier_group") template_name = frappe.get_cached_value("Supplier Group", supplier_group, "payment_terms") if template_name: - due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") + due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( + "%Y-%m-%d" + ) # If due date is calculated from bill_date, check this condition if getdate(due_date) < getdate(posting_date): due_date = posting_date return due_date + def get_due_date_from_template(template_name, posting_date, bill_date): """ Inspects all `Payment Term`s from the a `Payment Terms Template` and returns the due @@ -365,40 +518,55 @@ def get_due_date_from_template(template_name, posting_date, bill_date): """ due_date = getdate(bill_date or posting_date) - template = frappe.get_doc('Payment Terms Template', template_name) + template = frappe.get_doc("Payment Terms Template", template_name) for term in template.terms: - if term.due_date_based_on == 'Day(s) after invoice date': + if term.due_date_based_on == "Day(s) after invoice date": due_date = max(due_date, add_days(due_date, term.credit_days)) - elif term.due_date_based_on == 'Day(s) after the end of the invoice month': + elif term.due_date_based_on == "Day(s) after the end of the invoice month": due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days)) else: due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months)) return due_date -def validate_due_date(posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None): + +def validate_due_date( + posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None +): if getdate(due_date) < getdate(posting_date): frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date")) else: - if not template_name: return + if not template_name: + return - default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") + default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( + "%Y-%m-%d" + ) if not default_due_date: return if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date): - is_credit_controller = frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles() + is_credit_controller = ( + frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles() + ) if is_credit_controller: - msgprint(_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)") - .format(date_diff(due_date, default_due_date))) + msgprint( + _("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format( + date_diff(due_date, default_due_date) + ) + ) else: - frappe.throw(_("Due / Reference Date cannot be after {0}") - .format(formatdate(default_due_date))) + frappe.throw( + _("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date)) + ) + @frappe.whitelist() def get_address_tax_category(tax_category=None, billing_address=None, shipping_address=None): - addr_tax_category_from = frappe.db.get_single_value("Accounts Settings", "determine_address_tax_category_from") + addr_tax_category_from = frappe.db.get_single_value( + "Accounts Settings", "determine_address_tax_category_from" + ) if addr_tax_category_from == "Shipping Address": if shipping_address: tax_category = frappe.db.get_value("Address", shipping_address, "tax_category") or tax_category @@ -408,36 +576,48 @@ def get_address_tax_category(tax_category=None, billing_address=None, shipping_a return cstr(tax_category) + @frappe.whitelist() -def set_taxes(party, party_type, posting_date, company, customer_group=None, supplier_group=None, tax_category=None, - billing_address=None, shipping_address=None, use_for_shopping_cart=None): +def set_taxes( + party, + party_type, + posting_date, + company, + customer_group=None, + supplier_group=None, + tax_category=None, + billing_address=None, + shipping_address=None, + use_for_shopping_cart=None, +): from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template - args = { - party_type.lower(): party, - "company": company - } + + args = {party_type.lower(): party, "company": company} if tax_category: - args['tax_category'] = tax_category + args["tax_category"] = tax_category if customer_group: - args['customer_group'] = customer_group + args["customer_group"] = customer_group if supplier_group: - args['supplier_group'] = supplier_group + args["supplier_group"] = supplier_group if billing_address or shipping_address: - args.update(get_party_details(party, party_type, {"billing_address": billing_address, \ - "shipping_address": shipping_address })) + args.update( + get_party_details( + party, party_type, {"billing_address": billing_address, "shipping_address": shipping_address} + ) + ) else: args.update(get_party_details(party, party_type)) if party_type in ("Customer", "Lead"): args.update({"tax_type": "Sales"}) - if party_type=='Lead': - args['customer'] = None - del args['lead'] + if party_type == "Lead": + args["customer"] = None + del args["lead"] else: args.update({"tax_type": "Purchase"}) @@ -452,25 +632,27 @@ def get_payment_terms_template(party_name, party_type, company=None): if party_type not in ("Customer", "Supplier"): return template = None - if party_type == 'Customer': - customer = frappe.get_cached_value("Customer", party_name, - fieldname=['payment_terms', "customer_group"], as_dict=1) + if party_type == "Customer": + customer = frappe.get_cached_value( + "Customer", party_name, fieldname=["payment_terms", "customer_group"], as_dict=1 + ) template = customer.payment_terms if not template and customer.customer_group: - template = frappe.get_cached_value("Customer Group", - customer.customer_group, 'payment_terms') + template = frappe.get_cached_value("Customer Group", customer.customer_group, "payment_terms") else: - supplier = frappe.get_cached_value("Supplier", party_name, - fieldname=['payment_terms', "supplier_group"], as_dict=1) + supplier = frappe.get_cached_value( + "Supplier", party_name, fieldname=["payment_terms", "supplier_group"], as_dict=1 + ) template = supplier.payment_terms if not template and supplier.supplier_group: - template = frappe.get_cached_value("Supplier Group", supplier.supplier_group, 'payment_terms') + template = frappe.get_cached_value("Supplier Group", supplier.supplier_group, "payment_terms") if not template and company: - template = frappe.get_cached_value('Company', company, fieldname='payment_terms') + template = frappe.get_cached_value("Company", company, fieldname="payment_terms") return template + def validate_party_frozen_disabled(party_type, party_name): if frappe.flags.ignore_party_validation: @@ -482,7 +664,9 @@ def validate_party_frozen_disabled(party_type, party_name): if party.disabled: frappe.throw(_("{0} {1} is disabled").format(party_type, party_name), PartyDisabled) elif party.get("is_frozen"): - frozen_accounts_modifier = frappe.db.get_single_value( 'Accounts Settings', 'frozen_accounts_modifier') + frozen_accounts_modifier = frappe.db.get_single_value( + "Accounts Settings", "frozen_accounts_modifier" + ) if not frozen_accounts_modifier in frappe.get_roles(): frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen) @@ -490,99 +674,124 @@ def validate_party_frozen_disabled(party_type, party_name): if frappe.db.get_value("Employee", party_name, "status") != "Active": frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True) + def get_timeline_data(doctype, name): - '''returns timeline data for the past one year''' + """returns timeline data for the past one year""" from frappe.desk.form.load import get_communication_data out = {} - fields = 'creation, count(*)' - after = add_years(None, -1).strftime('%Y-%m-%d') - group_by='group by Date(creation)' + fields = "creation, count(*)" + after = add_years(None, -1).strftime("%Y-%m-%d") + group_by = "group by Date(creation)" - data = get_communication_data(doctype, name, after=after, group_by='group by creation', - fields='C.creation as creation, count(C.name)',as_dict=False) + data = get_communication_data( + doctype, + name, + after=after, + group_by="group by creation", + fields="C.creation as creation, count(C.name)", + as_dict=False, + ) # fetch and append data from Activity Log - data += frappe.db.sql("""select {fields} + data += frappe.db.sql( + """select {fields} from `tabActivity Log` where (reference_doctype=%(doctype)s and reference_name=%(name)s) or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s) or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s) and status!='Success' and creation > {after} {group_by} order by creation desc - """.format(fields=fields, group_by=group_by, after=after), { - "doctype": doctype, - "name": name - }, as_dict=False) + """.format( + fields=fields, group_by=group_by, after=after + ), + {"doctype": doctype, "name": name}, + as_dict=False, + ) timeline_items = dict(data) for date, count in iteritems(timeline_items): timestamp = get_timestamp(date) - out.update({ timestamp: count }) + out.update({timestamp: count}) return out + def get_dashboard_info(party_type, party, loyalty_program=None): current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True) - doctype = "Sales Invoice" if party_type=="Customer" else "Purchase Invoice" + doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" - companies = frappe.get_all(doctype, filters={ - 'docstatus': 1, - party_type.lower(): party - }, distinct=1, fields=['company']) + companies = frappe.get_all( + doctype, filters={"docstatus": 1, party_type.lower(): party}, distinct=1, fields=["company"] + ) company_wise_info = [] - company_wise_grand_total = frappe.get_all(doctype, + company_wise_grand_total = frappe.get_all( + doctype, filters={ - 'docstatus': 1, + "docstatus": 1, party_type.lower(): party, - 'posting_date': ('between', [current_fiscal_year.year_start_date, current_fiscal_year.year_end_date]) - }, - group_by="company", - fields=["company", "sum(grand_total) as grand_total", "sum(base_grand_total) as base_grand_total"] - ) + "posting_date": ( + "between", + [current_fiscal_year.year_start_date, current_fiscal_year.year_end_date], + ), + }, + group_by="company", + fields=[ + "company", + "sum(grand_total) as grand_total", + "sum(base_grand_total) as base_grand_total", + ], + ) loyalty_point_details = [] if party_type == "Customer": - loyalty_point_details = frappe._dict(frappe.get_all("Loyalty Point Entry", - filters={ - 'customer': party, - 'expiry_date': ('>=', getdate()), + loyalty_point_details = frappe._dict( + frappe.get_all( + "Loyalty Point Entry", + filters={ + "customer": party, + "expiry_date": (">=", getdate()), }, group_by="company", fields=["company", "sum(loyalty_points) as loyalty_points"], - as_list =1 - )) + as_list=1, + ) + ) company_wise_billing_this_year = frappe._dict() for d in company_wise_grand_total: company_wise_billing_this_year.setdefault( - d.company,{ - "grand_total": d.grand_total, - "base_grand_total": d.base_grand_total - }) + d.company, {"grand_total": d.grand_total, "base_grand_total": d.base_grand_total} + ) - - company_wise_total_unpaid = frappe._dict(frappe.db.sql(""" + company_wise_total_unpaid = frappe._dict( + frappe.db.sql( + """ select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where party_type = %s and party=%s and is_cancelled = 0 - group by company""", (party_type, party))) + group by company""", + (party_type, party), + ) + ) for d in companies: - company_default_currency = frappe.db.get_value("Company", d.company, 'default_currency') + company_default_currency = frappe.db.get_value("Company", d.company, "default_currency") party_account_currency = get_party_account_currency(party_type, party, d.company) - if party_account_currency==company_default_currency: - billing_this_year = flt(company_wise_billing_this_year.get(d.company,{}).get("base_grand_total")) + if party_account_currency == company_default_currency: + billing_this_year = flt( + company_wise_billing_this_year.get(d.company, {}).get("base_grand_total") + ) else: - billing_this_year = flt(company_wise_billing_this_year.get(d.company,{}).get("grand_total")) + billing_this_year = flt(company_wise_billing_this_year.get(d.company, {}).get("grand_total")) total_unpaid = flt(company_wise_total_unpaid.get(d.company)) @@ -605,6 +814,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): return company_wise_info + def get_party_shipping_address(doctype, name): """ Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true. @@ -617,50 +827,59 @@ def get_party_shipping_address(doctype, name): :return: String """ out = frappe.db.sql( - 'SELECT dl.parent ' - 'from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name ' - 'where ' - 'dl.link_doctype=%s ' - 'and dl.link_name=%s ' + "SELECT dl.parent " + "from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name " + "where " + "dl.link_doctype=%s " + "and dl.link_name=%s " 'and dl.parenttype="Address" ' - 'and ifnull(ta.disabled, 0) = 0 and' + "and ifnull(ta.disabled, 0) = 0 and" '(ta.address_type="Shipping" or ta.is_shipping_address=1) ' - 'order by ta.is_shipping_address desc, ta.address_type desc limit 1', - (doctype, name) + "order by ta.is_shipping_address desc, ta.address_type desc limit 1", + (doctype, name), ) if out: return out[0][0] else: - return '' + return "" -def get_partywise_advanced_payment_amount(party_type, posting_date = None, future_payment=0, company=None): + +def get_partywise_advanced_payment_amount( + party_type, posting_date=None, future_payment=0, company=None +): cond = "1=1" if posting_date: if future_payment: - cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' """.format(posting_date) + cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' " "".format(posting_date) else: cond = "posting_date <= '{0}'".format(posting_date) if company: cond += "and company = {0}".format(frappe.db.escape(company)) - data = frappe.db.sql(""" SELECT party, sum({0}) as amount + data = frappe.db.sql( + """ SELECT party, sum({0}) as amount FROM `tabGL Entry` WHERE party_type = %s and against_voucher is null and is_cancelled = 0 - and {1} GROUP BY party""" - .format(("credit") if party_type == "Customer" else "debit", cond) , party_type) + and {1} GROUP BY party""".format( + ("credit") if party_type == "Customer" else "debit", cond + ), + party_type, + ) if data: return frappe._dict(data) + def get_default_contact(doctype, name): """ - Returns default contact for the given doctype and name. - Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact. + Returns default contact for the given doctype and name. + Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact. """ - out = frappe.db.sql(""" + out = frappe.db.sql( + """ SELECT dl.parent, c.is_primary_contact, c.is_billing_contact FROM `tabDynamic Link` dl INNER JOIN tabContact c ON c.name = dl.parent @@ -669,7 +888,9 @@ def get_default_contact(doctype, name): dl.link_name=%s AND dl.parenttype = "Contact" ORDER BY is_primary_contact DESC, is_billing_contact DESC - """, (doctype, name)) + """, + (doctype, name), + ) if out: try: return out[0][0] diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index e6580493095..605ce8383e4 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -1,7 +1,8 @@ {%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} -{%- set einvoice = json.loads(doc.signed_einvoice) -%}
+ {% if doc.signed_einvoice %} + {%- set einvoice = json.loads(doc.signed_einvoice) -%}
{% if letter_head and not no_letterhead %}
{{ letter_head }}
@@ -170,4 +171,10 @@
+ {% else %} +
+ You must generate IRN before you can preview GST E-Invoice. +
+ {% endif %}
+ diff --git a/erpnext/accounts/report/account_balance/account_balance.py b/erpnext/accounts/report/account_balance/account_balance.py index a2c70a45f99..824a965cdcf 100644 --- a/erpnext/accounts/report/account_balance/account_balance.py +++ b/erpnext/accounts/report/account_balance/account_balance.py @@ -14,6 +14,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): columns = [ { @@ -21,7 +22,7 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "account", "options": "Account", - "width": 100 + "width": 100, }, { "label": _("Currency"), @@ -29,19 +30,20 @@ def get_columns(filters): "fieldname": "currency", "options": "Currency", "hidden": 1, - "width": 50 + "width": 50, }, { "label": _("Balance"), "fieldtype": "Currency", "fieldname": "balance", "options": "currency", - "width": 100 - } + "width": 100, + }, ] return columns + def get_conditions(filters): conditions = {} @@ -57,12 +59,14 @@ def get_conditions(filters): return conditions + def get_data(filters): data = [] conditions = get_conditions(filters) - accounts = frappe.db.get_all("Account", fields=["name", "account_currency"], - filters=conditions, order_by='name') + accounts = frappe.db.get_all( + "Account", fields=["name", "account_currency"], filters=conditions, order_by="name" + ) for d in accounts: balance = get_balance_on(d.name, date=filters.report_date) diff --git a/erpnext/accounts/report/account_balance/test_account_balance.py b/erpnext/accounts/report/account_balance/test_account_balance.py index 50b1a679b6c..13fa05d4743 100644 --- a/erpnext/accounts/report/account_balance/test_account_balance.py +++ b/erpnext/accounts/report/account_balance/test_account_balance.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -14,9 +13,9 @@ class TestAccountBalance(unittest.TestCase): frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") filters = { - 'company': '_Test Company 2', - 'report_date': getdate(), - 'root_type': 'Income', + "company": "_Test Company 2", + "report_date": getdate(), + "root_type": "Income", } make_sales_invoice() @@ -25,42 +24,45 @@ class TestAccountBalance(unittest.TestCase): expected_data = [ { - "account": 'Direct Income - _TC2', - "currency": 'EUR', + "account": "Direct Income - _TC2", + "currency": "EUR", "balance": -100.0, }, { - "account": 'Income - _TC2', - "currency": 'EUR', + "account": "Income - _TC2", + "currency": "EUR", "balance": -100.0, }, { - "account": 'Indirect Income - _TC2', - "currency": 'EUR', + "account": "Indirect Income - _TC2", + "currency": "EUR", "balance": 0.0, }, { - "account": 'Sales - _TC2', - "currency": 'EUR', + "account": "Sales - _TC2", + "currency": "EUR", "balance": -100.0, }, { - "account": 'Service - _TC2', - "currency": 'EUR', + "account": "Service - _TC2", + "currency": "EUR", "balance": 0.0, - } + }, ] self.assertEqual(expected_data, report[1]) + def make_sales_invoice(): frappe.set_user("Administrator") - create_sales_invoice(company="_Test Company 2", - customer = '_Test Customer 2', - currency = 'EUR', - warehouse = 'Finished Goods - _TC2', - debit_to = 'Debtors - _TC2', - income_account = 'Sales - _TC2', - expense_account = 'Cost of Goods Sold - _TC2', - cost_center = 'Main - _TC2') + create_sales_invoice( + company="_Test Company 2", + customer="_Test Customer 2", + currency="EUR", + warehouse="Finished Goods - _TC2", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + ) 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 a990f23cd6b..c9567f23a34 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -29,6 +29,7 @@ from erpnext.accounts.utils import get_currency_precision # 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party # 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" + def execute(filters=None): args = { "party_type": "Customer", @@ -36,18 +37,23 @@ def execute(filters=None): } return ReceivablePayableReport(filters).run(args) + class ReceivablePayableReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.filters.report_date = getdate(self.filters.report_date or nowdate()) - self.age_as_on = getdate(nowdate()) \ - if self.filters.report_date > getdate(nowdate()) \ + self.age_as_on = ( + getdate(nowdate()) + if self.filters.report_date > getdate(nowdate()) else self.filters.report_date + ) def run(self, args): self.filters.update(args) self.set_defaults() - self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + self.party_naming_by = frappe.db.get_value( + args.get("naming_by")[0], None, args.get("naming_by")[1] + ) self.get_columns() self.get_data() self.get_chart_data() @@ -55,8 +61,10 @@ class ReceivablePayableReport(object): def set_defaults(self): if not self.filters.get("company"): - self.filters.company = frappe.db.get_single_value('Global Defaults', 'default_company') - self.company_currency = frappe.get_cached_value('Company', self.filters.get("company"), "default_currency") + self.filters.company = frappe.db.get_single_value("Global Defaults", "default_company") + self.company_currency = frappe.get_cached_value( + "Company", self.filters.get("company"), "default_currency" + ) self.currency_precision = get_currency_precision() or 2 self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" self.party_type = self.filters.party_type @@ -64,8 +72,8 @@ class ReceivablePayableReport(object): self.invoices = set() self.skip_total_row = 0 - if self.filters.get('group_by_party'): - self.previous_party='' + if self.filters.get("group_by_party"): + self.previous_party = "" self.total_row_map = {} self.skip_total_row = 1 @@ -73,7 +81,7 @@ class ReceivablePayableReport(object): self.get_gl_entries() self.get_sales_invoices_or_customers_based_on_sales_person() self.voucher_balance = OrderedDict() - self.init_voucher_balance() # invoiced, paid, credit_note, outstanding + self.init_voucher_balance() # invoiced, paid, credit_note, outstanding # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -100,64 +108,73 @@ class ReceivablePayableReport(object): key = (gle.voucher_type, gle.voucher_no, gle.party) if not key in self.voucher_balance: self.voucher_balance[key] = frappe._dict( - voucher_type = gle.voucher_type, - voucher_no = gle.voucher_no, - party = gle.party, - posting_date = gle.posting_date, - account_currency = gle.account_currency, - remarks = gle.remarks if self.filters.get("show_remarks") else None, - invoiced = 0.0, - paid = 0.0, - credit_note = 0.0, - outstanding = 0.0, - invoiced_in_account_currency = 0.0, - paid_in_account_currency = 0.0, - credit_note_in_account_currency = 0.0, - outstanding_in_account_currency = 0.0 + 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, + invoiced=0.0, + paid=0.0, + credit_note=0.0, + outstanding=0.0, + invoiced_in_account_currency=0.0, + paid_in_account_currency=0.0, + credit_note_in_account_currency=0.0, + outstanding_in_account_currency=0.0, ) self.get_invoices(gle) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.init_subtotal_row(gle.party) - if self.filters.get('group_by_party'): - self.init_subtotal_row('Total') + if self.filters.get("group_by_party"): + self.init_subtotal_row("Total") def get_invoices(self, gle): - if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): if self.filters.get("sales_person"): - if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \ - or gle.party in self.sales_person_records.get("Customer", []): - self.invoices.add(gle.voucher_no) + if gle.voucher_no in self.sales_person_records.get( + "Sales Invoice", [] + ) or gle.party in self.sales_person_records.get("Customer", []): + self.invoices.add(gle.voucher_no) else: self.invoices.add(gle.voucher_no) def init_subtotal_row(self, party): if not self.total_row_map.get(party): - self.total_row_map.setdefault(party, { - 'party': party, - 'bold': 1 - }) + self.total_row_map.setdefault(party, {"party": party, "bold": 1}) for field in self.get_currency_fields(): self.total_row_map[party][field] = 0.0 def get_currency_fields(self): - return ['invoiced', 'paid', 'credit_note', 'outstanding', 'range1', - 'range2', 'range3', 'range4', 'range5'] + return [ + "invoiced", + "paid", + "credit_note", + "outstanding", + "range1", + "range2", + "range3", + "range4", + "range5", + ] def update_voucher_balance(self, gle): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance row = self.get_voucher_balance(gle) - if not row: return + if not row: + return # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle) if gle_balance > 0: - if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: + if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher: # debit against sales / purchase invoice row.paid -= gle_balance row.paid_in_account_currency -= gle_balance_in_account_currency @@ -177,7 +194,7 @@ class ReceivablePayableReport(object): row.paid_in_account_currency -= gle_balance_in_account_currency if gle.cost_center: - row.cost_center = str(gle.cost_center) + row.cost_center = str(gle.cost_center) def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) @@ -191,14 +208,16 @@ class ReceivablePayableReport(object): if sub_total_row: self.data.append(sub_total_row) self.data.append({}) - self.update_sub_total_row(sub_total_row, 'Total') + self.update_sub_total_row(sub_total_row, "Total") def get_voucher_balance(self, gle): if self.filters.get("sales_person"): against_voucher = gle.against_voucher or gle.voucher_no - if not (gle.party in self.sales_person_records.get("Customer", []) or \ - against_voucher in self.sales_person_records.get("Sales Invoice", [])): - return + if not ( + gle.party in self.sales_person_records.get("Customer", []) + or against_voucher in self.sales_person_records.get("Sales Invoice", []) + ): + return voucher_balance = None if gle.against_voucher: @@ -208,13 +227,15 @@ class ReceivablePayableReport(object): # If payment is made against credit note # and credit note is made against a Sales Invoice # then consider the payment against original sales invoice. - if gle.against_voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.against_voucher_type in ("Sales Invoice", "Purchase Invoice"): if gle.against_voucher in self.return_entries: return_against = self.return_entries.get(gle.against_voucher) if return_against: against_voucher = return_against - voucher_balance = self.voucher_balance.get((gle.against_voucher_type, against_voucher, gle.party)) + voucher_balance = self.voucher_balance.get( + (gle.against_voucher_type, against_voucher, gle.party) + ) if not voucher_balance: # no invoice, this is an invoice / stand-alone payment / credit note @@ -227,13 +248,18 @@ class ReceivablePayableReport(object): # as we can use this to filter out invoices without outstanding for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) - row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \ - row.credit_note_in_account_currency, self.currency_precision) + row.outstanding_in_account_currency = flt( + row.invoiced_in_account_currency + - row.paid_in_account_currency + - row.credit_note_in_account_currency, + self.currency_precision, + ) row.invoice_grand_total = row.invoiced - if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \ - (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision): + if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( + abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision + ): # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -254,10 +280,10 @@ class ReceivablePayableReport(object): else: self.append_row(row) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.append_subtotal_row(self.previous_party) if self.data: - self.data.append(self.total_row_map.get('Total')) + self.data.append(self.total_row_map.get("Total")) def append_row(self, row): self.allocate_future_payments(row) @@ -265,7 +291,7 @@ class ReceivablePayableReport(object): self.set_party_details(row) self.set_ageing(row) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.update_sub_total_row(row, row.party) if self.previous_party and (self.previous_party != row.party): self.append_subtotal_row(self.previous_party) @@ -279,39 +305,49 @@ class ReceivablePayableReport(object): invoice_details.pop("due_date", None) row.update(invoice_details) - if row.voucher_type == 'Sales Invoice': + if row.voucher_type == "Sales Invoice": if self.filters.show_delivery_notes: self.set_delivery_notes(row) if self.filters.show_sales_person and row.sales_team: row.sales_person = ", ".join(row.sales_team) - del row['sales_team'] + del row["sales_team"] def set_delivery_notes(self, row): delivery_notes = self.delivery_notes.get(row.voucher_no, []) if delivery_notes: - row.delivery_notes = ', '.join(delivery_notes) + row.delivery_notes = ", ".join(delivery_notes) def build_delivery_note_map(self): if self.invoices and self.filters.show_delivery_notes: self.delivery_notes = frappe._dict() # delivery note link inside sales invoice - si_against_dn = frappe.db.sql(""" + si_against_dn = frappe.db.sql( + """ select parent, delivery_note from `tabSales Invoice Item` where docstatus=1 and parent in (%s) - """ % (','.join(['%s'] * len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (",".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in si_against_dn: if d.delivery_note: self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note) - dn_against_si = frappe.db.sql(""" + dn_against_si = frappe.db.sql( + """ select distinct parent, against_sales_invoice from `tabDelivery Note Item` where against_sales_invoice in (%s) - """ % (','.join(['%s'] * len(self.invoices))), tuple(self.invoices) , as_dict=1) + """ + % (",".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in dn_against_si: self.delivery_notes.setdefault(d.against_sales_invoice, set()).add(d.parent) @@ -319,39 +355,55 @@ class ReceivablePayableReport(object): def get_invoice_details(self): self.invoice_details = frappe._dict() if self.party_type == "Customer": - si_list = frappe.db.sql(""" + si_list = frappe.db.sql( + """ select name, due_date, po_no from `tabSales Invoice` where posting_date <= %s - """,self.filters.report_date, as_dict=1) + """, + self.filters.report_date, + as_dict=1, + ) for d in si_list: self.invoice_details.setdefault(d.name, d) # Get Sales Team if self.filters.show_sales_person: - sales_team = frappe.db.sql(""" + sales_team = frappe.db.sql( + """ select parent, sales_person from `tabSales Team` where parenttype = 'Sales Invoice' - """, as_dict=1) + """, + as_dict=1, + ) for d in sales_team: - self.invoice_details.setdefault(d.parent, {})\ - .setdefault('sales_team', []).append(d.sales_person) + self.invoice_details.setdefault(d.parent, {}).setdefault("sales_team", []).append( + d.sales_person + ) if self.party_type == "Supplier": - for pi in frappe.db.sql(""" + for pi in frappe.db.sql( + """ select name, due_date, bill_no, bill_date from `tabPurchase Invoice` where posting_date <= %s - """, self.filters.report_date, as_dict=1): + """, + self.filters.report_date, + as_dict=1, + ): self.invoice_details.setdefault(pi.name, pi) # Invoices booked via Journal Entries - journal_entries = frappe.db.sql(""" + journal_entries = frappe.db.sql( + """ select name, due_date, bill_no, bill_date from `tabJournal Entry` where posting_date <= %s - """, self.filters.report_date, as_dict=1) + """, + self.filters.report_date, + as_dict=1, + ) for je in journal_entries: if je.bill_no: @@ -372,17 +424,18 @@ class ReceivablePayableReport(object): # update "paid" and "oustanding" for this term if not term.paid: - self.allocate_closing_to_term(row, term, 'paid') + self.allocate_closing_to_term(row, term, "paid") # update "credit_note" and "oustanding" for this term if term.outstanding: - self.allocate_closing_to_term(row, term, 'credit_note') + self.allocate_closing_to_term(row, term, "credit_note") - row.payment_terms = sorted(row.payment_terms, key=lambda x: x['due_date']) + row.payment_terms = sorted(row.payment_terms, key=lambda x: x["due_date"]) def get_payment_terms(self, row): # build payment_terms for row - payment_terms_details = frappe.db.sql(""" + payment_terms_details = frappe.db.sql( + """ select si.name, si.party_account_currency, si.currency, si.conversion_rate, ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount @@ -391,8 +444,12 @@ class ReceivablePayableReport(object): si.name = ps.parent and si.name = %s order by ps.paid_amount desc, due_date - """.format(row.voucher_type), row.voucher_no, as_dict = 1) - + """.format( + row.voucher_type + ), + row.voucher_no, + as_dict=1, + ) original_row = frappe._dict(row) row.payment_terms = [] @@ -406,23 +463,29 @@ class ReceivablePayableReport(object): self.append_payment_term(row, d, term) def append_payment_term(self, row, d, term): - if (self.filters.get("customer") or self.filters.get("supplier")) and d.currency == d.party_account_currency: + if ( + self.filters.get("customer") or self.filters.get("supplier") + ) and d.currency == d.party_account_currency: invoiced = d.payment_amount else: invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) - row.payment_terms.append(term.update({ - "due_date": d.due_date, - "invoiced": invoiced, - "invoice_grand_total": row.invoiced, - "payment_term": d.description or d.payment_term, - "paid": d.paid_amount + d.discounted_amount, - "credit_note": 0.0, - "outstanding": invoiced - d.paid_amount - d.discounted_amount - })) + row.payment_terms.append( + term.update( + { + "due_date": d.due_date, + "invoiced": invoiced, + "invoice_grand_total": row.invoiced, + "payment_term": d.description or d.payment_term, + "paid": d.paid_amount + d.discounted_amount, + "credit_note": 0.0, + "outstanding": invoiced - d.paid_amount - d.discounted_amount, + } + ) + ) if d.paid_amount: - row['paid'] -= d.paid_amount + d.discounted_amount + row["paid"] -= d.paid_amount + d.discounted_amount def allocate_closing_to_term(self, row, term, key): if row[key]: @@ -437,7 +500,7 @@ class ReceivablePayableReport(object): def allocate_extra_payments_or_credits(self, row): # allocate extra payments / credits additional_row = None - for key in ('paid', 'credit_note'): + for key in ("paid", "credit_note"): if row[key] > 0: if not additional_row: additional_row = frappe._dict(row) @@ -445,7 +508,9 @@ class ReceivablePayableReport(object): additional_row[key] = row[key] if additional_row: - additional_row.outstanding = additional_row.invoiced - additional_row.paid - additional_row.credit_note + additional_row.outstanding = ( + additional_row.invoiced - additional_row.paid - additional_row.credit_note + ) self.append_row(additional_row) def get_future_payments(self): @@ -459,7 +524,8 @@ class ReceivablePayableReport(object): self.future_payments.setdefault((d.invoice_no, d.party), []).append(d) def get_future_payments_from_payment_entry(self): - return frappe.db.sql(""" + return frappe.db.sql( + """ select ref.reference_name as invoice_no, payment_entry.party, @@ -475,16 +541,23 @@ class ReceivablePayableReport(object): payment_entry.docstatus < 2 and payment_entry.posting_date > %s and payment_entry.party_type = %s - """, (self.filters.report_date, self.party_type), as_dict=1) + """, + (self.filters.report_date, self.party_type), + as_dict=1, + ) def get_future_payments_from_journal_entry(self): - if self.filters.get('party'): - amount_field = ("jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.party_type == 'Supplier' else "jea.credit_in_account_currency - jea.debit_in_account_currency") + if self.filters.get("party"): + amount_field = ( + "jea.debit_in_account_currency - jea.credit_in_account_currency" + if self.party_type == "Supplier" + else "jea.credit_in_account_currency - jea.debit_in_account_currency" + ) else: - amount_field = ("jea.debit - " if self.party_type == 'Supplier' else "jea.credit") + amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit" - return frappe.db.sql(""" + return frappe.db.sql( + """ select jea.reference_name as invoice_no, jea.party, @@ -503,7 +576,12 @@ class ReceivablePayableReport(object): and jea.reference_name is not null and jea.reference_name != '' group by je.name, jea.reference_name having future_amount > 0 - """.format(amount_field), (self.filters.report_date, self.party_type), as_dict=1) + """.format( + amount_field + ), + (self.filters.report_date, self.party_type), + as_dict=1, + ) def allocate_future_payments(self, row): # future payments are captured in additional columns @@ -525,22 +603,21 @@ class ReceivablePayableReport(object): future.future_amount = 0 row.remaining_balance = row.outstanding - row.future_amount - row.setdefault('future_ref', []).append(cstr(future.future_ref) + '/' + cstr(future.future_date)) + row.setdefault("future_ref", []).append( + cstr(future.future_ref) + "/" + cstr(future.future_date) + ) if row.future_ref: - row.future_ref = ', '.join(row.future_ref) + row.future_ref = ", ".join(row.future_ref) def get_return_entries(self): doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" - filters={ - 'is_return': 1, - 'docstatus': 1 - } + filters = {"is_return": 1, "docstatus": 1} party_field = scrub(self.filters.party_type) if self.filters.get(party_field): filters.update({party_field: self.filters.get(party_field)}) self.return_entries = frappe._dict( - frappe.get_all(doctype, filters, ['name', 'return_against'], as_list=1) + frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1) ) def set_ageing(self, row): @@ -571,16 +648,26 @@ class ReceivablePayableReport(object): row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0 index = None - if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4): - self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = 30, 60, 90, 120 + if not ( + self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4 + ): + self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = ( + 30, + 60, + 90, + 120, + ) - for i, days in enumerate([self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]): + for i, days in enumerate( + [self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4] + ): if cint(row.age) <= cint(days): index = i break - if index is None: index = 4 - row['range' + str(index+1)] = row.outstanding + if index is None: + index = 4 + row["range" + str(index + 1)] = row.outstanding def get_gl_entries(self): # get all the GL entries filtered by the given filters @@ -605,7 +692,8 @@ class ReceivablePayableReport(object): remarks = ", remarks" if self.filters.get("show_remarks") else "" - self.gl_entries = frappe.db.sql(""" + self.gl_entries = frappe.db.sql( + """ select name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} @@ -616,20 +704,27 @@ class ReceivablePayableReport(object): and is_cancelled = 0 and party_type=%s and (party is not null and party != '') - {2} {3} {4}""" - .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) + {2} {3} {4}""".format( + select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks + ), + values, + as_dict=True, + ) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"]) - records = frappe.db.sql(""" + records = frappe.db.sql( + """ select distinct parent, parenttype from `tabSales Team` steam where parenttype in ('Customer', 'Sales Invoice') and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) - """, (lft, rgt), as_dict=1) + """, + (lft, rgt), + as_dict=1, + ) self.sales_person_records = frappe._dict() for d in records: @@ -642,10 +737,10 @@ class ReceivablePayableReport(object): self.add_common_filters(conditions, values, party_type_field) - if party_type_field=="customer": + if party_type_field == "customer": self.add_customer_filters(conditions, values) - elif party_type_field=="supplier": + elif party_type_field == "supplier": self.add_supplier_filters(conditions, values) if self.filters.cost_center: @@ -656,13 +751,16 @@ class ReceivablePayableReport(object): def get_cost_center_conditions(self, conditions): lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) - cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})] + cost_center_list = [ + center.name + for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)}) + ] cost_center_string = '", "'.join(cost_center_list) conditions.append('cost_center in ("{0}")'.format(cost_center_string)) def get_order_by_condition(self): - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): return "order by party, posting_date" else: return "order by posting_date, party" @@ -680,21 +778,28 @@ 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 accounts: - conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) - values += accounts + 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 def add_customer_filters(self, conditions, values): if self.filters.get("customer_group"): - conditions.append(self.get_hierarchical_filters('Customer Group', 'customer_group')) + conditions.append(self.get_hierarchical_filters("Customer Group", "customer_group")) if self.filters.get("territory"): - conditions.append(self.get_hierarchical_filters('Territory', 'territory')) + conditions.append(self.get_hierarchical_filters("Territory", "territory")) if self.filters.get("payment_terms_template"): conditions.append("party in (select name from tabCustomer where payment_terms=%s)") @@ -706,8 +811,10 @@ class ReceivablePayableReport(object): def add_supplier_filters(self, conditions, values): if self.filters.get("supplier_group"): - conditions.append("""party in (select name from tabSupplier - where supplier_group=%s)""") + conditions.append( + """party in (select name from tabSupplier + where supplier_group=%s)""" + ) values.append(self.filters.get("supplier_group")) if self.filters.get("payment_terms_template"): @@ -720,7 +827,8 @@ class ReceivablePayableReport(object): return """party in (select name from tabCustomer where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt} and name=tabCustomer.{key}))""".format( - doctype=doctype, lft=lft, rgt=rgt, key=key) + doctype=doctype, lft=lft, rgt=rgt, key=key + ) def add_accounting_dimensions_filters(self, conditions, values): accounting_dimensions = get_accounting_dimensions(as_list=False) @@ -728,9 +836,10 @@ class ReceivablePayableReport(object): if accounting_dimensions: for dimension in accounting_dimensions: if self.filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - self.filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - self.filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + self.filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, self.filters.get(dimension.fieldname) + ) conditions.append("{0} in %s".format(dimension.fieldname)) values.append(tuple(self.filters.get(dimension.fieldname))) @@ -740,123 +849,176 @@ class ReceivablePayableReport(object): def get_gle_balance_in_account_currency(self, gle): # get the balance of the GL (debit - credit) or reverse balance based on report type - return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle) + return gle.get( + self.dr_or_cr + "_in_account_currency" + ) - self.get_reverse_balance_in_account_currency(gle) def get_reverse_balance_in_account_currency(self, gle): - return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency') + return gle.get( + "debit_in_account_currency" if self.dr_or_cr == "credit" else "credit_in_account_currency" + ) def get_reverse_balance(self, gle): # get "credit" balance if report type is "debit" and vice versa - return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') + return gle.get("debit" if self.dr_or_cr == "credit" else "credit") def is_invoice(self, gle): - if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): return True def get_party_details(self, party): if not party in self.party_details: - if self.party_type == 'Customer': - self.party_details[party] = frappe.db.get_value('Customer', party, ['customer_name', - 'territory', 'customer_group', 'customer_primary_contact'], as_dict=True) + if self.party_type == "Customer": + self.party_details[party] = frappe.db.get_value( + "Customer", + party, + ["customer_name", "territory", "customer_group", "customer_primary_contact"], + as_dict=True, + ) else: - self.party_details[party] = frappe.db.get_value('Supplier', party, ['supplier_name', - 'supplier_group'], as_dict=True) + self.party_details[party] = frappe.db.get_value( + "Supplier", party, ["supplier_name", "supplier_group"], as_dict=True + ) return self.party_details[party] - def get_columns(self): self.columns = [] - self.add_column('Posting Date', fieldtype='Date') - self.add_column(label=_(self.party_type), fieldname='party', - fieldtype='Link', options=self.party_type, width=180) + self.add_column("Posting Date", fieldtype="Date") + self.add_column( + label=_(self.party_type), + fieldname="party", + fieldtype="Link", + 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(_('{0} Name').format(self.party_type), - fieldname = scrub(self.party_type) + '_name', fieldtype='Data') + self.add_column( + _("{0} Name").format(self.party_type), + fieldname=scrub(self.party_type) + "_name", + fieldtype="Data", + ) - if self.party_type == 'Customer': - self.add_column(_("Customer Contact"), fieldname='customer_primary_contact', - fieldtype='Link', options='Contact') + if self.party_type == "Customer": + self.add_column( + _("Customer Contact"), + fieldname="customer_primary_contact", + fieldtype="Link", + options="Contact", + ) - self.add_column(label=_('Cost Center'), fieldname='cost_center', fieldtype='Data') - self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data') - self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link', - options='voucher_type', width=180) + self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data") + self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data") + self.add_column( + label=_("Voucher No"), + fieldname="voucher_no", + fieldtype="Dynamic Link", + options="voucher_type", + width=180, + ) if self.filters.show_remarks: - self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200), + self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200), - self.add_column(label='Due Date', fieldtype='Date') + self.add_column(label="Due Date", fieldtype="Date") if self.party_type == "Supplier": - self.add_column(label=_('Bill No'), fieldname='bill_no', fieldtype='Data') - self.add_column(label=_('Bill Date'), fieldname='bill_date', fieldtype='Date') + self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") + self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date") if self.filters.based_on_payment_terms: - self.add_column(label=_('Payment Term'), fieldname='payment_term', fieldtype='Data') - self.add_column(label=_('Invoice Grand Total'), fieldname='invoice_grand_total') + self.add_column(label=_("Payment Term"), fieldname="payment_term", fieldtype="Data") + self.add_column(label=_("Invoice Grand Total"), fieldname="invoice_grand_total") - self.add_column(_('Invoiced Amount'), fieldname='invoiced') - self.add_column(_('Paid Amount'), fieldname='paid') + self.add_column(_("Invoiced Amount"), fieldname="invoiced") + self.add_column(_("Paid Amount"), fieldname="paid") if self.party_type == "Customer": - self.add_column(_('Credit Note'), fieldname='credit_note') + self.add_column(_("Credit Note"), fieldname="credit_note") else: # note: fieldname is still `credit_note` - self.add_column(_('Debit Note'), fieldname='credit_note') - self.add_column(_('Outstanding Amount'), fieldname='outstanding') + self.add_column(_("Debit Note"), fieldname="credit_note") + self.add_column(_("Outstanding Amount"), fieldname="outstanding") self.setup_ageing_columns() - self.add_column(label=_('Currency'), fieldname='currency', fieldtype='Link', options='Currency', width=80) + self.add_column( + label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80 + ) if self.filters.show_future_payments: - self.add_column(label=_('Future Payment Ref'), fieldname='future_ref', fieldtype='Data') - self.add_column(label=_('Future Payment Amount'), fieldname='future_amount') - self.add_column(label=_('Remaining Balance'), fieldname='remaining_balance') + self.add_column(label=_("Future Payment Ref"), fieldname="future_ref", fieldtype="Data") + self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") + self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.filters.party_type == 'Customer': - self.add_column(label=_('Customer LPO'), fieldname='po_no', fieldtype='Data') + if self.filters.party_type == "Customer": + self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data") # comma separated list of linked delivery notes if self.filters.show_delivery_notes: - self.add_column(label=_('Delivery Notes'), fieldname='delivery_notes', fieldtype='Data') - self.add_column(label=_('Territory'), fieldname='territory', fieldtype='Link', - options='Territory') - self.add_column(label=_('Customer Group'), fieldname='customer_group', fieldtype='Link', - options='Customer Group') + self.add_column(label=_("Delivery Notes"), fieldname="delivery_notes", fieldtype="Data") + self.add_column( + label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" + ) + self.add_column( + label=_("Customer Group"), + fieldname="customer_group", + fieldtype="Link", + options="Customer Group", + ) if self.filters.show_sales_person: - self.add_column(label=_('Sales Person'), fieldname='sales_person', fieldtype='Data') + self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") if self.filters.party_type == "Supplier": - self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', - options='Supplier Group') + self.add_column( + label=_("Supplier Group"), + fieldname="supplier_group", + fieldtype="Link", + options="Supplier Group", + ) - def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120): - if not fieldname: fieldname = scrub(label) - if fieldtype=='Currency': options='currency' - if fieldtype=='Date': width = 90 + def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): + if not fieldname: + fieldname = scrub(label) + if fieldtype == "Currency": + options = "currency" + if fieldtype == "Date": + width = 90 - self.columns.append(dict( - label=label, - fieldname=fieldname, - fieldtype=fieldtype, - options=options, - width=width - )) + self.columns.append( + dict(label=label, fieldname=fieldname, fieldtype=fieldtype, options=options, width=width) + ) def setup_ageing_columns(self): # for charts self.ageing_column_labels = [] - self.add_column(label=_('Age (Days)'), fieldname='age', fieldtype='Int', width=80) + self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80) - for i, label in enumerate(["0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), - "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), - "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), - "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): - self.add_column(label=label, fieldname='range' + str(i+1)) - self.ageing_column_labels.append(label) + for i, label in enumerate( + [ + "0-{range1}".format(range1=self.filters["range1"]), + "{range1}-{range2}".format( + range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"] + ), + "{range2}-{range3}".format( + range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"] + ), + "{range3}-{range4}".format( + range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] + ), + "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")), + ] + ): + self.add_column(label=label, fieldname="range" + str(i + 1)) + self.ageing_column_labels.append(label) def get_chart_data(self): rows = [] @@ -865,14 +1027,9 @@ class ReceivablePayableReport(object): if not cint(row.bold): values = [row.range1, row.range2, row.range3, row.range4, row.range5] precision = cint(frappe.db.get_default("float_precision")) or 2 - rows.append({ - 'values': [flt(val, precision) for val in values] - }) + rows.append({"values": [flt(val, precision) for val in values]}) self.chart = { - "data": { - 'labels': self.ageing_column_labels, - 'datasets': rows - }, - "type": 'percentage' + "data": {"labels": self.ageing_column_labels, "datasets": rows}, + "type": "percentage", } diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index b5408bd4c8f..f38890e980c 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -15,13 +14,13 @@ class TestAccountsReceivable(unittest.TestCase): frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") filters = { - 'company': '_Test Company 2', - 'based_on_payment_terms': 1, - 'report_date': today(), - 'range1': 30, - 'range2': 60, - 'range3': 90, - 'range4': 120 + "company": "_Test Company 2", + "based_on_payment_terms": 1, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, } # check invoice grand total and invoiced column's value for 3 payment terms @@ -31,8 +30,8 @@ class TestAccountsReceivable(unittest.TestCase): expected_data = [[100, 30], [100, 50], [100, 20]] for i in range(3): - row = report[1][i-1] - self.assertEqual(expected_data[i-1], [row.invoice_grand_total, row.invoiced]) + row = report[1][i - 1] + self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) # check invoice grand total, invoiced, paid and outstanding column's value after payment make_payment(name) @@ -41,41 +40,65 @@ class TestAccountsReceivable(unittest.TestCase): expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]] for i in range(2): - row = report[1][i-1] - self.assertEqual(expected_data_after_payment[i-1], - [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding]) + row = report[1][i - 1] + self.assertEqual( + expected_data_after_payment[i - 1], + [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding], + ) # check invoice grand total, invoiced, paid and outstanding column's value after credit note 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]) + self.assertEqual( + expected_data_after_credit_note, + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.party_account, + ], + ) + def make_sales_invoice(): frappe.set_user("Administrator") - si = create_sales_invoice(company="_Test Company 2", - customer = '_Test Customer 2', - currency = 'EUR', - warehouse = 'Finished Goods - _TC2', - debit_to = 'Debtors - _TC2', - income_account = 'Sales - _TC2', - expense_account = 'Cost of Goods Sold - _TC2', - cost_center = 'Main - _TC2', - do_not_save=1) + si = create_sales_invoice( + company="_Test Company 2", + customer="_Test Customer 2", + currency="EUR", + warehouse="Finished Goods - _TC2", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + do_not_save=1, + ) - si.append('payment_schedule', dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30)) - si.append('payment_schedule', dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50)) - si.append('payment_schedule', dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20)) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), + ) si.submit() return si.name + def make_payment(docname): pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40) pe.paid_from = "Debtors - _TC2" @@ -84,14 +107,16 @@ def make_payment(docname): def make_credit_note(docname): - create_sales_invoice(company="_Test Company 2", - customer = '_Test Customer 2', - currency = 'EUR', - qty = -1, - warehouse = 'Finished Goods - _TC2', - debit_to = 'Debtors - _TC2', - income_account = 'Sales - _TC2', - expense_account = 'Cost of Goods Sold - _TC2', - cost_center = 'Main - _TC2', - is_return = 1, - return_against = docname) + create_sales_invoice( + company="_Test Company 2", + customer="_Test Customer 2", + currency="EUR", + qty=-1, + warehouse="Finished Goods - _TC2", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + is_return=1, + return_against=docname, + ) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 4559fa94a4a..85baa82e83f 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -19,10 +19,13 @@ def execute(filters=None): return AccountsReceivableSummary(filters).run(args) + class AccountsReceivableSummary(ReceivablePayableReport): def run(self, args): - self.party_type = args.get('party_type') - self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + self.party_type = args.get("party_type") + self.party_naming_by = frappe.db.get_value( + args.get("naming_by")[0], None, args.get("naming_by")[1] + ) self.get_columns() self.get_data(args) return self.columns, self.data @@ -34,8 +37,15 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.get_party_total(args) - party_advance_amount = get_partywise_advanced_payment_amount(self.party_type, - self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {} + party_advance_amount = ( + get_partywise_advanced_payment_amount( + self.party_type, + self.filters.report_date, + self.filters.show_future_payments, + self.filters.company, + ) + or {} + ) if self.filters.show_gl_balance: gl_balance_map = get_gl_balance(self.filters.report_date) @@ -48,7 +58,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): row.party = party if self.party_naming_by == "Naming Series": - row.party_name = frappe.get_cached_value(self.party_type, party, scrub(self.party_type) + "_name") + row.party_name = frappe.get_cached_value( + self.party_type, party, scrub(self.party_type) + "_name" + ) row.update(party_dict) @@ -81,24 +93,29 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.set_party_details(d) def init_party_total(self, row): - self.party_total.setdefault(row.party, frappe._dict({ - "invoiced": 0.0, - "paid": 0.0, - "credit_note": 0.0, - "outstanding": 0.0, - "range1": 0.0, - "range2": 0.0, - "range3": 0.0, - "range4": 0.0, - "range5": 0.0, - "total_due": 0.0, - "sales_person": [] - })) + self.party_total.setdefault( + row.party, + frappe._dict( + { + "invoiced": 0.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 0.0, + "range1": 0.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 0.0, + "sales_person": [], + } + ), + ) def set_party_details(self, row): self.party_total[row.party].currency = row.currency - for key in ('territory', 'customer_group', 'supplier_group'): + for key in ("territory", "customer_group", "supplier_group"): if row.get(key): self.party_total[row.party][key] = row.get(key) @@ -107,52 +124,84 @@ class AccountsReceivableSummary(ReceivablePayableReport): def get_columns(self): self.columns = [] - self.add_column(label=_(self.party_type), fieldname='party', - fieldtype='Link', options=self.party_type, width=180) + self.add_column( + label=_(self.party_type), + fieldname="party", + fieldtype="Link", + options=self.party_type, + width=180, + ) if self.party_naming_by == "Naming Series": - self.add_column(_('{0} Name').format(self.party_type), - fieldname = 'party_name', fieldtype='Data') + self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data") - credit_debit_label = "Credit Note" if self.party_type == 'Customer' else "Debit Note" + credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note" - self.add_column(_('Advance Amount'), fieldname='advance') - self.add_column(_('Invoiced Amount'), fieldname='invoiced') - self.add_column(_('Paid Amount'), fieldname='paid') - self.add_column(_(credit_debit_label), fieldname='credit_note') - self.add_column(_('Outstanding Amount'), fieldname='outstanding') + self.add_column(_("Advance Amount"), fieldname="advance") + self.add_column(_("Invoiced Amount"), fieldname="invoiced") + self.add_column(_("Paid Amount"), fieldname="paid") + self.add_column(_(credit_debit_label), fieldname="credit_note") + self.add_column(_("Outstanding Amount"), fieldname="outstanding") if self.filters.show_gl_balance: - self.add_column(_('GL Balance'), fieldname='gl_balance') - self.add_column(_('Difference'), fieldname='diff') + self.add_column(_("GL Balance"), fieldname="gl_balance") + self.add_column(_("Difference"), fieldname="diff") self.setup_ageing_columns() if self.party_type == "Customer": - self.add_column(label=_('Territory'), fieldname='territory', fieldtype='Link', - options='Territory') - self.add_column(label=_('Customer Group'), fieldname='customer_group', fieldtype='Link', - options='Customer Group') + self.add_column( + label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" + ) + self.add_column( + label=_("Customer Group"), + fieldname="customer_group", + fieldtype="Link", + options="Customer Group", + ) if self.filters.show_sales_person: - self.add_column(label=_('Sales Person'), fieldname='sales_person', fieldtype='Data') + self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") else: - self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', - options='Supplier Group') + self.add_column( + label=_("Supplier Group"), + fieldname="supplier_group", + fieldtype="Link", + options="Supplier Group", + ) - self.add_column(label=_('Currency'), fieldname='currency', fieldtype='Link', - options='Currency', width=80) + self.add_column( + label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80 + ) def setup_ageing_columns(self): - for i, label in enumerate(["0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), - "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), - "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), - "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): - self.add_column(label=label, fieldname='range' + str(i+1)) + for i, label in enumerate( + [ + "0-{range1}".format(range1=self.filters["range1"]), + "{range1}-{range2}".format( + range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"] + ), + "{range2}-{range3}".format( + range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"] + ), + "{range3}-{range4}".format( + range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] + ), + "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")), + ] + ): + self.add_column(label=label, fieldname="range" + str(i + 1)) # Add column for total due amount - self.add_column(label="Total Amount Due", fieldname='total_due') + self.add_column(label="Total Amount Due", fieldname="total_due") + def get_gl_balance(report_date): - return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'], - filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1)) + return frappe._dict( + frappe.db.get_all( + "GL Entry", + fields=["party", "sum(debit - credit)"], + filters={"posting_date": ("<=", report_date), "is_cancelled": 0}, + group_by="party", + as_list=1, + ) + ) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index 98f5b74eaac..57d80492ae0 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -11,34 +11,44 @@ def execute(filters=None): columns, data = get_columns(), get_data(filters) return columns, data + def get_data(filters): data = [] - depreciation_accounts = frappe.db.sql_list(""" select name from tabAccount - where ifnull(account_type, '') = 'Depreciation' """) + depreciation_accounts = frappe.db.sql_list( + """ select name from tabAccount + where ifnull(account_type, '') = 'Depreciation' """ + ) - filters_data = [["company", "=", filters.get('company')], - ["posting_date", ">=", filters.get('from_date')], - ["posting_date", "<=", filters.get('to_date')], + filters_data = [ + ["company", "=", filters.get("company")], + ["posting_date", ">=", filters.get("from_date")], + ["posting_date", "<=", filters.get("to_date")], ["against_voucher_type", "=", "Asset"], - ["account", "in", depreciation_accounts]] + ["account", "in", depreciation_accounts], + ] if filters.get("asset"): filters_data.append(["against_voucher", "=", filters.get("asset")]) if filters.get("asset_category"): - assets = frappe.db.sql_list("""select name from tabAsset - where asset_category = %s and docstatus=1""", filters.get("asset_category")) + assets = frappe.db.sql_list( + """select name from tabAsset + where asset_category = %s and docstatus=1""", + filters.get("asset_category"), + ) filters_data.append(["against_voucher", "in", assets]) if filters.get("finance_book"): - filters_data.append(["finance_book", "in", ['', filters.get('finance_book')]]) + filters_data.append(["finance_book", "in", ["", filters.get("finance_book")]]) - gl_entries = frappe.get_all('GL Entry', - filters= filters_data, - fields = ["against_voucher", "debit_in_account_currency as debit", "voucher_no", "posting_date"], - order_by= "against_voucher, posting_date") + gl_entries = frappe.get_all( + "GL Entry", + filters=filters_data, + fields=["against_voucher", "debit_in_account_currency as debit", "voucher_no", "posting_date"], + order_by="against_voucher, posting_date", + ) if not gl_entries: return data @@ -55,29 +65,40 @@ def get_data(filters): asset_data.accumulated_depreciation_amount += d.debit row = frappe._dict(asset_data) - row.update({ - "depreciation_amount": d.debit, - "depreciation_date": d.posting_date, - "amount_after_depreciation": (flt(row.gross_purchase_amount) - - flt(row.accumulated_depreciation_amount)), - "depreciation_entry": d.voucher_no - }) + row.update( + { + "depreciation_amount": d.debit, + "depreciation_date": d.posting_date, + "amount_after_depreciation": ( + flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount) + ), + "depreciation_entry": d.voucher_no, + } + ) data.append(row) return data + def get_assets_details(assets): assets_details = {} - fields = ["name as asset", "gross_purchase_amount", - "asset_category", "status", "depreciation_method", "purchase_date"] + fields = [ + "name as asset", + "gross_purchase_amount", + "asset_category", + "status", + "depreciation_method", + "purchase_date", + ] - for d in frappe.get_all("Asset", fields = fields, filters = {'name': ('in', assets)}): + for d in frappe.get_all("Asset", fields=fields, filters={"name": ("in", assets)}): assets_details.setdefault(d.asset, d) return assets_details + def get_columns(): return [ { @@ -85,68 +106,58 @@ def get_columns(): "fieldname": "asset", "fieldtype": "Link", "options": "Asset", - "width": 120 + "width": 120, }, { "label": _("Depreciation Date"), "fieldname": "depreciation_date", "fieldtype": "Date", - "width": 120 + "width": 120, }, { "label": _("Purchase Amount"), "fieldname": "gross_purchase_amount", "fieldtype": "Currency", - "width": 120 + "width": 120, }, { "label": _("Depreciation Amount"), "fieldname": "depreciation_amount", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Accumulated Depreciation Amount"), "fieldname": "accumulated_depreciation_amount", "fieldtype": "Currency", - "width": 210 + "width": 210, }, { "label": _("Amount After Depreciation"), "fieldname": "amount_after_depreciation", "fieldtype": "Currency", - "width": 180 + "width": 180, }, { "label": _("Depreciation Entry"), "fieldname": "depreciation_entry", "fieldtype": "Link", "options": "Journal Entry", - "width": 140 + "width": 140, }, { "label": _("Asset Category"), "fieldname": "asset_category", "fieldtype": "Link", "options": "Asset Category", - "width": 120 - }, - { - "label": _("Current Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"label": _("Current Status"), "fieldname": "status", "fieldtype": "Data", "width": 120}, { "label": _("Depreciation Method"), "fieldname": "depreciation_method", "fieldtype": "Data", - "width": 130 + "width": 130, }, - { - "label": _("Purchase Date"), - "fieldname": "purchase_date", - "fieldtype": "Date", - "width": 120 - } + {"label": _("Purchase Date"), "fieldname": "purchase_date", "fieldtype": "Date", "width": 120}, ] diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 0f9435f4a57..ad9b1ba58eb 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -24,18 +24,33 @@ def get_data(filters): # row.asset_category = asset_category row.update(asset_category) - row.cost_as_on_to_date = (flt(row.cost_as_on_from_date) + flt(row.cost_of_new_purchase) - - flt(row.cost_of_sold_asset) - flt(row.cost_of_scrapped_asset)) + row.cost_as_on_to_date = ( + flt(row.cost_as_on_from_date) + + flt(row.cost_of_new_purchase) + - flt(row.cost_of_sold_asset) + - flt(row.cost_of_scrapped_asset) + ) - row.update(next(asset for asset in assets if asset["asset_category"] == asset_category.get("asset_category", ""))) - row.accumulated_depreciation_as_on_to_date = (flt(row.accumulated_depreciation_as_on_from_date) + - flt(row.depreciation_amount_during_the_period) - flt(row.depreciation_eliminated_during_the_period)) + row.update( + next( + asset + for asset in assets + if asset["asset_category"] == asset_category.get("asset_category", "") + ) + ) + row.accumulated_depreciation_as_on_to_date = ( + flt(row.accumulated_depreciation_as_on_from_date) + + flt(row.depreciation_amount_during_the_period) + - flt(row.depreciation_eliminated_during_the_period) + ) - row.net_asset_value_as_on_from_date = (flt(row.cost_as_on_from_date) - - flt(row.accumulated_depreciation_as_on_from_date)) + row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt( + row.accumulated_depreciation_as_on_from_date + ) - row.net_asset_value_as_on_to_date = (flt(row.cost_as_on_to_date) - - flt(row.accumulated_depreciation_as_on_to_date)) + row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt( + row.accumulated_depreciation_as_on_to_date + ) data.append(row) @@ -43,7 +58,8 @@ def get_data(filters): def get_asset_categories(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT asset_category, ifnull(sum(case when purchase_date < %(from_date)s then case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then @@ -84,10 +100,15 @@ def get_asset_categories(filters): from `tabAsset` where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s group by asset_category - """, {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1) + """, + {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + as_dict=1, + ) + def get_assets(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT results.asset_category, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, @@ -130,7 +151,10 @@ def get_assets(filters): where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s group by a.asset_category) as results group by results.asset_category - """, {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1) + """, + {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + as_dict=1, + ) def get_columns(filters): @@ -140,72 +164,72 @@ def get_columns(filters): "fieldname": "asset_category", "fieldtype": "Link", "options": "Asset Category", - "width": 120 + "width": 120, }, { "label": _("Cost as on") + " " + formatdate(filters.day_before_from_date), "fieldname": "cost_as_on_from_date", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Cost of New Purchase"), "fieldname": "cost_of_new_purchase", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Cost of Sold Asset"), "fieldname": "cost_of_sold_asset", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Cost of Scrapped Asset"), "fieldname": "cost_of_scrapped_asset", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Cost as on") + " " + formatdate(filters.to_date), "fieldname": "cost_as_on_to_date", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Accumulated Depreciation as on") + " " + formatdate(filters.day_before_from_date), "fieldname": "accumulated_depreciation_as_on_from_date", "fieldtype": "Currency", - "width": 270 + "width": 270, }, { "label": _("Depreciation Amount during the period"), "fieldname": "depreciation_amount_during_the_period", "fieldtype": "Currency", - "width": 240 + "width": 240, }, { "label": _("Depreciation Eliminated due to disposal of assets"), "fieldname": "depreciation_eliminated_during_the_period", "fieldtype": "Currency", - "width": 300 + "width": 300, }, { "label": _("Accumulated Depreciation as on") + " " + formatdate(filters.to_date), "fieldname": "accumulated_depreciation_as_on_to_date", "fieldtype": "Currency", - "width": 270 + "width": 270, }, { "label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date), "fieldname": "net_asset_value_as_on_from_date", "fieldtype": "Currency", - "width": 200 + "width": 200, }, { "label": _("Net Asset value as on") + " " + formatdate(filters.to_date), "fieldname": "net_asset_value_as_on_to_date", "fieldtype": "Currency", - "width": 200 - } + "width": 200, + }, ] diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index f10a5eab102..7b1e9793266 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -15,26 +15,53 @@ from erpnext.accounts.report.financial_statements import ( def execute(filters=None): - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.period_start_date, filters.period_end_date, filters.filter_based_on, - filters.periodicity, company=filters.company) + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) - currency = filters.presentation_currency or frappe.get_cached_value('Company', filters.company, "default_currency") + currency = filters.presentation_currency or frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) - asset = get_data(filters.company, "Asset", "Debit", period_list, - only_current_fiscal_year=False, filters=filters, - accumulated_values=filters.accumulated_values) + asset = get_data( + filters.company, + "Asset", + "Debit", + period_list, + only_current_fiscal_year=False, + filters=filters, + accumulated_values=filters.accumulated_values, + ) - liability = get_data(filters.company, "Liability", "Credit", period_list, - only_current_fiscal_year=False, filters=filters, - accumulated_values=filters.accumulated_values) + liability = get_data( + filters.company, + "Liability", + "Credit", + period_list, + only_current_fiscal_year=False, + filters=filters, + accumulated_values=filters.accumulated_values, + ) - equity = get_data(filters.company, "Equity", "Credit", period_list, - only_current_fiscal_year=False, filters=filters, - accumulated_values=filters.accumulated_values) + equity = get_data( + filters.company, + "Equity", + "Credit", + period_list, + only_current_fiscal_year=False, + filters=filters, + accumulated_values=filters.accumulated_values, + ) - provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity, - period_list, filters.company, currency) + provisional_profit_loss, total_credit = get_provisional_profit_loss( + asset, liability, equity, period_list, filters.company, currency + ) message, opening_balance = check_opening_balance(asset, liability, equity) @@ -42,19 +69,19 @@ def execute(filters=None): data.extend(asset or []) data.extend(liability or []) data.extend(equity or []) - if opening_balance and round(opening_balance,2) !=0: - unclosed ={ + if opening_balance and round(opening_balance, 2) != 0: + unclosed = { "account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "warn_if_negative": True, - "currency": currency + "currency": currency, } for period in period_list: unclosed[period.key] = opening_balance if provisional_profit_loss: provisional_profit_loss[period.key] = provisional_profit_loss[period.key] - opening_balance - unclosed["total"]=opening_balance + unclosed["total"] = opening_balance data.append(unclosed) if provisional_profit_loss: @@ -62,26 +89,32 @@ def execute(filters=None): if total_credit: data.append(total_credit) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, company=filters.company) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, company=filters.company + ) chart = get_chart_data(filters, columns, asset, liability, equity) - report_summary = get_report_summary(period_list, asset, liability, equity, provisional_profit_loss, - total_credit, currency, filters) + report_summary = get_report_summary( + period_list, asset, liability, equity, provisional_profit_loss, total_credit, currency, filters + ) return columns, data, message, chart, report_summary -def get_provisional_profit_loss(asset, liability, equity, period_list, company, currency=None, consolidated=False): + +def get_provisional_profit_loss( + asset, liability, equity, period_list, company, currency=None, consolidated=False +): provisional_profit_loss = {} total_row = {} if asset and (liability or equity): - total = total_row_total=0 - currency = currency or frappe.get_cached_value('Company', company, "default_currency") + total = total_row_total = 0 + currency = currency or frappe.get_cached_value("Company", company, "default_currency") total_row = { "account_name": "'" + _("Total (Credit)") + "'", "account": "'" + _("Total (Credit)") + "'", "warn_if_negative": True, - "currency": currency + "currency": currency, } has_value = False @@ -106,15 +139,18 @@ def get_provisional_profit_loss(asset, liability, equity, period_list, company, total_row["total"] = total_row_total if has_value: - provisional_profit_loss.update({ - "account_name": "'" + _("Provisional Profit / Loss (Credit)") + "'", - "account": "'" + _("Provisional Profit / Loss (Credit)") + "'", - "warn_if_negative": True, - "currency": currency - }) + provisional_profit_loss.update( + { + "account_name": "'" + _("Provisional Profit / Loss (Credit)") + "'", + "account": "'" + _("Provisional Profit / Loss (Credit)") + "'", + "warn_if_negative": True, + "currency": currency, + } + ) return provisional_profit_loss, total_row + def check_opening_balance(asset, liability, equity): # Check if previous year balance sheet closed opening_balance = 0 @@ -128,19 +164,29 @@ def check_opening_balance(asset, liability, equity): opening_balance = flt(opening_balance, float_precision) if opening_balance: - return _("Previous Financial Year is not closed"),opening_balance - return None,None + return _("Previous Financial Year is not closed"), opening_balance + return None, None -def get_report_summary(period_list, asset, liability, equity, provisional_profit_loss, total_credit, currency, - filters, consolidated=False): + +def get_report_summary( + period_list, + asset, + liability, + equity, + provisional_profit_loss, + total_credit, + currency, + filters, + consolidated=False, +): net_asset, net_liability, net_equity, net_provisional_profit_loss = 0.0, 0.0, 0.0, 0.0 - if filters.get('accumulated_values'): + if filters.get("accumulated_values"): period_list = [period_list[-1]] # from consolidated financial statement - if filters.get('accumulated_in_group_company'): + if filters.get("accumulated_in_group_company"): period_list = get_filtered_list_for_consolidated_report(filters, period_list) for period in period_list: @@ -155,33 +201,24 @@ def get_report_summary(period_list, asset, liability, equity, provisional_profit net_provisional_profit_loss += provisional_profit_loss.get(key) return [ - { - "value": net_asset, - "label": "Total Asset", - "datatype": "Currency", - "currency": currency - }, + {"value": net_asset, "label": "Total Asset", "datatype": "Currency", "currency": currency}, { "value": net_liability, "label": "Total Liability", "datatype": "Currency", - "currency": currency - }, - { - "value": net_equity, - "label": "Total Equity", - "datatype": "Currency", - "currency": currency + "currency": currency, }, + {"value": net_equity, "label": "Total Equity", "datatype": "Currency", "currency": currency}, { "value": net_provisional_profit_loss, "label": "Provisional Profit / Loss (Credit)", "indicator": "Green" if net_provisional_profit_loss > 0 else "Red", "datatype": "Currency", - "currency": currency - } + "currency": currency, + }, ] + def get_chart_data(filters, columns, asset, liability, equity): labels = [d.get("label") for d in columns[2:]] @@ -197,18 +234,13 @@ def get_chart_data(filters, columns, asset, liability, equity): datasets = [] if asset_data: - datasets.append({'name': _('Assets'), 'values': asset_data}) + datasets.append({"name": _("Assets"), "values": asset_data}) if liability_data: - datasets.append({'name': _('Liabilities'), 'values': liability_data}) + datasets.append({"name": _("Liabilities"), "values": liability_data}) if equity_data: - datasets.append({'name': _('Equity'), 'values': equity_data}) + datasets.append({"name": _("Equity"), "values": equity_data}) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - } - } + chart = {"data": {"labels": labels, "datasets": datasets}} if not filters.accumulated_values: chart["type"] = "bar" diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index b456e89f344..20f7643a1c9 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -8,86 +8,88 @@ from frappe.utils import getdate, nowdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() data = get_entries(filters) return columns, data + def get_columns(): - columns = [{ + columns = [ + { "label": _("Payment Document Type"), "fieldname": "payment_document_type", "fieldtype": "Link", "options": "Doctype", - "width": 130 + "width": 130, }, { "label": _("Payment Entry"), "fieldname": "payment_entry", "fieldtype": "Dynamic Link", "options": "payment_document_type", - "width": 140 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Cheque/Reference No"), - "fieldname": "cheque_no", - "width": 120 - }, - { - "label": _("Clearance Date"), - "fieldname": "clearance_date", - "fieldtype": "Date", - "width": 100 + "width": 140, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120}, + {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100}, { "label": _("Against Account"), "fieldname": "against", "fieldtype": "Link", "options": "Account", - "width": 170 + "width": 170, }, - { - "label": _("Amount"), - "fieldname": "amount", - "width": 120 - }] + {"label": _("Amount"), "fieldname": "amount", "width": 120}, + ] return columns + def get_conditions(filters): conditions = "" - if filters.get("from_date"): conditions += " and posting_date>=%(from_date)s" - if filters.get("to_date"): conditions += " and posting_date<=%(to_date)s" + if filters.get("from_date"): + conditions += " and posting_date>=%(from_date)s" + if filters.get("to_date"): + conditions += " and posting_date<=%(to_date)s" return conditions + def get_entries(filters): conditions = get_conditions(filters) - journal_entries = frappe.db.sql("""SELECT + journal_entries = frappe.db.sql( + """SELECT "Journal Entry", jv.name, jv.posting_date, jv.cheque_no, jv.clearance_date, jvd.against_account, jvd.debit - jvd.credit FROM `tabJournal Entry Account` jvd, `tabJournal Entry` jv WHERE jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {0} - order by posting_date DESC, jv.name DESC""".format(conditions), filters, as_list=1) + order by posting_date DESC, jv.name DESC""".format( + conditions + ), + filters, + as_list=1, + ) - payment_entries = frappe.db.sql("""SELECT + payment_entries = frappe.db.sql( + """SELECT "Payment Entry", name, posting_date, reference_no, clearance_date, party, if(paid_from=%(account)s, paid_amount * -1, received_amount) FROM `tabPayment Entry` WHERE docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {0} - order by posting_date DESC, name DESC""".format(conditions), filters, as_list=1) + order by posting_date DESC, name DESC""".format( + conditions + ), + filters, + as_list=1, + ) return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate())) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 6c401fb8f3b..f3ccc868c4c 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -4,121 +4,134 @@ import frappe from frappe import _ -from frappe.utils import flt, getdate, nowdate +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Sum +from frappe.utils import flt, getdate +from pypika import CustomFunction + +from erpnext.accounts.utils import get_balance_on def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() - if not filters.get("account"): return columns, [] + if not filters.get("account"): + return columns, [] account_currency = frappe.db.get_value("Account", filters.account, "account_currency") data = get_entries(filters) - from erpnext.accounts.utils import get_balance_on balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) - total_debit, total_credit = 0,0 + total_debit, total_credit = 0, 0 for d in data: total_debit += flt(d.debit) total_credit += flt(d.credit) amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) - bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \ + bank_bal = ( + flt(balance_as_per_system) + - flt(total_debit) + + flt(total_credit) + amounts_not_reflected_in_system + ) data += [ - get_balance_row(_("Bank Statement balance as per General Ledger"), balance_as_per_system, account_currency), + get_balance_row( + _("Bank Statement balance as per General Ledger"), balance_as_per_system, account_currency + ), {}, { "payment_entry": _("Outstanding Cheques and Deposits to clear"), "debit": total_debit, "credit": total_credit, - "account_currency": account_currency + "account_currency": account_currency, }, - get_balance_row(_("Cheques and Deposits incorrectly cleared"), amounts_not_reflected_in_system, - account_currency), + get_balance_row( + _("Cheques and Deposits incorrectly cleared"), amounts_not_reflected_in_system, account_currency + ), {}, - get_balance_row(_("Calculated Bank Statement balance"), bank_bal, account_currency) + get_balance_row(_("Calculated Bank Statement balance"), bank_bal, account_currency), ] return columns, data + def get_columns(): return [ - { - "fieldname": "posting_date", - "label": _("Posting Date"), - "fieldtype": "Date", - "width": 90 - }, + {"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 90}, { "fieldname": "payment_document", "label": _("Payment Document Type"), "fieldtype": "Data", - "width": 220 + "width": 220, }, { "fieldname": "payment_entry", "label": _("Payment Document"), "fieldtype": "Dynamic Link", "options": "payment_document", - "width": 220 + "width": 220, }, { "fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "options": "account_currency", - "width": 120 + "width": 120, }, { "fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "options": "account_currency", - "width": 120 + "width": 120, }, { "fieldname": "against_account", "label": _("Against Account"), "fieldtype": "Link", "options": "Account", - "width": 200 - }, - { - "fieldname": "reference_no", - "label": _("Reference"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "ref_date", - "label": _("Ref Date"), - "fieldtype": "Date", - "width": 110 - }, - { - "fieldname": "clearance_date", - "label": _("Clearance Date"), - "fieldtype": "Date", - "width": 110 + "width": 200, }, + {"fieldname": "reference_no", "label": _("Reference"), "fieldtype": "Data", "width": 100}, + {"fieldname": "ref_date", "label": _("Ref Date"), "fieldtype": "Date", "width": 110}, + {"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110}, { "fieldname": "account_currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "width": 100 - } + "width": 100, + }, ] + def get_entries(filters): - journal_entries = frappe.db.sql(""" + journal_entries = get_journal_entries(filters) + + payment_entries = get_payment_entries(filters) + + loan_entries = get_loan_entries(filters) + + pos_entries = [] + if filters.include_pos_transactions: + pos_entries = get_pos_entries(filters) + + return sorted( + list(payment_entries) + list(journal_entries + list(pos_entries) + list(loan_entries)), + key=lambda k: getdate(k["posting_date"]), + ) + + +def get_journal_entries(filters): + return frappe.db.sql( + """ select "Journal Entry" as payment_document, jv.posting_date, jv.name as payment_entry, jvd.debit_in_account_currency as debit, jvd.credit_in_account_currency as credit, jvd.against_account, @@ -128,9 +141,15 @@ def get_entries(filters): where jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s and jv.posting_date <= %(report_date)s and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s - and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1) + and ifnull(jv.is_opening, 'No') = 'No'""", + filters, + as_dict=1, + ) - payment_entries = frappe.db.sql(""" + +def get_payment_entries(filters): + return frappe.db.sql( + """ select "Payment Entry" as payment_document, name as payment_entry, reference_no, reference_date as ref_date, @@ -143,11 +162,15 @@ def get_entries(filters): (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1 and posting_date <= %(report_date)s and ifnull(clearance_date, '4000-01-01') > %(report_date)s - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) - pos_entries = [] - if filters.include_pos_transactions: - pos_entries = frappe.db.sql(""" + +def get_pos_entries(filters): + return frappe.db.sql( + """ select "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, si.posting_date, si.debit_to as against_account, sip.clearance_date, @@ -159,30 +182,110 @@ def get_entries(filters): ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s order by si.posting_date ASC, si.name DESC - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) + + +def get_loan_entries(filters): + loan_docs = [] + for doctype in ["Loan Disbursement", "Loan Repayment"]: + loan_doc = frappe.qb.DocType(doctype) + ifnull = CustomFunction("IFNULL", ["value", "default"]) + + if doctype == "Loan Disbursement": + amount_field = (loan_doc.disbursed_amount).as_("credit") + posting_date = (loan_doc.disbursement_date).as_("posting_date") + account = loan_doc.disbursement_account + else: + amount_field = (loan_doc.amount_paid).as_("debit") + posting_date = (loan_doc.posting_date).as_("posting_date") + account = loan_doc.payment_account + + query = ( + frappe.qb.from_(loan_doc) + .select( + ConstantColumn(doctype).as_("payment_document"), + (loan_doc.name).as_("payment_entry"), + (loan_doc.reference_number).as_("reference_no"), + (loan_doc.reference_date).as_("ref_date"), + amount_field, + posting_date, + ) + .where(loan_doc.docstatus == 1) + .where(account == filters.get("account")) + .where(posting_date <= getdate(filters.get("report_date"))) + .where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date"))) + ) + + if doctype == "Loan Repayment": + query.where(loan_doc.repay_from_salary == 0) + + entries = query.run(as_dict=1) + loan_docs.extend(entries) + + return loan_docs - return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), - key=lambda k: k['posting_date'] or getdate(nowdate())) def get_amounts_not_reflected_in_system(filters): - je_amount = frappe.db.sql(""" + je_amount = frappe.db.sql( + """ select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency) from `tabJournal Entry Account` jvd, `tabJournal Entry` jv where jvd.parent = jv.name and jv.docstatus=1 and jvd.account=%(account)s and jv.posting_date > %(report_date)s and jv.clearance_date <= %(report_date)s - and ifnull(jv.is_opening, 'No') = 'No' """, filters) + and ifnull(jv.is_opening, 'No') = 'No' """, + filters, + ) je_amount = flt(je_amount[0][0]) if je_amount else 0.0 - pe_amount = frappe.db.sql(""" + pe_amount = frappe.db.sql( + """ select sum(if(paid_from=%(account)s, paid_amount, received_amount)) from `tabPayment Entry` where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1 - and posting_date > %(report_date)s and clearance_date <= %(report_date)s""", filters) + and posting_date > %(report_date)s and clearance_date <= %(report_date)s""", + filters, + ) pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0 - return je_amount + pe_amount + loan_amount = get_loan_amount(filters) + + return je_amount + pe_amount + loan_amount + + +def get_loan_amount(filters): + total_amount = 0 + for doctype in ["Loan Disbursement", "Loan Repayment"]: + loan_doc = frappe.qb.DocType(doctype) + ifnull = CustomFunction("IFNULL", ["value", "default"]) + + if doctype == "Loan Disbursement": + amount_field = Sum(loan_doc.disbursed_amount) + posting_date = (loan_doc.disbursement_date).as_("posting_date") + account = loan_doc.disbursement_account + else: + amount_field = Sum(loan_doc.amount_paid) + posting_date = (loan_doc.posting_date).as_("posting_date") + account = loan_doc.payment_account + + amount = ( + frappe.qb.from_(loan_doc) + .select(amount_field) + .where(loan_doc.docstatus == 1) + .where(account == filters.get("account")) + .where(posting_date > getdate(filters.get("report_date"))) + .where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date"))) + .run()[0][0] + ) + + total_amount += flt(amount) + + return total_amount + def get_balance_row(label, amount, account_currency): if amount > 0: @@ -190,12 +293,12 @@ def get_balance_row(label, amount, account_currency): "payment_entry": label, "debit": amount, "credit": 0, - "account_currency": account_currency + "account_currency": account_currency, } else: return { "payment_entry": label, "debit": 0, "credit": abs(amount), - "account_currency": account_currency + "account_currency": account_currency, } diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py index 1d7463c8920..62bee82590b 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py +++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py @@ -12,97 +12,70 @@ def execute(filters=None): return columns, data + def get_data(report_filters): filters = get_report_filters(report_filters) fields = get_report_fields() - return frappe.get_all('Purchase Invoice', - fields= fields, filters=filters) + return frappe.get_all("Purchase Invoice", fields=fields, filters=filters) + def get_report_filters(report_filters): - filters = [['Purchase Invoice','company','=',report_filters.get('company')], - ['Purchase Invoice','posting_date','<=',report_filters.get('posting_date')], ['Purchase Invoice','docstatus','=',1], - ['Purchase Invoice','per_received','<',100], ['Purchase Invoice','update_stock','=',0]] + filters = [ + ["Purchase Invoice", "company", "=", report_filters.get("company")], + ["Purchase Invoice", "posting_date", "<=", report_filters.get("posting_date")], + ["Purchase Invoice", "docstatus", "=", 1], + ["Purchase Invoice", "per_received", "<", 100], + ["Purchase Invoice", "update_stock", "=", 0], + ] - if report_filters.get('purchase_invoice'): - filters.append(['Purchase Invoice','per_received','in',[report_filters.get('purchase_invoice')]]) + if report_filters.get("purchase_invoice"): + filters.append( + ["Purchase Invoice", "per_received", "in", [report_filters.get("purchase_invoice")]] + ) return filters + def get_report_fields(): fields = [] - for p_field in ['name', 'supplier', 'company', 'posting_date', 'currency']: - fields.append('`tabPurchase Invoice`.`{}`'.format(p_field)) + for p_field in ["name", "supplier", "company", "posting_date", "currency"]: + fields.append("`tabPurchase Invoice`.`{}`".format(p_field)) - for c_field in ['item_code', 'item_name', 'uom', 'qty', 'received_qty', 'rate', 'amount']: - fields.append('`tabPurchase Invoice Item`.`{}`'.format(c_field)) + for c_field in ["item_code", "item_name", "uom", "qty", "received_qty", "rate", "amount"]: + fields.append("`tabPurchase Invoice Item`.`{}`".format(c_field)) return fields + def get_columns(): return [ { - 'label': _('Purchase Invoice'), - 'fieldname': 'name', - 'fieldtype': 'Link', - 'options': 'Purchase Invoice', - 'width': 170 + "label": _("Purchase Invoice"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 170, }, { - 'label': _('Supplier'), - 'fieldname': 'supplier', - 'fieldtype': 'Link', - 'options': 'Supplier', - 'width': 120 + "label": _("Supplier"), + "fieldname": "supplier", + "fieldtype": "Link", + "options": "Supplier", + "width": 120, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, { - 'label': _('Posting Date'), - 'fieldname': 'posting_date', - 'fieldtype': 'Date', - 'width': 100 + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, }, - { - 'label': _('Item Code'), - 'fieldname': 'item_code', - 'fieldtype': 'Link', - 'options': 'Item', - 'width': 100 - }, - { - 'label': _('Item Name'), - 'fieldname': 'item_name', - 'fieldtype': 'Data', - 'width': 100 - }, - { - 'label': _('UOM'), - 'fieldname': 'uom', - 'fieldtype': 'Link', - 'options': 'UOM', - 'width': 100 - }, - { - 'label': _('Invoiced Qty'), - 'fieldname': 'qty', - 'fieldtype': 'Float', - 'width': 100 - }, - { - 'label': _('Received Qty'), - 'fieldname': 'received_qty', - 'fieldtype': 'Float', - 'width': 100 - }, - { - 'label': _('Rate'), - 'fieldname': 'rate', - 'fieldtype': 'Currency', - 'width': 100 - }, - { - 'label': _('Amount'), - 'fieldname': 'amount', - 'fieldtype': 'Currency', - 'width': 100 - } + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("UOM"), "fieldname": "uom", "fieldtype": "Link", "options": "UOM", "width": 100}, + {"label": _("Invoiced Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 100}, + {"label": _("Received Qty"), "fieldname": "received_qty", "fieldtype": "Float", "width": 100}, + {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 100}, ] diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index ead67766785..95159476f2d 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -31,22 +31,28 @@ def execute(filters=None): if dimension_items: data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0) else: - DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation + DCC_allocation = frappe.db.sql( + """SELECT parent, sum(percentage_allocation) as percentage_allocation FROM `tabDistributed Cost Center` WHERE cost_center IN %(dimension)s AND parent NOT IN %(dimension)s - GROUP BY parent''',{'dimension':[dimension]}) + GROUP BY parent""", + {"dimension": [dimension]}, + ) if DCC_allocation: - filters['budget_against_filter'] = [DCC_allocation[0][0]] + filters["budget_against_filter"] = [DCC_allocation[0][0]] ddc_cam_map = get_dimension_account_month_map(filters) dimension_items = ddc_cam_map.get(DCC_allocation[0][0]) if dimension_items: - data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1]) + data = get_final_data( + dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1] + ) chart = get_chart_data(filters, columns, data) return columns, data, None, chart + def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): for account, monthwise_data in iteritems(dimension_items): row = [dimension, account] @@ -66,16 +72,16 @@ def get_final_data(dimension, dimension_items, filters, period_month_ranges, dat period_data[0] += last_total if DCC_allocation: - period_data[0] = period_data[0]*(DCC_allocation/100) - period_data[1] = period_data[1]*(DCC_allocation/100) + period_data[0] = period_data[0] * (DCC_allocation / 100) + period_data[1] = period_data[1] * (DCC_allocation / 100) - if(filters.get("show_cumulative")): + if filters.get("show_cumulative"): last_total = period_data[0] - period_data[1] period_data[2] = period_data[0] - period_data[1] row += period_data totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly" : + if filters["period"] != "Yearly": row += totals data.append(row) @@ -85,19 +91,19 @@ def get_final_data(dimension, dimension_items, filters, period_month_ranges, dat def get_columns(filters): columns = [ { - 'label': _(filters.get("budget_against")), - 'fieldtype': 'Link', - 'fieldname': 'budget_against', - 'options': filters.get('budget_against'), - 'width': 150 + "label": _(filters.get("budget_against")), + "fieldtype": "Link", + "fieldname": "budget_against", + "options": filters.get("budget_against"), + "width": 150, }, { - 'label': _('Account'), - 'fieldname': 'Account', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 150 - } + "label": _("Account"), + "fieldname": "Account", + "fieldtype": "Link", + "options": "Account", + "width": 150, + }, ] group_months = False if filters["period"] == "Monthly" else True @@ -110,45 +116,34 @@ def get_columns(filters): labels = [ _("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), - _("Variance ") + " " + str(year[0]) + _("Variance ") + " " + str(year[0]), ] for label in labels: - columns.append({ - 'label': label, - 'fieldtype': 'Float', - 'fieldname': frappe.scrub(label), - 'width': 150 - }) + columns.append( + {"label": label, "fieldtype": "Float", "fieldname": frappe.scrub(label), "width": 150} + ) else: for label in [ _("Budget") + " (%s)" + " " + str(year[0]), _("Actual") + " (%s)" + " " + str(year[0]), - _("Variance") + " (%s)" + " " + str(year[0]) + _("Variance") + " (%s)" + " " + str(year[0]), ]: if group_months: label = label % ( - formatdate(from_date, format_string="MMM") - + "-" - + formatdate(to_date, format_string="MMM") + formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM") ) else: label = label % formatdate(from_date, format_string="MMM") - columns.append({ - 'label': label, - 'fieldtype': 'Float', - 'fieldname': frappe.scrub(label), - 'width': 150 - }) + columns.append( + {"label": label, "fieldtype": "Float", "fieldname": frappe.scrub(label), "width": 150} + ) if filters["period"] != "Yearly": for label in [_("Total Budget"), _("Total Actual"), _("Total Variance")]: - columns.append({ - 'label': label, - 'fieldtype': 'Float', - 'fieldname': frappe.scrub(label), - 'width': 150 - }) + columns.append( + {"label": label, "fieldtype": "Float", "fieldname": frappe.scrub(label), "width": 150} + ) return columns else: @@ -170,8 +165,11 @@ def get_cost_centers(filters): where company = %s {order_by} - """.format(tab=filters.get("budget_against"), order_by=order_by), - filters.get("company")) + """.format( + tab=filters.get("budget_against"), order_by=order_by + ), + filters.get("company"), + ) else: return frappe.db.sql_list( """ @@ -179,7 +177,10 @@ def get_cost_centers(filters): name from `tab{tab}` - """.format(tab=filters.get("budget_against"))) # nosec + """.format( + tab=filters.get("budget_against") + ) + ) # nosec # Get dimension & target details @@ -187,8 +188,9 @@ def get_dimension_target_details(filters): budget_against = frappe.scrub(filters.get("budget_against")) cond = "" if filters.get("budget_against_filter"): - cond += """ and b.{budget_against} in (%s)""".format( - budget_against=budget_against) % ", ".join(["%s"] * len(filters.get("budget_against_filter"))) + cond += """ and b.{budget_against} in (%s)""".format(budget_against=budget_against) % ", ".join( + ["%s"] * len(filters.get("budget_against_filter")) + ) return frappe.db.sql( """ @@ -222,7 +224,9 @@ def get_dimension_target_details(filters): filters.company, ] + (filters.get("budget_against_filter") or []) - ), as_dict=True) + ), + as_dict=True, + ) # Get target distribution details of accounts of cost center @@ -243,13 +247,14 @@ def get_target_distribution_details(filters): order by md.fiscal_year """, - (filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1): - target_details.setdefault(d.name, {}).setdefault( - d.month, flt(d.percentage_allocation) - ) + (filters.from_fiscal_year, filters.to_fiscal_year), + as_dict=1, + ): + target_details.setdefault(d.name, {}).setdefault(d.month, flt(d.percentage_allocation)) return target_details + # Get actual details from gl entry def get_actual_details(name, filters): budget_against = frappe.scrub(filters.get("budget_against")) @@ -260,7 +265,9 @@ def get_actual_details(name, filters): cond = """ and lft >= "{lft}" and rgt <= "{rgt}" - """.format(lft=cc_lft, rgt=cc_rgt) + """.format( + lft=cc_lft, rgt=cc_rgt + ) ac_details = frappe.db.sql( """ @@ -294,8 +301,12 @@ def get_actual_details(name, filters): group by gl.name order by gl.fiscal_year - """.format(tab=filters.budget_against, budget_against=budget_against, cond=cond), - (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1) + """.format( + tab=filters.budget_against, budget_against=budget_against, cond=cond + ), + (filters.from_fiscal_year, filters.to_fiscal_year, name), + as_dict=1, + ) cc_actual_details = {} for d in ac_details: @@ -303,6 +314,7 @@ def get_actual_details(name, filters): return cc_actual_details + def get_dimension_account_month_map(filters): dimension_target_details = get_dimension_target_details(filters) tdd = get_target_distribution_details(filters) @@ -314,17 +326,13 @@ def get_dimension_account_month_map(filters): for month_id in range(1, 13): month = datetime.date(2013, month_id, 1).strftime("%B") - cam_map.setdefault(ccd.budget_against, {}).setdefault( - ccd.account, {} - ).setdefault(ccd.fiscal_year, {}).setdefault( - month, frappe._dict({"target": 0.0, "actual": 0.0}) - ) + cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, {}).setdefault( + ccd.fiscal_year, {} + ).setdefault(month, frappe._dict({"target": 0.0, "actual": 0.0})) tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] month_percentage = ( - tdd.get(ccd.monthly_distribution, {}).get(month, 0) - if ccd.monthly_distribution - else 100.0 / 12 + tdd.get(ccd.monthly_distribution, {}).get(month, 0) if ccd.monthly_distribution else 100.0 / 12 ) tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 @@ -347,13 +355,12 @@ def get_fiscal_years(filters): where name between %(from_fiscal_year)s and %(to_fiscal_year)s """, - { - "from_fiscal_year": filters["from_fiscal_year"], - "to_fiscal_year": filters["to_fiscal_year"] - }) + {"from_fiscal_year": filters["from_fiscal_year"], "to_fiscal_year": filters["to_fiscal_year"]}, + ) return fiscal_year + def get_chart_data(filters, columns, data): if not data: @@ -366,12 +373,13 @@ def get_chart_data(filters, columns, data): for year in fiscal_year: for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): - if filters['period'] == 'Yearly': + if filters["period"] == "Yearly": labels.append(year[0]) else: if group_months: - label = formatdate(from_date, format_string="MMM") + "-" \ - + formatdate(to_date, format_string="MMM") + label = ( + formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM") + ) labels.append(label) else: label = formatdate(from_date, format_string="MMM") @@ -386,16 +394,16 @@ def get_chart_data(filters, columns, data): for i in range(no_of_columns): budget_values[i] += values[index] - actual_values[i] += values[index+1] + actual_values[i] += values[index + 1] index += 3 return { - 'data': { - 'labels': labels, - 'datasets': [ - {'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, - {'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} - ] + "data": { + "labels": labels, + "datasets": [ + {"name": "Budget", "chartType": "bar", "values": budget_values}, + {"name": "Actual Expense", "chartType": "bar", "values": actual_values}, + ], }, - 'type' : 'bar' + "type": "bar", } diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 75365b81f22..7929d4aa2ae 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -20,65 +20,103 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): - if cint(frappe.db.get_single_value('Accounts Settings', 'use_custom_cash_flow')): + if cint(frappe.db.get_single_value("Accounts Settings", "use_custom_cash_flow")): from erpnext.accounts.report.cash_flow.custom_cash_flow import execute as execute_custom + return execute_custom(filters=filters) - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.period_start_date, filters.period_end_date, filters.filter_based_on, - filters.periodicity, company=filters.company) + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) cash_flow_accounts = get_cash_flow_accounts() # compute net profit / loss - income = get_data(filters.company, "Income", "Credit", period_list, filters=filters, - accumulated_values=filters.accumulated_values, ignore_closing_entries=True, ignore_accumulated_values_for_fy= True) - expense = get_data(filters.company, "Expense", "Debit", period_list, filters=filters, - accumulated_values=filters.accumulated_values, ignore_closing_entries=True, ignore_accumulated_values_for_fy= True) + income = get_data( + filters.company, + "Income", + "Credit", + period_list, + filters=filters, + accumulated_values=filters.accumulated_values, + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + ) + expense = get_data( + filters.company, + "Expense", + "Debit", + period_list, + filters=filters, + accumulated_values=filters.accumulated_values, + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + ) net_profit_loss = get_net_profit_loss(income, expense, period_list, filters.company) data = [] summary_data = {} - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") for cash_flow_account in cash_flow_accounts: section_data = [] - data.append({ - "account_name": cash_flow_account['section_header'], - "parent_account": None, - "indent": 0.0, - "account": cash_flow_account['section_header'] - }) + data.append( + { + "account_name": cash_flow_account["section_header"], + "parent_account": None, + "indent": 0.0, + "account": cash_flow_account["section_header"], + } + ) if len(data) == 1: # add first net income in operations section if net_profit_loss: - net_profit_loss.update({ - "indent": 1, - "parent_account": cash_flow_accounts[0]['section_header'] - }) + net_profit_loss.update( + {"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]} + ) data.append(net_profit_loss) section_data.append(net_profit_loss) - for account in cash_flow_account['account_types']: - account_data = get_account_type_based_data(filters.company, - account['account_type'], period_list, filters.accumulated_values, filters) - account_data.update({ - "account_name": account['label'], - "account": account['label'], - "indent": 1, - "parent_account": cash_flow_account['section_header'], - "currency": company_currency - }) + for account in cash_flow_account["account_types"]: + account_data = get_account_type_based_data( + filters.company, account["account_type"], period_list, filters.accumulated_values, filters + ) + account_data.update( + { + "account_name": account["label"], + "account": account["label"], + "indent": 1, + "parent_account": cash_flow_account["section_header"], + "currency": company_currency, + } + ) data.append(account_data) section_data.append(account_data) - add_total_row_account(data, section_data, cash_flow_account['section_footer'], - period_list, company_currency, summary_data, filters) + add_total_row_account( + data, + section_data, + cash_flow_account["section_footer"], + period_list, + company_currency, + summary_data, + filters, + ) - add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + add_total_row_account( + data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters + ) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, filters.company + ) chart = get_chart_data(columns, data) @@ -86,6 +124,7 @@ def execute(filters=None): return columns, data, None, chart, report_summary + def get_cash_flow_accounts(): operation_accounts = { "section_name": "Operations", @@ -95,39 +134,37 @@ def get_cash_flow_accounts(): {"account_type": "Depreciation", "label": _("Depreciation")}, {"account_type": "Receivable", "label": _("Net Change in Accounts Receivable")}, {"account_type": "Payable", "label": _("Net Change in Accounts Payable")}, - {"account_type": "Stock", "label": _("Net Change in Inventory")} - ] + {"account_type": "Stock", "label": _("Net Change in Inventory")}, + ], } investing_accounts = { "section_name": "Investing", "section_footer": _("Net Cash from Investing"), "section_header": _("Cash Flow from Investing"), - "account_types": [ - {"account_type": "Fixed Asset", "label": _("Net Change in Fixed Asset")} - ] + "account_types": [{"account_type": "Fixed Asset", "label": _("Net Change in Fixed Asset")}], } financing_accounts = { "section_name": "Financing", "section_footer": _("Net Cash from Financing"), "section_header": _("Cash Flow from Financing"), - "account_types": [ - {"account_type": "Equity", "label": _("Net Change in Equity")} - ] + "account_types": [{"account_type": "Equity", "label": _("Net Change in Equity")}], } # combine all cash flow accounts for iteration return [operation_accounts, investing_accounts, financing_accounts] + def get_account_type_based_data(company, account_type, period_list, accumulated_values, filters): data = {} total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) - amount = get_account_type_based_gl_data(company, start_date, - period['to_date'], account_type, filters) + amount = get_account_type_based_gl_data( + company, start_date, period["to_date"], account_type, filters + ) if amount and account_type == "Depreciation": amount *= -1 @@ -138,31 +175,42 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ data["total"] = total return data + def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None): cond = "" filters = frappe._dict(filters or {}) if filters.include_default_book_entries: - company_fb = frappe.db.get_value("Company", company, 'default_finance_book') + company_fb = frappe.db.get_value("Company", company, "default_finance_book") cond = """ AND (finance_book in (%s, %s, '') OR finance_book IS NULL) - """ %(frappe.db.escape(filters.finance_book), frappe.db.escape(company_fb)) + """ % ( + frappe.db.escape(filters.finance_book), + frappe.db.escape(company_fb), + ) else: - cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" %(frappe.db.escape(cstr(filters.finance_book))) + cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" % ( + frappe.db.escape(cstr(filters.finance_book)) + ) - - gl_sum = frappe.db.sql_list(""" + gl_sum = frappe.db.sql_list( + """ select sum(credit) - sum(debit) from `tabGL Entry` where company=%s and posting_date >= %s and posting_date <= %s and voucher_type != 'Period Closing Voucher' and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond} - """.format(cond=cond), (company, start_date, end_date, account_type)) + """.format( + cond=cond + ), + (company, start_date, end_date, account_type), + ) return gl_sum[0] if gl_sum and gl_sum[0] else 0 + def get_start_date(period, accumulated_values, company): - if not accumulated_values and period.get('from_date'): - return period['from_date'] + if not accumulated_values and period.get("from_date"): + return period["from_date"] start_date = period["year_start_date"] if accumulated_values: @@ -170,23 +218,26 @@ def get_start_date(period, accumulated_values, company): return start_date -def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False): + +def add_total_row_account( + out, data, label, period_list, currency, summary_data, filters, consolidated=False +): total_row = { "account_name": "'" + _("{0}").format(label) + "'", "account": "'" + _("{0}").format(label) + "'", - "currency": currency + "currency": currency, } summary_data[label] = 0 # from consolidated financial statement - if filters.get('accumulated_in_group_company'): + if filters.get("accumulated_in_group_company"): period_list = get_filtered_list_for_consolidated_report(filters, period_list) for row in data: if row.get("parent_account"): for period in period_list: - key = period if consolidated else period['key'] + key = period if consolidated else period["key"] total_row.setdefault(key, 0.0) total_row[key] += row.get(key, 0.0) summary_data[label] += row.get(key) @@ -203,12 +254,7 @@ def get_report_summary(summary_data, currency): for label, value in iteritems(summary_data): report_summary.append( - { - "value": value, - "label": label, - "datatype": "Currency", - "currency": currency - } + {"value": value, "label": label, "datatype": "Currency", "currency": currency} ) return report_summary @@ -216,16 +262,14 @@ def get_report_summary(summary_data, currency): def get_chart_data(columns, data): labels = [d.get("label") for d in columns[2:]] - datasets = [{'name':account.get('account').replace("'", ""), 'values': [account.get('total')]} for account in data if account.get('parent_account') == None and account.get('currency')] + datasets = [ + {"name": account.get("account").replace("'", ""), "values": [account.get("total")]} + for account in data + if account.get("parent_account") == None and account.get("currency") + ] datasets = datasets[:-1] - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - }, - "type": "bar" - } + chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} chart["fieldtype"] = "Currency" diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index 45d147e7a21..b165c88c068 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -4,7 +4,8 @@ import frappe from frappe import _ -from frappe.utils import add_to_date +from frappe.query_builder.functions import Sum +from frappe.utils import add_to_date, flt, get_date_str from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( @@ -13,41 +14,59 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement def get_mapper_for(mappers, position): - mapper_list = list(filter(lambda x: x['position'] == position, mappers)) + mapper_list = list(filter(lambda x: x["position"] == position, mappers)) return mapper_list[0] if mapper_list else [] def get_mappers_from_db(): return frappe.get_all( - 'Cash Flow Mapper', + "Cash Flow Mapper", fields=[ - 'section_name', 'section_header', 'section_leader', 'section_subtotal', - 'section_footer', 'name', 'position'], - order_by='position' + "section_name", + "section_header", + "section_leader", + "section_subtotal", + "section_footer", + "name", + "position", + ], + order_by="position", ) def get_accounts_in_mappers(mapping_names): - return frappe.db.sql(''' - select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability, - cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment - from `tabCash Flow Mapping Accounts` cfma - join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name - where cfma.parent in (%s) - order by cfm.is_working_capital - ''', (', '.join('"%s"' % d for d in mapping_names))) + cfm = frappe.qb.DocType("Cash Flow Mapping") + cfma = frappe.qb.DocType("Cash Flow Mapping Accounts") + result = ( + frappe.qb.select( + cfma.name, + cfm.label, + cfm.is_working_capital, + cfm.is_income_tax_liability, + cfm.is_income_tax_expense, + cfm.is_finance_cost, + cfm.is_finance_cost_adjustment, + cfma.account, + ) + .from_(cfm) + .join(cfma) + .on(cfm.name == cfma.parent) + .where(cfma.parent.isin(mapping_names)) + ).run() + + return result def setup_mappers(mappers): cash_flow_accounts = [] for mapping in mappers: - mapping['account_types'] = [] - mapping['tax_liabilities'] = [] - mapping['tax_expenses'] = [] - mapping['finance_costs'] = [] - mapping['finance_costs_adjustments'] = [] - doc = frappe.get_doc('Cash Flow Mapper', mapping['name']) + mapping["account_types"] = [] + mapping["tax_liabilities"] = [] + mapping["tax_expenses"] = [] + mapping["finance_costs"] = [] + mapping["finance_costs_adjustments"] = [] + doc = frappe.get_doc("Cash Flow Mapper", mapping["name"]) mapping_names = [item.name for item in doc.accounts] if not mapping_names: @@ -57,96 +76,123 @@ def setup_mappers(mappers): account_types = [ dict( - name=account[0], label=account[1], is_working_capital=account[2], - is_income_tax_liability=account[3], is_income_tax_expense=account[4] - ) for account in accounts if not account[3]] + name=account[0], + account_name=account[7], + label=account[1], + is_working_capital=account[2], + is_income_tax_liability=account[3], + is_income_tax_expense=account[4], + ) + for account in accounts + if not account[3] + ] finance_costs_adjustments = [ dict( - name=account[0], label=account[1], is_finance_cost=account[5], - is_finance_cost_adjustment=account[6] - ) for account in accounts if account[6]] + name=account[0], + account_name=account[7], + label=account[1], + is_finance_cost=account[5], + is_finance_cost_adjustment=account[6], + ) + for account in accounts + if account[6] + ] tax_liabilities = [ dict( - name=account[0], label=account[1], is_income_tax_liability=account[3], - is_income_tax_expense=account[4] - ) for account in accounts if account[3]] + name=account[0], + account_name=account[7], + label=account[1], + is_income_tax_liability=account[3], + is_income_tax_expense=account[4], + ) + for account in accounts + if account[3] + ] tax_expenses = [ dict( - name=account[0], label=account[1], is_income_tax_liability=account[3], - is_income_tax_expense=account[4] - ) for account in accounts if account[4]] + name=account[0], + account_name=account[7], + label=account[1], + is_income_tax_liability=account[3], + is_income_tax_expense=account[4], + ) + for account in accounts + if account[4] + ] finance_costs = [ - dict( - name=account[0], label=account[1], is_finance_cost=account[5]) - for account in accounts if account[5]] + dict(name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5]) + for account in accounts + if account[5] + ] account_types_labels = sorted( set( - (d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense']) - for d in account_types + (d["label"], d["is_working_capital"], d["is_income_tax_liability"], d["is_income_tax_expense"]) + for d in account_types ), - key=lambda x: x[1] + key=lambda x: x[1], ) fc_adjustment_labels = sorted( set( - [(d['label'], d['is_finance_cost'], d['is_finance_cost_adjustment']) - for d in finance_costs_adjustments if d['is_finance_cost_adjustment']] + [ + (d["label"], d["is_finance_cost"], d["is_finance_cost_adjustment"]) + for d in finance_costs_adjustments + if d["is_finance_cost_adjustment"] + ] ), - key=lambda x: x[2] + key=lambda x: x[2], ) unique_liability_labels = sorted( set( - [(d['label'], d['is_income_tax_liability'], d['is_income_tax_expense']) - for d in tax_liabilities] + [ + (d["label"], d["is_income_tax_liability"], d["is_income_tax_expense"]) + for d in tax_liabilities + ] ), - key=lambda x: x[0] + key=lambda x: x[0], ) unique_expense_labels = sorted( set( - [(d['label'], d['is_income_tax_liability'], d['is_income_tax_expense']) - for d in tax_expenses] + [(d["label"], d["is_income_tax_liability"], d["is_income_tax_expense"]) for d in tax_expenses] ), - key=lambda x: x[0] + key=lambda x: x[0], ) unique_finance_costs_labels = sorted( - set( - [(d['label'], d['is_finance_cost']) for d in finance_costs] - ), - key=lambda x: x[0] + set([(d["label"], d["is_finance_cost"]) for d in finance_costs]), key=lambda x: x[0] ) for label in account_types_labels: - names = [d['name'] for d in account_types if d['label'] == label[0]] + names = [d["account_name"] for d in account_types if d["label"] == label[0]] m = dict(label=label[0], names=names, is_working_capital=label[1]) - mapping['account_types'].append(m) + mapping["account_types"].append(m) for label in fc_adjustment_labels: - names = [d['name'] for d in finance_costs_adjustments if d['label'] == label[0]] + names = [d["account_name"] for d in finance_costs_adjustments if d["label"] == label[0]] m = dict(label=label[0], names=names) - mapping['finance_costs_adjustments'].append(m) + mapping["finance_costs_adjustments"].append(m) for label in unique_liability_labels: - names = [d['name'] for d in tax_liabilities if d['label'] == label[0]] + names = [d["account_name"] for d in tax_liabilities if d["label"] == label[0]] m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2]) - mapping['tax_liabilities'].append(m) + mapping["tax_liabilities"].append(m) for label in unique_expense_labels: - names = [d['name'] for d in tax_expenses if d['label'] == label[0]] + names = [d["account_name"] for d in tax_expenses if d["label"] == label[0]] m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2]) - mapping['tax_expenses'].append(m) + mapping["tax_expenses"].append(m) for label in unique_finance_costs_labels: - names = [d['name'] for d in finance_costs if d['label'] == label[0]] + names = [d["account_name"] for d in finance_costs if d["label"] == label[0]] m = dict(label=label[0], names=names, is_finance_cost=label[1]) - mapping['finance_costs'].append(m) + mapping["finance_costs"].append(m) cash_flow_accounts.append(mapping) @@ -154,119 +200,145 @@ def setup_mappers(mappers): def add_data_for_operating_activities( - filters, company_currency, profit_data, period_list, light_mappers, mapper, data): + filters, company_currency, profit_data, period_list, light_mappers, mapper, data +): has_added_working_capital_header = False section_data = [] - data.append({ - "account_name": mapper['section_header'], - "parent_account": None, - "indent": 0.0, - "account": mapper['section_header'] - }) + data.append( + { + "account_name": mapper["section_header"], + "parent_account": None, + "indent": 0.0, + "account": mapper["section_header"], + } + ) if profit_data: - profit_data.update({ - "indent": 1, - "parent_account": get_mapper_for(light_mappers, position=1)['section_header'] - }) + profit_data.update( + {"indent": 1, "parent_account": get_mapper_for(light_mappers, position=1)["section_header"]} + ) data.append(profit_data) section_data.append(profit_data) - data.append({ - "account_name": mapper["section_leader"], - "parent_account": None, - "indent": 1.0, - "account": mapper["section_leader"] - }) - - for account in mapper['account_types']: - if account['is_working_capital'] and not has_added_working_capital_header: - data.append({ - "account_name": 'Movement in working capital', + data.append( + { + "account_name": mapper["section_leader"], "parent_account": None, "indent": 1.0, - "account": "" - }) + "account": mapper["section_leader"], + } + ) + + for account in mapper["account_types"]: + if account["is_working_capital"] and not has_added_working_capital_header: + data.append( + { + "account_name": "Movement in working capital", + "parent_account": None, + "indent": 1.0, + "account": "", + } + ) has_added_working_capital_header = True account_data = _get_account_type_based_data( - filters, account['names'], period_list, filters.accumulated_values) + filters, account["names"], period_list, filters.accumulated_values + ) - if not account['is_working_capital']: + if not account["is_working_capital"]: for key in account_data: - if key != 'total': + if key != "total": account_data[key] *= -1 - if account_data['total'] != 0: - account_data.update({ - "account_name": account['label'], - "account": account['names'], - "indent": 1.0, - "parent_account": mapper['section_header'], - "currency": company_currency - }) + if account_data["total"] != 0: + account_data.update( + { + "account_name": account["label"], + "account": account["names"], + "indent": 1.0, + "parent_account": mapper["section_header"], + "currency": company_currency, + } + ) data.append(account_data) section_data.append(account_data) _add_total_row_account( - data, section_data, mapper['section_subtotal'], period_list, company_currency, indent=1) + data, section_data, mapper["section_subtotal"], period_list, company_currency, indent=1 + ) # calculate adjustment for tax paid and add to data - if not mapper['tax_liabilities']: - mapper['tax_liabilities'] = [ - dict(label='Income tax paid', names=[''], tax_liability=1, tax_expense=0)] + if not mapper["tax_liabilities"]: + mapper["tax_liabilities"] = [ + dict(label="Income tax paid", names=[""], tax_liability=1, tax_expense=0) + ] - for account in mapper['tax_liabilities']: + for account in mapper["tax_liabilities"]: tax_paid = calculate_adjustment( - filters, mapper['tax_liabilities'], mapper['tax_expenses'], - filters.accumulated_values, period_list) + filters, + mapper["tax_liabilities"], + mapper["tax_expenses"], + filters.accumulated_values, + period_list, + ) if tax_paid: - tax_paid.update({ - 'parent_account': mapper['section_header'], - 'currency': company_currency, - 'account_name': account['label'], - 'indent': 1.0 - }) + tax_paid.update( + { + "parent_account": mapper["section_header"], + "currency": company_currency, + "account_name": account["label"], + "indent": 1.0, + } + ) data.append(tax_paid) section_data.append(tax_paid) - if not mapper['finance_costs_adjustments']: - mapper['finance_costs_adjustments'] = [dict(label='Interest Paid', names=[''])] + if not mapper["finance_costs_adjustments"]: + mapper["finance_costs_adjustments"] = [dict(label="Interest Paid", names=[""])] - for account in mapper['finance_costs_adjustments']: + for account in mapper["finance_costs_adjustments"]: interest_paid = calculate_adjustment( - filters, mapper['finance_costs_adjustments'], mapper['finance_costs'], - filters.accumulated_values, period_list + filters, + mapper["finance_costs_adjustments"], + mapper["finance_costs"], + filters.accumulated_values, + period_list, ) if interest_paid: - interest_paid.update({ - 'parent_account': mapper['section_header'], - 'currency': company_currency, - 'account_name': account['label'], - 'indent': 1.0 - }) + interest_paid.update( + { + "parent_account": mapper["section_header"], + "currency": company_currency, + "account_name": account["label"], + "indent": 1.0, + } + ) data.append(interest_paid) section_data.append(interest_paid) _add_total_row_account( - data, section_data, mapper['section_footer'], period_list, company_currency) + data, section_data, mapper["section_footer"], period_list, company_currency + ) -def calculate_adjustment(filters, non_expense_mapper, expense_mapper, use_accumulated_values, period_list): - liability_accounts = [d['names'] for d in non_expense_mapper] - expense_accounts = [d['names'] for d in expense_mapper] +def calculate_adjustment( + filters, non_expense_mapper, expense_mapper, use_accumulated_values, period_list +): + liability_accounts = [d["names"] for d in non_expense_mapper] + expense_accounts = [d["names"] for d in expense_mapper] - non_expense_closing = _get_account_type_based_data( - filters, liability_accounts, period_list, 0) + non_expense_closing = _get_account_type_based_data(filters, liability_accounts, period_list, 0) non_expense_opening = _get_account_type_based_data( - filters, liability_accounts, period_list, use_accumulated_values, opening_balances=1) + filters, liability_accounts, period_list, use_accumulated_values, opening_balances=1 + ) expense_data = _get_account_type_based_data( - filters, expense_accounts, period_list, use_accumulated_values) + filters, expense_accounts, period_list, use_accumulated_values + ) data = _calculate_adjustment(non_expense_closing, non_expense_opening, expense_data) return data @@ -276,7 +348,9 @@ def _calculate_adjustment(non_expense_closing, non_expense_opening, expense_data account_data = {} for month in non_expense_opening.keys(): if non_expense_opening[month] and non_expense_closing[month]: - account_data[month] = non_expense_opening[month] - expense_data[month] + non_expense_closing[month] + account_data[month] = ( + non_expense_opening[month] - expense_data[month] + non_expense_closing[month] + ) elif expense_data[month]: account_data[month] = expense_data[month] @@ -284,32 +358,39 @@ def _calculate_adjustment(non_expense_closing, non_expense_opening, expense_data def add_data_for_other_activities( - filters, company_currency, profit_data, period_list, light_mappers, mapper_list, data): + filters, company_currency, profit_data, period_list, light_mappers, mapper_list, data +): for mapper in mapper_list: section_data = [] - data.append({ - "account_name": mapper['section_header'], - "parent_account": None, - "indent": 0.0, - "account": mapper['section_header'] - }) + data.append( + { + "account_name": mapper["section_header"], + "parent_account": None, + "indent": 0.0, + "account": mapper["section_header"], + } + ) - for account in mapper['account_types']: - account_data = _get_account_type_based_data(filters, - account['names'], period_list, filters.accumulated_values) - if account_data['total'] != 0: - account_data.update({ - "account_name": account['label'], - "account": account['names'], - "indent": 1, - "parent_account": mapper['section_header'], - "currency": company_currency - }) + for account in mapper["account_types"]: + account_data = _get_account_type_based_data( + filters, account["names"], period_list, filters.accumulated_values + ) + if account_data["total"] != 0: + account_data.update( + { + "account_name": account["label"], + "account": account["names"], + "indent": 1, + "parent_account": mapper["section_header"], + "currency": company_currency, + } + ) data.append(account_data) section_data.append(account_data) - _add_total_row_account(data, section_data, mapper['section_footer'], - period_list, company_currency) + _add_total_row_account( + data, section_data, mapper["section_footer"], period_list, company_currency + ) def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper): @@ -318,13 +399,18 @@ def compute_data(filters, company_currency, profit_data, period_list, light_mapp operating_activities_mapper = get_mapper_for(light_mappers, position=1) other_mappers = [ get_mapper_for(light_mappers, position=2), - get_mapper_for(light_mappers, position=3) + get_mapper_for(light_mappers, position=3), ] if operating_activities_mapper: add_data_for_operating_activities( - filters, company_currency, profit_data, period_list, light_mappers, - operating_activities_mapper, data + filters, + company_currency, + profit_data, + period_list, + light_mappers, + operating_activities_mapper, + data, ) if all(other_mappers): @@ -336,10 +422,17 @@ def compute_data(filters, company_currency, profit_data, period_list, light_mapp def execute(filters=None): - if not filters.periodicity: filters.periodicity = "Monthly" - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.period_start_date, filters.period_end_date, filters.filter_based_on, - filters.periodicity, company=filters.company) + if not filters.periodicity: + filters.periodicity = "Monthly" + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) mappers = get_mappers_from_db() @@ -347,43 +440,72 @@ def execute(filters=None): # compute net profit / loss income = get_data( - filters.company, "Income", "Credit", period_list, filters=filters, - accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True + filters.company, + "Income", + "Credit", + period_list, + filters=filters, + accumulated_values=filters.accumulated_values, + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, ) expense = get_data( - filters.company, "Expense", "Debit", period_list, filters=filters, - accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True + filters.company, + "Expense", + "Debit", + period_list, + filters=filters, + accumulated_values=filters.accumulated_values, + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, ) net_profit_loss = get_net_profit_loss(income, expense, period_list, filters.company) - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") - data = compute_data(filters, company_currency, net_profit_loss, period_list, mappers, cash_flow_accounts) + data = compute_data( + filters, company_currency, net_profit_loss, period_list, mappers, cash_flow_accounts + ) _add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, filters.company + ) return columns, data -def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0): +def _get_account_type_based_data( + filters, account_names, period_list, accumulated_values, opening_balances=0 +): + if not account_names or not account_names[0] or not type(account_names[0]) == str: + # only proceed if account_names is a list of account names + return {} + from erpnext.accounts.report.cash_flow.cash_flow import get_start_date company = filters.company data = {} total = 0 + GLEntry = frappe.qb.DocType("GL Entry") + Account = frappe.qb.DocType("Account") + for period in period_list: start_date = get_start_date(period, accumulated_values, company) - accounts = ', '.join('"%s"' % d for d in account_names) + + account_subquery = ( + frappe.qb.from_(Account) + .where((Account.name.isin(account_names)) | (Account.parent_account.isin(account_names))) + .select(Account.name) + .as_("account_subquery") + ) if opening_balances: date_info = dict(date=start_date) - months_map = {'Monthly': -1, 'Quarterly': -3, 'Half-Yearly': -6} - years_map = {'Yearly': -1} + months_map = {"Monthly": -1, "Quarterly": -3, "Half-Yearly": -6} + years_map = {"Yearly": -1} if months_map.get(filters.periodicity): date_info.update(months=months_map[filters.periodicity]) @@ -391,36 +513,35 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate date_info.update(years=years_map[filters.periodicity]) if accumulated_values: - start, end = add_to_date(start_date, years=-1), add_to_date(period['to_date'], years=-1) + start, end = add_to_date(start_date, years=-1), add_to_date(period["to_date"], years=-1) else: start, end = add_to_date(**date_info), add_to_date(**date_info) - gl_sum = frappe.db.sql_list(""" - select sum(credit) - sum(debit) - from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s - and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE name IN (%s) - OR parent_account IN (%s)) - """, (company, start, end, accounts, accounts)) - else: - gl_sum = frappe.db.sql_list(""" - select sum(credit) - sum(debit) - from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s - and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE name IN (%s) - OR parent_account IN (%s)) - """, (company, start_date if accumulated_values else period['from_date'], - period['to_date'], accounts, accounts)) + start, end = get_date_str(start), get_date_str(end) - if gl_sum and gl_sum[0]: - amount = gl_sum[0] else: - amount = 0 + start, end = start_date if accumulated_values else period["from_date"], period["to_date"] + start, end = get_date_str(start), get_date_str(end) - total += amount - data.setdefault(period["key"], amount) + result = ( + frappe.qb.from_(GLEntry) + .select(Sum(GLEntry.credit) - Sum(GLEntry.debit)) + .where( + (GLEntry.company == company) + & (GLEntry.posting_date >= start) + & (GLEntry.posting_date <= end) + & (GLEntry.voucher_type != "Period Closing Voucher") + & (GLEntry.account.isin(account_subquery)) + ) + ).run() + + if result and result[0]: + gl_sum = result[0][0] + else: + gl_sum = 0 + + total += flt(gl_sum) + data.setdefault(period["key"], flt(gl_sum)) data["total"] = total return data @@ -431,7 +552,7 @@ def _add_total_row_account(out, data, label, period_list, currency, indent=0.0): "indent": indent, "account_name": "'" + _("{0}").format(label) + "'", "account": "'" + _("{0}").format(label) + "'", - "currency": currency + "currency": currency, } for row in data: if row.get("parent_account"): diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 1e20f7be3e4..98dbbf6c449 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -42,26 +42,32 @@ from erpnext.accounts.report.utils import convert, convert_to_presentation_curre def execute(filters=None): columns, data, message, chart = [], [], [], [] - if not filters.get('company'): + if not filters.get("company"): return columns, data, message, chart - fiscal_year = get_fiscal_year_data(filters.get('from_fiscal_year'), filters.get('to_fiscal_year')) + fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year")) companies_column, companies = get_companies(filters) columns = get_columns(companies_column, filters) - if filters.get('report') == "Balance Sheet": - data, message, chart, report_summary = get_balance_sheet_data(fiscal_year, companies, columns, filters) - elif filters.get('report') == "Profit and Loss Statement": - data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters) + if filters.get("report") == "Balance Sheet": + data, message, chart, report_summary = get_balance_sheet_data( + fiscal_year, companies, columns, filters + ) + elif filters.get("report") == "Profit and Loss Statement": + data, message, chart, report_summary = get_profit_loss_data( + fiscal_year, companies, columns, filters + ) else: - if cint(frappe.db.get_single_value('Accounts Settings', 'use_custom_cash_flow')): + if cint(frappe.db.get_single_value("Accounts Settings", "use_custom_cash_flow")): from erpnext.accounts.report.cash_flow.custom_cash_flow import execute as execute_custom + return execute_custom(filters=filters) data, report_summary = get_cash_flow_data(fiscal_year, companies, filters) return columns, data, message, chart, report_summary + def get_balance_sheet_data(fiscal_year, companies, columns, filters): asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters) @@ -75,24 +81,27 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): data.extend(equity or []) company_currency = get_company_currency(filters) - provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity, - companies, filters.get('company'), company_currency, True) + provisional_profit_loss, total_credit = get_provisional_profit_loss( + asset, liability, equity, companies, filters.get("company"), company_currency, True + ) - message, opening_balance = prepare_companywise_opening_balance(asset, liability, equity, companies) + message, opening_balance = prepare_companywise_opening_balance( + asset, liability, equity, companies + ) if opening_balance: unclosed = { "account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "warn_if_negative": True, - "currency": company_currency + "currency": company_currency, } for company in companies: unclosed[company] = opening_balance.get(company) if provisional_profit_loss and provisional_profit_loss.get(company): - provisional_profit_loss[company] = ( - flt(provisional_profit_loss[company]) - flt(opening_balance.get(company)) + provisional_profit_loss[company] = flt(provisional_profit_loss[company]) - flt( + opening_balance.get(company) ) unclosed["total"] = opening_balance.get(company) @@ -103,13 +112,23 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): if total_credit: data.append(total_credit) - report_summary = get_bs_summary(companies, asset, liability, equity, provisional_profit_loss, total_credit, - company_currency, filters, True) + report_summary = get_bs_summary( + companies, + asset, + liability, + equity, + provisional_profit_loss, + total_credit, + company_currency, + filters, + True, + ) chart = get_chart_data(filters, columns, asset, liability, equity) return data, message, chart, report_summary + def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, companies): opening_balance = {} for company in companies: @@ -119,29 +138,36 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, for data in [asset_data, liability_data, equity_data]: if data: account_name = get_root_account_name(data[0].root_type, company) - opening_value += (get_opening_balance(account_name, data, company) or 0.0) + opening_value += get_opening_balance(account_name, data, company) or 0.0 opening_balance[company] = opening_value if opening_balance: return _("Previous Financial Year is not closed"), opening_balance - return '', {} + return "", {} + def get_opening_balance(account_name, data, company): for row in data: - if row.get('account_name') == account_name: - return row.get('company_wise_opening_bal', {}).get(company, 0.0) + if row.get("account_name") == account_name: + return row.get("company_wise_opening_bal", {}).get(company, 0.0) + def get_root_account_name(root_type, company): return frappe.get_all( - 'Account', - fields=['account_name'], - filters = {'root_type': root_type, 'is_group': 1, - 'company': company, 'parent_account': ('is', 'not set')}, - as_list=1 + "Account", + fields=["account_name"], + filters={ + "root_type": root_type, + "is_group": 1, + "company": company, + "parent_account": ("is", "not set"), + }, + as_list=1, )[0][0] + def get_profit_loss_data(fiscal_year, companies, columns, filters): income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters) company_currency = get_company_currency(filters) @@ -154,20 +180,26 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters): chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss) - report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, company_currency, filters, True) + report_summary = get_pl_summary( + companies, "", income, expense, net_profit_loss, company_currency, filters, True + ) return data, None, chart, report_summary + def get_income_expense_data(companies, fiscal_year, filters): company_currency = get_company_currency(filters) income = get_data(companies, "Income", "Credit", fiscal_year, filters, True) expense = get_data(companies, "Expense", "Debit", fiscal_year, filters, True) - net_profit_loss = get_net_profit_loss(income, expense, companies, filters.company, company_currency, True) + net_profit_loss = get_net_profit_loss( + income, expense, companies, filters.company, company_currency, True + ) return income, expense, net_profit_loss + def get_cash_flow_data(fiscal_year, companies, filters): cash_flow_accounts = get_cash_flow_accounts() @@ -179,50 +211,67 @@ def get_cash_flow_data(fiscal_year, companies, filters): for cash_flow_account in cash_flow_accounts: section_data = [] - data.append({ - "account_name": cash_flow_account['section_header'], - "parent_account": None, - "indent": 0.0, - "account": cash_flow_account['section_header'] - }) + data.append( + { + "account_name": cash_flow_account["section_header"], + "parent_account": None, + "indent": 0.0, + "account": cash_flow_account["section_header"], + } + ) if len(data) == 1: # add first net income in operations section if net_profit_loss: - net_profit_loss.update({ - "indent": 1, - "parent_account": cash_flow_accounts[0]['section_header'] - }) + net_profit_loss.update( + {"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]} + ) data.append(net_profit_loss) section_data.append(net_profit_loss) - for account in cash_flow_account['account_types']: - account_data = get_account_type_based_data(account['account_type'], companies, fiscal_year, filters) - account_data.update({ - "account_name": account['label'], - "account": account['label'], - "indent": 1, - "parent_account": cash_flow_account['section_header'], - "currency": company_currency - }) + for account in cash_flow_account["account_types"]: + account_data = get_account_type_based_data( + account["account_type"], companies, fiscal_year, filters + ) + account_data.update( + { + "account_name": account["label"], + "account": account["label"], + "indent": 1, + "parent_account": cash_flow_account["section_header"], + "currency": company_currency, + } + ) data.append(account_data) section_data.append(account_data) - add_total_row_account(data, section_data, cash_flow_account['section_footer'], - companies, company_currency, summary_data, filters, True) + add_total_row_account( + data, + section_data, + cash_flow_account["section_footer"], + companies, + company_currency, + summary_data, + filters, + True, + ) - add_total_row_account(data, data, _("Net Change in Cash"), companies, company_currency, summary_data, filters, True) + add_total_row_account( + data, data, _("Net Change in Cash"), companies, company_currency, summary_data, filters, True + ) report_summary = get_cash_flow_summary(summary_data, company_currency) return data, report_summary + def get_account_type_based_data(account_type, companies, fiscal_year, filters): data = {} total = 0 for company in companies: - amount = get_account_type_based_gl_data(company, - fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters) + amount = get_account_type_based_gl_data( + company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters + ) if amount and account_type == "Depreciation": amount *= -1 @@ -233,6 +282,7 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters): data["total"] = total return data + def get_columns(companies, filters): columns = [ { @@ -240,14 +290,15 @@ def get_columns(companies, filters): "label": _("Account"), "fieldtype": "Link", "options": "Account", - "width": 300 - }, { + "width": 300, + }, + { "fieldname": "currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "hidden": 1 - } + "hidden": 1, + }, ] for company in companies: @@ -256,69 +307,96 @@ def get_columns(companies, filters): if not currency: currency = erpnext.get_company_currency(company) - columns.append({ - "fieldname": company, - "label": f'{company} ({currency})', - "fieldtype": "Currency", - "options": "currency", - "width": 150, - "apply_currency_formatter": apply_currency_formatter, - "company_name": company - }) + columns.append( + { + "fieldname": company, + "label": f"{company} ({currency})", + "fieldtype": "Currency", + "options": "currency", + "width": 150, + "apply_currency_formatter": apply_currency_formatter, + "company_name": company, + } + ) return columns -def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False): - accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, - companies, filters) - if not accounts: return [] +def get_data( + companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False +): + accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, companies, filters) + + if not accounts: + return [] company_currency = get_company_currency(filters) - if filters.filter_based_on == 'Fiscal Year': - start_date = fiscal_year.year_start_date if filters.report != 'Balance Sheet' else None + if filters.filter_based_on == "Fiscal Year": + start_date = fiscal_year.year_start_date if filters.report != "Balance Sheet" else None end_date = fiscal_year.year_end_date else: - start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None + start_date = filters.period_start_date if filters.report != "Balance Sheet" else None end_date = filters.period_end_date filters.end_date = end_date gl_entries_by_account = {} - for root in frappe.db.sql("""select lft, rgt from tabAccount - where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1): + for root in frappe.db.sql( + """select lft, rgt from tabAccount + where root_type=%s and ifnull(parent_account, '') = ''""", + root_type, + as_dict=1, + ): - set_gl_entries_by_account(start_date, - end_date, root.lft, root.rgt, filters, - gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False) + set_gl_entries_by_account( + start_date, + end_date, + root.lft, + root.rgt, + filters, + gl_entries_by_account, + accounts_by_name, + accounts, + ignore_closing_entries=False, + ) calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year) accumulate_values_into_parents(accounts, accounts_by_name, companies) - out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters) + out = prepare_data( + accounts, start_date, end_date, balance_must_be, companies, company_currency, filters + ) - out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values")) + out = filter_out_zero_value_rows( + out, parent_children_map, show_zero_values=filters.get("show_zero_values") + ) if out: add_total_row(out, root_type, balance_must_be, companies, company_currency) return out + def get_company_currency(filters=None): - return (filters.get('presentation_currency') - or frappe.get_cached_value('Company', filters.company, "default_currency")) + return filters.get("presentation_currency") or frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) + def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year): - start_date = (fiscal_year.year_start_date - if filters.filter_based_on == 'Fiscal Year' else filters.period_start_date) + start_date = ( + fiscal_year.year_start_date + if filters.filter_based_on == "Fiscal Year" + else filters.period_start_date + ) for entries in gl_entries_by_account.values(): for entry in entries: if entry.account_number: - account_name = entry.account_number + ' - ' + entry.account_name + account_name = entry.account_number + " - " + entry.account_name else: - account_name = entry.account_name + account_name = entry.account_name d = accounts_by_name.get(account_name) @@ -326,28 +404,34 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters debit, credit = 0, 0 for company in companies: # check if posting date is within the period - if (entry.company == company or (filters.get('accumulated_in_group_company')) - and entry.company in companies.get(company)): + if ( + entry.company == company + or (filters.get("accumulated_in_group_company")) + and entry.company in companies.get(company) + ): parent_company_currency = erpnext.get_company_currency(d.company) child_company_currency = erpnext.get_company_currency(entry.company) debit, credit = flt(entry.debit), flt(entry.credit) - if (not filters.get('presentation_currency') + if ( + not filters.get("presentation_currency") and entry.company != company and parent_company_currency != child_company_currency - and filters.get('accumulated_in_group_company')): + and filters.get("accumulated_in_group_company") + ): debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date) credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date) d[company] = d.get(company, 0.0) + flt(debit) - flt(credit) if entry.posting_date < getdate(start_date): - d['company_wise_opening_bal'][company] += (flt(debit) - flt(credit)) + d["company_wise_opening_bal"][company] += flt(debit) - flt(credit) if entry.posting_date < getdate(start_date): d["opening_balance"] = d.get("opening_balance", 0.0) + flt(debit) - flt(credit) + def accumulate_values_into_parents(accounts, accounts_by_name, companies): """accumulate children's values in parent accounts""" for d in reversed(accounts): @@ -355,13 +439,18 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): account = d.parent_account_name for company in companies: - accounts_by_name[account][company] = \ - accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0) + accounts_by_name[account][company] = accounts_by_name[account].get(company, 0.0) + d.get( + company, 0.0 + ) - accounts_by_name[account]['company_wise_opening_bal'][company] += d.get('company_wise_opening_bal', {}).get(company, 0.0) + accounts_by_name[account]["company_wise_opening_bal"][company] += d.get( + "company_wise_opening_bal", {} + ).get(company, 0.0) + + accounts_by_name[account]["opening_balance"] = accounts_by_name[account].get( + "opening_balance", 0.0 + ) + d.get("opening_balance", 0.0) - accounts_by_name[account]["opening_balance"] = \ - accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) def get_account_heads(root_type, companies, filters): accounts = get_accounts(root_type, companies) @@ -375,20 +464,21 @@ def get_account_heads(root_type, companies, filters): return accounts, accounts_by_name, parent_children_map + def update_parent_account_names(accounts): """Update parent_account_name in accounts list. - parent_name is `name` of parent account which could have other prefix - of account_number and suffix of company abbr. This function adds key called - `parent_account_name` which does not have such prefix/suffix. + parent_name is `name` of parent account which could have other prefix + of account_number and suffix of company abbr. This function adds key called + `parent_account_name` which does not have such prefix/suffix. """ name_to_account_map = {} for d in accounts: if d.account_number: - account_name = d.account_number + ' - ' + d.account_name + account_name = d.account_number + " - " + d.account_name else: - account_name = d.account_name + account_name = d.account_name name_to_account_map[d.name] = account_name for account in accounts: @@ -397,10 +487,11 @@ def update_parent_account_names(accounts): return accounts + def get_companies(filters): companies = {} - all_companies = get_subsidiary_companies(filters.get('company')) - companies.setdefault(filters.get('company'), all_companies) + all_companies = get_subsidiary_companies(filters.get("company")) + companies.setdefault(filters.get("company"), all_companies) for d in all_companies: if d not in companies: @@ -409,47 +500,73 @@ def get_companies(filters): return all_companies, companies -def get_subsidiary_companies(company): - lft, rgt = frappe.get_cached_value('Company', - company, ["lft", "rgt"]) - return frappe.db.sql_list("""select name from `tabCompany` - where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt)) +def get_subsidiary_companies(company): + lft, rgt = frappe.get_cached_value("Company", company, ["lft", "rgt"]) + + return frappe.db.sql_list( + """select name from `tabCompany` + where lft >= {0} and rgt <= {1} order by lft, rgt""".format( + lft, rgt + ) + ) + def get_accounts(root_type, companies): accounts = [] added_accounts = [] for company in companies: - for account in frappe.get_all("Account", fields=["name", "is_group", "company", - "parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"], - filters={"company": company, "root_type": root_type}): + for account in frappe.get_all( + "Account", + fields=[ + "name", + "is_group", + "company", + "parent_account", + "lft", + "rgt", + "root_type", + "report_type", + "account_name", + "account_number", + ], + filters={"company": company, "root_type": root_type}, + ): if account.account_name not in added_accounts: accounts.append(account) added_accounts.append(account.account_name) return accounts -def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters): + +def prepare_data( + accounts, start_date, end_date, balance_must_be, companies, company_currency, filters +): data = [] for d in accounts: # add to output has_value = False total = 0 - row = frappe._dict({ - "account_name": ('%s - %s' %(_(d.account_number), _(d.account_name)) - if d.account_number else _(d.account_name)), - "account": _(d.name), - "parent_account": _(d.parent_account), - "indent": flt(d.indent), - "year_start_date": start_date, - "root_type": d.root_type, - "year_end_date": end_date, - "currency": filters.presentation_currency, - "company_wise_opening_bal": d.company_wise_opening_bal, - "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1) - }) + row = frappe._dict( + { + "account_name": ( + "%s - %s" % (_(d.account_number), _(d.account_name)) + if d.account_number + else _(d.account_name) + ), + "account": _(d.name), + "parent_account": _(d.parent_account), + "indent": flt(d.indent), + "year_start_date": start_date, + "root_type": d.root_type, + "year_end_date": end_date, + "currency": filters.presentation_currency, + "company_wise_opening_bal": d.company_wise_opening_bal, + "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1), + } + ) for company in companies: if d.get(company) and balance_must_be == "Credit": @@ -470,32 +587,49 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com return data -def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, - accounts_by_name, accounts, ignore_closing_entries=False): + +def set_gl_entries_by_account( + from_date, + to_date, + root_lft, + root_rgt, + filters, + gl_entries_by_account, + accounts_by_name, + accounts, + ignore_closing_entries=False, +): """Returns a dict like { "account": [gl entries], ... }""" - company_lft, company_rgt = frappe.get_cached_value('Company', - filters.get('company'), ["lft", "rgt"]) + company_lft, company_rgt = frappe.get_cached_value( + "Company", filters.get("company"), ["lft", "rgt"] + ) additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) - companies = frappe.db.sql(""" select name, default_currency from `tabCompany` - where lft >= %(company_lft)s and rgt <= %(company_rgt)s""", { + companies = frappe.db.sql( + """ select name, default_currency from `tabCompany` + where lft >= %(company_lft)s and rgt <= %(company_rgt)s""", + { "company_lft": company_lft, "company_rgt": company_rgt, - }, as_dict=1) + }, + as_dict=1, + ) - currency_info = frappe._dict({ - 'report_date': to_date, - 'presentation_currency': filters.get('presentation_currency') - }) + currency_info = frappe._dict( + {"report_date": to_date, "presentation_currency": filters.get("presentation_currency")} + ) for d in companies: - gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, + gl_entries = frappe.db.sql( + """select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, acc.account_name, acc.account_number from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0 {additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s - order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions), + order by gl.account, gl.posting_date""".format( + additional_conditions=additional_conditions + ), { "from_date": from_date, "to_date": to_date, @@ -503,29 +637,47 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g "rgt": root_rgt, "company": d.name, "finance_book": filters.get("finance_book"), - "company_fb": frappe.db.get_value("Company", d.name, 'default_finance_book') + "company_fb": frappe.db.get_value("Company", d.name, "default_finance_book"), }, - as_dict=True) + as_dict=True, + ) - if filters and filters.get('presentation_currency') != d.default_currency: - currency_info['company'] = d.name - currency_info['company_currency'] = d.default_currency - convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) + if filters and filters.get("presentation_currency") != d.default_currency: + currency_info["company"] = d.name + currency_info["company_currency"] = d.default_currency + convert_to_presentation_currency(gl_entries, currency_info, filters.get("company")) for entry in gl_entries: if entry.account_number: - account_name = entry.account_number + ' - ' + entry.account_name + account_name = entry.account_number + " - " + entry.account_name else: - account_name = entry.account_name + account_name = entry.account_name validate_entries(account_name, entry, accounts_by_name, accounts) gl_entries_by_account.setdefault(account_name, []).append(entry) return gl_entries_by_account + def get_account_details(account): - return frappe.get_cached_value('Account', account, ['name', 'report_type', 'root_type', 'company', - 'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1) + return frappe.get_cached_value( + "Account", + account, + [ + "name", + "report_type", + "root_type", + "company", + "is_group", + "account_name", + "account_number", + "parent_account", + "lft", + "rgt", + ], + as_dict=1, + ) + def validate_entries(key, entry, accounts_by_name, accounts): # If an account present in the child company and not in the parent company @@ -535,15 +687,17 @@ def validate_entries(key, entry, accounts_by_name, accounts): if args.parent_account: parent_args = get_account_details(args.parent_account) - args.update({ - 'lft': parent_args.lft + 1, - 'rgt': parent_args.rgt - 1, - 'indent': 3, - 'root_type': parent_args.root_type, - 'report_type': parent_args.report_type, - 'parent_account_name': parent_args.account_name, - 'company_wise_opening_bal': defaultdict(float) - }) + args.update( + { + "lft": parent_args.lft + 1, + "rgt": parent_args.rgt - 1, + "indent": 3, + "root_type": parent_args.root_type, + "report_type": parent_args.report_type, + "parent_account_name": parent_args.account_name, + "company_wise_opening_bal": defaultdict(float), + } + ) accounts_by_name.setdefault(key, args) @@ -554,7 +708,8 @@ def validate_entries(key, entry, accounts_by_name, accounts): idx = index break - accounts.insert(idx+1, args) + accounts.insert(idx + 1, args) + def get_additional_conditions(from_date, ignore_closing_entries, filters): additional_conditions = [] @@ -566,17 +721,20 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters): additional_conditions.append("gl.posting_date >= %(from_date)s") if filters.get("include_default_book_entries"): - additional_conditions.append("(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)") + additional_conditions.append( + "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) else: additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" + def add_total_row(out, root_type, balance_must_be, companies, company_currency): total_row = { "account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", "account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", - "currency": company_currency + "currency": company_currency, } for row in out: @@ -595,15 +753,16 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency): # blank row after Total out.append({}) + def filter_accounts(accounts, depth=10): parent_children_map = {} accounts_by_name = {} for d in accounts: if d.account_number: - account_name = d.account_number + ' - ' + d.account_name + account_name = d.account_number + " - " + d.account_name else: - account_name = d.account_name - d['company_wise_opening_bal'] = defaultdict(float) + account_name = d.account_name + d["company_wise_opening_bal"] = defaultdict(float) accounts_by_name[account_name] = d parent_children_map.setdefault(d.parent_account or None, []).append(d) @@ -613,7 +772,7 @@ def filter_accounts(accounts, depth=10): def add_to_list(parent, level): if level < depth: children = parent_children_map.get(parent) or [] - sort_accounts(children, is_root=True if parent==None else False) + sort_accounts(children, is_root=True if parent == None else False) for child in children: child.indent = level diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 29546198464..3beaa2bfe74 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -15,14 +15,16 @@ class PartyLedgerSummaryReport(object): self.filters.to_date = getdate(self.filters.to_date or nowdate()) if not self.filters.get("company"): - self.filters["company"] = frappe.db.get_single_value('Global Defaults', 'default_company') + self.filters["company"] = frappe.db.get_single_value("Global Defaults", "default_company") def run(self, args): if self.filters.from_date > self.filters.to_date: frappe.throw(_("From Date must be before To Date")) self.filters.party_type = args.get("party_type") - self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + self.party_naming_by = frappe.db.get_value( + args.get("naming_by")[0], None, args.get("naming_by")[1] + ) self.get_gl_entries() self.get_return_invoices() @@ -33,21 +35,25 @@ class PartyLedgerSummaryReport(object): return columns, data def get_columns(self): - columns = [{ - "label": _(self.filters.party_type), - "fieldtype": "Link", - "fieldname": "party", - "options": self.filters.party_type, - "width": 200 - }] + columns = [ + { + "label": _(self.filters.party_type), + "fieldtype": "Link", + "fieldname": "party", + "options": self.filters.party_type, + "width": 200, + } + ] if self.party_naming_by == "Naming Series": - columns.append({ - "label": _(self.filters.party_type + "Name"), - "fieldtype": "Data", - "fieldname": "party_name", - "width": 110 - }) + columns.append( + { + "label": _(self.filters.party_type + "Name"), + "fieldtype": "Data", + "fieldname": "party_name", + "width": 110, + } + ) credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note" @@ -57,40 +63,42 @@ class PartyLedgerSummaryReport(object): "fieldname": "opening_balance", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "label": _("Invoiced Amount"), "fieldname": "invoiced_amount", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "label": _(credit_or_debit_note), "fieldname": "return_amount", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, ] for account in self.party_adjustment_accounts: - columns.append({ - "label": account, - "fieldname": "adj_" + scrub(account), - "fieldtype": "Currency", - "options": "currency", - "width": 120, - "is_adjustment": 1 - }) + columns.append( + { + "label": account, + "fieldname": "adj_" + scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + "is_adjustment": 1, + } + ) columns += [ { @@ -98,36 +106,43 @@ class PartyLedgerSummaryReport(object): "fieldname": "closing_balance", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "label": _("Currency"), "fieldname": "currency", "fieldtype": "Link", "options": "Currency", - "width": 50 - } + "width": 50, + }, ] return columns def get_data(self): - company_currency = frappe.get_cached_value('Company', self.filters.get("company"), "default_currency") + company_currency = frappe.get_cached_value( + "Company", self.filters.get("company"), "default_currency" + ) invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" self.party_data = frappe._dict({}) for gle in self.gl_entries: - self.party_data.setdefault(gle.party, frappe._dict({ - "party": gle.party, - "party_name": gle.party_name, - "opening_balance": 0, - "invoiced_amount": 0, - "paid_amount": 0, - "return_amount": 0, - "closing_balance": 0, - "currency": company_currency - })) + self.party_data.setdefault( + gle.party, + frappe._dict( + { + "party": gle.party, + "party_name": gle.party_name, + "opening_balance": 0, + "invoiced_amount": 0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 0, + "currency": company_currency, + } + ), + ) amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) self.party_data[gle.party].closing_balance += amount @@ -144,8 +159,16 @@ class PartyLedgerSummaryReport(object): out = [] for party, row in iteritems(self.party_data): - if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount: - total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {}))) + if ( + row.opening_balance + or row.invoiced_amount + or row.paid_amount + or row.return_amount + or row.closing_amount + ): + total_party_adjustment = sum( + amount for amount in itervalues(self.party_adjustment_details.get(party, {})) + ) row.paid_amount -= total_party_adjustment adjustments = self.party_adjustment_details.get(party, {}) @@ -166,7 +189,8 @@ class PartyLedgerSummaryReport(object): join_field = ", p.supplier_name as party_name" join = "left join `tabSupplier` p on gle.party = p.name" - self.gl_entries = frappe.db.sql(""" + self.gl_entries = frappe.db.sql( + """ select gle.posting_date, gle.party, gle.voucher_type, gle.voucher_no, gle.against_voucher_type, gle.against_voucher, gle.debit, gle.credit, gle.is_opening {join_field} @@ -176,7 +200,12 @@ class PartyLedgerSummaryReport(object): gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' and gle.posting_date <= %(to_date)s {conditions} order by gle.posting_date - """.format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True) + """.format( + join=join, join_field=join_field, conditions=conditions + ), + self.filters, + as_dict=True, + ) def prepare_conditions(self): conditions = [""] @@ -192,57 +221,88 @@ class PartyLedgerSummaryReport(object): if self.filters.party_type == "Customer": if self.filters.get("customer_group"): - lft, rgt = frappe.db.get_value("Customer Group", - self.filters.get("customer_group"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value( + "Customer Group", self.filters.get("customer_group"), ["lft", "rgt"] + ) - conditions.append("""party in (select name from tabCustomer + conditions.append( + """party in (select name from tabCustomer where exists(select name from `tabCustomer Group` where lft >= {0} and rgt <= {1} - and name=tabCustomer.customer_group))""".format(lft, rgt)) + and name=tabCustomer.customer_group))""".format( + lft, rgt + ) + ) if self.filters.get("territory"): - lft, rgt = frappe.db.get_value("Territory", - self.filters.get("territory"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value("Territory", self.filters.get("territory"), ["lft", "rgt"]) - conditions.append("""party in (select name from tabCustomer + conditions.append( + """party in (select name from tabCustomer where exists(select name from `tabTerritory` where lft >= {0} and rgt <= {1} - and name=tabCustomer.territory))""".format(lft, rgt)) + and name=tabCustomer.territory))""".format( + lft, rgt + ) + ) if self.filters.get("payment_terms_template"): - conditions.append("party in (select name from tabCustomer where payment_terms=%(payment_terms_template)s)") + conditions.append( + "party in (select name from tabCustomer where payment_terms=%(payment_terms_template)s)" + ) if self.filters.get("sales_partner"): - conditions.append("party in (select name from tabCustomer where default_sales_partner=%(sales_partner)s)") + conditions.append( + "party in (select name from tabCustomer where default_sales_partner=%(sales_partner)s)" + ) if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value( + "Sales Person", self.filters.get("sales_person"), ["lft", "rgt"] + ) - conditions.append("""exists(select name from `tabSales Team` steam where + conditions.append( + """exists(select name from `tabSales Team` steam where steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) and ((steam.parent = voucher_no and steam.parenttype = voucher_type) or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) + or (steam.parent = party and steam.parenttype = 'Customer')))""".format( + lft, rgt + ) + ) if self.filters.party_type == "Supplier": if self.filters.get("supplier_group"): - conditions.append("""party in (select name from tabSupplier - where supplier_group=%(supplier_group)s)""") + conditions.append( + """party in (select name from tabSupplier + where supplier_group=%(supplier_group)s)""" + ) return " and ".join(conditions) def get_return_invoices(self): doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice" - self.return_invoices = [d.name for d in frappe.get_all(doctype, filters={"is_return": 1, "docstatus": 1, - "posting_date": ["between", [self.filters.from_date, self.filters.to_date]]})] + self.return_invoices = [ + d.name + for d in frappe.get_all( + doctype, + filters={ + "is_return": 1, + "docstatus": 1, + "posting_date": ["between", [self.filters.from_date, self.filters.to_date]], + }, + ) + ] def get_party_adjustment_amounts(self): conditions = self.prepare_conditions() - income_or_expense = "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + income_or_expense = ( + "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + ) invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" - round_off_account = frappe.get_cached_value('Company', self.filters.company, "round_off_account") + round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account") - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select posting_date, account, party, voucher_type, voucher_no, debit, credit from @@ -258,7 +318,12 @@ class PartyLedgerSummaryReport(object): where gle.party_type=%(party_type)s and ifnull(party, '') != '' and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions} ) - """.format(conditions=conditions, income_or_expense=income_or_expense), self.filters, as_dict=True) + """.format( + conditions=conditions, income_or_expense=income_or_expense + ), + self.filters, + as_dict=True, + ) self.party_adjustment_details = {} self.party_adjustment_accounts = set() @@ -300,6 +365,7 @@ class PartyLedgerSummaryReport(object): self.party_adjustment_details[party].setdefault(account, 0) self.party_adjustment_details[party][account] += amount + def execute(filters=None): args = { "party_type": "Customer", diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index 3a51db8a97f..1eb257ac853 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -361,7 +361,8 @@ class Deferred_Revenue_and_Expense_Report(object): "fieldname": period.key, "fieldtype": "Currency", "read_only": 1, - }) + } + ) return columns def generate_report_data(self): @@ -408,11 +409,9 @@ class Deferred_Revenue_and_Expense_Report(object): } if self.filters.with_upcoming_postings: - chart["data"]["datasets"].append({ - "name": "Expected", - "chartType": "line", - "values": [x.total for x in self.period_total] - }) + chart["data"]["datasets"].append( + {"name": "Expected", "chartType": "line", "values": [x.total for x in self.period_total]} + ) return chart diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index 86eb2134fe8..023ff225eea 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -88,10 +88,12 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): posting_date="2021-05-01", parent_cost_center="Main - _CD", cost_center="Main - _CD", - do_not_submit=True, + do_not_save=True, rate=300, price_list_rate=300, ) + + si.items[0].income_account = "Sales - _CD" si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2021-05-01" si.items[0].service_end_date = "2021-08-01" @@ -269,11 +271,13 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): posting_date="2021-05-01", parent_cost_center="Main - _CD", cost_center="Main - _CD", - do_not_submit=True, + do_not_save=True, rate=300, price_list_rate=300, ) + si.items[0].enable_deferred_revenue = 1 + si.items[0].income_account = "Sales - _CD" si.items[0].deferred_revenue_account = deferred_revenue_account si.items[0].income_account = "Sales - _CD" si.save() @@ -318,6 +322,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): ] self.assertEqual(report.period_total, expected) + def create_company(): company = frappe.db.exists("Company", "_Test Company DR") if not company: diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py index 004d09250ab..59914dc29ac 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py @@ -13,6 +13,7 @@ def execute(filters=None): data = get_ordered_to_be_billed_data(args) return columns, data + def get_column(): return [ { @@ -20,90 +21,76 @@ def get_column(): "fieldname": "name", "fieldtype": "Link", "options": "Delivery Note", - "width": 160 - }, - { - "label": _("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 100 + "width": 160, }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 100}, { "label": _("Customer"), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", - "width": 120 - }, - { - "label": _("Customer Name"), - "fieldname": "customer_name", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120}, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 120 + "width": 120, }, { "label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 100, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Billed Amount"), "fieldname": "billed_amount", "fieldtype": "Currency", "width": 100, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Returned Amount"), "fieldname": "returned_amount", "fieldtype": "Currency", "width": 120, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Pending Amount"), "fieldname": "pending_amount", "fieldtype": "Currency", "width": 120, - "options": "Company:company:default_currency" - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 120 + "options": "Company:company:default_currency", }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 120}, { "label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 - } + "width": 120, + }, ] + def get_args(): - return {'doctype': 'Delivery Note', 'party': 'customer', - 'date': 'posting_date', 'order': 'name', 'order_by': 'desc'} + return { + "doctype": "Delivery Note", + "party": "customer", + "date": "posting_date", + "order": "name", + "order_by": "desc", + } diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js index 6a0394861b8..ea05a35b259 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js @@ -39,12 +39,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("From Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_start_date"), + "reqd": 1 }, { "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_end_date"), + "reqd": 1 }, { "fieldname": "finance_book", @@ -56,6 +58,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldname": "dimension", "label": __("Select Dimension"), "fieldtype": "Select", + "default": "Cost Center", "options": get_accounting_dimension_options(), "reqd": 1, }, @@ -70,7 +73,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { }); function get_accounting_dimension_options() { - let options =["", "Cost Center", "Project"]; + let options =["Cost Center", "Project"]; frappe.db.get_list('Accounting Dimension', {fields:['document_type']}).then((res) => { res.forEach((dimension) => { diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index c69bb3f70c5..9bc4c4b71da 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -16,21 +16,24 @@ from erpnext.accounts.report.trial_balance.trial_balance import validate_filters def execute(filters=None): - validate_filters(filters) - dimension_items_list = get_dimension_items_list(filters.dimension, filters.company) - if not dimension_items_list: + validate_filters(filters) + dimension_list = get_dimensions(filters) + + if not dimension_list: return [], [] - dimension_items_list = [''.join(d) for d in dimension_items_list] - columns = get_columns(dimension_items_list) - data = get_data(filters, dimension_items_list) + columns = get_columns(dimension_list) + data = get_data(filters, dimension_list) return columns, data -def get_data(filters, dimension_items_list): + +def get_data(filters, dimension_list): company_currency = erpnext.get_company_currency(filters.company) - acc = frappe.db.sql(""" + + acc = frappe.db.sql( + """ select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group @@ -38,88 +41,104 @@ def get_data(filters, dimension_items_list): `tabAccount` where company=%s - order by lft""", (filters.company), as_dict=True) + order by lft""", + (filters.company), + as_dict=True, + ) if not acc: return None accounts, accounts_by_name, parent_children_map = filter_accounts(acc) - min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount` - where company=%s""", (filters.company))[0] + min_lft, max_rgt = frappe.db.sql( + """select min(lft), max(rgt) from `tabAccount` + where company=%s""", + (filters.company), + )[0] - account = frappe.db.sql_list("""select name from `tabAccount` - where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company)) + account = frappe.db.sql_list( + """select name from `tabAccount` + where lft >= %s and rgt <= %s and company = %s""", + (min_lft, max_rgt, filters.company), + ) gl_entries_by_account = {} - set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account) - format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list) - accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list) - out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list) + set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account) + format_gl_entries( + gl_entries_by_account, accounts_by_name, dimension_list, frappe.scrub(filters.get("dimension")) + ) + accumulate_values_into_parents(accounts, accounts_by_name, dimension_list) + out = prepare_data(accounts, filters, company_currency, dimension_list) out = filter_out_zero_value_rows(out, parent_children_map) return out -def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account): - for item in dimension_items_list: - condition = get_condition(filters.from_date, item, filters.dimension) - if account: - condition += " and account in ({})"\ - .format(", ".join([frappe.db.escape(d) for d in account])) - gl_filters = { - "company": filters.get("company"), - "from_date": filters.get("from_date"), - "to_date": filters.get("to_date"), - "finance_book": cstr(filters.get("finance_book")) - } +def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account): + condition = get_condition(filters.get("dimension")) - gl_filters['item'] = ''.join(item) + if account: + condition += " and account in ({})".format(", ".join([frappe.db.escape(d) for d in account])) - if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value("Company", - filters.company, 'default_finance_book') + gl_filters = { + "company": filters.get("company"), + "from_date": filters.get("from_date"), + "to_date": filters.get("to_date"), + "finance_book": cstr(filters.get("finance_book")), + } - for key, value in filters.items(): - if value: - gl_filters.update({ - key: value - }) + gl_filters["dimensions"] = set(dimension_list) - gl_entries = frappe.db.sql(""" + if filters.get("include_default_book_entries"): + gl_filters["company_fb"] = frappe.db.get_value( + "Company", filters.company, "default_finance_book" + ) + + gl_entries = frappe.db.sql( + """ select - posting_date, account, debit, credit, is_opening, fiscal_year, + posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` where company=%(company)s {condition} + and posting_date >= %(from_date)s and posting_date <= %(to_date)s and is_cancelled = 0 order by account, posting_date""".format( - condition=condition), - gl_filters, as_dict=True) #nosec + dimension=frappe.scrub(filters.get("dimension")), condition=condition + ), + gl_filters, + as_dict=True, + ) # nosec - for entry in gl_entries: - entry['dimension_item'] = ''.join(item) - gl_entries_by_account.setdefault(entry.account, []).append(entry) + for entry in gl_entries: + gl_entries_by_account.setdefault(entry.account, []).append(entry) -def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list): + +def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type): for entries in itervalues(gl_entries_by_account): for entry in entries: d = accounts_by_name.get(entry.account) if not d: frappe.msgprint( - _("Could not retrieve information for {0}.").format(entry.account), title="Error", - raise_exception=1 + _("Could not retrieve information for {0}.").format(entry.account), + title="Error", + raise_exception=1, ) - for item in dimension_items_list: - if item == entry.dimension_item: - d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit) -def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list): + for dimension in dimension_list: + if dimension == entry.get(dimension_type): + d[frappe.scrub(dimension)] = ( + d.get(frappe.scrub(dimension), 0.0) + flt(entry.debit) - flt(entry.credit) + ) + + +def prepare_data(accounts, filters, company_currency, dimension_list): data = [] for d in accounts: @@ -132,17 +151,18 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen "from_date": filters.from_date, "to_date": filters.to_date, "currency": company_currency, - "account_name": ('{} - {}'.format(d.account_number, d.account_name) - if d.account_number else d.account_name) + "account_name": ( + "{} - {}".format(d.account_number, d.account_name) if d.account_number else d.account_name + ), } - for item in dimension_items_list: - row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3) + for dimension in dimension_list: + row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3) - if abs(row[frappe.scrub(item)]) >= 0.005: + if abs(row[frappe.scrub(dimension)]) >= 0.005: # ignore zero values has_value = True - total += flt(d.get(frappe.scrub(item), 0.0), 3) + total += flt(d.get(frappe.scrub(dimension), 0.0), 3) row["has_value"] = has_value row["total"] = total @@ -150,68 +170,72 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen return data -def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list): + +def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: - for item in dimension_items_list: - accounts_by_name[d.parent_account][frappe.scrub(item)] = \ - accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0) + for dimension in dimension_list: + accounts_by_name[d.parent_account][frappe.scrub(dimension)] = accounts_by_name[ + d.parent_account + ].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0) -def get_condition(from_date, item, dimension): + +def get_condition(dimension): conditions = [] - if from_date: - conditions.append("posting_date >= %(from_date)s") - if dimension: - if dimension not in ['Cost Center', 'Project']: - if dimension in ['Customer', 'Supplier']: - dimension = 'Party' - else: - dimension = 'Voucher No' - txt = "{0} = %(item)s".format(frappe.scrub(dimension)) - conditions.append(txt) + conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension))) return " and {}".format(" and ".join(conditions)) if conditions else "" -def get_dimension_items_list(dimension, company): - meta = frappe.get_meta(dimension, cached=False) - fieldnames = [d.fieldname for d in meta.get("fields")] - filters = {} - if 'company' in fieldnames: - filters['company'] = company - return frappe.get_all(dimension, filters, as_list=True) -def get_columns(dimension_items_list, accumulated_values=1, company=None): - columns = [{ - "fieldname": "account", - "label": _("Account"), - "fieldtype": "Link", - "options": "Account", - "width": 300 - }] - if company: - columns.append({ +def get_dimensions(filters): + meta = frappe.get_meta(filters.get("dimension"), cached=False) + query_filters = {} + + if meta.has_field("company"): + query_filters = {"company": filters.get("company")} + + return frappe.get_all(filters.get("dimension"), filters=query_filters, pluck="name") + + +def get_columns(dimension_list): + columns = [ + { + "fieldname": "account", + "label": _("Account"), + "fieldtype": "Link", + "options": "Account", + "width": 300, + }, + { "fieldname": "currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "hidden": 1 - }) - for item in dimension_items_list: - columns.append({ - "fieldname": frappe.scrub(item), - "label": item, - "fieldtype": "Currency", - "options": "currency", - "width": 150 - }) - columns.append({ + "hidden": 1, + }, + ] + + for dimension in dimension_list: + columns.append( + { + "fieldname": frappe.scrub(dimension), + "label": dimension, + "fieldtype": "Currency", + "options": "currency", + "width": 150, + } + ) + + columns.append( + { "fieldname": "total", "label": "Total", "fieldtype": "Currency", "options": "currency", - "width": 150 - }) + "width": 150, + } + ) return columns diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 03afd1e5268..e1c9f55917b 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -1,4 +1,3 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt @@ -21,12 +20,22 @@ from erpnext.accounts.report.utils import convert_to_presentation_currency, get_ from erpnext.accounts.utils import get_fiscal_year -def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_end_date, filter_based_on, periodicity, accumulated_values=False, - company=None, reset_period_on_fy_change=True, ignore_fiscal_year=False): +def get_period_list( + from_fiscal_year, + to_fiscal_year, + period_start_date, + period_end_date, + filter_based_on, + periodicity, + accumulated_values=False, + company=None, + reset_period_on_fy_change=True, + ignore_fiscal_year=False, +): """Get a list of dict {"from_date": from_date, "to_date": to_date, "key": key, "label": label} - Periodicity can be (Yearly, Quarterly, Monthly)""" + Periodicity can be (Yearly, Quarterly, Monthly)""" - if filter_based_on == 'Fiscal Year': + if filter_based_on == "Fiscal Year": fiscal_year = get_fiscal_year_data(from_fiscal_year, to_fiscal_year) validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year) year_start_date = getdate(fiscal_year.year_start_date) @@ -36,12 +45,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ year_start_date = getdate(period_start_date) year_end_date = getdate(period_end_date) - months_to_add = { - "Yearly": 12, - "Half-Yearly": 6, - "Quarterly": 3, - "Monthly": 1 - }[periodicity] + months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity] period_list = [] @@ -49,11 +53,9 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ months = get_months(year_start_date, year_end_date) for i in range(cint(math.ceil(months / months_to_add))): - period = frappe._dict({ - "from_date": start_date - }) + period = frappe._dict({"from_date": start_date}) - if i==0 and filter_based_on == 'Date Range': + if i == 0 and filter_based_on == "Date Range": to_date = add_months(get_first_day(start_date), months_to_add) else: to_date = add_months(start_date, months_to_add) @@ -93,32 +95,38 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ else: label = get_label(periodicity, period_list[0].from_date, opts["to_date"]) - opts.update({ - "key": key.replace(" ", "_").replace("-", "_"), - "label": label, - "year_start_date": year_start_date, - "year_end_date": year_end_date - }) + opts.update( + { + "key": key.replace(" ", "_").replace("-", "_"), + "label": label, + "year_start_date": year_start_date, + "year_end_date": year_end_date, + } + ) return period_list def get_fiscal_year_data(from_fiscal_year, to_fiscal_year): - fiscal_year = frappe.db.sql("""select min(year_start_date) as year_start_date, + fiscal_year = frappe.db.sql( + """select min(year_start_date) as year_start_date, max(year_end_date) as year_end_date from `tabFiscal Year` where name between %(from_fiscal_year)s and %(to_fiscal_year)s""", - {'from_fiscal_year': from_fiscal_year, 'to_fiscal_year': to_fiscal_year}, as_dict=1) + {"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year}, + as_dict=1, + ) return fiscal_year[0] if fiscal_year else {} def validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year): - if not fiscal_year.get('year_start_date') or not fiscal_year.get('year_end_date'): + if not fiscal_year.get("year_start_date") or not fiscal_year.get("year_end_date"): frappe.throw(_("Start Year and End Year are mandatory")) - if getdate(fiscal_year.get('year_end_date')) < getdate(fiscal_year.get('year_start_date')): + if getdate(fiscal_year.get("year_end_date")) < getdate(fiscal_year.get("year_start_date")): frappe.throw(_("End Year cannot be before Start Year")) + def validate_dates(from_date, to_date): if not from_date or not to_date: frappe.throw(_("From Date and To Date are mandatory")) @@ -126,6 +134,7 @@ def validate_dates(from_date, to_date): if to_date < from_date: frappe.throw(_("To Date cannot be less than From Date")) + def get_months(start_date, end_date): diff = (12 * end_date.year + end_date.month) - (12 * start_date.year + start_date.month) return diff + 1 @@ -144,9 +153,17 @@ def get_label(periodicity, from_date, to_date): def get_data( - company, root_type, balance_must_be, period_list, filters=None, - accumulated_values=1, only_current_fiscal_year=True, ignore_closing_entries=False, - ignore_accumulated_values_for_fy=False , total = True): + company, + root_type, + balance_must_be, + period_list, + filters=None, + accumulated_values=1, + only_current_fiscal_year=True, + ignore_closing_entries=False, + ignore_accumulated_values_for_fy=False, + total=True, +): accounts = get_accounts(company, root_type) if not accounts: @@ -157,19 +174,31 @@ def get_data( company_currency = get_appropriate_currency(company, filters) gl_entries_by_account = {} - for root in frappe.db.sql("""select lft, rgt from tabAccount - where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1): + for root in frappe.db.sql( + """select lft, rgt from tabAccount + where root_type=%s and ifnull(parent_account, '') = ''""", + root_type, + as_dict=1, + ): set_gl_entries_by_account( company, period_list[0]["year_start_date"] if only_current_fiscal_year else None, period_list[-1]["to_date"], - root.lft, root.rgt, filters, - gl_entries_by_account, ignore_closing_entries=ignore_closing_entries + root.lft, + root.rgt, + filters, + gl_entries_by_account, + ignore_closing_entries=ignore_closing_entries, ) calculate_values( - accounts_by_name, gl_entries_by_account, period_list, accumulated_values, ignore_accumulated_values_for_fy) + accounts_by_name, + gl_entries_by_account, + period_list, + accumulated_values, + ignore_accumulated_values_for_fy, + ) accumulate_values_into_parents(accounts, accounts_by_name, period_list) out = prepare_data(accounts, balance_must_be, period_list, company_currency) out = filter_out_zero_value_rows(out, parent_children_map) @@ -184,26 +213,32 @@ def get_appropriate_currency(company, filters=None): if filters and filters.get("presentation_currency"): return filters["presentation_currency"] else: - return frappe.get_cached_value('Company', company, "default_currency") + return frappe.get_cached_value("Company", company, "default_currency") def calculate_values( - accounts_by_name, gl_entries_by_account, period_list, accumulated_values, ignore_accumulated_values_for_fy): + accounts_by_name, + gl_entries_by_account, + period_list, + accumulated_values, + ignore_accumulated_values_for_fy, +): for entries in itervalues(gl_entries_by_account): for entry in entries: d = accounts_by_name.get(entry.account) if not d: frappe.msgprint( - _("Could not retrieve information for {0}.").format(entry.account), title="Error", - raise_exception=1 + _("Could not retrieve information for {0}.").format(entry.account), + title="Error", + raise_exception=1, ) for period in period_list: # check if posting date is within the period if entry.posting_date <= period.to_date: - if (accumulated_values or entry.posting_date >= period.from_date) and \ - (not ignore_accumulated_values_for_fy or - entry.fiscal_year == period.to_date_fiscal_year): + if (accumulated_values or entry.posting_date >= period.from_date) and ( + not ignore_accumulated_values_for_fy or entry.fiscal_year == period.to_date_fiscal_year + ): d[period.key] = d.get(period.key, 0.0) + flt(entry.debit) - flt(entry.credit) if entry.posting_date < period_list[0].year_start_date: @@ -215,11 +250,13 @@ def accumulate_values_into_parents(accounts, accounts_by_name, period_list): for d in reversed(accounts): if d.parent_account: for period in period_list: - accounts_by_name[d.parent_account][period.key] = \ - accounts_by_name[d.parent_account].get(period.key, 0.0) + d.get(period.key, 0.0) + accounts_by_name[d.parent_account][period.key] = accounts_by_name[d.parent_account].get( + period.key, 0.0 + ) + d.get(period.key, 0.0) - accounts_by_name[d.parent_account]["opening_balance"] = \ - accounts_by_name[d.parent_account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) + accounts_by_name[d.parent_account]["opening_balance"] = accounts_by_name[d.parent_account].get( + "opening_balance", 0.0 + ) + d.get("opening_balance", 0.0) def prepare_data(accounts, balance_must_be, period_list, company_currency): @@ -231,20 +268,25 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency): # add to output has_value = False total = 0 - row = frappe._dict({ - "account": _(d.name), - "parent_account": _(d.parent_account) if d.parent_account else '', - "indent": flt(d.indent), - "year_start_date": year_start_date, - "year_end_date": year_end_date, - "currency": company_currency, - "include_in_gross": d.include_in_gross, - "account_type": d.account_type, - "is_group": d.is_group, - "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be=="Debit" else -1), - "account_name": ('%s - %s' %(_(d.account_number), _(d.account_name)) - if d.account_number else _(d.account_name)) - }) + row = frappe._dict( + { + "account": _(d.name), + "parent_account": _(d.parent_account) if d.parent_account else "", + "indent": flt(d.indent), + "year_start_date": year_start_date, + "year_end_date": year_end_date, + "currency": company_currency, + "include_in_gross": d.include_in_gross, + "account_type": d.account_type, + "is_group": d.is_group, + "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1), + "account_name": ( + "%s - %s" % (_(d.account_number), _(d.account_name)) + if d.account_number + else _(d.account_name) + ), + } + ) for period in period_list: if d.get(period.key) and balance_must_be == "Credit": # change sign based on Debit or Credit, since calculation is done using (debit - credit) @@ -286,7 +328,7 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "currency": company_currency, - "opening_balance": 0.0 + "opening_balance": 0.0, } for row in out: @@ -309,10 +351,14 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency def get_accounts(company, root_type): - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group, lft, rgt from `tabAccount` - where company=%s and root_type=%s order by lft""", (company, root_type), as_dict=True) + where company=%s and root_type=%s order by lft""", + (company, root_type), + as_dict=True, + ) def filter_accounts(accounts, depth=20): @@ -327,7 +373,7 @@ def filter_accounts(accounts, depth=20): def add_to_list(parent, level): if level < depth: children = parent_children_map.get(parent) or [] - sort_accounts(children, is_root=True if parent==None else False) + sort_accounts(children, is_root=True if parent == None else False) for child in children: child.indent = level @@ -343,7 +389,7 @@ def sort_accounts(accounts, is_root=False, key="name"): """Sort root types as Asset, Liability, Equity, Income, Expense""" def compare_accounts(a, b): - if re.split(r'\W+', a[key])[0].isdigit(): + if re.split(r"\W+", a[key])[0].isdigit(): # if chart of accounts is numbered, then sort by number return cmp(a[key], b[key]) elif is_root: @@ -360,40 +406,50 @@ def sort_accounts(accounts, is_root=False, key="name"): return cmp(a[key], b[key]) return 1 - accounts.sort(key = functools.cmp_to_key(compare_accounts)) + accounts.sort(key=functools.cmp_to_key(compare_accounts)) + def set_gl_entries_by_account( - company, from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, ignore_closing_entries=False): + company, + from_date, + to_date, + root_lft, + root_rgt, + filters, + gl_entries_by_account, + ignore_closing_entries=False, +): """Returns a dict like { "account": [gl entries], ... }""" additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) - accounts = frappe.db.sql_list("""select name from `tabAccount` - where lft >= %s and rgt <= %s and company = %s""", (root_lft, root_rgt, company)) + accounts = frappe.db.sql_list( + """select name from `tabAccount` + where lft >= %s and rgt <= %s and company = %s""", + (root_lft, root_rgt, company), + ) if accounts: - additional_conditions += " and account in ({})"\ - .format(", ".join(frappe.db.escape(d) for d in accounts)) + additional_conditions += " and account in ({})".format( + ", ".join(frappe.db.escape(d) for d in accounts) + ) gl_filters = { "company": company, "from_date": from_date, "to_date": to_date, - "finance_book": cstr(filters.get("finance_book")) + "finance_book": cstr(filters.get("finance_book")), } if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value("Company", - company, 'default_finance_book') + gl_filters["company_fb"] = frappe.db.get_value("Company", company, "default_finance_book") for key, value in filters.items(): if value: - gl_filters.update({ - key: value - }) + gl_filters.update({key: value}) distributed_cost_center_query = "" - if filters and filters.get('cost_center'): + if filters and filters.get("cost_center"): distributed_cost_center_query = """ UNION ALL SELECT posting_date, @@ -418,19 +474,26 @@ def set_gl_entries_by_account( AND posting_date <= %(to_date)s AND is_cancelled = 0 AND cost_center = DCC_allocation.parent - """.format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", '')) + """.format( + additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", "") + ) - gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` + gl_entries = frappe.db.sql( + """select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` where company=%(company)s {additional_conditions} and posting_date <= %(to_date)s and is_cancelled = 0 {distributed_cost_center_query}""".format( additional_conditions=additional_conditions, - distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec + distributed_cost_center_query=distributed_cost_center_query, + ), + gl_filters, + as_dict=True, + ) # nosec - if filters and filters.get('presentation_currency'): - convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company')) + if filters and filters.get("presentation_currency"): + convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get("company")) for entry in gl_entries: gl_entries_by_account.setdefault(entry.account, []).append(entry) @@ -461,25 +524,29 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters): additional_conditions.append("cost_center in %(cost_center)s") if filters.get("include_default_book_entries"): - additional_conditions.append("(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)") + additional_conditions.append( + "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) else: additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") if accounting_dimensions: for dimension in accounting_dimensions: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) additional_conditions.append("{0} in %({0})s".format(dimension.fieldname)) else: additional_conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" + def get_cost_centers_with_children(cost_centers): if not isinstance(cost_centers, list): - cost_centers = [d.strip() for d in cost_centers.strip().split(',') if d] + cost_centers = [d.strip() for d in cost_centers.strip().split(",") if d] all_cost_centers = [] for d in cost_centers: @@ -492,45 +559,50 @@ def get_cost_centers_with_children(cost_centers): return list(set(all_cost_centers)) + def get_columns(periodicity, period_list, accumulated_values=1, company=None): - columns = [{ - "fieldname": "account", - "label": _("Account"), - "fieldtype": "Link", - "options": "Account", - "width": 300 - }] - if company: - columns.append({ - "fieldname": "currency", - "label": _("Currency"), + columns = [ + { + "fieldname": "account", + "label": _("Account"), "fieldtype": "Link", - "options": "Currency", - "hidden": 1 - }) + "options": "Account", + "width": 300, + } + ] + if company: + columns.append( + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1, + } + ) for period in period_list: - columns.append({ - "fieldname": period.key, - "label": period.label, - "fieldtype": "Currency", - "options": "currency", - "width": 150 - }) - if periodicity!="Yearly": - if not accumulated_values: - columns.append({ - "fieldname": "total", - "label": _("Total"), + columns.append( + { + "fieldname": period.key, + "label": period.label, "fieldtype": "Currency", - "width": 150 - }) + "options": "currency", + "width": 150, + } + ) + if periodicity != "Yearly": + if not accumulated_values: + columns.append( + {"fieldname": "total", "label": _("Total"), "fieldtype": "Currency", "width": 150} + ) return columns + def get_filtered_list_for_consolidated_report(filters, period_list): filtered_summary_list = [] for period in period_list: - if period == filters.get('company'): + if period == filters.get("company"): filtered_summary_list.append(period) return filtered_summary_list diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 452a60d3055..85b4d153e9f 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -21,20 +21,20 @@ from erpnext.accounts.utils import get_account_currency # to cache translations TRANSLATIONS = frappe._dict() + def execute(filters=None): if not filters: return [], [] account_details = {} - if filters and filters.get('print_in_account_currency') and \ - not filters.get('account'): + if filters and filters.get("print_in_account_currency") and not filters.get("account"): frappe.throw(_("Select an account to print in account currency")) for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): account_details.setdefault(acc.name, acc) - if filters.get('party'): + if filters.get("party"): filters.party = frappe.parse_json(filters.get("party")) validate_filters(filters, account_details) @@ -51,46 +51,45 @@ def execute(filters=None): return columns, res + def update_translations(): TRANSLATIONS.update( - dict( - OPENING = _('Opening'), - TOTAL = _('Total'), - CLOSING_TOTAL = _('Closing (Opening + Total)') - ) + dict(OPENING=_("Opening"), TOTAL=_("Total"), CLOSING_TOTAL=_("Closing (Opening + Total)")) ) + def validate_filters(filters, account_details): if not filters.get("company"): frappe.throw(_("{0} is mandatory").format(_("Company"))) if not filters.get("from_date") and not filters.get("to_date"): - frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) + frappe.throw( + _("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))) + ) - if filters.get('account'): - filters.account = frappe.parse_json(filters.get('account')) + if filters.get("account"): + filters.account = frappe.parse_json(filters.get("account")) for account in filters.account: if not account_details.get(account): frappe.throw(_("Account {0} does not exists").format(account)) - if (filters.get("account") and filters.get("group_by") == 'Group by Account'): - filters.account = frappe.parse_json(filters.get('account')) + if filters.get("account") and filters.get("group_by") == "Group by Account": + filters.account = frappe.parse_json(filters.get("account")) for account in filters.account: if account_details[account].is_group == 0: frappe.throw(_("Can not filter based on Child Account, if grouped by Account")) - if (filters.get("voucher_no") - and filters.get("group_by") in ['Group by Voucher']): + if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]: frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher")) if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) - if filters.get('project'): - filters.project = frappe.parse_json(filters.get('project')) + if filters.get("project"): + filters.project = frappe.parse_json(filters.get("project")) - if filters.get('cost_center'): - filters.cost_center = frappe.parse_json(filters.get('cost_center')) + if filters.get("cost_center"): + filters.cost_center = frappe.parse_json(filters.get("cost_center")) def validate_party(filters): @@ -101,9 +100,12 @@ def validate_party(filters): if not frappe.db.exists(party_type, d): frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) + def set_account_currency(filters): - if filters.get("account") or (filters.get('party') and len(filters.party) == 1): - filters["company_currency"] = frappe.get_cached_value('Company', filters.company, "default_currency") + if filters.get("account") or (filters.get("party") and len(filters.party) == 1): + filters["company_currency"] = frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) account_currency = None if filters.get("account"): @@ -122,17 +124,19 @@ def set_account_currency(filters): elif filters.get("party"): gle_currency = frappe.db.get_value( - "GL Entry", { - "party_type": filters.party_type, "party": filters.party[0], "company": filters.company - }, - "account_currency" + "GL Entry", + {"party_type": filters.party_type, "party": filters.party[0], "company": filters.company}, + "account_currency", ) if gle_currency: account_currency = gle_currency else: - account_currency = (None if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] else - frappe.db.get_value(filters.party_type, filters.party[0], "default_currency")) + account_currency = ( + None + if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] + else frappe.db.get_value(filters.party_type, filters.party[0], "default_currency") + ) filters["account_currency"] = account_currency or filters.company_currency if filters.account_currency != filters.company_currency and not filters.presentation_currency: @@ -140,6 +144,7 @@ def set_account_currency(filters): return filters + def get_result(filters, account_details): accounting_dimensions = [] if filters.get("include_dimensions"): @@ -147,13 +152,13 @@ def get_result(filters, account_details): gl_entries = get_gl_entries(filters, accounting_dimensions) - data = get_data_with_opening_closing(filters, account_details, - accounting_dimensions, gl_entries) + data = get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries) result = get_result_as_list(data, filters) return result + def get_gl_entries(filters, accounting_dimensions): currency_map = get_currency(filters) select_fields = """, debit, credit, debit_in_account_currency, @@ -170,15 +175,16 @@ def get_gl_entries(filters, accounting_dimensions): order_by_statement = "order by account, posting_date, creation" if filters.get("include_default_book_entries"): - filters['company_fb'] = frappe.db.get_value("Company", - filters.get("company"), 'default_finance_book') + filters["company_fb"] = frappe.db.get_value( + "Company", filters.get("company"), "default_finance_book" + ) dimension_fields = "" if accounting_dimensions: - dimension_fields = ', '.join(accounting_dimensions) + ',' + dimension_fields = ", ".join(accounting_dimensions) + "," distributed_cost_center_query = "" - if filters and filters.get('cost_center'): + if filters and filters.get("cost_center"): select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, @@ -211,7 +217,11 @@ def get_gl_entries(filters, accounting_dimensions): {conditions} AND posting_date <= %(to_date)s AND cost_center = DCC_allocation.parent - """.format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) + """.format( + dimension_fields=dimension_fields, + select_fields_with_percentage=select_fields_with_percentage, + conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ""), + ) gl_entries = frappe.db.sql( """ @@ -226,13 +236,18 @@ def get_gl_entries(filters, accounting_dimensions): {distributed_cost_center_query} {order_by_statement} """.format( - dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, - order_by_statement=order_by_statement + dimension_fields=dimension_fields, + select_fields=select_fields, + conditions=get_conditions(filters), + distributed_cost_center_query=distributed_cost_center_query, + order_by_statement=order_by_statement, ), - filters, as_dict=1) + filters, + as_dict=1, + ) - if filters.get('presentation_currency'): - return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) + if filters.get("presentation_currency"): + return convert_to_presentation_currency(gl_entries, currency_map, filters.get("company")) else: return gl_entries @@ -260,8 +275,11 @@ def get_conditions(filters): if filters.get("party"): conditions.append("party in %(party)s") - if not (filters.get("account") or filters.get("party") or - filters.get("group_by") in ["Group by Account", "Group by Party"]): + if not ( + filters.get("account") + or filters.get("party") + or filters.get("group_by") in ["Group by Account", "Group by Party"] + ): conditions.append("posting_date >=%(from_date)s") conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") @@ -271,7 +289,9 @@ def get_conditions(filters): if filters.get("finance_book"): if filters.get("include_default_book_entries"): - conditions.append("(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)") + conditions.append( + "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) else: conditions.append("finance_book in (%(finance_book)s)") @@ -279,6 +299,7 @@ def get_conditions(filters): conditions.append("is_cancelled = 0") from frappe.desk.reportview import build_match_conditions + match_conditions = build_match_conditions("GL Entry") if match_conditions: @@ -291,18 +312,20 @@ def get_conditions(filters): for dimension in accounting_dimensions: if not dimension.disabled: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) conditions.append("{0} in %({0})s".format(dimension.fieldname)) else: conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) return "and {}".format(" and ".join(conditions)) if conditions else "" + def get_accounts_with_children(accounts): if not isinstance(accounts, list): - accounts = [d.strip() for d in accounts.strip().split(',') if d] + accounts = [d.strip() for d in accounts.strip().split(",") if d] all_accounts = [] for d in accounts: @@ -315,6 +338,7 @@ def get_accounts_with_children(accounts): return list(set(all_accounts)) + def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): data = [] @@ -325,7 +349,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension # Opening for filtered account data.append(totals.opening) - if filters.get("group_by") != 'Group by Voucher (Consolidated)': + if filters.get("group_by") != "Group by Voucher (Consolidated)": for acc, acc_dict in iteritems(gle_map): # acc if acc_dict.entries: @@ -354,6 +378,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension return data + def get_totals_dict(): def _get_debit_credit_dict(label): return _dict( @@ -361,25 +386,28 @@ def get_totals_dict(): debit=0.0, credit=0.0, debit_in_account_currency=0.0, - credit_in_account_currency=0.0 + credit_in_account_currency=0.0, ) + return _dict( - opening = _get_debit_credit_dict(TRANSLATIONS.OPENING), - total = _get_debit_credit_dict(TRANSLATIONS.TOTAL), - closing = _get_debit_credit_dict(TRANSLATIONS.CLOSING_TOTAL) + opening=_get_debit_credit_dict(TRANSLATIONS.OPENING), + total=_get_debit_credit_dict(TRANSLATIONS.TOTAL), + closing=_get_debit_credit_dict(TRANSLATIONS.CLOSING_TOTAL), ) + def group_by_field(group_by): - if group_by == 'Group by Party': - return 'party' - elif group_by in ['Group by Voucher (Consolidated)', 'Group by Account']: - return 'account' + if group_by == "Group by Party": + return "party" + elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]: + return "account" else: - return 'voucher_no' + return "voucher_no" + def initialize_gle_map(gl_entries, filters): gle_map = OrderedDict() - group_by = group_by_field(filters.get('group_by')) + group_by = group_by_field(filters.get("group_by")) for gle in gl_entries: gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[])) @@ -390,11 +418,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): totals = get_totals_dict() entries = [] consolidated_gle = OrderedDict() - group_by = group_by_field(filters.get('group_by')) - group_by_voucher_consolidated = filters.get("group_by") == 'Group by Voucher (Consolidated)' + group_by = group_by_field(filters.get("group_by")) + group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)" - if filters.get('show_net_values_in_party_account'): - account_type_map = get_account_type_map(filters.get('company')) + if filters.get("show_net_values_in_party_account"): + account_type_map = get_account_type_map(filters.get("company")) def update_value_in_dict(data, key, gle): data[key].debit += gle.debit @@ -403,26 +431,28 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): data[key].debit_in_account_currency += gle.debit_in_account_currency data[key].credit_in_account_currency += gle.credit_in_account_currency - if filters.get('show_net_values_in_party_account') and \ - account_type_map.get(data[key].account) in ('Receivable', 'Payable'): + if filters.get("show_net_values_in_party_account") and account_type_map.get( + data[key].account + ) in ("Receivable", "Payable"): net_value = data[key].debit - data[key].credit - net_value_in_account_currency = data[key].debit_in_account_currency \ - - data[key].credit_in_account_currency + net_value_in_account_currency = ( + data[key].debit_in_account_currency - data[key].credit_in_account_currency + ) if net_value < 0: - dr_or_cr = 'credit' - rev_dr_or_cr = 'debit' + dr_or_cr = "credit" + rev_dr_or_cr = "debit" else: - dr_or_cr = 'debit' - rev_dr_or_cr = 'credit' + dr_or_cr = "debit" + rev_dr_or_cr = "credit" data[key][dr_or_cr] = abs(net_value) - data[key][dr_or_cr+'_in_account_currency'] = abs(net_value_in_account_currency) + data[key][dr_or_cr + "_in_account_currency"] = abs(net_value_in_account_currency) data[key][rev_dr_or_cr] = 0 - data[key][rev_dr_or_cr+'_in_account_currency'] = 0 + data[key][rev_dr_or_cr + "_in_account_currency"] = 0 if data[key].against_voucher and gle.against_voucher: - data[key].against_voucher += ', ' + gle.against_voucher + data[key].against_voucher += ", " + gle.against_voucher from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) show_opening_entries = filters.get("show_opening_entries") @@ -430,25 +460,31 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): for gle in gl_entries: group_by_value = gle.get(group_by) - if (gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries)): + if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries): if not group_by_voucher_consolidated: - update_value_in_dict(gle_map[group_by_value].totals, 'opening', gle) - update_value_in_dict(gle_map[group_by_value].totals, 'closing', gle) + update_value_in_dict(gle_map[group_by_value].totals, "opening", gle) + update_value_in_dict(gle_map[group_by_value].totals, "closing", gle) - update_value_in_dict(totals, 'opening', gle) - update_value_in_dict(totals, 'closing', gle) + update_value_in_dict(totals, "opening", gle) + update_value_in_dict(totals, "closing", gle) elif gle.posting_date <= to_date: if not group_by_voucher_consolidated: - update_value_in_dict(gle_map[group_by_value].totals, 'total', gle) - update_value_in_dict(gle_map[group_by_value].totals, 'closing', gle) - update_value_in_dict(totals, 'total', gle) - update_value_in_dict(totals, 'closing', gle) + update_value_in_dict(gle_map[group_by_value].totals, "total", gle) + update_value_in_dict(gle_map[group_by_value].totals, "closing", gle) + update_value_in_dict(totals, "total", gle) + update_value_in_dict(totals, "closing", gle) gle_map[group_by_value].entries.append(gle) elif group_by_voucher_consolidated: - keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] + keylist = [ + gle.get("voucher_type"), + gle.get("voucher_no"), + gle.get("account"), + gle.get("party_type"), + gle.get("party"), + ] if filters.get("include_dimensions"): for dim in accounting_dimensions: keylist.append(gle.get(dim)) @@ -461,47 +497,58 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): update_value_in_dict(consolidated_gle, key, gle) for key, value in consolidated_gle.items(): - update_value_in_dict(totals, 'total', value) - update_value_in_dict(totals, 'closing', value) + update_value_in_dict(totals, "total", value) + update_value_in_dict(totals, "closing", value) entries.append(value) return totals, entries + def get_account_type_map(company): - account_type_map = frappe._dict(frappe.get_all('Account', fields=['name', 'account_type'], - filters={'company': company}, as_list=1)) + account_type_map = frappe._dict( + frappe.get_all( + "Account", fields=["name", "account_type"], filters={"company": company}, as_list=1 + ) + ) return account_type_map + def get_result_as_list(data, filters): balance, balance_in_account_currency = 0, 0 inv_details = get_supplier_invoice_details() for d in data: - if not d.get('posting_date'): + if not d.get("posting_date"): balance, balance_in_account_currency = 0, 0 - balance = get_balance(d, balance, 'debit', 'credit') - d['balance'] = balance + balance = get_balance(d, balance, "debit", "credit") + d["balance"] = balance - d['account_currency'] = filters.account_currency - d['bill_no'] = inv_details.get(d.get('against_voucher'), '') + d["account_currency"] = filters.account_currency + d["bill_no"] = inv_details.get(d.get("against_voucher"), "") return data + def get_supplier_invoice_details(): inv_details = {} - for d in frappe.db.sql(""" select name, bill_no from `tabPurchase Invoice` - where docstatus = 1 and bill_no is not null and bill_no != '' """, as_dict=1): + for d in frappe.db.sql( + """ select name, bill_no from `tabPurchase Invoice` + where docstatus = 1 and bill_no is not null and bill_no != '' """, + as_dict=1, + ): inv_details[d.name] = d.bill_no return inv_details + def get_balance(row, balance, debit_field, credit_field): - balance += (row.get(debit_field, 0) - row.get(credit_field, 0)) + balance += row.get(debit_field, 0) - row.get(credit_field, 0) return balance + def get_columns(filters): if filters.get("presentation_currency"): currency = filters["presentation_currency"] @@ -518,116 +565,75 @@ def get_columns(filters): "fieldname": "gl_entry", "fieldtype": "Link", "options": "GL Entry", - "hidden": 1 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 90 + "hidden": 1, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, { "label": _("Account"), "fieldname": "account", "fieldtype": "Link", "options": "Account", - "width": 180 + "width": 180, }, { "label": _("Debit ({0})").format(currency), "fieldname": "debit", "fieldtype": "Float", - "width": 100 + "width": 100, }, { "label": _("Credit ({0})").format(currency), "fieldname": "credit", "fieldtype": "Float", - "width": 100 + "width": 100, }, { "label": _("Balance ({0})").format(currency), "fieldname": "balance", "fieldtype": "Float", - "width": 130 - } + "width": 130, + }, ] - columns.extend([ - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": 120 - }, - { - "label": _("Voucher No"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 180 - }, - { - "label": _("Against Account"), - "fieldname": "against", - "width": 120 - }, - { - "label": _("Party Type"), - "fieldname": "party_type", - "width": 100 - }, - { - "label": _("Party"), - "fieldname": "party", - "width": 100 - }, - { - "label": _("Project"), - "options": "Project", - "fieldname": "project", - "width": 100 - } - ]) + columns.extend( + [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120}, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 180, + }, + {"label": _("Against Account"), "fieldname": "against", "width": 120}, + {"label": _("Party Type"), "fieldname": "party_type", "width": 100}, + {"label": _("Party"), "fieldname": "party", "width": 100}, + {"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}, + ] + ) if filters.get("include_dimensions"): - for dim in get_accounting_dimensions(as_list = False): - columns.append({ - "label": _(dim.label), - "options": dim.label, - "fieldname": dim.fieldname, - "width": 100 - }) - columns.append({ - "label": _("Cost Center"), - "options": "Cost Center", - "fieldname": "cost_center", - "width": 100 - }) + for dim in get_accounting_dimensions(as_list=False): + columns.append( + {"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100} + ) + columns.append( + {"label": _("Cost Center"), "options": "Cost Center", "fieldname": "cost_center", "width": 100} + ) - columns.extend([ - { - "label": _("Against Voucher Type"), - "fieldname": "against_voucher_type", - "width": 100 - }, - { - "label": _("Against Voucher"), - "fieldname": "against_voucher", - "fieldtype": "Dynamic Link", - "options": "against_voucher_type", - "width": 100 - }, - { - "label": _("Supplier Invoice No"), - "fieldname": "bill_no", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Remarks"), - "fieldname": "remarks", - "width": 400 - } - ]) + columns.extend( + [ + {"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100}, + { + "label": _("Against Voucher"), + "fieldname": "against_voucher", + "fieldtype": "Dynamic Link", + "options": "against_voucher_type", + "width": 100, + }, + {"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100}, + {"label": _("Remarks"), "fieldname": "remarks", "width": 400}, + ] + ) return columns diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py new file mode 100644 index 00000000000..b10e7696187 --- /dev/null +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -0,0 +1,152 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.general_ledger.general_ledger import execute + + +class TestGeneralLedger(FrappeTestCase): + def test_foreign_account_balance_after_exchange_rate_revaluation(self): + """ + Checks the correctness of balance after exchange rate revaluation + """ + # create a new account with USD currency + account_name = "Test USD Account for Revalutation" + company = "_Test Company" + account = frappe.get_doc( + { + "account_name": account_name, + "is_group": 0, + "company": company, + "root_type": "Asset", + "report_type": "Balance Sheet", + "account_currency": "USD", + "inter_company_account": 0, + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + "doctype": "Account", + } + ) + account.insert(ignore_if_duplicate=True) + # create a JV to debit 1000 USD at 75 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set( + "accounts", + [ + { + "account": account.name, + "debit_in_account_currency": 1000, + "credit_in_account_currency": 0, + "exchange_rate": 75, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 75000, + "cost_center": "_Test Cost Center - _TC", + }, + ], + ) + jv.save() + jv.submit() + # create a JV to credit 900 USD at 100 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set( + "accounts", + [ + { + "account": account.name, + "debit_in_account_currency": 0, + "credit_in_account_currency": 900, + "exchange_rate": 100, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 90000, + "credit_in_account_currency": 0, + "cost_center": "_Test Cost Center - _TC", + }, + ], + ) + jv.save() + jv.submit() + + # create an exchange rate revaluation entry at 77 exchange rate + revaluation = frappe.new_doc("Exchange Rate Revaluation") + revaluation.posting_date = today() + revaluation.company = company + revaluation.set( + "accounts", + [ + { + "account": account.name, + "account_currency": "USD", + "new_exchange_rate": 77, + "new_balance_in_base_currency": 7700, + "balance_in_base_currency": -15000, + "balance_in_account_currency": 100, + "current_exchange_rate": -150, + } + ], + ) + revaluation.save() + revaluation.submit() + + # post journal entry to revaluate + frappe.db.set_value( + "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" + ) + revaluation_jv = revaluation.make_jv_entry() + revaluation_jv = frappe.get_doc(revaluation_jv) + revaluation_jv.cost_center = "_Test Cost Center - _TC" + for acc in revaluation_jv.get("accounts"): + acc.cost_center = "_Test Cost Center - _TC" + revaluation_jv.save() + revaluation_jv.submit() + + # check the balance of the account + balance = frappe.db.sql( + """ + select sum(debit_in_account_currency) - sum(credit_in_account_currency) + from `tabGL Entry` + where account = %s + group by account + """, + account.name, + ) + + self.assertEqual(balance[0][0], 100) + + # check if general ledger shows correct balance + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + } + ) + ) + + self.assertEqual(data[1]["account"], account.name) + self.assertEqual(data[1]["debit"], 1000) + self.assertEqual(data[1]["credit"], 0) + self.assertEqual(data[2]["debit"], 0) + self.assertEqual(data[2]["credit"], 900) + self.assertEqual(data[3]["debit"], 100) + self.assertEqual(data[3]["credit"], 100) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index b18b940fd2b..9d566785416 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -12,30 +12,57 @@ from erpnext.accounts.report.financial_statements import get_columns, get_data, def execute(filters=None): - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, filters.period_start_date, - filters.period_end_date, filters.filter_based_on, filters.periodicity, filters.accumulated_values, filters.company) + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + filters.accumulated_values, + filters.company, + ) columns, data = [], [] - income = get_data(filters.company, "Income", "Credit", period_list, filters = filters, + income = get_data( + filters.company, + "Income", + "Credit", + period_list, + filters=filters, accumulated_values=filters.accumulated_values, - ignore_closing_entries=True, ignore_accumulated_values_for_fy= True, total= False) + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + total=False, + ) - expense = get_data(filters.company, "Expense", "Debit", period_list, filters=filters, + expense = get_data( + filters.company, + "Expense", + "Debit", + period_list, + filters=filters, accumulated_values=filters.accumulated_values, - ignore_closing_entries=True, ignore_accumulated_values_for_fy= True, total= False) - - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + total=False, + ) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, filters.company + ) gross_income = get_revenue(income, period_list) gross_expense = get_revenue(expense, period_list) - if(len(gross_income)==0 and len(gross_expense)== 0): - data.append({ - "account_name": "'" + _("Nothing is included in gross") + "'", - "account": "'" + _("Nothing is included in gross") + "'" - }) + if len(gross_income) == 0 and len(gross_expense) == 0: + data.append( + { + "account_name": "'" + _("Nothing is included in gross") + "'", + "account": "'" + _("Nothing is included in gross") + "'", + } + ) return columns, data # to avoid error eg: gross_income[0] : list index out of range @@ -44,10 +71,12 @@ def execute(filters=None): if not gross_expense: gross_expense = [{}] - data.append({ - "account_name": "'" + _("Included in Gross Profit") + "'", - "account": "'" + _("Included in Gross Profit") + "'" - }) + data.append( + { + "account_name": "'" + _("Included in Gross Profit") + "'", + "account": "'" + _("Included in Gross Profit") + "'", + } + ) data.append({}) data.extend(gross_income or []) @@ -56,7 +85,14 @@ def execute(filters=None): data.extend(gross_expense or []) data.append({}) - gross_profit = get_profit(gross_income, gross_expense, period_list, filters.company, 'Gross Profit',filters.presentation_currency) + gross_profit = get_profit( + gross_income, + gross_expense, + period_list, + filters.company, + "Gross Profit", + filters.presentation_currency, + ) data.append(gross_profit) non_gross_income = get_revenue(income, period_list, 0) @@ -67,28 +103,40 @@ def execute(filters=None): data.append({}) data.extend(non_gross_expense or []) - net_profit = get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expense, period_list, filters.company,filters.presentation_currency) + net_profit = get_net_profit( + non_gross_income, + gross_income, + gross_expense, + non_gross_expense, + period_list, + filters.company, + filters.presentation_currency, + ) data.append({}) data.append(net_profit) return columns, data -def get_revenue(data, period_list, include_in_gross=1): - revenue = [item for item in data if item['include_in_gross']==include_in_gross or item['is_group']==1] - data_to_be_removed =True +def get_revenue(data, period_list, include_in_gross=1): + revenue = [ + item for item in data if item["include_in_gross"] == include_in_gross or item["is_group"] == 1 + ] + + data_to_be_removed = True while data_to_be_removed: revenue, data_to_be_removed = remove_parent_with_no_child(revenue, period_list) revenue = adjust_account(revenue, period_list) return copy.deepcopy(revenue) + def remove_parent_with_no_child(data, period_list): data_to_be_removed = False for parent in data: - if 'is_group' in parent and parent.get("is_group") == 1: + if "is_group" in parent and parent.get("is_group") == 1: have_child = False for child in data: - if 'parent_account' in child and child.get("parent_account") == parent.get("account"): + if "parent_account" in child and child.get("parent_account") == parent.get("account"): have_child = True break @@ -98,8 +146,9 @@ def remove_parent_with_no_child(data, period_list): return data, data_to_be_removed -def adjust_account(data, period_list, consolidated= False): - leaf_nodes = [item for item in data if item['is_group'] == 0] + +def adjust_account(data, period_list, consolidated=False): + leaf_nodes = [item for item in data if item["is_group"] == 0] totals = {} for node in leaf_nodes: set_total(node, node["total"], data, totals) @@ -107,25 +156,30 @@ def adjust_account(data, period_list, consolidated= False): for period in period_list: key = period if consolidated else period.key d[key] = totals[d["account"]] - d['total'] = totals[d["account"]] + d["total"] = totals[d["account"]] return data + def set_total(node, value, complete_list, totals): - if not totals.get(node['account']): + if not totals.get(node["account"]): totals[node["account"]] = 0 totals[node["account"]] += value - parent = node['parent_account'] - if not parent == '': - return set_total(next(item for item in complete_list if item['account'] == parent), value, complete_list, totals) + parent = node["parent_account"] + if not parent == "": + return set_total( + next(item for item in complete_list if item["account"] == parent), value, complete_list, totals + ) -def get_profit(gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False): +def get_profit( + gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False +): profit_loss = { "account_name": "'" + _(profit_type) + "'", "account": "'" + _(profit_type) + "'", "warn_if_negative": True, - "currency": currency or frappe.get_cached_value('Company', company, "default_currency") + "currency": currency or frappe.get_cached_value("Company", company, "default_currency"), } has_value = False @@ -137,17 +191,27 @@ def get_profit(gross_income, gross_expense, period_list, company, profit_type, c profit_loss[key] = gross_income_for_period - gross_expense_for_period if profit_loss[key]: - has_value=True + has_value = True if has_value: return profit_loss -def get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expense, period_list, company, currency=None, consolidated=False): + +def get_net_profit( + non_gross_income, + gross_income, + gross_expense, + non_gross_expense, + period_list, + company, + currency=None, + consolidated=False, +): profit_loss = { "account_name": "'" + _("Net Profit") + "'", "account": "'" + _("Net Profit") + "'", "warn_if_negative": True, - "currency": currency or frappe.get_cached_value('Company', company, "default_currency") + "currency": currency or frappe.get_cached_value("Company", company, "default_currency"), } has_value = False @@ -165,7 +229,7 @@ def get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expe profit_loss[key] = flt(total_income) - flt(total_expense) if profit_loss[key]: - has_value=True + has_value = True if has_value: return profit_loss diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 2ba649da07f..158ff4d3437 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = { "label": __("Company"), "fieldtype": "Link", "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 }, { "fieldname":"from_date", "label": __("From Date"), "fieldtype": "Date", - "default": frappe.defaults.get_user_default("year_start_date") + "default": frappe.defaults.get_user_default("year_start_date"), + "reqd": 1 }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", - "default": frappe.defaults.get_user_default("year_end_date") + "default": frappe.defaults.get_user_default("year_end_date"), + "reqd": 1 }, { "fieldname":"sales_invoice", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 76c560ad247..0730ffd77e5 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,5 +1,5 @@ { - "add_total_row": 0, + "add_total_row": 1, "columns": [], "creation": "2013-02-25 17:03:34", "disable_prepared_report": 0, @@ -9,7 +9,7 @@ "filters": [], "idx": 3, "is_standard": "Yes", - "modified": "2021-11-13 19:14:23.730198", + "modified": "2022-02-11 10:18:36.956558", "modified_by": "Administrator", "module": "Accounts", "name": "Gross Profit", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index a8b5a0e28bd..2bc5208993d 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -11,38 +11,125 @@ from erpnext.stock.utils import get_incoming_rate def execute(filters=None): - if not filters: filters = frappe._dict() - filters.currency = frappe.get_cached_value('Company', filters.company, "default_currency") + if not filters: + filters = frappe._dict() + filters.currency = frappe.get_cached_value("Company", filters.company, "default_currency") gross_profit_data = GrossProfitGenerator(filters) data = [] - group_wise_columns = frappe._dict({ - "invoice": ["invoice_or_item", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", - "warehouse", "qty", "base_rate", "buying_rate", "base_amount", - "buying_amount", "gross_profit", "gross_profit_percent", "project"], - "item_code": ["item_code", "item_name", "brand", "description", "qty", "base_rate", - "buying_rate", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], - "warehouse": ["warehouse", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "brand": ["brand", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "item_group": ["item_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "customer": ["customer", "customer_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "customer_group": ["customer_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "sales_person": ["sales_person", "allocated_amount", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], - "territory": ["territory", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"] - }) + group_wise_columns = frappe._dict( + { + "invoice": [ + "invoice_or_item", + "customer", + "customer_group", + "posting_date", + "item_code", + "item_name", + "item_group", + "brand", + "description", + "warehouse", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + "project", + ], + "item_code": [ + "item_code", + "item_name", + "brand", + "description", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "warehouse": [ + "warehouse", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "brand": [ + "brand", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "item_group": [ + "item_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "customer": [ + "customer", + "customer_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "customer_group": [ + "customer_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "sales_person": [ + "sales_person", + "allocated_amount", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], + "territory": [ + "territory", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + } + ) columns = get_columns(group_wise_columns, filters) - if filters.group_by == 'Invoice': + if filters.group_by == "Invoice": get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data) else: @@ -50,11 +137,14 @@ def execute(filters=None): return columns, data -def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data): + +def get_data_when_grouped_by_invoice( + columns, gross_profit_data, filters, group_wise_columns, data +): column_names = get_column_names() # to display item as Item Code: Item Name - columns[0] = 'Sales Invoice:Link/Item:300' + columns[0] = "Sales Invoice:Link/Item:300" # removing Item Code and Item Name columns del columns[4:6] @@ -69,80 +159,207 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_ data.append(row) + def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data): - for idx, src in enumerate(gross_profit_data.grouped_data): + for src in gross_profit_data.grouped_data: row = [] for col in group_wise_columns.get(scrub(filters.group_by)): row.append(src.get(col)) row.append(filters.currency) - if idx == len(gross_profit_data.grouped_data)-1: - row[0] = "Total" data.append(row) + def get_columns(group_wise_columns, filters): columns = [] - column_map = frappe._dict({ - "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date:100", - "posting_time": _("Posting Time") + ":Data:100", - "item_code": _("Item Code") + ":Link/Item:100", - "item_name": _("Item Name") + ":Data:100", - "item_group": _("Item Group") + ":Link/Item Group:100", - "brand": _("Brand") + ":Link/Brand:100", - "description": _("Description") +":Data:100", - "warehouse": _("Warehouse") + ":Link/Warehouse:100", - "qty": _("Qty") + ":Float:80", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", - "buying_rate": _("Valuation Rate") + ":Currency/currency:100", - "base_amount": _("Selling Amount") + ":Currency/currency:100", - "buying_amount": _("Buying Amount") + ":Currency/currency:100", - "gross_profit": _("Gross Profit") + ":Currency/currency:100", - "gross_profit_percent": _("Gross Profit %") + ":Percent:100", - "project": _("Project") + ":Link/Project:100", - "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", - "customer": _("Customer") + ":Link/Customer:100", - "customer_group": _("Customer Group") + ":Link/Customer Group:100", - "territory": _("Territory") + ":Link/Territory:100" - }) + column_map = frappe._dict( + { + "parent": { + "label": _("Sales Invoice"), + "fieldname": "parent_invoice", + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, + }, + "invoice_or_item": { + "label": _("Sales Invoice"), + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, + }, + "posting_date": { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + "posting_time": { + "label": _("Posting Time"), + "fieldname": "posting_time", + "fieldtype": "Data", + "width": 100, + }, + "item_code": { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + "item_name": { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 100, + }, + "item_group": { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + "brand": {"label": _("Brand"), "fieldtype": "Link", "options": "Brand", "width": 100}, + "description": { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "width": 100, + }, + "warehouse": { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "warehouse", + "width": 100, + }, + "qty": {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80}, + "base_rate": { + "label": _("Avg. Selling Rate"), + "fieldname": "avg._selling_rate", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "buying_rate": { + "label": _("Valuation Rate"), + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "base_amount": { + "label": _("Selling Amount"), + "fieldname": "selling_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "buying_amount": { + "label": _("Buying Amount"), + "fieldname": "buying_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "gross_profit": { + "label": _("Gross Profit"), + "fieldname": "gross_profit", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "gross_profit_percent": { + "label": _("Gross Profit Percent"), + "fieldname": "gross_profit_%", + "fieldtype": "Percent", + "width": 100, + }, + "project": { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 100, + }, + "sales_person": { + "label": _("Sales Person"), + "fieldname": "sales_person", + "fieldtype": "Data", + "width": 100, + }, + "allocated_amount": { + "label": _("Allocated Amount"), + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "customer": { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 100, + }, + "customer_group": { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "customer", + "width": 100, + }, + "territory": { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "territory", + "width": 100, + }, + } + ) for col in group_wise_columns.get(scrub(filters.group_by)): columns.append(column_map.get(col)) - columns.append({ - "fieldname": "currency", - "label" : _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "hidden": 1 - }) + columns.append( + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1, + } + ) return columns + def get_column_names(): - return frappe._dict({ - 'invoice_or_item': 'sales_invoice', - 'customer': 'customer', - 'customer_group': 'customer_group', - 'posting_date': 'posting_date', - 'item_code': 'item_code', - 'item_name': 'item_name', - 'item_group': 'item_group', - 'brand': 'brand', - 'description': 'description', - 'warehouse': 'warehouse', - 'qty': 'qty', - 'base_rate': 'avg._selling_rate', - 'buying_rate': 'valuation_rate', - 'base_amount': 'selling_amount', - 'buying_amount': 'buying_amount', - 'gross_profit': 'gross_profit', - 'gross_profit_percent': 'gross_profit_%', - 'project': 'project' - }) + return frappe._dict( + { + "invoice_or_item": "sales_invoice", + "customer": "customer", + "customer_group": "customer_group", + "posting_date": "posting_date", + "item_code": "item_code", + "item_name": "item_name", + "item_group": "item_group", + "brand": "brand", + "description": "description", + "warehouse": "warehouse", + "qty": "qty", + "base_rate": "avg._selling_rate", + "buying_rate": "valuation_rate", + "base_amount": "selling_amount", + "buying_amount": "buying_amount", + "gross_profit": "gross_profit", + "gross_profit_percent": "gross_profit_%", + "project": "project", + } + ) + class GrossProfitGenerator(object): def __init__(self, filters=None): @@ -151,7 +368,7 @@ class GrossProfitGenerator(object): self.filters = frappe._dict(filters) self.load_invoice_items() - if filters.group_by == 'Invoice': + if filters.group_by == "Invoice": self.group_items_by_invoice() self.load_stock_ledger_entries() @@ -173,7 +390,7 @@ class GrossProfitGenerator(object): buying_amount = 0 for row in reversed(self.si_list): - if self.skip_row(row, self.product_bundles): + if self.skip_row(row): continue row.base_amount = flt(row.base_net_amount, self.currency_precision) @@ -182,17 +399,19 @@ class GrossProfitGenerator(object): if row.update_stock: product_bundles = self.product_bundles.get(row.parenttype, {}).get(row.parent, frappe._dict()) elif row.dn_detail: - product_bundles = self.product_bundles.get("Delivery Note", {})\ - .get(row.delivery_note, frappe._dict()) + product_bundles = self.product_bundles.get("Delivery Note", {}).get( + row.delivery_note, frappe._dict() + ) row.item_row = row.dn_detail # get buying amount if row.item_code in product_bundles: - row.buying_amount = flt(self.get_buying_amount_from_product_bundle(row, - product_bundles[row.item_code]), self.currency_precision) + row.buying_amount = flt( + self.get_buying_amount_from_product_bundle(row, product_bundles[row.item_code]), + self.currency_precision, + ) else: - row.buying_amount = flt(self.get_buying_amount(row, row.item_code), - self.currency_precision) + row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision) if grouped_by_invoice: if row.indent == 1.0: @@ -212,7 +431,9 @@ class GrossProfitGenerator(object): # calculate gross profit row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision) if row.base_amount: - row.gross_profit_percent = flt((row.gross_profit / row.base_amount) * 100.0, self.currency_precision) + row.gross_profit_percent = flt( + (row.gross_profit / row.base_amount) * 100.0, self.currency_precision + ) else: row.gross_profit_percent = 0.0 @@ -223,20 +444,10 @@ class GrossProfitGenerator(object): self.get_average_rate_based_on_group_by() def get_average_rate_based_on_group_by(self): - # sum buying / selling totals for group - self.totals = frappe._dict( - qty=0, - base_amount=0, - buying_amount=0, - gross_profit=0, - gross_profit_percent=0, - base_rate=0, - buying_rate=0 - ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): - if i==0: + if i == 0: new_row = row else: new_row.qty += flt(row.qty) @@ -244,55 +455,53 @@ class GrossProfitGenerator(object): new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) - self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): if row.indent == 1.0: - if row.parent in self.returned_invoices \ - and row.item_code in self.returned_invoices[row.parent]: + if ( + row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent] + ): returned_item_rows = self.returned_invoices[row.parent][row.item_code] for returned_item_row in returned_item_rows: row.qty += flt(returned_item_row.qty) row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) - if (flt(row.qty) or row.base_amount): + if flt(row.qty) or row.base_amount: row = self.set_average_rate(row) self.grouped_data.append(row) - self.add_to_totals(row) - - self.set_average_gross_profit(self.totals) - - if self.filters.get("group_by") == "Invoice": - self.totals.indent = 0.0 - self.totals.parent_invoice = "" - self.totals.invoice_or_item = "Total" - self.si_list.append(self.totals) - else: - self.grouped_data.append(self.totals) def is_not_invoice_row(self, row): - return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice" + return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get( + "group_by" + ) != "Invoice" def set_average_rate(self, new_row): self.set_average_gross_profit(new_row) - new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + new_row.buying_rate = ( + flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + ) + new_row.base_rate = ( + flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + ) return new_row def set_average_gross_profit(self, new_row): new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) - new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ - if new_row.base_amount else 0 - new_row.buying_rate = flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 - new_row.base_rate = flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 - - def add_to_totals(self, new_row): - for key in self.totals: - if new_row.get(key): - self.totals[key] += new_row[key] + new_row.gross_profit_percent = ( + flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) + if new_row.base_amount + else 0 + ) + new_row.buying_rate = ( + flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 + ) + new_row.base_rate = ( + flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 + ) def get_returned_invoice_items(self): - returned_invoices = frappe.db.sql(""" + returned_invoices = frappe.db.sql( + """ select si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against from @@ -301,24 +510,27 @@ class GrossProfitGenerator(object): si.name = si_item.parent and si.docstatus = 1 and si.is_return = 1 - """, as_dict=1) + """, + as_dict=1, + ) self.returned_invoices = frappe._dict() for inv in returned_invoices: - self.returned_invoices.setdefault(inv.return_against, frappe._dict())\ - .setdefault(inv.item_code, []).append(inv) + self.returned_invoices.setdefault(inv.return_against, frappe._dict()).setdefault( + inv.item_code, [] + ).append(inv) - def skip_row(self, row, product_bundles): + def skip_row(self, row): if self.filters.get("group_by") != "Invoice": if not row.get(scrub(self.filters.get("group_by", ""))): return True - elif row.get("is_return") == 1: - return True + + return False def get_buying_amount_from_product_bundle(self, row, product_bundle): buying_amount = 0.0 for packed_item in product_bundle: - if packed_item.get("parent_detail_docname")==row.item_row: + if packed_item.get("parent_detail_docname") == row.item_row: buying_amount += self.get_buying_amount(row, packed_item.item_code) return flt(buying_amount, self.currency_precision) @@ -328,7 +540,7 @@ class GrossProfitGenerator(object): # stock_ledger_entries should already be filtered by item_code and warehouse and # sorted by posting_date desc, posting_time desc if item_code in self.non_stock_items and (row.project or row.cost_center): - #Issue 6089-Get last purchasing rate for non-stock item + # Issue 6089-Get last purchasing rate for non-stock item item_rate = self.get_last_purchase_rate(item_code, row) return flt(row.qty) * item_rate @@ -341,15 +553,17 @@ class GrossProfitGenerator(object): for i, sle in enumerate(my_sle): # find the stock valution rate from stock ledger entry - if sle.voucher_type == parenttype and parent == sle.voucher_no and \ - sle.voucher_detail_no == row.item_row: - previous_stock_value = len(my_sle) > i+1 and \ - flt(my_sle[i+1].stock_value) or 0.0 + if ( + sle.voucher_type == parenttype + and parent == sle.voucher_no + and sle.voucher_detail_no == row.item_row + ): + previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 - if previous_stock_value: - return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) - else: - return flt(row.qty) * self.get_average_buying_rate(row, item_code) + if previous_stock_value: + return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) + else: + return flt(row.qty) * self.get_average_buying_rate(row, item_code) else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) @@ -358,33 +572,43 @@ class GrossProfitGenerator(object): def get_average_buying_rate(self, row, item_code): args = row if not item_code in self.average_buying_rate: - args.update({ - 'voucher_type': row.parenttype, - 'voucher_no': row.parent, - 'allow_zero_valuation': True, - 'company': self.filters.company - }) + args.update( + { + "voucher_type": row.parenttype, + "voucher_no": row.parent, + "allow_zero_valuation": True, + "company": self.filters.company, + } + ) average_buying_rate = get_incoming_rate(args) - self.average_buying_rate[item_code] = flt(average_buying_rate) + self.average_buying_rate[item_code] = flt(average_buying_rate) return self.average_buying_rate[item_code] def get_last_purchase_rate(self, item_code, row): - condition = '' - if row.project: - condition += " AND a.project=%s" % (frappe.db.escape(row.project)) - elif row.cost_center: - condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) - if self.filters.to_date: - condition += " AND modified='%s'" % (self.filters.to_date) + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") - last_purchase_rate = frappe.db.sql(""" - select (a.base_rate / a.conversion_factor) - from `tabPurchase Invoice Item` a - where a.item_code = %s and a.docstatus=1 - {0} - order by a.modified desc limit 1""".format(condition), item_code) + query = ( + frappe.qb.from_(purchase_invoice_item) + .inner_join(purchase_invoice) + .on(purchase_invoice.name == purchase_invoice_item.parent) + .select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor) + .where(purchase_invoice.docstatus == 1) + .where(purchase_invoice.posting_date <= self.filters.to_date) + .where(purchase_invoice_item.item_code == item_code) + ) + + if row.project: + query.where(purchase_invoice_item.project == row.project) + + if row.cost_center: + query.where(purchase_invoice_item.cost_center == row.cost_center) + + query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc) + query.limit(1) + last_purchase_rate = query.run() return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 @@ -397,7 +621,7 @@ class GrossProfitGenerator(object): if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" - if self.filters.group_by=="Sales Person": + if self.filters.group_by == "Sales Person": sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" else: @@ -410,7 +634,8 @@ class GrossProfitGenerator(object): if self.filters.get("item_code"): conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" - self.si_list = frappe.db.sql(""" + self.si_list = frappe.db.sql( + """ select `tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time, @@ -432,13 +657,19 @@ class GrossProfitGenerator(object): where `tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond} order by - `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""" - .format(conditions=conditions, sales_person_cols=sales_person_cols, - sales_team_table=sales_team_table, match_cond = get_match_cond('Sales Invoice')), self.filters, as_dict=1) + `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format( + conditions=conditions, + sales_person_cols=sales_person_cols, + sales_team_table=sales_team_table, + match_cond=get_match_cond("Sales Invoice"), + ), + self.filters, + as_dict=1, + ) def group_items_by_invoice(self): """ - Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. + Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. """ parents = [] @@ -461,94 +692,96 @@ class GrossProfitGenerator(object): row.parent_invoice = row.parent row.invoice_or_item = row.item_code - if frappe.db.exists('Product Bundle', row.item_code): + if frappe.db.exists("Product Bundle", row.item_code): self.add_bundle_items(row, index) def get_invoice_row(self, row): - return frappe._dict({ - 'parent_invoice': "", - 'indent': 0.0, - 'invoice_or_item': row.parent, - 'parent': None, - 'posting_date': row.posting_date, - 'posting_time': row.posting_time, - 'project': row.project, - 'update_stock': row.update_stock, - 'customer': row.customer, - 'customer_group': row.customer_group, - 'item_code': None, - 'item_name': None, - 'description': None, - 'warehouse': None, - 'item_group': None, - 'brand': None, - 'dn_detail': None, - 'delivery_note': None, - 'qty': None, - 'item_row': None, - 'is_return': row.is_return, - 'cost_center': row.cost_center, - 'base_net_amount': frappe.db.get_value('Sales Invoice', row.parent, 'base_net_total') - }) + return frappe._dict( + { + "parent_invoice": "", + "indent": 0.0, + "invoice_or_item": row.parent, + "parent": None, + "posting_date": row.posting_date, + "posting_time": row.posting_time, + "project": row.project, + "update_stock": row.update_stock, + "customer": row.customer, + "customer_group": row.customer_group, + "item_code": None, + "item_name": None, + "description": None, + "warehouse": None, + "item_group": None, + "brand": None, + "dn_detail": None, + "delivery_note": None, + "qty": None, + "item_row": None, + "is_return": row.is_return, + "cost_center": row.cost_center, + "base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"), + } + ) def add_bundle_items(self, product_bundle, index): bundle_items = self.get_bundle_items(product_bundle) for i, item in enumerate(bundle_items): bundle_item = self.get_bundle_item_row(product_bundle, item) - self.si_list.insert((index+i+1), bundle_item) + self.si_list.insert((index + i + 1), bundle_item) def get_bundle_items(self, product_bundle): return frappe.get_all( - 'Product Bundle Item', - filters = { - 'parent': product_bundle.item_code - }, - fields = ['item_code', 'qty'] + "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] ) def get_bundle_item_row(self, product_bundle, item): item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code) - return frappe._dict({ - 'parent_invoice': product_bundle.item_code, - 'indent': product_bundle.indent + 1, - 'parent': None, - 'invoice_or_item': item.item_code, - 'posting_date': product_bundle.posting_date, - 'posting_time': product_bundle.posting_time, - 'project': product_bundle.project, - 'customer': product_bundle.customer, - 'customer_group': product_bundle.customer_group, - 'item_code': item.item_code, - 'item_name': item_name, - 'description': description, - 'warehouse': product_bundle.warehouse, - 'item_group': item_group, - 'brand': brand, - 'dn_detail': product_bundle.dn_detail, - 'delivery_note': product_bundle.delivery_note, - 'qty': (flt(product_bundle.qty) * flt(item.qty)), - 'item_row': None, - 'is_return': product_bundle.is_return, - 'cost_center': product_bundle.cost_center - }) + return frappe._dict( + { + "parent_invoice": product_bundle.item_code, + "indent": product_bundle.indent + 1, + "parent": None, + "invoice_or_item": item.item_code, + "posting_date": product_bundle.posting_date, + "posting_time": product_bundle.posting_time, + "project": product_bundle.project, + "customer": product_bundle.customer, + "customer_group": product_bundle.customer_group, + "item_code": item.item_code, + "item_name": item_name, + "description": description, + "warehouse": product_bundle.warehouse, + "item_group": item_group, + "brand": brand, + "dn_detail": product_bundle.dn_detail, + "delivery_note": product_bundle.delivery_note, + "qty": (flt(product_bundle.qty) * flt(item.qty)), + "item_row": None, + "is_return": product_bundle.is_return, + "cost_center": product_bundle.cost_center, + } + ) def get_bundle_item_details(self, item_code): return frappe.db.get_value( - 'Item', - item_code, - ['item_name', 'description', 'item_group', 'brand'] + "Item", item_code, ["item_name", "description", "item_group", "brand"] ) def load_stock_ledger_entries(self): - res = frappe.db.sql("""select item_code, voucher_type, voucher_no, + res = frappe.db.sql( + """select item_code, voucher_type, voucher_no, voucher_detail_no, stock_value, warehouse, actual_qty as qty from `tabStock Ledger Entry` where company=%(company)s and is_cancelled = 0 order by item_code desc, warehouse desc, posting_date desc, - posting_time desc, creation desc""", self.filters, as_dict=True) + posting_time desc, creation desc""", + self.filters, + as_dict=True, + ) self.sle = {} for r in res: if (r.item_code, r.warehouse) not in self.sle: @@ -559,12 +792,18 @@ class GrossProfitGenerator(object): def load_product_bundle(self): self.product_bundles = {} - for d in frappe.db.sql("""select parenttype, parent, parent_item, + for d in frappe.db.sql( + """select parenttype, parent, parent_item, item_code, warehouse, -1*qty as total_qty, parent_detail_docname - from `tabPacked Item` where docstatus=1""", as_dict=True): - self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault(d.parent, - frappe._dict()).setdefault(d.parent_item, []).append(d) + from `tabPacked Item` where docstatus=1""", + as_dict=True, + ): + self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault( + d.parent, frappe._dict() + ).setdefault(d.parent_item, []).append(d) def load_non_stock_items(self): - self.non_stock_items = frappe.db.sql_list("""select name from tabItem - where is_stock_item=0""") + self.non_stock_items = frappe.db.sql_list( + """select name from tabItem + where is_stock_item=0""" + ) diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py index 2f23c8ed1d6..8db72de22f3 100644 --- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py +++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py @@ -12,6 +12,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(): columns = [ { @@ -19,53 +20,36 @@ def get_columns(): "fieldtype": "Link", "label": _("Territory"), "options": "Territory", - "width": 100 + "width": 100, }, { "fieldname": "item_group", "fieldtype": "Link", "label": _("Item Group"), "options": "Item Group", - "width": 150 + "width": 150, }, - { - "fieldname": "item", - "fieldtype": "Link", - "options": "Item", - "label": "Item", - "width": 150 - }, - { - "fieldname": "item_name", - "fieldtype": "Data", - "label": _("Item Name"), - "width": 150 - }, - + {"fieldname": "item", "fieldtype": "Link", "options": "Item", "label": "Item", "width": 150}, + {"fieldname": "item_name", "fieldtype": "Data", "label": _("Item Name"), "width": 150}, { "fieldname": "customer", "fieldtype": "Link", "label": _("Customer"), "options": "Customer", - "width": 100 + "width": 100, }, { "fieldname": "last_order_date", "fieldtype": "Date", "label": _("Last Order Date"), - "width": 100 - }, - { - "fieldname": "qty", - "fieldtype": "Float", - "label": _("Quantity"), - "width": 100 + "width": 100, }, + {"fieldname": "qty", "fieldtype": "Float", "label": _("Quantity"), "width": 100}, { "fieldname": "days_since_last_order", "fieldtype": "Int", "label": _("Days Since Last Order"), - "width": 100 + "width": 100, }, ] @@ -84,19 +68,21 @@ def get_data(filters): "territory": territory.name, "item_group": item.item_group, "item": item.item_code, - "item_name": item.item_name + "item_name": item.item_name, } - if sales_invoice_data.get((territory.name,item.item_code)): - item_obj = sales_invoice_data[(territory.name,item.item_code)] - if item_obj.days_since_last_order > cint(filters['days']): - row.update({ - "territory": item_obj.territory, - "customer": item_obj.customer, - "last_order_date": item_obj.last_order_date, - "qty": item_obj.qty, - "days_since_last_order": item_obj.days_since_last_order - }) + if sales_invoice_data.get((territory.name, item.item_code)): + item_obj = sales_invoice_data[(territory.name, item.item_code)] + if item_obj.days_since_last_order > cint(filters["days"]): + row.update( + { + "territory": item_obj.territory, + "customer": item_obj.customer, + "last_order_date": item_obj.last_order_date, + "qty": item_obj.qty, + "days_since_last_order": item_obj.days_since_last_order, + } + ) else: continue @@ -111,45 +97,49 @@ def get_sales_details(filters): date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date" - sales_data = frappe.db.sql(""" + sales_data = frappe.db.sql( + """ select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date, DATEDIFF(CURDATE(), {date_field}) as days_since_last_order from `tab{doctype}` s, `tab{doctype} Item` si where s.name = si.parent and s.docstatus = 1 - order by days_since_last_order """ #nosec - .format(date_field = date_field, doctype = filters['based_on']), as_dict=1) + order by days_since_last_order """.format( # nosec + date_field=date_field, doctype=filters["based_on"] + ), + as_dict=1, + ) for d in sales_data: - item_details_map.setdefault((d.territory,d.item_code), d) + item_details_map.setdefault((d.territory, d.item_code), d) return item_details_map + def get_territories(filters): filter_dict = {} if filters.get("territory"): - filter_dict.update({'name': filters['territory']}) + filter_dict.update({"name": filters["territory"]}) territories = frappe.get_all("Territory", fields=["name"], filters=filter_dict) return territories + def get_items(filters): - filters_dict = { - "disabled": 0, - "is_stock_item": 1 - } + filters_dict = {"disabled": 0, "is_stock_item": 1} if filters.get("item_group"): - filters_dict.update({ - "item_group": filters["item_group"] - }) + filters_dict.update({"item_group": filters["item_group"]}) if filters.get("item"): - filters_dict.update({ - "name": filters["item"] - }) + filters_dict.update({"name": filters["item"]}) - items = frappe.get_all("Item", fields=["name", "item_group", "item_name", "item_code"], filters=filters_dict, order_by="name") + items = frappe.get_all( + "Item", + fields=["name", "item_group", "item_name", "item_code"], + filters=filters_dict, + order_by="name", + ) return items diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index aaed58d070d..c04b9c71252 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -21,8 +21,10 @@ from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history impo def execute(filters=None): return _execute(filters) + def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(additional_table_columns, filters) company_currency = erpnext.get_company_currency(filters.company) @@ -30,18 +32,23 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum item_list = get_items(filters, additional_query_columns) aii_account_map = get_aii_accounts() if item_list: - itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency, - doctype='Purchase Invoice', tax_doctype='Purchase Taxes and Charges') + itemised_tax, tax_columns = get_tax_accounts( + item_list, + columns, + company_currency, + doctype="Purchase Invoice", + tax_doctype="Purchase Taxes and Charges", + ) po_pr_map = get_purchase_receipts_against_purchase_order(item_list) data = [] total_row_map = {} skip_total_row = 0 - prev_group_by_value = '' + prev_group_by_value = "" - if filters.get('group_by'): - grand_total = get_grand_total(filters, 'Purchase Invoice') + if filters.get("group_by"): + grand_total = get_grand_total(filters, "Purchase Invoice") item_details = get_item_details() @@ -57,71 +64,81 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum elif d.po_detail: purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, [])) - expense_account = d.unrealized_profit_loss_account or d.expense_account \ - or aii_account_map.get(d.company) + expense_account = ( + d.unrealized_profit_loss_account or d.expense_account or aii_account_map.get(d.company) + ) row = { - 'item_code': d.item_code, - 'item_name': item_record.item_name if item_record else d.item_name, - 'item_group': item_record.item_group if item_record else d.item_group, - 'description': d.description, - 'invoice': d.parent, - 'posting_date': d.posting_date, - 'supplier': d.supplier, - 'supplier_name': d.supplier_name + "item_code": d.item_code, + "item_name": item_record.item_name if item_record else d.item_name, + "item_group": item_record.item_group if item_record else d.item_group, + "description": d.description, + "invoice": d.parent, + "posting_date": d.posting_date, + "supplier": d.supplier, + "supplier_name": d.supplier_name, } if additional_query_columns: for col in additional_query_columns: - row.update({ - col: d.get(col) - }) + row.update({col: d.get(col)}) - row.update({ - 'credit_to': d.credit_to, - 'mode_of_payment': d.mode_of_payment, - 'project': d.project, - 'company': d.company, - 'purchase_order': d.purchase_order, - 'purchase_receipt': d.purchase_receipt, - 'expense_account': expense_account, - 'stock_qty': d.stock_qty, - 'stock_uom': d.stock_uom, - 'rate': d.base_net_amount / d.stock_qty, - 'amount': d.base_net_amount - }) + row.update( + { + "credit_to": d.credit_to, + "mode_of_payment": d.mode_of_payment, + "project": d.project, + "company": d.company, + "purchase_order": d.purchase_order, + "purchase_receipt": d.purchase_receipt, + "expense_account": expense_account, + "stock_qty": d.stock_qty, + "stock_uom": d.stock_uom, + "rate": d.base_net_amount / d.stock_qty, + "amount": d.base_net_amount, + } + ) total_tax = 0 for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) - row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0), - frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0), - }) - total_tax += flt(item_tax.get('tax_amount')) + row.update( + { + frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0), + frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), + } + ) + total_tax += flt(item_tax.get("tax_amount")) - row.update({ - 'total_tax': total_tax, - 'total': d.base_net_amount + total_tax, - 'currency': company_currency - }) + row.update( + {"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency} + ) - if filters.get('group_by'): - row.update({'percent_gt': flt(row['total']/grand_total) * 100}) + if filters.get("group_by"): + row.update({"percent_gt": flt(row["total"] / grand_total) * 100}) group_by_field, subtotal_display_field = get_group_by_and_display_fields(filters) - data, prev_group_by_value = add_total_row(data, filters, prev_group_by_value, d, total_row_map, - group_by_field, subtotal_display_field, grand_total, tax_columns) - add_sub_total_row(row, total_row_map, d.get(group_by_field, ''), tax_columns) + data, prev_group_by_value = add_total_row( + data, + filters, + prev_group_by_value, + d, + total_row_map, + group_by_field, + subtotal_display_field, + grand_total, + tax_columns, + ) + add_sub_total_row(row, total_row_map, d.get(group_by_field, ""), tax_columns) data.append(row) - if filters.get('group_by') and item_list: - total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) - total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) + if filters.get("group_by") and item_list: + total_row = total_row_map.get(prev_group_by_value or d.get("item_name")) + total_row["percent_gt"] = flt(total_row["total"] / grand_total * 100) data.append(total_row) data.append({}) - add_sub_total_row(total_row, total_row_map, 'total_row', tax_columns) - data.append(total_row_map.get('total_row')) + add_sub_total_row(total_row, total_row_map, "total_row", tax_columns) + data.append(total_row_map.get("total_row")) skip_total_row = 1 return columns, data, None, None, None, skip_total_row @@ -131,195 +148,180 @@ def get_columns(additional_table_columns, filters): columns = [] - if filters.get('group_by') != ('Item'): + if filters.get("group_by") != ("Item"): columns.extend( [ { - 'label': _('Item Code'), - 'fieldname': 'item_code', - 'fieldtype': 'Link', - 'options': 'Item', - 'width': 120 + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + ] + ) + + if filters.get("group_by") not in ("Item", "Item Group"): + columns.extend( + [ { - 'label': _('Item Name'), - 'fieldname': 'item_name', - 'fieldtype': 'Data', - 'width': 120 + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 120, } ] ) - if filters.get('group_by') not in ('Item', 'Item Group'): - columns.extend([ + columns.extend( + [ + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 150}, { - 'label': _('Item Group'), - 'fieldname': 'item_group', - 'fieldtype': 'Link', - 'options': 'Item Group', - 'width': 120 - } - ]) - - columns.extend([ - { - 'label': _('Description'), - 'fieldname': 'description', - 'fieldtype': 'Data', - 'width': 150 - }, - { - 'label': _('Invoice'), - 'fieldname': 'invoice', - 'fieldtype': 'Link', - 'options': 'Purchase Invoice', - 'width': 120 - }, - { - 'label': _('Posting Date'), - 'fieldname': 'posting_date', - 'fieldtype': 'Date', - 'width': 120 - } - ]) - - if filters.get('group_by') != 'Supplier': - columns.extend([ - { - 'label': _('Supplier'), - 'fieldname': 'supplier', - 'fieldtype': 'Link', - 'options': 'Supplier', - 'width': 120 + "label": _("Invoice"), + "fieldname": "invoice", + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 120, }, - { - 'label': _('Supplier Name'), - 'fieldname': 'supplier_name', - 'fieldtype': 'Data', - 'width': 120 - } - ]) + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, + ] + ) + + if filters.get("group_by") != "Supplier": + columns.extend( + [ + { + "label": _("Supplier"), + "fieldname": "supplier", + "fieldtype": "Link", + "options": "Supplier", + "width": 120, + }, + {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 120}, + ] + ) if additional_table_columns: columns += additional_table_columns columns += [ { - 'label': _('Payable Account'), - 'fieldname': 'credit_to', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 80 + "label": _("Payable Account"), + "fieldname": "credit_to", + "fieldtype": "Link", + "options": "Account", + "width": 80, }, { - 'label': _('Mode Of Payment'), - 'fieldname': 'mode_of_payment', - 'fieldtype': 'Link', - 'options': 'Mode of Payment', - 'width': 120 + "label": _("Mode Of Payment"), + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "options": "Mode of Payment", + "width": 120, }, { - 'label': _('Project'), - 'fieldname': 'project', - 'fieldtype': 'Link', - 'options': 'Project', - 'width': 80 + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 80, }, { - 'label': _('Company'), - 'fieldname': 'company', - 'fieldtype': 'Link', - 'options': 'Company', - 'width': 80 + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 80, }, { - 'label': _('Purchase Order'), - 'fieldname': 'purchase_order', - 'fieldtype': 'Link', - 'options': 'Purchase Order', - 'width': 100 + "label": _("Purchase Order"), + "fieldname": "purchase_order", + "fieldtype": "Link", + "options": "Purchase Order", + "width": 100, }, { - 'label': _("Purchase Receipt"), - 'fieldname': 'Purchase Receipt', - 'fieldtype': 'Link', - 'options': 'Purchase Receipt', - 'width': 100 + "label": _("Purchase Receipt"), + "fieldname": "Purchase Receipt", + "fieldtype": "Link", + "options": "Purchase Receipt", + "width": 100, }, { - 'label': _('Expense Account'), - 'fieldname': 'expense_account', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 100 + "label": _("Expense Account"), + "fieldname": "expense_account", + "fieldtype": "Link", + "options": "Account", + "width": 100, + }, + {"label": _("Stock Qty"), "fieldname": "stock_qty", "fieldtype": "Float", "width": 100}, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, }, { - 'label': _('Stock Qty'), - 'fieldname': 'stock_qty', - 'fieldtype': 'Float', - 'width': 100 + "label": _("Rate"), + "fieldname": "rate", + "fieldtype": "Float", + "options": "currency", + "width": 100, }, { - 'label': _('Stock UOM'), - 'fieldname': 'stock_uom', - 'fieldtype': 'Link', - 'options': 'UOM', - 'width': 100 + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, }, - { - 'label': _('Rate'), - 'fieldname': 'rate', - 'fieldtype': 'Float', - 'options': 'currency', - 'width': 100 - }, - { - 'label': _('Amount'), - 'fieldname': 'amount', - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 - } ] - if filters.get('group_by'): - columns.append({ - 'label': _('% Of Grand Total'), - 'fieldname': 'percent_gt', - 'fieldtype': 'Float', - 'width': 80 - }) + if filters.get("group_by"): + columns.append( + {"label": _("% Of Grand Total"), "fieldname": "percent_gt", "fieldtype": "Float", "width": 80} + ) return columns + def get_conditions(filters): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"), ("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"), ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), ("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"), - ("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s")): - if filters.get(opts[0]): - conditions += opts[1] + ("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"), + ): + if filters.get(opts[0]): + conditions += opts[1] if not filters.get("group_by"): - conditions += "ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc" + conditions += ( + "ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc" + ) else: - conditions += get_group_by_conditions(filters, 'Purchase Invoice') + conditions += get_group_by_conditions(filters, "Purchase Invoice") return conditions + def get_items(filters, additional_query_columns): conditions = get_conditions(filters) if additional_query_columns: - additional_query_columns = ', ' + ', '.join(additional_query_columns) + additional_query_columns = ", " + ", ".join(additional_query_columns) else: - additional_query_columns = '' + additional_query_columns = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ select `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, @@ -335,22 +337,35 @@ def get_items(filters, additional_query_columns): from `tabPurchase Invoice`, `tabPurchase Invoice Item` where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and `tabPurchase Invoice`.docstatus = 1 %s - """.format(additional_query_columns) % (conditions), filters, as_dict=1) + """.format( + additional_query_columns + ) + % (conditions), + filters, + as_dict=1, + ) + def get_aii_accounts(): return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany")) + def get_purchase_receipts_against_purchase_order(item_list): po_pr_map = frappe._dict() po_item_rows = list(set(d.po_detail for d in item_list)) if po_item_rows: - purchase_receipts = frappe.db.sql(""" + purchase_receipts = frappe.db.sql( + """ select parent, purchase_order_item from `tabPurchase Receipt Item` where docstatus=1 and purchase_order_item in (%s) group by purchase_order_item, parent - """ % (', '.join(['%s']*len(po_item_rows))), tuple(po_item_rows), as_dict=1) + """ + % (", ".join(["%s"] * len(po_item_rows))), + tuple(po_item_rows), + as_dict=1, + ) for pr in purchase_receipts: po_pr_map.setdefault(pr.po_detail, []).append(pr.parent) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 9b35538bb68..2e7213f42b1 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -18,11 +18,13 @@ from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history impo def execute(filters=None): return _execute(filters) + def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(additional_table_columns, filters) - company_currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") item_list = get_items(filters, additional_query_columns) if item_list: @@ -34,10 +36,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum data = [] total_row_map = {} skip_total_row = 0 - prev_group_by_value = '' + prev_group_by_value = "" - if filters.get('group_by'): - grand_total = get_grand_total(filters, 'Sales Invoice') + if filters.get("group_by"): + grand_total = get_grand_total(filters, "Sales Invoice") customer_details = get_customer_details() item_details = get_item_details() @@ -56,289 +58,279 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum delivery_note = d.parent row = { - 'item_code': d.item_code, - 'item_name': item_record.item_name if item_record else d.item_name, - 'item_group': item_record.item_group if item_record else d.item_group, - 'description': d.description, - 'invoice': d.parent, - 'posting_date': d.posting_date, - 'customer': d.customer, - 'customer_name': customer_record.customer_name, - 'customer_group': customer_record.customer_group, + "item_code": d.item_code, + "item_name": item_record.item_name if item_record else d.item_name, + "item_group": item_record.item_group if item_record else d.item_group, + "description": d.description, + "invoice": d.parent, + "posting_date": d.posting_date, + "customer": d.customer, + "customer_name": customer_record.customer_name, + "customer_group": customer_record.customer_group, } if additional_query_columns: for col in additional_query_columns: - row.update({ - col: d.get(col) - }) + row.update({col: d.get(col)}) - row.update({ - 'debit_to': d.debit_to, - 'mode_of_payment': ", ".join(mode_of_payments.get(d.parent, [])), - 'territory': d.territory, - 'project': d.project, - 'company': d.company, - 'sales_order': d.sales_order, - 'delivery_note': d.delivery_note, - 'income_account': d.unrealized_profit_loss_account if d.is_internal_customer == 1 else d.income_account, - 'cost_center': d.cost_center, - 'stock_qty': d.stock_qty, - 'stock_uom': d.stock_uom - }) + row.update( + { + "debit_to": d.debit_to, + "mode_of_payment": ", ".join(mode_of_payments.get(d.parent, [])), + "territory": d.territory, + "project": d.project, + "company": d.company, + "sales_order": d.sales_order, + "delivery_note": d.delivery_note, + "income_account": d.unrealized_profit_loss_account + if d.is_internal_customer == 1 + else d.income_account, + "cost_center": d.cost_center, + "stock_qty": d.stock_qty, + "stock_uom": d.stock_uom, + } + ) if d.stock_uom != d.uom and d.stock_qty: - row.update({ - 'rate': (d.base_net_rate * d.qty)/d.stock_qty, - 'amount': d.base_net_amount - }) + row.update({"rate": (d.base_net_rate * d.qty) / d.stock_qty, "amount": d.base_net_amount}) else: - row.update({ - 'rate': d.base_net_rate, - 'amount': d.base_net_amount - }) + row.update({"rate": d.base_net_rate, "amount": d.base_net_amount}) total_tax = 0 for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) - row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0), - frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0), - }) - total_tax += flt(item_tax.get('tax_amount')) + row.update( + { + frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0), + frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), + } + ) + total_tax += flt(item_tax.get("tax_amount")) - row.update({ - 'total_tax': total_tax, - 'total': d.base_net_amount + total_tax, - 'currency': company_currency - }) + row.update( + {"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency} + ) - if filters.get('group_by'): - row.update({'percent_gt': flt(row['total']/grand_total) * 100}) + if filters.get("group_by"): + row.update({"percent_gt": flt(row["total"] / grand_total) * 100}) group_by_field, subtotal_display_field = get_group_by_and_display_fields(filters) - data, prev_group_by_value = add_total_row(data, filters, prev_group_by_value, d, total_row_map, - group_by_field, subtotal_display_field, grand_total, tax_columns) - add_sub_total_row(row, total_row_map, d.get(group_by_field, ''), tax_columns) + data, prev_group_by_value = add_total_row( + data, + filters, + prev_group_by_value, + d, + total_row_map, + group_by_field, + subtotal_display_field, + grand_total, + tax_columns, + ) + add_sub_total_row(row, total_row_map, d.get(group_by_field, ""), tax_columns) data.append(row) - if filters.get('group_by') and item_list: - total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) - total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) + if filters.get("group_by") and item_list: + total_row = total_row_map.get(prev_group_by_value or d.get("item_name")) + total_row["percent_gt"] = flt(total_row["total"] / grand_total * 100) data.append(total_row) data.append({}) - add_sub_total_row(total_row, total_row_map, 'total_row', tax_columns) - data.append(total_row_map.get('total_row')) + add_sub_total_row(total_row, total_row_map, "total_row", tax_columns) + data.append(total_row_map.get("total_row")) skip_total_row = 1 return columns, data, None, None, None, skip_total_row + def get_columns(additional_table_columns, filters): columns = [] - if filters.get('group_by') != ('Item'): + if filters.get("group_by") != ("Item"): columns.extend( [ { - 'label': _('Item Code'), - 'fieldname': 'item_code', - 'fieldtype': 'Link', - 'options': 'Item', - 'width': 120 + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + ] + ) + + if filters.get("group_by") not in ("Item", "Item Group"): + columns.extend( + [ { - 'label': _('Item Name'), - 'fieldname': 'item_name', - 'fieldtype': 'Data', - 'width': 120 + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 120, } ] ) - if filters.get('group_by') not in ('Item', 'Item Group'): - columns.extend([ + columns.extend( + [ + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 150}, { - 'label': _('Item Group'), - 'fieldname': 'item_group', - 'fieldtype': 'Link', - 'options': 'Item Group', - 'width': 120 - } - ]) - - columns.extend([ - { - 'label': _('Description'), - 'fieldname': 'description', - 'fieldtype': 'Data', - 'width': 150 - }, - { - 'label': _('Invoice'), - 'fieldname': 'invoice', - 'fieldtype': 'Link', - 'options': 'Sales Invoice', - 'width': 120 - }, - { - 'label': _('Posting Date'), - 'fieldname': 'posting_date', - 'fieldtype': 'Date', - 'width': 120 - } - ]) - - if filters.get('group_by') != 'Customer': - columns.extend([ - { - 'label': _('Customer Group'), - 'fieldname': 'customer_group', - 'fieldtype': 'Link', - 'options': 'Customer Group', - 'width': 120 - } - ]) - - if filters.get('group_by') not in ('Customer', 'Customer Group'): - columns.extend([ - { - 'label': _('Customer'), - 'fieldname': 'customer', - 'fieldtype': 'Link', - 'options': 'Customer', - 'width': 120 + "label": _("Invoice"), + "fieldname": "invoice", + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, }, - { - 'label': _('Customer Name'), - 'fieldname': 'customer_name', - 'fieldtype': 'Data', - 'width': 120 - } - ]) + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, + ] + ) + + if filters.get("group_by") != "Customer": + columns.extend( + [ + { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + "width": 120, + } + ] + ) + + if filters.get("group_by") not in ("Customer", "Customer Group"): + columns.extend( + [ + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 120, + }, + {"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120}, + ] + ) if additional_table_columns: columns += additional_table_columns columns += [ { - 'label': _('Receivable Account'), - 'fieldname': 'debit_to', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 80 + "label": _("Receivable Account"), + "fieldname": "debit_to", + "fieldtype": "Link", + "options": "Account", + "width": 80, }, { - 'label': _('Mode Of Payment'), - 'fieldname': 'mode_of_payment', - 'fieldtype': 'Data', - 'width': 120 - } + "label": _("Mode Of Payment"), + "fieldname": "mode_of_payment", + "fieldtype": "Data", + "width": 120, + }, ] - if filters.get('group_by') != 'Territory': - columns.extend([ - { - 'label': _('Territory'), - 'fieldname': 'territory', - 'fieldtype': 'Link', - 'options': 'Territory', - 'width': 80 - } - ]) - + if filters.get("group_by") != "Territory": + columns.extend( + [ + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 80, + } + ] + ) columns += [ { - 'label': _('Project'), - 'fieldname': 'project', - 'fieldtype': 'Link', - 'options': 'Project', - 'width': 80 + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 80, }, { - 'label': _('Company'), - 'fieldname': 'company', - 'fieldtype': 'Link', - 'options': 'Company', - 'width': 80 + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 80, }, { - 'label': _('Sales Order'), - 'fieldname': 'sales_order', - 'fieldtype': 'Link', - 'options': 'Sales Order', - 'width': 100 + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 100, }, { - 'label': _("Delivery Note"), - 'fieldname': 'delivery_note', - 'fieldtype': 'Link', - 'options': 'Delivery Note', - 'width': 100 + "label": _("Delivery Note"), + "fieldname": "delivery_note", + "fieldtype": "Link", + "options": "Delivery Note", + "width": 100, }, { - 'label': _('Income Account'), - 'fieldname': 'income_account', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 100 + "label": _("Income Account"), + "fieldname": "income_account", + "fieldtype": "Link", + "options": "Account", + "width": 100, }, { - 'label': _("Cost Center"), - 'fieldname': 'cost_center', - 'fieldtype': 'Link', - 'options': 'Cost Center', - 'width': 100 + "label": _("Cost Center"), + "fieldname": "cost_center", + "fieldtype": "Link", + "options": "Cost Center", + "width": 100, + }, + {"label": _("Stock Qty"), "fieldname": "stock_qty", "fieldtype": "Float", "width": 100}, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, }, { - 'label': _('Stock Qty'), - 'fieldname': 'stock_qty', - 'fieldtype': 'Float', - 'width': 100 + "label": _("Rate"), + "fieldname": "rate", + "fieldtype": "Float", + "options": "currency", + "width": 100, }, { - 'label': _('Stock UOM'), - 'fieldname': 'stock_uom', - 'fieldtype': 'Link', - 'options': 'UOM', - 'width': 100 + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, }, - { - 'label': _('Rate'), - 'fieldname': 'rate', - 'fieldtype': 'Float', - 'options': 'currency', - 'width': 100 - }, - { - 'label': _('Amount'), - 'fieldname': 'amount', - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 - } ] - if filters.get('group_by'): - columns.append({ - 'label': _('% Of Grand Total'), - 'fieldname': 'percent_gt', - 'fieldtype': 'Float', - 'width': 80 - }) + if filters.get("group_by"): + columns.append( + {"label": _("% Of Grand Total"), "fieldname": "percent_gt", "fieldtype": "Float", "width": 80} + ) return columns + def get_conditions(filters): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("customer", " and `tabSales Invoice`.customer = %(customer)s"), ("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"), ("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"), - ("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] + ("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s"), + ): + if filters.get(opts[0]): + conditions += opts[1] if filters.get("mode_of_payment"): conditions += """ and exists(select name from `tabSales Invoice Payment` @@ -346,41 +338,45 @@ def get_conditions(filters): and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)""" if filters.get("warehouse"): - conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" - + conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" if filters.get("brand"): - conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s""" + conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s""" if filters.get("item_group"): - conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s""" + conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s""" if not filters.get("group_by"): - conditions += "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc" + conditions += ( + "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc" + ) else: - conditions += get_group_by_conditions(filters, 'Sales Invoice') + conditions += get_group_by_conditions(filters, "Sales Invoice") return conditions + def get_group_by_conditions(filters, doctype): - if filters.get("group_by") == 'Invoice': + if filters.get("group_by") == "Invoice": return "ORDER BY `tab{0} Item`.parent desc".format(doctype) - elif filters.get("group_by") == 'Item': + elif filters.get("group_by") == "Item": return "ORDER BY `tab{0} Item`.`item_code`".format(doctype) - elif filters.get("group_by") == 'Item Group': - return "ORDER BY `tab{0} Item`.{1}".format(doctype, frappe.scrub(filters.get('group_by'))) - elif filters.get("group_by") in ('Customer', 'Customer Group', 'Territory', 'Supplier'): - return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get('group_by'))) + elif filters.get("group_by") == "Item Group": + return "ORDER BY `tab{0} Item`.{1}".format(doctype, frappe.scrub(filters.get("group_by"))) + elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"): + return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by"))) + def get_items(filters, additional_query_columns): conditions = get_conditions(filters) if additional_query_columns: - additional_query_columns = ', ' + ', '.join(additional_query_columns) + additional_query_columns = ", " + ", ".join(additional_query_columns) else: - additional_query_columns = '' + additional_query_columns = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ select `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, @@ -399,47 +395,80 @@ def get_items(filters, additional_query_columns): from `tabSales Invoice`, `tabSales Invoice Item` where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and `tabSales Invoice`.docstatus = 1 {1} - """.format(additional_query_columns or '', conditions), filters, as_dict=1) #nosec + """.format( + additional_query_columns or "", conditions + ), + filters, + as_dict=1, + ) # nosec + def get_delivery_notes_against_sales_order(item_list): so_dn_map = frappe._dict() so_item_rows = list(set([d.so_detail for d in item_list])) if so_item_rows: - delivery_notes = frappe.db.sql(""" + delivery_notes = frappe.db.sql( + """ select parent, so_detail from `tabDelivery Note Item` where docstatus=1 and so_detail in (%s) group by so_detail, parent - """ % (', '.join(['%s']*len(so_item_rows))), tuple(so_item_rows), as_dict=1) + """ + % (", ".join(["%s"] * len(so_item_rows))), + tuple(so_item_rows), + as_dict=1, + ) for dn in delivery_notes: so_dn_map.setdefault(dn.so_detail, []).append(dn.parent) return so_dn_map + def get_grand_total(filters, doctype): - return frappe.db.sql(""" SELECT + return frappe.db.sql( + """ SELECT SUM(`tab{0}`.base_grand_total) FROM `tab{0}` WHERE `tab{0}`.docstatus = 1 and posting_date between %s and %s - """.format(doctype), (filters.get('from_date'), filters.get('to_date')))[0][0] #nosec + """.format( + doctype + ), + (filters.get("from_date"), filters.get("to_date")), + )[0][ + 0 + ] # nosec + def get_deducted_taxes(): - return frappe.db.sql_list("select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'") + return frappe.db.sql_list( + "select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'" + ) -def get_tax_accounts(item_list, columns, company_currency, - doctype='Sales Invoice', tax_doctype='Sales Taxes and Charges'): + +def get_tax_accounts( + item_list, + columns, + company_currency, + doctype="Sales Invoice", + tax_doctype="Sales Taxes and Charges", +): import json + item_row_map = {} tax_columns = [] invoice_item_row = {} itemised_tax = {} - tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field('tax_amount'), - currency=company_currency) or 2 + tax_amount_precision = ( + get_field_precision( + frappe.get_meta(tax_doctype).get_field("tax_amount"), currency=company_currency + ) + or 2 + ) for d in item_list: invoice_item_row.setdefault(d.parent, []).append(d) @@ -450,7 +479,8 @@ def get_tax_accounts(item_list, columns, company_currency, conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0" deducted_tax = get_deducted_taxes() - tax_details = frappe.db.sql(""" + tax_details = frappe.db.sql( + """ select name, parent, description, item_wise_tax_detail, charge_type, base_tax_amount_after_discount_amount @@ -461,8 +491,10 @@ def get_tax_accounts(item_list, columns, company_currency, and parent in (%s) %s order by description - """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), - tuple([doctype] + list(invoice_item_row))) + """ + % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), + tuple([doctype] + list(invoice_item_row)), + ) for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details: description = handle_html(description) @@ -483,151 +515,187 @@ def get_tax_accounts(item_list, columns, company_currency, tax_rate = tax_data tax_amount = 0 - if charge_type == 'Actual' and not tax_rate: - tax_rate = 'NA' + if charge_type == "Actual" and not tax_rate: + tax_rate = "NA" - item_net_amount = sum([flt(d.base_net_amount) - for d in item_row_map.get(parent, {}).get(item_code, [])]) + item_net_amount = sum( + [flt(d.base_net_amount) for d in item_row_map.get(parent, {}).get(item_code, [])] + ) for d in item_row_map.get(parent, {}).get(item_code, []): - item_tax_amount = flt((tax_amount * d.base_net_amount) / item_net_amount) \ - if item_net_amount else 0 + item_tax_amount = ( + flt((tax_amount * d.base_net_amount) / item_net_amount) if item_net_amount else 0 + ) if item_tax_amount: tax_value = flt(item_tax_amount, tax_amount_precision) - tax_value = (tax_value * -1 - if (doctype == 'Purchase Invoice' and name in deducted_tax) else tax_value) + tax_value = ( + tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value + ) - itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - 'tax_rate': tax_rate, - 'tax_amount': tax_value - }) + itemised_tax.setdefault(d.name, {})[description] = frappe._dict( + {"tax_rate": tax_rate, "tax_amount": tax_value} + ) except ValueError: continue - elif charge_type == 'Actual' and tax_amount: + elif charge_type == "Actual" and tax_amount: for d in invoice_item_row.get(parent, []): - itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - 'tax_rate': 'NA', - 'tax_amount': flt((tax_amount * d.base_net_amount) / d.base_net_total, - tax_amount_precision) - }) + itemised_tax.setdefault(d.name, {})[description] = frappe._dict( + { + "tax_rate": "NA", + "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total, tax_amount_precision), + } + ) tax_columns.sort() for desc in tax_columns: - columns.append({ - 'label': _(desc + ' Rate'), - 'fieldname': frappe.scrub(desc + ' Rate'), - 'fieldtype': 'Float', - 'width': 100 - }) + columns.append( + { + "label": _(desc + " Rate"), + "fieldname": frappe.scrub(desc + " Rate"), + "fieldtype": "Float", + "width": 100, + } + ) - columns.append({ - 'label': _(desc + ' Amount'), - 'fieldname': frappe.scrub(desc + ' Amount'), - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 - }) + columns.append( + { + "label": _(desc + " Amount"), + "fieldname": frappe.scrub(desc + " Amount"), + "fieldtype": "Currency", + "options": "currency", + "width": 100, + } + ) columns += [ { - 'label': _('Total Tax'), - 'fieldname': 'total_tax', - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 + "label": _("Total Tax"), + "fieldname": "total_tax", + "fieldtype": "Currency", + "options": "currency", + "width": 100, }, { - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 + "label": _("Total"), + "fieldname": "total", + "fieldtype": "Currency", + "options": "currency", + "width": 100, }, { - 'fieldname': 'currency', - 'label': _('Currency'), - 'fieldtype': 'Currency', - 'width': 80, - 'hidden': 1 - } + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Currency", + "width": 80, + "hidden": 1, + }, ] return itemised_tax, tax_columns -def add_total_row(data, filters, prev_group_by_value, item, total_row_map, - group_by_field, subtotal_display_field, grand_total, tax_columns): - if prev_group_by_value != item.get(group_by_field, ''): + +def add_total_row( + data, + filters, + prev_group_by_value, + item, + total_row_map, + group_by_field, + subtotal_display_field, + grand_total, + tax_columns, +): + if prev_group_by_value != item.get(group_by_field, ""): if prev_group_by_value: total_row = total_row_map.get(prev_group_by_value) data.append(total_row) data.append({}) - add_sub_total_row(total_row, total_row_map, 'total_row', tax_columns) + add_sub_total_row(total_row, total_row_map, "total_row", tax_columns) - prev_group_by_value = item.get(group_by_field, '') + prev_group_by_value = item.get(group_by_field, "") - total_row_map.setdefault(item.get(group_by_field, ''), { - subtotal_display_field: get_display_value(filters, group_by_field, item), - 'stock_qty': 0.0, - 'amount': 0.0, - 'bold': 1, - 'total_tax': 0.0, - 'total': 0.0, - 'percent_gt': 0.0 - }) + total_row_map.setdefault( + item.get(group_by_field, ""), + { + subtotal_display_field: get_display_value(filters, group_by_field, item), + "stock_qty": 0.0, + "amount": 0.0, + "bold": 1, + "total_tax": 0.0, + "total": 0.0, + "percent_gt": 0.0, + }, + ) - total_row_map.setdefault('total_row', { - subtotal_display_field: 'Total', - 'stock_qty': 0.0, - 'amount': 0.0, - 'bold': 1, - 'total_tax': 0.0, - 'total': 0.0, - 'percent_gt': 0.0 - }) + total_row_map.setdefault( + "total_row", + { + subtotal_display_field: "Total", + "stock_qty": 0.0, + "amount": 0.0, + "bold": 1, + "total_tax": 0.0, + "total": 0.0, + "percent_gt": 0.0, + }, + ) return data, prev_group_by_value + def get_display_value(filters, group_by_field, item): - if filters.get('group_by') == 'Item': - if item.get('item_code') != item.get('item_name'): - value = cstr(item.get('item_code')) + "

" + \ - "" + cstr(item.get('item_name')) + "" + if filters.get("group_by") == "Item": + if item.get("item_code") != item.get("item_name"): + value = ( + cstr(item.get("item_code")) + + "

" + + "" + + cstr(item.get("item_name")) + + "" + ) else: - value = item.get('item_code', '') - elif filters.get('group_by') in ('Customer', 'Supplier'): - party = frappe.scrub(filters.get('group_by')) - if item.get(party) != item.get(party+'_name'): - value = item.get(party) + "

" + \ - "" + item.get(party+'_name') + "" + value = item.get("item_code", "") + elif filters.get("group_by") in ("Customer", "Supplier"): + party = frappe.scrub(filters.get("group_by")) + if item.get(party) != item.get(party + "_name"): + value = ( + item.get(party) + + "

" + + "" + + item.get(party + "_name") + + "" + ) else: - value = item.get(party) + value = item.get(party) else: value = item.get(group_by_field) return value + def get_group_by_and_display_fields(filters): - if filters.get('group_by') == 'Item': - group_by_field = 'item_code' - subtotal_display_field = 'invoice' - elif filters.get('group_by') == 'Invoice': - group_by_field = 'parent' - subtotal_display_field = 'item_code' + if filters.get("group_by") == "Item": + group_by_field = "item_code" + subtotal_display_field = "invoice" + elif filters.get("group_by") == "Invoice": + group_by_field = "parent" + subtotal_display_field = "item_code" else: - group_by_field = frappe.scrub(filters.get('group_by')) - subtotal_display_field = 'item_code' + group_by_field = frappe.scrub(filters.get("group_by")) + subtotal_display_field = "item_code" return group_by_field, subtotal_display_field + def add_sub_total_row(item, total_row_map, group_by_value, tax_columns): total_row = total_row_map.get(group_by_value) - total_row['stock_qty'] += item['stock_qty'] - total_row['amount'] += item['amount'] - total_row['total_tax'] += item['total_tax'] - total_row['total'] += item['total'] - total_row['percent_gt'] += item['percent_gt'] + total_row["stock_qty"] += item["stock_qty"] + total_row["amount"] += item["amount"] + total_row["total_tax"] += item["total_tax"] + total_row["total"] += item["total"] + total_row["percent_gt"] += item["percent_gt"] for tax in tax_columns: - total_row.setdefault(frappe.scrub(tax + ' Amount'), 0.0) - total_row[frappe.scrub(tax + ' Amount')] += flt(item[frappe.scrub(tax + ' Amount')]) + total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0) + total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")]) diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index a421bc5c206..39c5311cd99 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -9,14 +9,19 @@ from erpnext import get_default_currency def get_ordered_to_be_billed_data(args): - doctype, party = args.get('doctype'), args.get('party') + doctype, party = args.get("doctype"), args.get("party") child_tab = doctype + " Item" - precision = get_field_precision(frappe.get_meta(child_tab).get_field("billed_amt"), - currency=get_default_currency()) or 2 + precision = ( + get_field_precision( + frappe.get_meta(child_tab).get_field("billed_amt"), currency=get_default_currency() + ) + or 2 + ) project_field = get_project_field(doctype, party) - return frappe.db.sql(""" + return frappe.db.sql( + """ Select `{parent_tab}`.name, `{parent_tab}`.{date_field}, `{parent_tab}`.{party}, `{parent_tab}`.{party}_name, @@ -40,9 +45,20 @@ def get_ordered_to_be_billed_data(args): (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0 order by `{parent_tab}`.{order} {order_by} - """.format(parent_tab = 'tab' + doctype, child_tab = 'tab' + child_tab, precision= precision, party = party, - date_field = args.get('date'), project_field = project_field, order= args.get('order'), order_by = args.get('order_by'))) + """.format( + parent_tab="tab" + doctype, + child_tab="tab" + child_tab, + precision=precision, + party=party, + date_field=args.get("date"), + project_field=project_field, + order=args.get("order"), + order_by=args.get("order_by"), + ) + ) + def get_project_field(doctype, party): - if party == "supplier": doctype = doctype + ' Item' - return "`tab%s`.project"%(doctype) + if party == "supplier": + doctype = doctype + " Item" + return "`tab%s`.project" % (doctype) diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 6c12093763d..00f5948a1b6 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -28,21 +28,28 @@ def execute(filters=None): else: payment_amount = flt(d.credit) or -1 * flt(d.debit) - d.update({ - "range1": 0, - "range2": 0, - "range3": 0, - "range4": 0, - "outstanding": payment_amount - }) + d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount}) if d.against_voucher: ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d) row = [ - d.voucher_type, d.voucher_no, d.party_type, d.party, d.posting_date, d.against_voucher, - invoice.posting_date, invoice.due_date, d.debit, d.credit, d.remarks, - d.age, d.range1, d.range2, d.range3, d.range4 + d.voucher_type, + d.voucher_no, + d.party_type, + d.party, + d.posting_date, + d.against_voucher, + invoice.posting_date, + invoice.due_date, + d.debit, + d.credit, + d.remarks, + d.age, + d.range1, + d.range2, + d.range3, + d.range4, ] if invoice.due_date: @@ -52,11 +59,17 @@ def execute(filters=None): return columns, data + def validate_filters(filters): - if (filters.get("payment_type") == _("Incoming") and filters.get("party_type") == "Supplier") or \ - (filters.get("payment_type") == _("Outgoing") and filters.get("party_type") == "Customer"): - frappe.throw(_("{0} payment entries can not be filtered by {1}")\ - .format(filters.payment_type, filters.party_type)) + if (filters.get("payment_type") == _("Incoming") and filters.get("party_type") == "Supplier") or ( + filters.get("payment_type") == _("Outgoing") and filters.get("party_type") == "Customer" + ): + frappe.throw( + _("{0} payment entries can not be filtered by {1}").format( + filters.payment_type, filters.party_type + ) + ) + def get_columns(filters): return [ @@ -64,109 +77,57 @@ def get_columns(filters): "fieldname": "payment_document", "label": _("Payment Document Type"), "fieldtype": "Data", - "width": 100 + "width": 100, }, { "fieldname": "payment_entry", "label": _("Payment Document"), "fieldtype": "Dynamic Link", "options": "payment_document", - "width": 160 - }, - { - "fieldname": "party_type", - "label": _("Party Type"), - "fieldtype": "Data", - "width": 100 + "width": 160, }, + {"fieldname": "party_type", "label": _("Party Type"), "fieldtype": "Data", "width": 100}, { "fieldname": "party", "label": _("Party"), "fieldtype": "Dynamic Link", "options": "party_type", - "width": 160 - }, - { - "fieldname": "posting_date", - "label": _("Posting Date"), - "fieldtype": "Date", - "width": 100 + "width": 160, }, + {"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 100}, { "fieldname": "invoice", "label": _("Invoice"), "fieldtype": "Link", - "options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice", - "width": 160 + "options": "Purchase Invoice" + if filters.get("payment_type") == _("Outgoing") + else "Sales Invoice", + "width": 160, }, { "fieldname": "invoice_posting_date", "label": _("Invoice Posting Date"), "fieldtype": "Date", - "width": 100 + "width": 100, }, + {"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100}, + {"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140}, + {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140}, + {"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200}, + {"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50}, + {"fieldname": "range1", "label": "0-30", "fieldtype": "Currency", "width": 140}, + {"fieldname": "range2", "label": "30-60", "fieldtype": "Currency", "width": 140}, + {"fieldname": "range3", "label": "60-90", "fieldtype": "Currency", "width": 140}, + {"fieldname": "range4", "label": _("90 Above"), "fieldtype": "Currency", "width": 140}, { - "fieldname": "due_date", - "label": _("Payment Due Date"), - "fieldtype": "Date", - "width": 100 - }, - { - "fieldname": "debit", - "label": _("Debit"), - "fieldtype": "Currency", - "width": 140 - }, - { - "fieldname": "credit", - "label": _("Credit"), - "fieldtype": "Currency", - "width": 140 - }, - { - "fieldname": "remarks", - "label": _("Remarks"), - "fieldtype": "Data", - "width": 200 - }, - { - "fieldname": "age", - "label": _("Age"), - "fieldtype": "Int", - "width": 50 - }, - { - "fieldname": "range1", - "label": "0-30", - "fieldtype": "Currency", - "width": 140 - }, - { - "fieldname": "range2", - "label": "30-60", - "fieldtype": "Currency", - "width": 140 - }, - { - "fieldname": "range3", - "label": "60-90", - "fieldtype": "Currency", - "width": 140 - }, - { - "fieldname": "range4", - "label": _("90 Above"), - "fieldtype": "Currency", - "width": 140 - }, - { "fieldname": "delay_in_payment", "label": _("Delay in payment (Days)"), "fieldtype": "Int", - "width": 100 - } + "width": 100, + }, ] + def get_conditions(filters): conditions = [] @@ -184,7 +145,9 @@ def get_conditions(filters): if filters.party_type: conditions.append("against_voucher_type=%(reference_type)s") - filters["reference_type"] = "Sales Invoice" if filters.party_type=="Customer" else "Purchase Invoice" + filters["reference_type"] = ( + "Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice" + ) if filters.get("from_date"): conditions.append("posting_date >= %(from_date)s") @@ -194,12 +157,20 @@ def get_conditions(filters): return "and " + " and ".join(conditions) if conditions else "" + def get_entries(filters): - return frappe.db.sql("""select + return frappe.db.sql( + """select voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher from `tabGL Entry` where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') {0} - """.format(get_conditions(filters)), filters, as_dict=1) + """.format( + get_conditions(filters) + ), + filters, + as_dict=1, + ) + def get_invoice_posting_date_map(filters): invoice_details = {} diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py index 77e7568533e..1bda0d8ae38 100644 --- a/erpnext/accounts/report/pos_register/pos_register.py +++ b/erpnext/accounts/report/pos_register/pos_register.py @@ -37,11 +37,14 @@ def execute(filters=None): add_subtotal_row(grouped_data, invoices, group_by_field, key) # move group by column to first position - column_index = next((index for (index, d) in enumerate(columns) if d["fieldname"] == group_by_field), None) + column_index = next( + (index for (index, d) in enumerate(columns) if d["fieldname"] == group_by_field), None + ) columns.insert(0, columns.pop(column_index)) return columns, grouped_data + def get_pos_entries(filters, group_by_field): conditions = get_conditions(filters) order_by = "p.posting_date" @@ -74,8 +77,12 @@ def get_pos_entries(filters, group_by_field): from_sales_invoice_payment=from_sales_invoice_payment, group_by_mop_condition=group_by_mop_condition, conditions=conditions, - order_by=order_by - ), filters, as_dict=1) + order_by=order_by, + ), + filters, + as_dict=1, + ) + def concat_mode_of_payments(pos_entries): mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries)) @@ -83,41 +90,50 @@ def concat_mode_of_payments(pos_entries): if mode_of_payments.get(entry.pos_invoice): entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, [])) + def add_subtotal_row(data, group_invoices, group_by_field, group_by_value): grand_total = sum(d.grand_total for d in group_invoices) paid_amount = sum(d.paid_amount for d in group_invoices) - data.append({ - group_by_field: group_by_value, - "grand_total": grand_total, - "paid_amount": paid_amount, - "bold": 1 - }) + data.append( + { + group_by_field: group_by_value, + "grand_total": grand_total, + "paid_amount": paid_amount, + "bold": 1, + } + ) data.append({}) + def validate_filters(filters): if not filters.get("company"): frappe.throw(_("{0} is mandatory").format(_("Company"))) if not filters.get("from_date") and not filters.get("to_date"): - frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) + frappe.throw( + _("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))) + ) if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) - if (filters.get("pos_profile") and filters.get("group_by") == _('POS Profile')): + if filters.get("pos_profile") and filters.get("group_by") == _("POS Profile"): frappe.throw(_("Can not filter based on POS Profile, if grouped by POS Profile")) - if (filters.get("customer") and filters.get("group_by") == _('Customer')): + if filters.get("customer") and filters.get("group_by") == _("Customer"): frappe.throw(_("Can not filter based on Customer, if grouped by Customer")) - if (filters.get("owner") and filters.get("group_by") == _('Cashier')): + if filters.get("owner") and filters.get("group_by") == _("Cashier"): frappe.throw(_("Can not filter based on Cashier, if grouped by Cashier")) - if (filters.get("mode_of_payment") and filters.get("group_by") == _('Payment Method')): + if filters.get("mode_of_payment") and filters.get("group_by") == _("Payment Method"): frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method")) + def get_conditions(filters): - conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s" + conditions = ( + "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s" + ) if filters.get("pos_profile"): conditions += " AND pos_profile = %(pos_profile)s" @@ -140,6 +156,7 @@ def get_conditions(filters): return conditions + def get_group_by_field(group_by): group_by_field = "" @@ -154,68 +171,59 @@ def get_group_by_field(group_by): return group_by_field + def get_columns(filters): columns = [ - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 90 - }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, { "label": _("POS Invoice"), "fieldname": "pos_invoice", "fieldtype": "Link", "options": "POS Invoice", - "width": 120 + "width": 120, }, { "label": _("Customer"), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", - "width": 120 + "width": 120, }, { "label": _("POS Profile"), "fieldname": "pos_profile", "fieldtype": "Link", "options": "POS Profile", - "width": 160 + "width": 160, }, { "label": _("Cashier"), "fieldname": "owner", "fieldtype": "Link", "options": "User", - "width": 140 + "width": 140, }, { "label": _("Grand Total"), "fieldname": "grand_total", "fieldtype": "Currency", "options": "company:currency", - "width": 120 + "width": 120, }, { "label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", "options": "company:currency", - "width": 120 + "width": 120, }, { "label": _("Payment Method"), "fieldname": "mode_of_payment", "fieldtype": "Data", - "width": 150 - }, - { - "label": _("Is Return"), - "fieldname": "is_return", - "fieldtype": "Data", - "width": 80 + "width": 150, }, + {"label": _("Is Return"), "fieldname": "is_return", "fieldtype": "Data", "width": 80}, ] return columns diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 882e411246b..66353358a06 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -15,19 +15,41 @@ from erpnext.accounts.report.financial_statements import ( def execute(filters=None): - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.period_start_date, filters.period_end_date, filters.filter_based_on, filters.periodicity, - company=filters.company) + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) - income = get_data(filters.company, "Income", "Credit", period_list, filters = filters, + income = get_data( + filters.company, + "Income", + "Credit", + period_list, + filters=filters, accumulated_values=filters.accumulated_values, - ignore_closing_entries=True, ignore_accumulated_values_for_fy= True) + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + ) - expense = get_data(filters.company, "Expense", "Debit", period_list, filters=filters, + expense = get_data( + filters.company, + "Expense", + "Debit", + period_list, + filters=filters, accumulated_values=filters.accumulated_values, - ignore_closing_entries=True, ignore_accumulated_values_for_fy= True) + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + ) - net_profit_loss = get_net_profit_loss(income, expense, period_list, filters.company, filters.presentation_currency) + net_profit_loss = get_net_profit_loss( + income, expense, period_list, filters.company, filters.presentation_currency + ) data = [] data.extend(income or []) @@ -35,20 +57,29 @@ def execute(filters=None): if net_profit_loss: data.append(net_profit_loss) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, filters.company + ) chart = get_chart_data(filters, columns, income, expense, net_profit_loss) - currency = filters.presentation_currency or frappe.get_cached_value('Company', filters.company, "default_currency") - report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters) + currency = filters.presentation_currency or frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) + report_summary = get_report_summary( + period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters + ) return columns, data, None, chart, report_summary -def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, currency, filters, consolidated=False): + +def get_report_summary( + period_list, periodicity, income, expense, net_profit_loss, currency, filters, consolidated=False +): net_income, net_expense, net_profit = 0.0, 0.0, 0.0 # from consolidated financial statement - if filters.get('accumulated_in_group_company'): + if filters.get("accumulated_in_group_company"): period_list = get_filtered_list_for_consolidated_report(filters, period_list) for period in period_list: @@ -60,37 +91,27 @@ def get_report_summary(period_list, periodicity, income, expense, net_profit_los if net_profit_loss: net_profit += net_profit_loss.get(key) - if (len(period_list) == 1 and periodicity== 'Yearly'): - profit_label = _("Profit This Year") - income_label = _("Total Income This Year") - expense_label = _("Total Expense This Year") + if len(period_list) == 1 and periodicity == "Yearly": + profit_label = _("Profit This Year") + income_label = _("Total Income This Year") + expense_label = _("Total Expense This Year") else: profit_label = _("Net Profit") income_label = _("Total Income") expense_label = _("Total Expense") return [ - { - "value": net_income, - "label": income_label, - "datatype": "Currency", - "currency": currency - }, - { "type": "separator", "value": "-"}, - { - "value": net_expense, - "label": expense_label, - "datatype": "Currency", - "currency": currency - }, - { "type": "separator", "value": "=", "color": "blue"}, + {"value": net_income, "label": income_label, "datatype": "Currency", "currency": currency}, + {"type": "separator", "value": "-"}, + {"value": net_expense, "label": expense_label, "datatype": "Currency", "currency": currency}, + {"type": "separator", "value": "=", "color": "blue"}, { "value": net_profit, "indicator": "Green" if net_profit > 0 else "Red", "label": profit_label, "datatype": "Currency", - "currency": currency - } + "currency": currency, + }, ] @@ -100,7 +121,7 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co "account_name": "'" + _("Profit for the year") + "'", "account": "'" + _("Profit for the year") + "'", "warn_if_negative": True, - "currency": currency or frappe.get_cached_value('Company', company, "default_currency") + "currency": currency or frappe.get_cached_value("Company", company, "default_currency"), } has_value = False @@ -113,7 +134,7 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co net_profit_loss[key] = total_income - total_expense if net_profit_loss[key]: - has_value=True + has_value = True total += flt(net_profit_loss[key]) net_profit_loss["total"] = total @@ -121,6 +142,7 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co if has_value: return net_profit_loss + def get_chart_data(filters, columns, income, expense, net_profit_loss): labels = [d.get("label") for d in columns[2:]] @@ -136,18 +158,13 @@ def get_chart_data(filters, columns, income, expense, net_profit_loss): datasets = [] if income_data: - datasets.append({'name': _('Income'), 'values': income_data}) + datasets.append({"name": _("Income"), "values": income_data}) if expense_data: - datasets.append({'name': _('Expense'), 'values': expense_data}) + datasets.append({"name": _("Expense"), "values": expense_data}) if net_profit: - datasets.append({'name': _('Net Profit/Loss'), 'values': net_profit}) + datasets.append({"name": _("Net Profit/Loss"), "values": net_profit}) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - } - } + chart = {"data": {"labels": labels, "datasets": datasets}} if not filters.accumulated_values: chart["type"] = "bar" diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 3dcb86267c1..7b1e48d817a 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -14,31 +14,39 @@ from erpnext.accounts.report.trial_balance.trial_balance import validate_filters value_fields = ("income", "expense", "gross_profit_loss") -def execute(filters=None): - if not filters.get('based_on'): filters["based_on"] = 'Cost Center' - based_on = filters.based_on.replace(' ', '_').lower() +def execute(filters=None): + if not filters.get("based_on"): + filters["based_on"] = "Cost Center" + + based_on = filters.based_on.replace(" ", "_").lower() validate_filters(filters) accounts = get_accounts_data(based_on, filters.get("company")) data = get_data(accounts, filters, based_on) columns = get_columns(filters) return columns, data + def get_accounts_data(based_on, company): - if based_on == 'cost_center': - return frappe.db.sql("""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt - from `tabCost Center` where company=%s order by name""", company, as_dict=True) - elif based_on == 'project': - return frappe.get_all('Project', fields = ["name"], filters = {'company': company}, order_by = 'name') + if based_on == "cost_center": + return frappe.db.sql( + """select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt + from `tabCost Center` where company=%s order by name""", + company, + as_dict=True, + ) + elif based_on == "project": + return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name") else: filters = {} doctype = frappe.unscrub(based_on) - has_company = frappe.db.has_column(doctype, 'company') + has_company = frappe.db.has_column(doctype, "company") if has_company: - filters.update({'company': company}) + filters.update({"company": company}) + + return frappe.get_all(doctype, fields=["name"], filters=filters, order_by="name") - return frappe.get_all(doctype, fields = ["name"], filters = filters, order_by = 'name') def get_data(accounts, filters, based_on): if not accounts: @@ -48,24 +56,28 @@ def get_data(accounts, filters, based_on): gl_entries_by_account = {} - set_gl_entries_by_account(filters.get("company"), filters.get("from_date"), - filters.get("to_date"), based_on, gl_entries_by_account, ignore_closing_entries=not flt(filters.get("with_period_closing_entry"))) + set_gl_entries_by_account( + filters.get("company"), + filters.get("from_date"), + filters.get("to_date"), + based_on, + gl_entries_by_account, + ignore_closing_entries=not flt(filters.get("with_period_closing_entry")), + ) total_row = calculate_values(accounts, gl_entries_by_account, filters) accumulate_values_into_parents(accounts, accounts_by_name) data = prepare_data(accounts, filters, total_row, parent_children_map, based_on) - data = filter_out_zero_value_rows(data, parent_children_map, - show_zero_values=filters.get("show_zero_values")) + data = filter_out_zero_value_rows( + data, parent_children_map, show_zero_values=filters.get("show_zero_values") + ) return data + def calculate_values(accounts, gl_entries_by_account, filters): - init = { - "income": 0.0, - "expense": 0.0, - "gross_profit_loss": 0.0 - } + init = {"income": 0.0, "expense": 0.0, "gross_profit_loss": 0.0} total_row = { "cost_center": None, @@ -77,7 +89,7 @@ def calculate_values(accounts, gl_entries_by_account, filters): "account": "'" + _("Total") + "'", "parent_account": None, "indent": 0, - "has_value": True + "has_value": True, } for d in accounts: @@ -87,9 +99,9 @@ def calculate_values(accounts, gl_entries_by_account, filters): for entry in gl_entries_by_account.get(d.name, []): if cstr(entry.is_opening) != "Yes": - if entry.type == 'Income': + if entry.type == "Income": d["income"] += flt(entry.credit) - flt(entry.debit) - if entry.type == 'Expense': + if entry.type == "Expense": d["expense"] += flt(entry.debit) - flt(entry.credit) d["gross_profit_loss"] = d.get("income") - d.get("expense") @@ -101,16 +113,18 @@ def calculate_values(accounts, gl_entries_by_account, filters): return total_row + def accumulate_values_into_parents(accounts, accounts_by_name): for d in reversed(accounts): if d.parent_account: for key in value_fields: accounts_by_name[d.parent_account][key] += d[key] + def prepare_data(accounts, filters, total_row, parent_children_map, based_on): data = [] new_accounts = accounts - company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") + company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") for d in accounts: has_value = False @@ -121,21 +135,24 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on): "indent": d.indent, "fiscal_year": filters.get("fiscal_year"), "currency": company_currency, - "based_on": based_on + "based_on": based_on, } - if based_on == 'cost_center': - cost_center_doc = frappe.get_doc("Cost Center",d.name) + if based_on == "cost_center": + cost_center_doc = frappe.get_doc("Cost Center", d.name) if not cost_center_doc.enable_distributed_cost_center: - DCC_allocation = frappe.db.sql("""SELECT parent, sum(percentage_allocation) as percentage_allocation + DCC_allocation = frappe.db.sql( + """SELECT parent, sum(percentage_allocation) as percentage_allocation FROM `tabDistributed Cost Center` WHERE cost_center IN %(cost_center)s AND parent NOT IN %(cost_center)s - GROUP BY parent""",{'cost_center': [d.name]}) + GROUP BY parent""", + {"cost_center": [d.name]}, + ) if DCC_allocation: for account in new_accounts: - if account['name'] == DCC_allocation[0][0]: + if account["name"] == DCC_allocation[0][0]: for value in value_fields: - d[value] += account[value]*(DCC_allocation[0][1]/100) + d[value] += account[value] * (DCC_allocation[0][1] / 100) for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) @@ -147,10 +164,11 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on): row["has_value"] = has_value data.append(row) - data.extend([{},total_row]) + data.extend([{}, total_row]) return data + def get_columns(filters): return [ { @@ -158,43 +176,42 @@ def get_columns(filters): "label": _(filters.get("based_on")), "fieldtype": "Link", "options": filters.get("based_on"), - "width": 300 + "width": 300, }, { "fieldname": "currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "hidden": 1 + "hidden": 1, }, { "fieldname": "income", "label": _("Income"), "fieldtype": "Currency", "options": "currency", - "width": 305 - + "width": 305, }, { "fieldname": "expense", "label": _("Expense"), "fieldtype": "Currency", "options": "currency", - "width": 305 - + "width": 305, }, { "fieldname": "gross_profit_loss", "label": _("Gross Profit / Loss"), "fieldtype": "Currency", "options": "currency", - "width": 307 - - } + "width": 307, + }, ] -def set_gl_entries_by_account(company, from_date, to_date, based_on, gl_entries_by_account, - ignore_closing_entries=False): + +def set_gl_entries_by_account( + company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False +): """Returns a dict like { "account": [gl entries], ... }""" additional_conditions = [] @@ -204,19 +221,19 @@ def set_gl_entries_by_account(company, from_date, to_date, based_on, gl_entries_ if from_date: additional_conditions.append("and posting_date >= %(from_date)s") - gl_entries = frappe.db.sql("""select posting_date, {based_on} as based_on, debit, credit, + gl_entries = frappe.db.sql( + """select posting_date, {based_on} as based_on, debit, credit, is_opening, (select root_type from `tabAccount` where name = account) as type from `tabGL Entry` where company=%(company)s {additional_conditions} and posting_date <= %(to_date)s and {based_on} is not null - order by {based_on}, posting_date""".format(additional_conditions="\n".join(additional_conditions), based_on= based_on), - { - "company": company, - "from_date": from_date, - "to_date": to_date - }, - as_dict=True) + order by {based_on}, posting_date""".format( + additional_conditions="\n".join(additional_conditions), based_on=based_on + ), + {"company": company, "from_date": from_date, "to_date": to_date}, + as_dict=True, + ) for entry in gl_entries: gl_entries_by_account.setdefault(entry.based_on, []).append(entry) diff --git a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.py b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.py index 406f7a50e8b..8af9bb3ac89 100644 --- a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.py +++ b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.py @@ -6,7 +6,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Purchase Invoice") data = get_data(filters, conditions) diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index a9696bd104a..a73c72c6d82 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -15,12 +15,15 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( def execute(filters=None): return _execute(filters) + def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): - if not filters: filters = {} + if not filters: + filters = {} invoice_list = get_invoices(filters, additional_query_columns) - columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts \ - = get_columns(invoice_list, additional_table_columns) + columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns( + invoice_list, additional_table_columns + ) if not invoice_list: msgprint(_("No record found")) @@ -28,13 +31,14 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum invoice_expense_map = get_invoice_expense_map(invoice_list) internal_invoice_map = get_internal_invoice_map(invoice_list) - invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list, - invoice_expense_map, expense_accounts) + invoice_expense_map, invoice_tax_map = get_invoice_tax_map( + invoice_list, invoice_expense_map, expense_accounts + ) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) suppliers = list(set(d.supplier for d in invoice_list)) supplier_details = get_supplier_details(suppliers) - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") data = [] for inv in invoice_list: @@ -50,10 +54,17 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row.append(inv.get(col)) row += [ - supplier_details.get(inv.supplier), # supplier_group - inv.tax_id, inv.credit_to, inv.mode_of_payment, ", ".join(project), - inv.bill_no, inv.bill_date, inv.remarks, - ", ".join(purchase_order), ", ".join(purchase_receipt), company_currency + supplier_details.get(inv.supplier), # supplier_group + inv.tax_id, + inv.credit_to, + inv.mode_of_payment, + ", ".join(project), + inv.bill_no, + inv.bill_date, + inv.remarks, + ", ".join(purchase_order), + ", ".join(purchase_receipt), + company_currency, ] # map expense values @@ -91,85 +102,117 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum def get_columns(invoice_list, additional_table_columns): """return columns based on filters""" columns = [ - _("Invoice") + ":Link/Purchase Invoice:120", _("Posting Date") + ":Date:80", - _("Supplier Id") + "::120", _("Supplier Name") + "::120"] + _("Invoice") + ":Link/Purchase Invoice:120", + _("Posting Date") + ":Date:80", + _("Supplier Id") + "::120", + _("Supplier Name") + "::120", + ] if additional_table_columns: columns += additional_table_columns columns += [ - _("Supplier Group") + ":Link/Supplier Group:120", _("Tax Id") + "::80", _("Payable Account") + ":Link/Account:120", - _("Mode of Payment") + ":Link/Mode of Payment:80", _("Project") + ":Link/Project:80", - _("Bill No") + "::120", _("Bill Date") + ":Date:80", _("Remarks") + "::150", + _("Supplier Group") + ":Link/Supplier Group:120", + _("Tax Id") + "::80", + _("Payable Account") + ":Link/Account:120", + _("Mode of Payment") + ":Link/Mode of Payment:80", + _("Project") + ":Link/Project:80", + _("Bill No") + "::120", + _("Bill Date") + ":Date:80", + _("Remarks") + "::150", _("Purchase Order") + ":Link/Purchase Order:100", _("Purchase Receipt") + ":Link/Purchase Receipt:100", - { - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, ] - expense_accounts = tax_accounts = expense_columns = tax_columns = unrealized_profit_loss_accounts = \ - unrealized_profit_loss_account_columns = [] + + expense_accounts = [] + tax_accounts = [] + unrealized_profit_loss_accounts = [] if invoice_list: - expense_accounts = frappe.db.sql_list("""select distinct expense_account + expense_accounts = frappe.db.sql_list( + """select distinct expense_account from `tabPurchase Invoice Item` where docstatus = 1 and (expense_account is not null and expense_account != '') - and parent in (%s) order by expense_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + and parent in (%s) order by expense_account""" + % ", ".join(["%s"] * len(invoice_list)), + tuple([inv.name for inv in invoice_list]), + ) - tax_accounts = frappe.db.sql_list("""select distinct account_head + tax_accounts = frappe.db.sql_list( + """select distinct account_head from `tabPurchase Taxes and Charges` where parenttype = 'Purchase Invoice' and docstatus = 1 and (account_head is not null and account_head != '') and category in ('Total', 'Valuation and Total') - and parent in (%s) order by account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) + and parent in (%s) order by account_head""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + ) - unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account + unrealized_profit_loss_accounts = frappe.db.sql_list( + """SELECT distinct unrealized_profit_loss_account from `tabPurchase Invoice` where docstatus = 1 and name in (%s) and ifnull(unrealized_profit_loss_account, '') != '' - order by unrealized_profit_loss_account""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) + order by unrealized_profit_loss_account""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + ) expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts] - unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts] + unrealized_profit_loss_account_columns = [ + (account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts + ] + tax_columns = [ + (account + ":Currency/currency:120") + for account in tax_accounts + if account not in expense_accounts + ] - for account in tax_accounts: - if account not in expense_accounts: - tax_columns.append(account + ":Currency/currency:120") - - columns = columns + expense_columns + unrealized_profit_loss_account_columns + \ - [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ - [_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120", - _("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"] + columns = ( + columns + + expense_columns + + unrealized_profit_loss_account_columns + + [_("Net Total") + ":Currency/currency:120"] + + tax_columns + + [ + _("Total Tax") + ":Currency/currency:120", + _("Grand Total") + ":Currency/currency:120", + _("Rounded Total") + ":Currency/currency:120", + _("Outstanding Amount") + ":Currency/currency:120", + ] + ) return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts + def get_conditions(filters): conditions = "" - if filters.get("company"): conditions += " and company=%(company)s" - if filters.get("supplier"): conditions += " and supplier = %(supplier)s" + if filters.get("company"): + conditions += " and company=%(company)s" + if filters.get("supplier"): + conditions += " and supplier = %(supplier)s" - if filters.get("from_date"): conditions += " and posting_date>=%(from_date)s" - if filters.get("to_date"): conditions += " and posting_date<=%(to_date)s" + if filters.get("from_date"): + conditions += " and posting_date>=%(from_date)s" + if filters.get("to_date"): + conditions += " and posting_date<=%(to_date)s" - if filters.get("mode_of_payment"): conditions += " and ifnull(mode_of_payment, '') = %(mode_of_payment)s" + if filters.get("mode_of_payment"): + conditions += " and ifnull(mode_of_payment, '') = %(mode_of_payment)s" if filters.get("cost_center"): - conditions += """ and exists(select name from `tabPurchase Invoice Item` + conditions += """ and exists(select name from `tabPurchase Invoice Item` where parent=`tabPurchase Invoice`.name and ifnull(`tabPurchase Invoice Item`.cost_center, '') = %(cost_center)s)""" if filters.get("warehouse"): - conditions += """ and exists(select name from `tabPurchase Invoice Item` + conditions += """ and exists(select name from `tabPurchase Invoice Item` where parent=`tabPurchase Invoice`.name and ifnull(`tabPurchase Invoice Item`.warehouse, '') = %(warehouse)s)""" if filters.get("item_group"): - conditions += """ and exists(select name from `tabPurchase Invoice Item` + conditions += """ and exists(select name from `tabPurchase Invoice Item` where parent=`tabPurchase Invoice`.name and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s)""" @@ -182,38 +225,58 @@ def get_conditions(filters): """ for dimension in accounting_dimensions: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) - conditions += common_condition + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname) + conditions += ( + common_condition + + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname) + ) else: - conditions += common_condition + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname) + conditions += ( + common_condition + + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname) + ) return conditions + def get_invoices(filters, additional_query_columns): if additional_query_columns: - additional_query_columns = ', ' + ', '.join(additional_query_columns) + additional_query_columns = ", " + ", ".join(additional_query_columns) conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, posting_date, credit_to, supplier, supplier_name, tax_id, bill_no, bill_date, remarks, base_net_total, base_grand_total, outstanding_amount, mode_of_payment {0} from `tabPurchase Invoice` where docstatus = 1 %s - order by posting_date desc, name desc""".format(additional_query_columns or '') % conditions, filters, as_dict=1) + order by posting_date desc, name desc""".format( + additional_query_columns or "" + ) + % conditions, + filters, + as_dict=1, + ) def get_invoice_expense_map(invoice_list): - expense_details = frappe.db.sql(""" + expense_details = frappe.db.sql( + """ select parent, expense_account, sum(base_net_amount) as amount from `tabPurchase Invoice Item` where parent in (%s) group by parent, expense_account - """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + """ + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) invoice_expense_map = {} for d in expense_details: @@ -222,11 +285,16 @@ def get_invoice_expense_map(invoice_list): return invoice_expense_map + def get_internal_invoice_map(invoice_list): - unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, + unrealized_amount_details = frappe.db.sql( + """SELECT name, unrealized_profit_loss_account, base_net_total as amount from `tabPurchase Invoice` where name in (%s) - and is_internal_supplier = 1 and company = represents_company""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + and is_internal_supplier = 1 and company = represents_company""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) internal_invoice_map = {} for d in unrealized_amount_details: @@ -235,15 +303,21 @@ def get_internal_invoice_map(invoice_list): return internal_invoice_map + def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): - tax_details = frappe.db.sql(""" + tax_details = frappe.db.sql( + """ select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount) else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount from `tabPurchase Taxes and Charges` where parent in (%s) and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0 group by parent, account_head, add_deduct_tax - """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + """ + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) invoice_tax_map = {} for d in tax_details: @@ -258,48 +332,71 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): return invoice_expense_map, invoice_tax_map + def get_invoice_po_pr_map(invoice_list): - pi_items = frappe.db.sql(""" + pi_items = frappe.db.sql( + """ select parent, purchase_order, purchase_receipt, po_detail, project from `tabPurchase Invoice Item` where parent in (%s) - """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + """ + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) invoice_po_pr_map = {} for d in pi_items: if d.purchase_order: - invoice_po_pr_map.setdefault(d.parent, frappe._dict()).setdefault( - "purchase_order", []).append(d.purchase_order) + invoice_po_pr_map.setdefault(d.parent, frappe._dict()).setdefault("purchase_order", []).append( + d.purchase_order + ) pr_list = None if d.purchase_receipt: pr_list = [d.purchase_receipt] elif d.po_detail: - pr_list = frappe.db.sql_list("""select distinct parent from `tabPurchase Receipt Item` - where docstatus=1 and purchase_order_item=%s""", d.po_detail) + pr_list = frappe.db.sql_list( + """select distinct parent from `tabPurchase Receipt Item` + where docstatus=1 and purchase_order_item=%s""", + d.po_detail, + ) if pr_list: invoice_po_pr_map.setdefault(d.parent, frappe._dict()).setdefault("purchase_receipt", pr_list) if d.project: - invoice_po_pr_map.setdefault(d.parent, frappe._dict()).setdefault( - "project", []).append(d.project) + invoice_po_pr_map.setdefault(d.parent, frappe._dict()).setdefault("project", []).append( + d.project + ) return invoice_po_pr_map + def get_account_details(invoice_list): account_map = {} accounts = list(set([inv.credit_to for inv in invoice_list])) - for acc in frappe.db.sql("""select name, parent_account from tabAccount - where name in (%s)""" % ", ".join(["%s"]*len(accounts)), tuple(accounts), as_dict=1): - account_map[acc.name] = acc.parent_account + for acc in frappe.db.sql( + """select name, parent_account from tabAccount + where name in (%s)""" + % ", ".join(["%s"] * len(accounts)), + tuple(accounts), + as_dict=1, + ): + account_map[acc.name] = acc.parent_account return account_map + def get_supplier_details(suppliers): supplier_details = {} - for supp in frappe.db.sql("""select name, supplier_group from `tabSupplier` - where name in (%s)""" % ", ".join(["%s"]*len(suppliers)), tuple(suppliers), as_dict=1): - supplier_details.setdefault(supp.name, supp.supplier_group) + for supp in frappe.db.sql( + """select name, supplier_group from `tabSupplier` + where name in (%s)""" + % ", ".join(["%s"] * len(suppliers)), + tuple(suppliers), + as_dict=1, + ): + supplier_details.setdefault(supp.name, supp.supplier_group) return supplier_details diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py index e88675bb8d6..1dcacb97420 100644 --- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py +++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py @@ -13,6 +13,7 @@ def execute(filters=None): data = get_ordered_to_be_billed_data(args) return columns, data + def get_column(): return [ { @@ -20,90 +21,76 @@ def get_column(): "fieldname": "name", "fieldtype": "Link", "options": "Purchase Receipt", - "width": 160 - }, - { - "label": _("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 100 + "width": 160, }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 100}, { "label": _("Supplier"), "fieldname": "supplier", "fieldtype": "Link", "options": "Supplier", - "width": 120 - }, - { - "label": _("Supplier Name"), - "fieldname": "supplier_name", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 120}, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 120 + "width": 120, }, { "label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 100, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Billed Amount"), "fieldname": "billed_amount", "fieldtype": "Currency", "width": 100, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Returned Amount"), "fieldname": "returned_amount", "fieldtype": "Currency", "width": 120, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Pending Amount"), "fieldname": "pending_amount", "fieldtype": "Currency", "width": 120, - "options": "Company:company:default_currency" - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 120 + "options": "Company:company:default_currency", }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 120}, { "label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 - } + "width": 120, + }, ] + def get_args(): - return {'doctype': 'Purchase Receipt', 'party': 'supplier', - 'date': 'posting_date', 'order': 'name', 'order_by': 'desc'} + return { + "doctype": "Purchase Receipt", + "party": "supplier", + "date": "posting_date", + "order": "name", + "order_by": "desc", + } diff --git a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.py b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.py index 966b1d4fd01..483debaf8bf 100644 --- a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.py +++ b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.py @@ -6,7 +6,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Sales Invoice") data = get_data(filters, conditions) diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py index 3b736282cf9..0432ad181fe 100644 --- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py +++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py @@ -9,7 +9,11 @@ from frappe.utils import cstr def execute(filters=None): columns, data = [], [] columns = get_columns(filters) - data = get_pos_sales_payment_data(filters) if filters.get('is_pos') else get_sales_payment_data(filters, columns) + data = ( + get_pos_sales_payment_data(filters) + if filters.get("is_pos") + else get_sales_payment_data(filters, columns) + ) return columns, data @@ -22,12 +26,12 @@ def get_pos_columns(): _("Taxes") + ":Currency/currency:120", _("Payments") + ":Currency/currency:120", _("Warehouse") + ":Data:200", - _("Cost Center") + ":Data:200" + _("Cost Center") + ":Data:200", ] def get_columns(filters): - if filters.get('is_pos'): + if filters.get("is_pos"): return get_pos_columns() else: return [ @@ -44,15 +48,17 @@ def get_pos_sales_payment_data(filters): sales_invoice_data = get_pos_invoice_data(filters) data = [ [ - row['posting_date'], - row['owner'], - row['mode_of_payment'], - row['net_total'], - row['total_taxes'], - row['paid_amount'], - row['warehouse'], - row['cost_center'] - ] for row in sales_invoice_data] + row["posting_date"], + row["owner"], + row["mode_of_payment"], + row["net_total"], + row["total_taxes"], + row["paid_amount"], + row["warehouse"], + row["cost_center"], + ] + for row in sales_invoice_data + ] return data @@ -71,19 +77,25 @@ def get_sales_payment_data(filters, columns): show_payment_detail = False for inv in sales_invoice_data: - owner_posting_date = inv["owner"]+cstr(inv["posting_date"]) + owner_posting_date = inv["owner"] + cstr(inv["posting_date"]) if show_payment_detail: - row = [inv.posting_date, inv.owner," ",inv.net_total,inv.total_taxes, 0] + row = [inv.posting_date, inv.owner, " ", inv.net_total, inv.total_taxes, 0] data.append(row) - for mop_detail in mode_of_payment_details.get(owner_posting_date,[]): - row = [inv.posting_date, inv.owner,mop_detail[0],0,0,mop_detail[1],0] + for mop_detail in mode_of_payment_details.get(owner_posting_date, []): + row = [inv.posting_date, inv.owner, mop_detail[0], 0, 0, mop_detail[1], 0] data.append(row) else: total_payment = 0 - for mop_detail in mode_of_payment_details.get(owner_posting_date,[]): + for mop_detail in mode_of_payment_details.get(owner_posting_date, []): total_payment = total_payment + mop_detail[1] - row = [inv.posting_date, inv.owner,", ".join(mode_of_payments.get(owner_posting_date, [])), - inv.net_total,inv.total_taxes,total_payment] + row = [ + inv.posting_date, + inv.owner, + ", ".join(mode_of_payments.get(owner_posting_date, [])), + inv.net_total, + inv.total_taxes, + total_payment, + ] data.append(row) return data @@ -107,40 +119,44 @@ def get_conditions(filters): def get_pos_invoice_data(filters): conditions = get_conditions(filters) - result = frappe.db.sql('' - 'SELECT ' - 'posting_date, owner, sum(net_total) as "net_total", sum(total_taxes) as "total_taxes", ' - 'sum(paid_amount) as "paid_amount", sum(outstanding_amount) as "outstanding_amount", ' - 'mode_of_payment, warehouse, cost_center ' - 'FROM (' - 'SELECT ' - 'parent, item_code, sum(amount) as "base_total", warehouse, cost_center ' - 'from `tabSales Invoice Item` group by parent' - ') t1 ' - 'left join ' - '(select parent, mode_of_payment from `tabSales Invoice Payment` group by parent) t3 ' - 'on (t3.parent = t1.parent) ' - 'JOIN (' - 'SELECT ' - 'docstatus, company, is_pos, name, posting_date, owner, sum(base_total) as "base_total", ' - 'sum(net_total) as "net_total", sum(total_taxes_and_charges) as "total_taxes", ' - 'sum(base_paid_amount) as "paid_amount", sum(outstanding_amount) as "outstanding_amount" ' - 'FROM `tabSales Invoice` ' - 'GROUP BY name' - ') a ' - 'ON (' - 't1.parent = a.name and t1.base_total = a.base_total) ' - 'WHERE a.docstatus = 1' - ' AND {conditions} ' - 'GROUP BY ' - 'owner, posting_date, warehouse'.format(conditions=conditions), filters, as_dict=1 - ) + result = frappe.db.sql( + "" + "SELECT " + 'posting_date, owner, sum(net_total) as "net_total", sum(total_taxes) as "total_taxes", ' + 'sum(paid_amount) as "paid_amount", sum(outstanding_amount) as "outstanding_amount", ' + "mode_of_payment, warehouse, cost_center " + "FROM (" + "SELECT " + 'parent, item_code, sum(amount) as "base_total", warehouse, cost_center ' + "from `tabSales Invoice Item` group by parent" + ") t1 " + "left join " + "(select parent, mode_of_payment from `tabSales Invoice Payment` group by parent) t3 " + "on (t3.parent = t1.parent) " + "JOIN (" + "SELECT " + 'docstatus, company, is_pos, name, posting_date, owner, sum(base_total) as "base_total", ' + 'sum(net_total) as "net_total", sum(total_taxes_and_charges) as "total_taxes", ' + 'sum(base_paid_amount) as "paid_amount", sum(outstanding_amount) as "outstanding_amount" ' + "FROM `tabSales Invoice` " + "GROUP BY name" + ") a " + "ON (" + "t1.parent = a.name and t1.base_total = a.base_total) " + "WHERE a.docstatus = 1" + " AND {conditions} " + "GROUP BY " + "owner, posting_date, warehouse".format(conditions=conditions), + filters, + as_dict=1, + ) return result def get_sales_invoice_data(filters): conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select a.posting_date, a.owner, sum(a.net_total) as "net_total", @@ -152,15 +168,21 @@ def get_sales_invoice_data(filters): and {conditions} group by a.owner, a.posting_date - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) def get_mode_of_payments(filters): mode_of_payments = {} invoice_list = get_invoices(filters) - invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list) + invoice_list_names = ",".join('"' + invoice["name"] + '"' for invoice in invoice_list) if invoice_list: - inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment + inv_mop = frappe.db.sql( + """select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment from `tabSales Invoice` a, `tabSales Invoice Payment` b where a.name = b.parent and a.docstatus = 1 @@ -180,26 +202,36 @@ def get_mode_of_payments(filters): and a.docstatus = 1 and b.reference_type = "Sales Invoice" and b.reference_name in ({invoice_list_names}) - """.format(invoice_list_names=invoice_list_names), as_dict=1) + """.format( + invoice_list_names=invoice_list_names + ), + as_dict=1, + ) for d in inv_mop: - mode_of_payments.setdefault(d["owner"]+cstr(d["posting_date"]), []).append(d.mode_of_payment) + mode_of_payments.setdefault(d["owner"] + cstr(d["posting_date"]), []).append(d.mode_of_payment) return mode_of_payments def get_invoices(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select a.name + return frappe.db.sql( + """select a.name from `tabSales Invoice` a - where a.docstatus = 1 and {conditions}""".format(conditions=conditions), - filters, as_dict=1) + where a.docstatus = 1 and {conditions}""".format( + conditions=conditions + ), + filters, + as_dict=1, + ) def get_mode_of_payment_details(filters): mode_of_payment_details = {} invoice_list = get_invoices(filters) - invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list) + invoice_list_names = ",".join('"' + invoice["name"] + '"' for invoice in invoice_list) if invoice_list: - inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date, + inv_mop_detail = frappe.db.sql( + """select a.owner, a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount from `tabSales Invoice` a, `tabSales Invoice Payment` b where a.name = b.parent @@ -224,24 +256,39 @@ def get_mode_of_payment_details(filters): and b.reference_type = "Sales Invoice" and b.reference_name in ({invoice_list_names}) group by a.owner, a.posting_date, mode_of_payment - """.format(invoice_list_names=invoice_list_names), as_dict=1) + """.format( + invoice_list_names=invoice_list_names + ), + as_dict=1, + ) - inv_change_amount = frappe.db.sql("""select a.owner, a.posting_date, + inv_change_amount = frappe.db.sql( + """select a.owner, a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment, sum(a.base_change_amount) as change_amount from `tabSales Invoice` a, `tabSales Invoice Payment` b where a.name = b.parent and a.name in ({invoice_list_names}) and b.mode_of_payment = 'Cash' and a.base_change_amount > 0 - group by a.owner, a.posting_date, mode_of_payment""".format(invoice_list_names=invoice_list_names), as_dict=1) + group by a.owner, a.posting_date, mode_of_payment""".format( + invoice_list_names=invoice_list_names + ), + as_dict=1, + ) for d in inv_change_amount: for det in inv_mop_detail: - if det["owner"] == d["owner"] and det["posting_date"] == d["posting_date"] and det["mode_of_payment"] == d["mode_of_payment"]: + if ( + det["owner"] == d["owner"] + and det["posting_date"] == d["posting_date"] + and det["mode_of_payment"] == d["mode_of_payment"] + ): paid_amount = det["paid_amount"] - d["change_amount"] det["paid_amount"] = paid_amount for d in inv_mop_detail: - mode_of_payment_details.setdefault(d["owner"]+cstr(d["posting_date"]), []).append((d.mode_of_payment,d.paid_amount)) + mode_of_payment_details.setdefault(d["owner"] + cstr(d["posting_date"]), []).append( + (d.mode_of_payment, d.paid_amount) + ) return mode_of_payment_details diff --git a/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py index b3f6c72a3ef..3ad0ff2ce26 100644 --- a/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py +++ b/erpnext/accounts/report/sales_payment_summary/test_sales_payment_summary.py @@ -15,6 +15,7 @@ from erpnext.accounts.report.sales_payment_summary.sales_payment_summary import test_dependencies = ["Sales Invoice"] + class TestSalesPaymentSummary(unittest.TestCase): @classmethod def setUpClass(self): @@ -37,7 +38,7 @@ class TestSalesPaymentSummary(unittest.TestCase): si.insert() si.submit() - if int(si.name[-3:])%2 == 0: + if int(si.name[-3:]) % 2 == 0: bank_account = "_Test Cash - _TC" mode_of_payment = "Cash" else: @@ -52,18 +53,22 @@ class TestSalesPaymentSummary(unittest.TestCase): pe.submit() mop = get_mode_of_payments(filters) - self.assertTrue('Credit Card' in list(mop.values())[0]) - self.assertTrue('Cash' in list(mop.values())[0]) + self.assertTrue("Credit Card" in list(mop.values())[0]) + self.assertTrue("Cash" in list(mop.values())[0]) # Cancel all Cash payment entry and check if this mode of payment is still fetched. - payment_entries = frappe.get_all("Payment Entry", filters={"mode_of_payment": "Cash", "docstatus": 1}, fields=["name", "docstatus"]) + payment_entries = frappe.get_all( + "Payment Entry", + filters={"mode_of_payment": "Cash", "docstatus": 1}, + fields=["name", "docstatus"], + ) for payment_entry in payment_entries: pe = frappe.get_doc("Payment Entry", payment_entry.name) pe.cancel() mop = get_mode_of_payments(filters) - self.assertTrue('Credit Card' in list(mop.values())[0]) - self.assertTrue('Cash' not in list(mop.values())[0]) + self.assertTrue("Credit Card" in list(mop.values())[0]) + self.assertTrue("Cash" not in list(mop.values())[0]) def test_get_mode_of_payments_details(self): filters = get_filters() @@ -73,7 +78,7 @@ class TestSalesPaymentSummary(unittest.TestCase): si.insert() si.submit() - if int(si.name[-3:])%2 == 0: + if int(si.name[-3:]) % 2 == 0: bank_account = "_Test Cash - _TC" mode_of_payment = "Cash" else: @@ -95,7 +100,11 @@ class TestSalesPaymentSummary(unittest.TestCase): cc_init_amount = mopd_value[1] # Cancel one Credit Card Payment Entry and check that it is not fetched in mode of payment details. - payment_entries = frappe.get_all("Payment Entry", filters={"mode_of_payment": "Credit Card", "docstatus": 1}, fields=["name", "docstatus"]) + payment_entries = frappe.get_all( + "Payment Entry", + filters={"mode_of_payment": "Credit Card", "docstatus": 1}, + fields=["name", "docstatus"], + ) for payment_entry in payment_entries[:1]: pe = frappe.get_doc("Payment Entry", payment_entry.name) pe.cancel() @@ -108,63 +117,72 @@ class TestSalesPaymentSummary(unittest.TestCase): self.assertTrue(cc_init_amount > cc_final_amount) + def get_filters(): - return { - "from_date": "1900-01-01", - "to_date": today(), - "company": "_Test Company" - } + return {"from_date": "1900-01-01", "to_date": today(), "company": "_Test Company"} + def create_sales_invoice_record(qty=1): # return sales invoice doc object - return frappe.get_doc({ - "doctype": "Sales Invoice", - "customer": frappe.get_doc('Customer', {"customer_name": "Prestiga-Biz"}).name, - "company": '_Test Company', - "due_date": today(), - "posting_date": today(), - "currency": "INR", - "taxes_and_charges": "", - "debit_to": "Debtors - _TC", - "taxes": [], - "items": [{ - 'doctype': 'Sales Invoice Item', - 'item_code': frappe.get_doc('Item', {'item_name': 'Consulting'}).name, - 'qty': qty, - "rate": 10000, - 'income_account': 'Sales - _TC', - 'cost_center': 'Main - _TC', - 'expense_account': 'Cost of Goods Sold - _TC' - }] - }) + return frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": frappe.get_doc("Customer", {"customer_name": "Prestiga-Biz"}).name, + "company": "_Test Company", + "due_date": today(), + "posting_date": today(), + "currency": "INR", + "taxes_and_charges": "", + "debit_to": "Debtors - _TC", + "taxes": [], + "items": [ + { + "doctype": "Sales Invoice Item", + "item_code": frappe.get_doc("Item", {"item_name": "Consulting"}).name, + "qty": qty, + "rate": 10000, + "income_account": "Sales - _TC", + "cost_center": "Main - _TC", + "expense_account": "Cost of Goods Sold - _TC", + } + ], + } + ) + def create_records(): if frappe.db.exists("Customer", "Prestiga-Biz"): return - #customer - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "Prestiga-Biz", - "customer_type": "Company", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert() + # customer + frappe.get_doc( + { + "customer_group": "_Test Customer Group", + "customer_name": "Prestiga-Biz", + "customer_type": "Company", + "doctype": "Customer", + "territory": "_Test Territory", + } + ).insert() # item - item = frappe.get_doc({ - "doctype": "Item", - "item_code": "Consulting", - "item_name": "Consulting", - "item_group": "All Item Groups", - "company": "_Test Company", - "is_stock_item": 0 - }).insert() + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": "Consulting", + "item_name": "Consulting", + "item_group": "All Item Groups", + "company": "_Test Company", + "is_stock_item": 0, + } + ).insert() # item price - frappe.get_doc({ - "doctype": "Item Price", - "price_list": "Standard Selling", - "item_code": item.item_code, - "price_list_rate": 10000 - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "Standard Selling", + "item_code": item.item_code, + "price_list_rate": 10000, + } + ).insert() diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index a9d0081bacc..fc48dd28169 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -16,11 +16,15 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( def execute(filters=None): return _execute(filters) + def _execute(filters, additional_table_columns=None, additional_query_columns=None): - if not filters: filters = frappe._dict({}) + if not filters: + filters = frappe._dict({}) invoice_list = get_invoices(filters, additional_query_columns) - columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(invoice_list, additional_table_columns) + columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns( + invoice_list, additional_table_columns + ) if not invoice_list: msgprint(_("No record found")) @@ -28,12 +32,13 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No invoice_income_map = get_invoice_income_map(invoice_list) internal_invoice_map = get_internal_invoice_map(invoice_list) - invoice_income_map, invoice_tax_map = get_invoice_tax_map(invoice_list, - invoice_income_map, income_accounts) - #Cost Center & Warehouse Map + invoice_income_map, invoice_tax_map = get_invoice_tax_map( + invoice_list, invoice_income_map, income_accounts + ) + # Cost Center & Warehouse Map invoice_cc_wh_map = get_invoice_cc_wh_map(invoice_list) invoice_so_dn_map = get_invoice_so_dn_map(invoice_list) - company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") + company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") mode_of_payments = get_mode_of_payments([inv.name for inv in invoice_list]) data = [] @@ -45,33 +50,33 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", []))) row = { - 'invoice': inv.name, - 'posting_date': inv.posting_date, - 'customer': inv.customer, - 'customer_name': inv.customer_name + "invoice": inv.name, + "posting_date": inv.posting_date, + "customer": inv.customer, + "customer_name": inv.customer_name, } if additional_query_columns: for col in additional_query_columns: - row.update({ - col: inv.get(col) - }) + row.update({col: inv.get(col)}) - row.update({ - 'customer_group': inv.get("customer_group"), - 'territory': inv.get("territory"), - 'tax_id': inv.get("tax_id"), - 'receivable_account': inv.debit_to, - 'mode_of_payment': ", ".join(mode_of_payments.get(inv.name, [])), - 'project': inv.project, - 'owner': inv.owner, - 'remarks': inv.remarks, - 'sales_order': ", ".join(sales_order), - 'delivery_note': ", ".join(delivery_note), - 'cost_center': ", ".join(cost_center), - 'warehouse': ", ".join(warehouse), - 'currency': company_currency - }) + row.update( + { + "customer_group": inv.get("customer_group"), + "territory": inv.get("territory"), + "tax_id": inv.get("tax_id"), + "receivable_account": inv.debit_to, + "mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])), + "project": inv.project, + "owner": inv.owner, + "remarks": inv.remarks, + "sales_order": ", ".join(sales_order), + "delivery_note": ", ".join(delivery_note), + "cost_center": ", ".join(cost_center), + "warehouse": ", ".join(warehouse), + "currency": company_currency, + } + ) # map income values base_net_total = 0 @@ -82,164 +87,138 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) base_net_total += income_amount - row.update({ - frappe.scrub(income_acc): income_amount - }) + row.update({frappe.scrub(income_acc): income_amount}) # Add amount in unrealized account for account in unrealized_profit_loss_accounts: - row.update({ - frappe.scrub(account+"_unrealized"): flt(internal_invoice_map.get((inv.name, account))) - }) + row.update( + {frappe.scrub(account + "_unrealized"): flt(internal_invoice_map.get((inv.name, account)))} + ) # net total - row.update({'net_total': base_net_total or inv.base_net_total}) + row.update({"net_total": base_net_total or inv.base_net_total}) # tax account total_tax = 0 for tax_acc in tax_accounts: if tax_acc not in income_accounts: - tax_amount_precision = get_field_precision(frappe.get_meta("Sales Taxes and Charges").get_field("tax_amount"), currency=company_currency) or 2 + tax_amount_precision = ( + get_field_precision( + frappe.get_meta("Sales Taxes and Charges").get_field("tax_amount"), currency=company_currency + ) + or 2 + ) tax_amount = flt(invoice_tax_map.get(inv.name, {}).get(tax_acc), tax_amount_precision) total_tax += tax_amount - row.update({ - frappe.scrub(tax_acc): tax_amount - }) + row.update({frappe.scrub(tax_acc): tax_amount}) # total tax, grand total, outstanding amount & rounded total - row.update({ - 'tax_total': total_tax, - 'grand_total': inv.base_grand_total, - 'rounded_total': inv.base_rounded_total, - 'outstanding_amount': inv.outstanding_amount - }) + row.update( + { + "tax_total": total_tax, + "grand_total": inv.base_grand_total, + "rounded_total": inv.base_rounded_total, + "outstanding_amount": inv.outstanding_amount, + } + ) data.append(row) return columns, data + def get_columns(invoice_list, additional_table_columns): """return columns based on filters""" columns = [ { - 'label': _("Invoice"), - 'fieldname': 'invoice', - 'fieldtype': 'Link', - 'options': 'Sales Invoice', - 'width': 120 + "label": _("Invoice"), + "fieldname": "invoice", + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 80}, { - 'label': _("Posting Date"), - 'fieldname': 'posting_date', - 'fieldtype': 'Date', - 'width': 80 - }, - { - 'label': _("Customer"), - 'fieldname': 'customer', - 'fieldtype': 'Link', - 'options': 'Customer', - 'width': 120 - }, - { - 'label': _("Customer Name"), - 'fieldname': 'customer_name', - 'fieldtype': 'Data', - 'width': 120 + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 120, }, + {"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120}, ] if additional_table_columns: columns += additional_table_columns - columns +=[ + columns += [ { - 'label': _("Customer Group"), - 'fieldname': 'customer_group', - 'fieldtype': 'Link', - 'options': 'Customer Group', - 'width': 120 + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + "width": 120, }, { - 'label': _("Territory"), - 'fieldname': 'territory', - 'fieldtype': 'Link', - 'options': 'Territory', - 'width': 80 + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 80, + }, + {"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 120}, + { + "label": _("Receivable Account"), + "fieldname": "receivable_account", + "fieldtype": "Link", + "options": "Account", + "width": 80, }, { - 'label': _("Tax Id"), - 'fieldname': 'tax_id', - 'fieldtype': 'Data', - 'width': 120 - }, - { - 'label': _("Receivable Account"), - 'fieldname': 'receivable_account', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 80 - }, - { - 'label': _("Mode Of Payment"), - 'fieldname': 'mode_of_payment', - 'fieldtype': 'Data', - 'width': 120 - }, - { - 'label': _("Project"), - 'fieldname': 'project', - 'fieldtype': 'Link', - 'options': 'Project', - 'width': 80 - }, - { - 'label': _("Owner"), - 'fieldname': 'owner', - 'fieldtype': 'Data', - 'width': 150 - }, - { - 'label': _("Remarks"), - 'fieldname': 'remarks', - 'fieldtype': 'Data', - 'width': 150 - }, - { - 'label': _("Sales Order"), - 'fieldname': 'sales_order', - 'fieldtype': 'Link', - 'options': 'Sales Order', - 'width': 100 - }, - { - 'label': _("Delivery Note"), - 'fieldname': 'delivery_note', - 'fieldtype': 'Link', - 'options': 'Delivery Note', - 'width': 100 - }, - { - 'label': _("Cost Center"), - 'fieldname': 'cost_center', - 'fieldtype': 'Link', - 'options': 'Cost Center', - 'width': 100 - }, - { - 'label': _("Warehouse"), - 'fieldname': 'warehouse', - 'fieldtype': 'Link', - 'options': 'Warehouse', - 'width': 100 - }, - { - "fieldname": "currency", - "label": _("Currency"), + "label": _("Mode Of Payment"), + "fieldname": "mode_of_payment", "fieldtype": "Data", - "width": 80 - } + "width": 120, + }, + { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 80, + }, + {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 150}, + {"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Data", "width": 150}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 100, + }, + { + "label": _("Delivery Note"), + "fieldname": "delivery_note", + "fieldtype": "Link", + "options": "Delivery Note", + "width": 100, + }, + { + "label": _("Cost Center"), + "fieldname": "cost_center", + "fieldtype": "Link", + "options": "Cost Center", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, + {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, ] income_accounts = [] @@ -250,106 +229,135 @@ def get_columns(invoice_list, additional_table_columns): unrealized_profit_loss_account_columns = [] if invoice_list: - income_accounts = frappe.db.sql_list("""select distinct income_account + income_accounts = frappe.db.sql_list( + """select distinct income_account from `tabSales Invoice Item` where docstatus = 1 and parent in (%s) - order by income_account""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) + order by income_account""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + ) - tax_accounts = frappe.db.sql_list("""select distinct account_head + tax_accounts = frappe.db.sql_list( + """select distinct account_head from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice' and docstatus = 1 and base_tax_amount_after_discount_amount != 0 - and parent in (%s) order by account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) + and parent in (%s) order by account_head""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + ) - unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account + unrealized_profit_loss_accounts = frappe.db.sql_list( + """SELECT distinct unrealized_profit_loss_account from `tabSales Invoice` where docstatus = 1 and name in (%s) and is_internal_customer = 1 and ifnull(unrealized_profit_loss_account, '') != '' - order by unrealized_profit_loss_account""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) + order by unrealized_profit_loss_account""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + ) for account in income_accounts: - income_columns.append({ - "label": account, - "fieldname": frappe.scrub(account), - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }) - - for account in tax_accounts: - if account not in income_accounts: - tax_columns.append({ + income_columns.append( + { "label": account, "fieldname": frappe.scrub(account), "fieldtype": "Currency", "options": "currency", - "width": 120 - }) + "width": 120, + } + ) + + for account in tax_accounts: + if account not in income_accounts: + tax_columns.append( + { + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) for account in unrealized_profit_loss_accounts: - unrealized_profit_loss_account_columns.append({ - "label": account, - "fieldname": frappe.scrub(account+"_unrealized"), + unrealized_profit_loss_account_columns.append( + { + "label": account, + "fieldname": frappe.scrub(account + "_unrealized"), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) + + net_total_column = [ + { + "label": _("Net Total"), + "fieldname": "net_total", "fieldtype": "Currency", "options": "currency", - "width": 120 - }) - - net_total_column = [{ - "label": _("Net Total"), - "fieldname": "net_total", - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }] + "width": 120, + } + ] total_columns = [ { "label": _("Tax Total"), "fieldname": "tax_total", "fieldtype": "Currency", - "options": 'currency', - "width": 120 + "options": "currency", + "width": 120, }, { "label": _("Grand Total"), "fieldname": "grand_total", "fieldtype": "Currency", - "options": 'currency', - "width": 120 + "options": "currency", + "width": 120, }, { "label": _("Rounded Total"), "fieldname": "rounded_total", "fieldtype": "Currency", - "options": 'currency', - "width": 120 + "options": "currency", + "width": 120, }, { "label": _("Outstanding Amount"), "fieldname": "outstanding_amount", "fieldtype": "Currency", - "options": 'currency', - "width": 120 - } + "options": "currency", + "width": 120, + }, ] - columns = columns + income_columns + unrealized_profit_loss_account_columns + \ - net_total_column + tax_columns + total_columns + columns = ( + columns + + income_columns + + unrealized_profit_loss_account_columns + + net_total_column + + tax_columns + + total_columns + ) return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts + def get_conditions(filters): conditions = "" - if filters.get("company"): conditions += " and company=%(company)s" - if filters.get("customer"): conditions += " and customer = %(customer)s" + if filters.get("company"): + conditions += " and company=%(company)s" + if filters.get("customer"): + conditions += " and customer = %(customer)s" - if filters.get("from_date"): conditions += " and posting_date >= %(from_date)s" - if filters.get("to_date"): conditions += " and posting_date <= %(to_date)s" + if filters.get("from_date"): + conditions += " and posting_date >= %(from_date)s" + if filters.get("to_date"): + conditions += " and posting_date <= %(to_date)s" - if filters.get("owner"): conditions += " and owner = %(owner)s" + if filters.get("owner"): + conditions += " and owner = %(owner)s" if filters.get("mode_of_payment"): conditions += """ and exists(select name from `tabSales Invoice Payment` @@ -357,22 +365,22 @@ def get_conditions(filters): and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)""" if filters.get("cost_center"): - conditions += """ and exists(select name from `tabSales Invoice Item` + conditions += """ and exists(select name from `tabSales Invoice Item` where parent=`tabSales Invoice`.name and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)""" if filters.get("warehouse"): - conditions += """ and exists(select name from `tabSales Invoice Item` + conditions += """ and exists(select name from `tabSales Invoice Item` where parent=`tabSales Invoice`.name and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)""" if filters.get("brand"): - conditions += """ and exists(select name from `tabSales Invoice Item` + conditions += """ and exists(select name from `tabSales Invoice Item` where parent=`tabSales Invoice`.name and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)""" if filters.get("item_group"): - conditions += """ and exists(select name from `tabSales Invoice Item` + conditions += """ and exists(select name from `tabSales Invoice Item` where parent=`tabSales Invoice`.name and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)""" @@ -385,34 +393,53 @@ def get_conditions(filters): """ for dimension in accounting_dimensions: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) - conditions += common_condition + "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname) + conditions += ( + common_condition + + "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname) + ) else: - conditions += common_condition + "and ifnull(`tabSales Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname) + conditions += ( + common_condition + + "and ifnull(`tabSales Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname) + ) return conditions + def get_invoices(filters, additional_query_columns): if additional_query_columns: - additional_query_columns = ', ' + ', '.join(additional_query_columns) + additional_query_columns = ", " + ", ".join(additional_query_columns) conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, posting_date, debit_to, project, customer, customer_name, owner, remarks, territory, tax_id, customer_group, base_net_total, base_grand_total, base_rounded_total, outstanding_amount, is_internal_customer, represents_company, company {0} from `tabSales Invoice` - where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') % - conditions, filters, as_dict=1) + where docstatus = 1 %s order by posting_date desc, name desc""".format( + additional_query_columns or "" + ) + % conditions, + filters, + as_dict=1, + ) + def get_invoice_income_map(invoice_list): - income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount - from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + income_details = frappe.db.sql( + """select parent, income_account, sum(base_net_amount) as amount + from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) invoice_income_map = {} for d in income_details: @@ -421,11 +448,16 @@ def get_invoice_income_map(invoice_list): return invoice_income_map + def get_internal_invoice_map(invoice_list): - unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, + unrealized_amount_details = frappe.db.sql( + """SELECT name, unrealized_profit_loss_account, base_net_total as amount from `tabSales Invoice` where name in (%s) - and is_internal_customer = 1 and company = represents_company""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + and is_internal_customer = 1 and company = represents_company""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) internal_invoice_map = {} for d in unrealized_amount_details: @@ -434,11 +466,16 @@ def get_internal_invoice_map(invoice_list): return internal_invoice_map + def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): - tax_details = frappe.db.sql("""select parent, account_head, + tax_details = frappe.db.sql( + """select parent, account_head, sum(base_tax_amount_after_discount_amount) as tax_amount - from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) invoice_tax_map = {} for d in tax_details: @@ -453,54 +490,77 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): return invoice_income_map, invoice_tax_map + def get_invoice_so_dn_map(invoice_list): - si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail + si_items = frappe.db.sql( + """select parent, sales_order, delivery_note, so_detail from `tabSales Invoice Item` where parent in (%s) - and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) invoice_so_dn_map = {} for d in si_items: if d.sales_order: - invoice_so_dn_map.setdefault(d.parent, frappe._dict()).setdefault( - "sales_order", []).append(d.sales_order) + invoice_so_dn_map.setdefault(d.parent, frappe._dict()).setdefault("sales_order", []).append( + d.sales_order + ) delivery_note_list = None if d.delivery_note: delivery_note_list = [d.delivery_note] elif d.sales_order: - delivery_note_list = frappe.db.sql_list("""select distinct parent from `tabDelivery Note Item` - where docstatus=1 and so_detail=%s""", d.so_detail) + delivery_note_list = frappe.db.sql_list( + """select distinct parent from `tabDelivery Note Item` + where docstatus=1 and so_detail=%s""", + d.so_detail, + ) if delivery_note_list: - invoice_so_dn_map.setdefault(d.parent, frappe._dict()).setdefault("delivery_note", delivery_note_list) + invoice_so_dn_map.setdefault(d.parent, frappe._dict()).setdefault( + "delivery_note", delivery_note_list + ) return invoice_so_dn_map + def get_invoice_cc_wh_map(invoice_list): - si_items = frappe.db.sql("""select parent, cost_center, warehouse + si_items = frappe.db.sql( + """select parent, cost_center, warehouse from `tabSales Invoice Item` where parent in (%s) - and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" % - ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) + and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(inv.name for inv in invoice_list), + as_dict=1, + ) invoice_cc_wh_map = {} for d in si_items: if d.cost_center: - invoice_cc_wh_map.setdefault(d.parent, frappe._dict()).setdefault( - "cost_center", []).append(d.cost_center) + invoice_cc_wh_map.setdefault(d.parent, frappe._dict()).setdefault("cost_center", []).append( + d.cost_center + ) if d.warehouse: - invoice_cc_wh_map.setdefault(d.parent, frappe._dict()).setdefault( - "warehouse", []).append(d.warehouse) + invoice_cc_wh_map.setdefault(d.parent, frappe._dict()).setdefault("warehouse", []).append( + d.warehouse + ) return invoice_cc_wh_map + def get_mode_of_payments(invoice_list): mode_of_payments = {} if invoice_list: - inv_mop = frappe.db.sql("""select parent, mode_of_payment - from `tabSales Invoice Payment` where parent in (%s) group by parent, mode_of_payment""" % - ', '.join(['%s']*len(invoice_list)), tuple(invoice_list), as_dict=1) + inv_mop = frappe.db.sql( + """select parent, mode_of_payment + from `tabSales Invoice Payment` where parent in (%s) group by parent, mode_of_payment""" + % ", ".join(["%s"] * len(invoice_list)), + tuple(invoice_list), + as_dict=1, + ) for d in inv_mop: mode_of_payments.setdefault(d.parent, []).append(d.mode_of_payment) diff --git a/erpnext/accounts/report/share_balance/share_balance.py b/erpnext/accounts/report/share_balance/share_balance.py index 943c4e150f0..d02f53b0d2b 100644 --- a/erpnext/accounts/report/share_balance/share_balance.py +++ b/erpnext/accounts/report/share_balance/share_balance.py @@ -7,7 +7,8 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if not filters.get("date"): frappe.throw(_("Please select date")) @@ -38,22 +39,29 @@ def execute(filters=None): break # new entry if not row: - row = [filters.get("shareholder"), - share_entry.share_type, share_entry.no_of_shares, share_entry.rate, share_entry.amount] + row = [ + filters.get("shareholder"), + share_entry.share_type, + share_entry.no_of_shares, + share_entry.rate, + share_entry.amount, + ] data.append(row) return columns, data + def get_columns(filters): columns = [ _("Shareholder") + ":Link/Shareholder:150", _("Share Type") + "::90", _("No of Shares") + "::90", _("Average Rate") + ":Currency:90", - _("Amount") + ":Currency:90" + _("Amount") + ":Currency:90", ] return columns + def get_all_shares(shareholder): - return frappe.get_doc('Shareholder', shareholder).share_balance + return frappe.get_doc("Shareholder", shareholder).share_balance diff --git a/erpnext/accounts/report/share_ledger/share_ledger.py b/erpnext/accounts/report/share_ledger/share_ledger.py index b3ff6e48a6f..d6c3bd059f4 100644 --- a/erpnext/accounts/report/share_ledger/share_ledger.py +++ b/erpnext/accounts/report/share_ledger/share_ledger.py @@ -7,7 +7,8 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if not filters.get("date"): frappe.throw(_("Please select date")) @@ -23,19 +24,28 @@ def execute(filters=None): else: transfers = get_all_transfers(date, filters.get("shareholder")) for transfer in transfers: - if transfer.transfer_type == 'Transfer': + if transfer.transfer_type == "Transfer": if transfer.from_shareholder == filters.get("shareholder"): - transfer.transfer_type += ' to {}'.format(transfer.to_shareholder) + transfer.transfer_type += " to {}".format(transfer.to_shareholder) else: - transfer.transfer_type += ' from {}'.format(transfer.from_shareholder) - row = [filters.get("shareholder"), transfer.date, transfer.transfer_type, - transfer.share_type, transfer.no_of_shares, transfer.rate, transfer.amount, - transfer.company, transfer.name] + transfer.transfer_type += " from {}".format(transfer.from_shareholder) + row = [ + filters.get("shareholder"), + transfer.date, + transfer.transfer_type, + transfer.share_type, + transfer.no_of_shares, + transfer.rate, + transfer.amount, + transfer.company, + transfer.name, + ] data.append(row) return columns, data + def get_columns(filters): columns = [ _("Shareholder") + ":Link/Shareholder:150", @@ -46,16 +56,22 @@ def get_columns(filters): _("Rate") + ":Currency:90", _("Amount") + ":Currency:90", _("Company") + "::150", - _("Share Transfer") + ":Link/Share Transfer:90" + _("Share Transfer") + ":Link/Share Transfer:90", ] return columns + def get_all_transfers(date, shareholder): - condition = ' ' + condition = " " # if company: # condition = 'AND company = %(company)s ' - return frappe.db.sql("""SELECT * FROM `tabShare Transfer` + return frappe.db.sql( + """SELECT * FROM `tabShare Transfer` WHERE (DATE(date) <= %(date)s AND from_shareholder = %(shareholder)s {condition}) OR (DATE(date) <= %(date)s AND to_shareholder = %(shareholder)s {condition}) - ORDER BY date""".format(condition=condition), - {'date': date, 'shareholder': shareholder}, as_dict=1) + ORDER BY date""".format( + condition=condition + ), + {"date": date, "shareholder": shareholder}, + as_dict=1, + ) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index d576c273a27..08d20086823 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ @@ -12,7 +11,7 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): validate_filters(filters) - filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name') + filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name") columns = get_columns(filters) tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters) @@ -22,8 +21,9 @@ def execute(filters=None): return columns, final_result + def validate_filters(filters): - ''' Validate if dates are properly set and lie in the same fiscal year''' + """Validate if dates are properly set and lie in the same fiscal year""" if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) @@ -34,26 +34,32 @@ def validate_filters(filters): filters["fiscal_year"] = from_year + def group_by_supplier_and_category(data): supplier_category_wise_map = {} for row in data: - supplier_category_wise_map.setdefault((row.get('supplier'), row.get('section_code')), { - 'pan': row.get('pan'), - 'supplier': row.get('supplier'), - 'supplier_name': row.get('supplier_name'), - 'section_code': row.get('section_code'), - 'entity_type': row.get('entity_type'), - 'tds_rate': row.get('tds_rate'), - 'total_amount_credited': 0.0, - 'tds_deducted': 0.0 - }) + supplier_category_wise_map.setdefault( + (row.get("supplier"), row.get("section_code")), + { + "pan": row.get("pan"), + "supplier": row.get("supplier"), + "supplier_name": row.get("supplier_name"), + "section_code": row.get("section_code"), + "entity_type": row.get("entity_type"), + "tds_rate": row.get("tds_rate"), + "total_amount_credited": 0.0, + "tds_deducted": 0.0, + }, + ) - supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['total_amount_credited'] += \ - row.get('total_amount_credited', 0.0) + supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[ + "total_amount_credited" + ] += row.get("total_amount_credited", 0.0) - supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['tds_deducted'] += \ - row.get('tds_deducted', 0.0) + supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[ + "tds_deducted" + ] += row.get("tds_deducted", 0.0) final_result = get_final_result(supplier_category_wise_map) @@ -67,62 +73,48 @@ def get_final_result(supplier_category_wise_map): return out + def get_columns(filters): columns = [ - { - "label": _("PAN"), - "fieldname": "pan", - "fieldtype": "Data", - "width": 90 - }, + {"label": _("PAN"), "fieldname": "pan", "fieldtype": "Data", "width": 90}, { "label": _("Supplier"), "options": "Supplier", "fieldname": "supplier", "fieldtype": "Link", - "width": 180 - }] + "width": 180, + }, + ] - if filters.naming_series == 'Naming Series': - columns.append({ - "label": _("Supplier Name"), - "fieldname": "supplier_name", - "fieldtype": "Data", - "width": 180 - }) + if filters.naming_series == "Naming Series": + columns.append( + {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180} + ) - columns.extend([ - { - "label": _("Section Code"), - "options": "Tax Withholding Category", - "fieldname": "section_code", - "fieldtype": "Link", - "width": 180 - }, - { - "label": _("Entity Type"), - "fieldname": "entity_type", - "fieldtype": "Data", - "width": 180 - }, - { - "label": _("TDS Rate %"), - "fieldname": "tds_rate", - "fieldtype": "Percent", - "width": 90 - }, - { - "label": _("Total Amount Credited"), - "fieldname": "total_amount_credited", - "fieldtype": "Float", - "width": 90 - }, - { - "label": _("Amount of TDS Deducted"), - "fieldname": "tds_deducted", - "fieldtype": "Float", - "width": 90 - } - ]) + columns.extend( + [ + { + "label": _("Section Code"), + "options": "Tax Withholding Category", + "fieldname": "section_code", + "fieldtype": "Link", + "width": 180, + }, + {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180}, + {"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90}, + { + "label": _("Total Amount Credited"), + "fieldname": "total_amount_credited", + "fieldtype": "Float", + "width": 90, + }, + { + "label": _("Amount of TDS Deducted"), + "fieldname": "tds_deducted", + "fieldtype": "Float", + "width": 90, + }, + ] + ) return columns diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index e6cbff5d429..30ea5a9e4e1 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -15,11 +15,13 @@ def execute(filters=None): res = get_result(filters, tds_docs, tds_accounts, tax_category_map) return columns, res + def validate_filters(filters): - ''' Validate if dates are properly set ''' + """Validate if dates are properly set""" if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) + def get_result(filters, tds_docs, tds_accounts, tax_category_map): supplier_map = get_supplier_pan_map() tax_rate_map = get_tax_rate_map(filters) @@ -37,57 +39,63 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): voucher_type = entry.voucher_type if not tax_withholding_category: - tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category') + tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category") rate = tax_rate_map.get(tax_withholding_category) if entry.account in tds_accounts: - tds_deducted += (entry.credit - entry.debit) + tds_deducted += entry.credit - entry.debit total_amount_credited += entry.credit if tds_deducted: row = { - 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'), - 'supplier': supplier_map.get(supplier, {}).get('name') + "pan" + if frappe.db.has_column("Supplier", "pan") + else "tax_id": supplier_map.get(supplier, {}).get("pan"), + "supplier": supplier_map.get(supplier, {}).get("name"), } - if filters.naming_series == 'Naming Series': - row.update({'supplier_name': supplier_map.get(supplier, {}).get('supplier_name')}) + if filters.naming_series == "Naming Series": + row.update({"supplier_name": supplier_map.get(supplier, {}).get("supplier_name")}) - row.update({ - 'section_code': tax_withholding_category, - 'entity_type': supplier_map.get(supplier, {}).get('supplier_type'), - 'tds_rate': rate, - 'total_amount_credited': total_amount_credited, - 'tds_deducted': tds_deducted, - 'transaction_date': posting_date, - 'transaction_type': voucher_type, - 'ref_no': name - }) + row.update( + { + "section_code": tax_withholding_category, + "entity_type": supplier_map.get(supplier, {}).get("supplier_type"), + "tds_rate": rate, + "total_amount_credited": total_amount_credited, + "tds_deducted": tds_deducted, + "transaction_date": posting_date, + "transaction_type": voucher_type, + "ref_no": name, + } + ) out.append(row) return out + def get_supplier_pan_map(): supplier_map = frappe._dict() - suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category']) + suppliers = frappe.db.get_all( + "Supplier", fields=["name", "pan", "supplier_type", "supplier_name", "tax_withholding_category"] + ) for d in suppliers: supplier_map[d.name] = d return supplier_map + def get_gle_map(documents): # create gle_map of the form # {"purchase_invoice": list of dict of all gle created for this invoice} gle_map = {} - gle = frappe.db.get_all('GL Entry', - { - "voucher_no": ["in", documents], - "is_cancelled": 0 - }, + gle = frappe.db.get_all( + "GL Entry", + {"voucher_no": ["in", documents], "is_cancelled": 0}, ["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"], ) @@ -99,85 +107,68 @@ def get_gle_map(documents): return gle_map + def get_columns(filters): pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id" columns = [ - { - "label": _(frappe.unscrub(pan)), - "fieldname": pan, - "fieldtype": "Data", - "width": 90 - }, + {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90}, { "label": _("Supplier"), "options": "Supplier", "fieldname": "supplier", "fieldtype": "Link", - "width": 180 - }] + "width": 180, + }, + ] - if filters.naming_series == 'Naming Series': - columns.append({ - "label": _("Supplier Name"), - "fieldname": "supplier_name", - "fieldtype": "Data", - "width": 180 - }) + if filters.naming_series == "Naming Series": + columns.append( + {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180} + ) - columns.extend([ - { - "label": _("Section Code"), - "options": "Tax Withholding Category", - "fieldname": "section_code", - "fieldtype": "Link", - "width": 180 - }, - { - "label": _("Entity Type"), - "fieldname": "entity_type", - "fieldtype": "Data", - "width": 180 - }, - { - "label": _("TDS Rate %"), - "fieldname": "tds_rate", - "fieldtype": "Percent", - "width": 90 - }, - { - "label": _("Total Amount Credited"), - "fieldname": "total_amount_credited", - "fieldtype": "Float", - "width": 90 - }, - { - "label": _("Amount of TDS Deducted"), - "fieldname": "tds_deducted", - "fieldtype": "Float", - "width": 90 - }, - { - "label": _("Date of Transaction"), - "fieldname": "transaction_date", - "fieldtype": "Date", - "width": 90 - }, - { - "label": _("Transaction Type"), - "fieldname": "transaction_type", - "width": 90 - }, - { - "label": _("Reference No."), - "fieldname": "ref_no", - "fieldtype": "Dynamic Link", - "options": "transaction_type", - "width": 90 - } - ]) + columns.extend( + [ + { + "label": _("Section Code"), + "options": "Tax Withholding Category", + "fieldname": "section_code", + "fieldtype": "Link", + "width": 180, + }, + {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180}, + {"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90}, + { + "label": _("Total Amount Credited"), + "fieldname": "total_amount_credited", + "fieldtype": "Float", + "width": 90, + }, + { + "label": _("Amount of TDS Deducted"), + "fieldname": "tds_deducted", + "fieldtype": "Float", + "width": 90, + }, + { + "label": _("Date of Transaction"), + "fieldname": "transaction_date", + "fieldtype": "Date", + "width": 90, + }, + {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 90}, + { + "label": _("Reference No."), + "fieldname": "ref_no", + "fieldtype": "Dynamic Link", + "options": "transaction_type", + "width": 90, + }, + ] + ) return columns + def get_tds_docs(filters): tds_documents = [] purchase_invoices = [] @@ -185,27 +176,30 @@ def get_tds_docs(filters): journal_entries = [] tax_category_map = {} or_filters = {} - bank_accounts = frappe.get_all('Account', {'is_group': 0, 'account_type': 'Bank'}, pluck="name") + bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") - tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')}, - pluck="account") + tds_accounts = frappe.get_all( + "Tax Withholding Account", {"company": filters.get("company")}, pluck="account" + ) query_filters = { "account": ("in", tds_accounts), "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), "is_cancelled": 0, - "against": ("not in", bank_accounts) + "against": ("not in", bank_accounts), } if filters.get("supplier"): del query_filters["account"] del query_filters["against"] - or_filters = { - "against": filters.get('supplier'), - "party": filters.get('supplier') - } + or_filters = {"against": filters.get("supplier"), "party": filters.get("supplier")} - tds_docs = frappe.get_all("GL Entry", filters=query_filters, or_filters=or_filters, fields=["voucher_no", "voucher_type", "against", "party"]) + tds_docs = frappe.get_all( + "GL Entry", + filters=query_filters, + or_filters=or_filters, + fields=["voucher_no", "voucher_type", "against", "party"], + ) for d in tds_docs: if d.voucher_type == "Purchase Invoice": @@ -218,24 +212,39 @@ def get_tds_docs(filters): tds_documents.append(d.voucher_no) if purchase_invoices: - get_tax_category_map(purchase_invoices, 'Purchase Invoice', tax_category_map) + get_tax_category_map(purchase_invoices, "Purchase Invoice", tax_category_map) if payment_entries: - get_tax_category_map(payment_entries, 'Payment Entry', tax_category_map) + get_tax_category_map(payment_entries, "Payment Entry", tax_category_map) if journal_entries: - get_tax_category_map(journal_entries, 'Journal Entry', tax_category_map) + get_tax_category_map(journal_entries, "Journal Entry", tax_category_map) return tds_documents, tds_accounts, tax_category_map + def get_tax_category_map(vouchers, doctype, tax_category_map): - tax_category_map.update(frappe._dict(frappe.get_all(doctype, - filters = {'name': ('in', vouchers)}, fields=['name', 'tax_withholding_category'], as_list=1))) + tax_category_map.update( + frappe._dict( + frappe.get_all( + doctype, + filters={"name": ("in", vouchers)}, + fields=["name", "tax_withholding_category"], + as_list=1, + ) + ) + ) + def get_tax_rate_map(filters): - rate_map = frappe.get_all('Tax Withholding Rate', filters={ - 'from_date': ('<=', filters.get('from_date')), - 'to_date': ('>=', filters.get('to_date')) - }, fields=['parent', 'tax_withholding_rate'], as_list=1) + rate_map = frappe.get_all( + "Tax Withholding Rate", + filters={ + "from_date": ("<=", filters.get("from_date")), + "to_date": (">=", filters.get("to_date")), + }, + fields=["parent", "tax_withholding_rate"], + as_list=1, + ) - return frappe._dict(rate_map) \ No newline at end of file + return frappe._dict(rate_map) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index bda44f66aa1..dd0ac756544 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -17,7 +17,15 @@ from erpnext.accounts.report.financial_statements import ( set_gl_entries_by_account, ) -value_fields = ("opening_debit", "opening_credit", "debit", "credit", "closing_debit", "closing_credit") +value_fields = ( + "opening_debit", + "opening_credit", + "debit", + "credit", + "closing_debit", + "closing_credit", +) + def execute(filters=None): validate_filters(filters) @@ -25,11 +33,14 @@ def execute(filters=None): columns = get_columns() return columns, data + def validate_filters(filters): if not filters.fiscal_year: frappe.throw(_("Fiscal Year {0} is required").format(filters.fiscal_year)) - fiscal_year = frappe.db.get_value("Fiscal Year", filters.fiscal_year, ["year_start_date", "year_end_date"], as_dict=True) + fiscal_year = frappe.db.get_value( + "Fiscal Year", filters.fiscal_year, ["year_start_date", "year_end_date"], as_dict=True + ) if not fiscal_year: frappe.throw(_("Fiscal Year {0} does not exist").format(filters.fiscal_year)) else: @@ -49,21 +60,32 @@ def validate_filters(filters): frappe.throw(_("From Date cannot be greater than To Date")) if (filters.from_date < filters.year_start_date) or (filters.from_date > filters.year_end_date): - frappe.msgprint(_("From Date should be within the Fiscal Year. Assuming From Date = {0}")\ - .format(formatdate(filters.year_start_date))) + frappe.msgprint( + _("From Date should be within the Fiscal Year. Assuming From Date = {0}").format( + formatdate(filters.year_start_date) + ) + ) filters.from_date = filters.year_start_date if (filters.to_date < filters.year_start_date) or (filters.to_date > filters.year_end_date): - frappe.msgprint(_("To Date should be within the Fiscal Year. Assuming To Date = {0}")\ - .format(formatdate(filters.year_end_date))) + frappe.msgprint( + _("To Date should be within the Fiscal Year. Assuming To Date = {0}").format( + formatdate(filters.year_end_date) + ) + ) filters.to_date = filters.year_end_date + def get_data(filters): - accounts = frappe.db.sql("""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt + accounts = frappe.db.sql( + """select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt - from `tabAccount` where company=%s order by lft""", filters.company, as_dict=True) + from `tabAccount` where company=%s order by lft""", + filters.company, + as_dict=True, + ) company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company) if not accounts: @@ -71,28 +93,44 @@ def get_data(filters): accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) - min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount` - where company=%s""", (filters.company,))[0] + min_lft, max_rgt = frappe.db.sql( + """select min(lft), max(rgt) from `tabAccount` + where company=%s""", + (filters.company,), + )[0] gl_entries_by_account = {} opening_balances = get_opening_balances(filters) - #add filter inside list so that the query in financial_statements.py doesn't break + # add filter inside list so that the query in financial_statements.py doesn't break if filters.project: filters.project = [filters.project] - set_gl_entries_by_account(filters.company, filters.from_date, - filters.to_date, min_lft, max_rgt, filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry)) + set_gl_entries_by_account( + filters.company, + filters.from_date, + filters.to_date, + min_lft, + max_rgt, + filters, + gl_entries_by_account, + ignore_closing_entries=not flt(filters.with_period_closing_entry), + ) - total_row = calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency) + total_row = calculate_values( + accounts, gl_entries_by_account, opening_balances, filters, company_currency + ) accumulate_values_into_parents(accounts, accounts_by_name) data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency) - data = filter_out_zero_value_rows(data, parent_children_map, show_zero_values=filters.get("show_zero_values")) + data = filter_out_zero_value_rows( + data, parent_children_map, show_zero_values=filters.get("show_zero_values") + ) return data + def get_opening_balances(filters): balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet") pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss") @@ -104,16 +142,20 @@ def get_opening_balances(filters): def get_rootwise_opening_balances(filters, report_type): additional_conditions = "" if not filters.show_unclosed_fy_pl_balances: - additional_conditions = " and posting_date >= %(year_start_date)s" \ - if report_type == "Profit and Loss" else "" + additional_conditions = ( + " and posting_date >= %(year_start_date)s" if report_type == "Profit and Loss" else "" + ) if not flt(filters.with_period_closing_entry): additional_conditions += " and ifnull(voucher_type, '')!='Period Closing Voucher'" if filters.cost_center: - lft, rgt = frappe.db.get_value('Cost Center', filters.cost_center, ['lft', 'rgt']) + lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) additional_conditions += """ and cost_center in (select name from `tabCost Center` - where lft >= %s and rgt <= %s)""" % (lft, rgt) + where lft >= %s and rgt <= %s)""" % ( + lft, + rgt, + ) if filters.project: additional_conditions += " and project = %(project)s" @@ -121,7 +163,9 @@ def get_rootwise_opening_balances(filters, report_type): if filters.finance_book: fb_conditions = " AND finance_book = %(finance_book)s" if filters.include_default_book_entries: - fb_conditions = " AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + fb_conditions = ( + " AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) additional_conditions += fb_conditions @@ -134,24 +178,24 @@ def get_rootwise_opening_balances(filters, report_type): "year_start_date": filters.year_start_date, "project": filters.project, "finance_book": filters.finance_book, - "company_fb": frappe.db.get_value("Company", filters.company, 'default_finance_book') + "company_fb": frappe.db.get_value("Company", filters.company, "default_finance_book"), } if accounting_dimensions: for dimension in accounting_dimensions: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) additional_conditions += "and {0} in %({0})s".format(dimension.fieldname) else: additional_conditions += "and {0} in (%({0})s)".format(dimension.fieldname) - query_filters.update({ - dimension.fieldname: filters.get(dimension.fieldname) - }) + query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)}) - gle = frappe.db.sql(""" + gle = frappe.db.sql( + """ select account, sum(debit) as opening_debit, sum(credit) as opening_credit from `tabGL Entry` @@ -161,7 +205,12 @@ def get_rootwise_opening_balances(filters, report_type): and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') and account in (select name from `tabAccount` where report_type=%(report_type)s) and is_cancelled = 0 - group by account""".format(additional_conditions=additional_conditions), query_filters , as_dict=True) + group by account""".format( + additional_conditions=additional_conditions + ), + query_filters, + as_dict=True, + ) opening = frappe._dict() for d in gle: @@ -169,6 +218,7 @@ def get_rootwise_opening_balances(filters, report_type): return opening + def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency): init = { "opening_debit": 0.0, @@ -176,7 +226,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, "debit": 0.0, "credit": 0.0, "closing_debit": 0.0, - "closing_credit": 0.0 + "closing_credit": 0.0, } total_row = { @@ -192,7 +242,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, "parent_account": None, "indent": 0, "has_value": True, - "currency": company_currency + "currency": company_currency, } for d in accounts: @@ -217,12 +267,14 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, return total_row + def accumulate_values_into_parents(accounts, accounts_by_name): for d in reversed(accounts): if d.parent_account: for key in value_fields: accounts_by_name[d.parent_account][key] += d[key] + def prepare_data(accounts, filters, total_row, parent_children_map, company_currency): data = [] @@ -239,8 +291,9 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr "from_date": filters.from_date, "to_date": filters.to_date, "currency": company_currency, - "account_name": ('{} - {}'.format(d.account_number, d.account_name) - if d.account_number else d.account_name) + "account_name": ( + "{} - {}".format(d.account_number, d.account_name) if d.account_number else d.account_name + ), } for key in value_fields: @@ -253,10 +306,11 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr row["has_value"] = has_value data.append(row) - data.extend([{},total_row]) + data.extend([{}, total_row]) return data + def get_columns(): return [ { @@ -264,59 +318,60 @@ def get_columns(): "label": _("Account"), "fieldtype": "Link", "options": "Account", - "width": 300 + "width": 300, }, { "fieldname": "currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "hidden": 1 + "hidden": 1, }, { "fieldname": "opening_debit", "label": _("Opening (Dr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "opening_credit", "label": _("Opening (Cr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "closing_debit", "label": _("Closing (Dr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "closing_credit", "label": _("Closing (Cr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 - } + "width": 120, + }, ] + def prepare_opening_closing(row): dr_or_cr = "debit" if row["root_type"] in ["Asset", "Equity", "Expense"] else "credit" reverse_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index d843dfd3ce3..869f6aaf942 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -19,102 +19,101 @@ def execute(filters=None): return columns, data + def get_data(filters, show_party_name): - if filters.get('party_type') in ('Customer', 'Supplier', 'Employee', 'Member'): - party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) - elif filters.get('party_type') == 'Student': - party_name_field = 'first_name' - elif filters.get('party_type') == 'Shareholder': - party_name_field = 'title' + if filters.get("party_type") in ("Customer", "Supplier", "Employee", "Member"): + party_name_field = "{0}_name".format(frappe.scrub(filters.get("party_type"))) + elif filters.get("party_type") == "Student": + party_name_field = "first_name" + elif filters.get("party_type") == "Shareholder": + party_name_field = "title" else: - party_name_field = 'name' + party_name_field = "name" party_filters = {"name": filters.get("party")} if filters.get("party") else {} - parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field], - filters = party_filters, order_by="name") - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") + parties = frappe.get_all( + filters.get("party_type"), + fields=["name", party_name_field], + filters=party_filters, + order_by="name", + ) + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") opening_balances = get_opening_balances(filters) balances_within_period = get_balances_within_period(filters) data = [] # total_debit, total_credit = 0, 0 - total_row = frappe._dict({ - "opening_debit": 0, - "opening_credit": 0, - "debit": 0, - "credit": 0, - "closing_debit": 0, - "closing_credit": 0 - }) + total_row = frappe._dict( + { + "opening_debit": 0, + "opening_credit": 0, + "debit": 0, + "credit": 0, + "closing_debit": 0, + "closing_credit": 0, + } + ) for party in parties: - row = { "party": party.name } + row = {"party": party.name} if show_party_name: row["party_name"] = party.get(party_name_field) # opening opening_debit, opening_credit = opening_balances.get(party.name, [0, 0]) - row.update({ - "opening_debit": opening_debit, - "opening_credit": opening_credit - }) + row.update({"opening_debit": opening_debit, "opening_credit": opening_credit}) # within period debit, credit = balances_within_period.get(party.name, [0, 0]) - row.update({ - "debit": debit, - "credit": credit - }) + row.update({"debit": debit, "credit": credit}) # closing - closing_debit, closing_credit = toggle_debit_credit(opening_debit + debit, opening_credit + credit) - row.update({ - "closing_debit": closing_debit, - "closing_credit": closing_credit - }) + closing_debit, closing_credit = toggle_debit_credit( + opening_debit + debit, opening_credit + credit + ) + row.update({"closing_debit": closing_debit, "closing_credit": closing_credit}) # totals for col in total_row: total_row[col] += row.get(col) - row.update({ - "currency": company_currency - }) + row.update({"currency": company_currency}) has_value = False - if (opening_debit or opening_credit or debit or credit or closing_debit or closing_credit): - has_value =True + if opening_debit or opening_credit or debit or credit or closing_debit or closing_credit: + has_value = True if cint(filters.show_zero_values) or has_value: data.append(row) # Add total row - total_row.update({ - "party": "'" + _("Totals") + "'", - "currency": company_currency - }) + total_row.update({"party": "'" + _("Totals") + "'", "currency": company_currency}) data.append(total_row) return data + def get_opening_balances(filters): - account_filter = '' - if filters.get('account'): - account_filter = "and account = %s" % (frappe.db.escape(filters.get('account'))) + account_filter = "" + if filters.get("account"): + account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) - gle = frappe.db.sql(""" + gle = frappe.db.sql( + """ select party, sum(debit) as opening_debit, sum(credit) as opening_credit from `tabGL Entry` where company=%(company)s + and is_cancelled=0 and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') {account_filter} - group by party""".format(account_filter=account_filter), { - "company": filters.company, - "from_date": filters.from_date, - "party_type": filters.party_type - }, as_dict=True) + group by party""".format( + account_filter=account_filter + ), + {"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type}, + as_dict=True, + ) opening = frappe._dict() for d in gle: @@ -123,26 +122,34 @@ def get_opening_balances(filters): return opening + def get_balances_within_period(filters): - account_filter = '' - if filters.get('account'): - account_filter = "and account = %s" % (frappe.db.escape(filters.get('account'))) + account_filter = "" + if filters.get("account"): + account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) - gle = frappe.db.sql(""" + gle = frappe.db.sql( + """ select party, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where company=%(company)s + and is_cancelled = 0 and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and posting_date >= %(from_date)s and posting_date <= %(to_date)s and ifnull(is_opening, 'No') = 'No' {account_filter} - group by party""".format(account_filter=account_filter), { + group by party""".format( + account_filter=account_filter + ), + { "company": filters.company, "from_date": filters.from_date, "to_date": filters.to_date, - "party_type": filters.party_type - }, as_dict=True) + "party_type": filters.party_type, + }, + as_dict=True, + ) balances_within_period = frappe._dict() for d in gle: @@ -150,6 +157,7 @@ def get_balances_within_period(filters): return balances_within_period + def toggle_debit_credit(debit, credit): if flt(debit) > flt(credit): debit = flt(debit) - flt(credit) @@ -160,6 +168,7 @@ def toggle_debit_credit(debit, credit): return debit, credit + def get_columns(filters, show_party_name): columns = [ { @@ -167,73 +176,77 @@ def get_columns(filters, show_party_name): "label": _(filters.party_type), "fieldtype": "Link", "options": filters.party_type, - "width": 200 + "width": 200, }, { "fieldname": "opening_debit", "label": _("Opening (Dr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "opening_credit", "label": _("Opening (Cr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "closing_debit", "label": _("Closing (Dr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "closing_credit", "label": _("Closing (Cr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "hidden": 1 - } + "hidden": 1, + }, ] if show_party_name: - columns.insert(1, { - "fieldname": "party_name", - "label": _(filters.party_type) + " Name", - "fieldtype": "Data", - "width": 200 - }) + columns.insert( + 1, + { + "fieldname": "party_name", + "label": _(filters.party_type) + " Name", + "fieldtype": "Data", + "width": 200, + }, + ) return columns + def is_party_name_visible(filters): show_party_name = False - if filters.get('party_type') in ['Customer', 'Supplier']: + if filters.get("party_type") in ["Customer", "Supplier"]: if filters.get("party_type") == "Customer": party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name") else: diff --git a/erpnext/accounts/report/unpaid_expense_claim/unpaid_expense_claim.py b/erpnext/accounts/report/unpaid_expense_claim/unpaid_expense_claim.py index 26b938966b3..62b4f63b3a3 100644 --- a/erpnext/accounts/report/unpaid_expense_claim/unpaid_expense_claim.py +++ b/erpnext/accounts/report/unpaid_expense_claim/unpaid_expense_claim.py @@ -12,16 +12,25 @@ def execute(filters=None): data = get_unclaimed_expese_claims(filters) return columns, data + def get_columns(): - return [_("Employee") + ":Link/Employee:120", _("Employee Name") + "::120",_("Expense Claim") + ":Link/Expense Claim:120", - _("Sanctioned Amount") + ":Currency:120", _("Paid Amount") + ":Currency:120", _("Outstanding Amount") + ":Currency:150"] + return [ + _("Employee") + ":Link/Employee:120", + _("Employee Name") + "::120", + _("Expense Claim") + ":Link/Expense Claim:120", + _("Sanctioned Amount") + ":Currency:120", + _("Paid Amount") + ":Currency:120", + _("Outstanding Amount") + ":Currency:150", + ] + def get_unclaimed_expese_claims(filters): cond = "1=1" if filters.get("employee"): cond = "ec.employee = %(employee)s" - return frappe.db.sql(""" + return frappe.db.sql( + """ select ec.employee, ec.employee_name, ec.name, ec.total_sanctioned_amount, ec.total_amount_reimbursed, sum(gle.credit_in_account_currency - gle.debit_in_account_currency) as outstanding_amt @@ -32,4 +41,9 @@ def get_unclaimed_expese_claims(filters): and gle.party is not null and ec.docstatus = 1 and ec.is_paid = 0 and {cond} group by ec.name having outstanding_amt > 0 - """.format(cond=cond), filters, as_list=1) + """.format( + cond=cond + ), + filters, + as_list=1, + ) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 89cc0a8c8cc..eed58367739 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -1,4 +1,3 @@ - import frappe from frappe.utils import flt, formatdate, get_datetime_str @@ -8,6 +7,7 @@ from erpnext.setup.utils import get_exchange_rate __exchange_rates = {} + def get_currency(filters): """ Returns a dictionary containing currency information. The keys of the dict are @@ -24,15 +24,22 @@ def get_currency(filters): """ company = get_appropriate_company(filters) company_currency = get_company_currency(company) - presentation_currency = filters['presentation_currency'] if filters.get('presentation_currency') else company_currency + presentation_currency = ( + filters["presentation_currency"] if filters.get("presentation_currency") else company_currency + ) - report_date = filters.get('to_date') + report_date = filters.get("to_date") if not report_date: - fiscal_year_to_date = get_from_and_to_date(filters.get('to_fiscal_year'))["to_date"] + fiscal_year_to_date = get_from_and_to_date(filters.get("to_fiscal_year"))["to_date"] report_date = formatdate(get_datetime_str(fiscal_year_to_date), "dd-MM-yyyy") - currency_map = dict(company=company, company_currency=company_currency, presentation_currency=presentation_currency, report_date=report_date) + currency_map = dict( + company=company, + company_currency=company_currency, + presentation_currency=presentation_currency, + report_date=report_date, + ) return currency_map @@ -63,13 +70,14 @@ def get_rate_as_at(date, from_currency, to_currency): :return: Retrieved exchange rate """ - rate = __exchange_rates.get('{0}-{1}@{2}'.format(from_currency, to_currency, date)) + rate = __exchange_rates.get("{0}-{1}@{2}".format(from_currency, to_currency, date)) if not rate: rate = get_exchange_rate(from_currency, to_currency, date) or 1 - __exchange_rates['{0}-{1}@{2}'.format(from_currency, to_currency, date)] = rate + __exchange_rates["{0}-{1}@{2}".format(from_currency, to_currency, date)] = rate return rate + def convert_to_presentation_currency(gl_entries, currency_info, company): """ Take a list of GL Entries and change the 'debit' and 'credit' values to currencies @@ -79,35 +87,35 @@ def convert_to_presentation_currency(gl_entries, currency_info, company): :return: """ converted_gl_list = [] - presentation_currency = currency_info['presentation_currency'] - company_currency = currency_info['company_currency'] + presentation_currency = currency_info["presentation_currency"] + company_currency = currency_info["company_currency"] - account_currencies = list(set(entry['account_currency'] for entry in gl_entries)) + account_currencies = list(set(entry["account_currency"] for entry in gl_entries)) for entry in gl_entries: - account = entry['account'] - debit = flt(entry['debit']) - credit = flt(entry['credit']) - debit_in_account_currency = flt(entry['debit_in_account_currency']) - credit_in_account_currency = flt(entry['credit_in_account_currency']) - account_currency = entry['account_currency'] + account = entry["account"] + debit = flt(entry["debit"]) + credit = flt(entry["credit"]) + debit_in_account_currency = flt(entry["debit_in_account_currency"]) + credit_in_account_currency = flt(entry["credit_in_account_currency"]) + account_currency = entry["account_currency"] if len(account_currencies) == 1 and account_currency == presentation_currency: - if entry.get('debit'): - entry['debit'] = debit_in_account_currency + if debit_in_account_currency: + entry["debit"] = debit_in_account_currency - if entry.get('credit'): - entry['credit'] = credit_in_account_currency + if credit_in_account_currency: + entry["credit"] = credit_in_account_currency else: - date = currency_info['report_date'] + date = currency_info["report_date"] converted_debit_value = convert(debit, presentation_currency, company_currency, date) converted_credit_value = convert(credit, presentation_currency, company_currency, date) - if entry.get('debit'): - entry['debit'] = converted_debit_value + if entry.get("debit"): + entry["debit"] = converted_debit_value - if entry.get('credit'): - entry['credit'] = converted_credit_value + if entry.get("credit"): + entry["credit"] = converted_credit_value converted_gl_list.append(entry) @@ -115,26 +123,29 @@ def convert_to_presentation_currency(gl_entries, currency_info, company): def get_appropriate_company(filters): - if filters.get('company'): - company = filters['company'] + if filters.get("company"): + company = filters["company"] else: company = get_default_company() return company + @frappe.whitelist() -def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=None, with_item_data=False): +def get_invoiced_item_gross_margin( + sales_invoice=None, item_code=None, company=None, with_item_data=False +): from erpnext.accounts.report.gross_profit.gross_profit import GrossProfitGenerator - sales_invoice = sales_invoice or frappe.form_dict.get('sales_invoice') - item_code = item_code or frappe.form_dict.get('item_code') - company = company or frappe.get_cached_value("Sales Invoice", sales_invoice, 'company') + sales_invoice = sales_invoice or frappe.form_dict.get("sales_invoice") + item_code = item_code or frappe.form_dict.get("item_code") + company = company or frappe.get_cached_value("Sales Invoice", sales_invoice, "company") filters = { - 'sales_invoice': sales_invoice, - 'item_code': item_code, - 'company': company, - 'group_by': 'Invoice' + "sales_invoice": sales_invoice, + "item_code": item_code, + "company": company, + "group_by": "Invoice", } gross_profit_data = GrossProfitGenerator(filters) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 4aca40cf6c0..77c40bae2d9 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -1,11 +1,17 @@ - import unittest +import frappe from frappe.test_runner import make_test_objects from erpnext.accounts.party import get_party_shipping_address -from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries +from erpnext.accounts.utils import ( + get_future_stock_vouchers, + get_voucherwise_gl_entries, + sort_stock_vouchers_by_posting_date, +) +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestUtils(unittest.TestCase): @@ -44,9 +50,29 @@ class TestUtils(unittest.TestCase): posting_date = "2021-01-01" gl_entries = get_voucherwise_gl_entries(future_vouchers, posting_date) self.assertTrue( - voucher_type_and_no in gl_entries, msg="get_voucherwise_gl_entries not returning expected GLes", + voucher_type_and_no in gl_entries, + msg="get_voucherwise_gl_entries not returning expected GLes", ) + def test_stock_voucher_sorting(self): + vouchers = [] + + item = make_item().name + + stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10} + + se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry) + se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry) + se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry) + + for doc in (se1, se2, se3): + vouchers.append((doc.doctype, doc.name)) + + vouchers.append(("Stock Entry", "Wat")) + + sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers))) + self.assertEqual(sorted_vouchers, vouchers) + ADDRESS_RECORDS = [ { diff --git a/erpnext/accounts/test_party.py b/erpnext/accounts/test_party.py new file mode 100644 index 00000000000..9d3de5e8282 --- /dev/null +++ b/erpnext/accounts/test_party.py @@ -0,0 +1,18 @@ +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.accounts.party import get_default_price_list + + +class PartyTestCase(FrappeTestCase): + def test_get_default_price_list_should_return_none_for_invalid_group(self): + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "test customer", + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + customer.customer_group = None + customer.save() + price_list = get_default_price_list(customer) + assert price_list is None diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 10c61e7b20f..0bf2939336a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -3,6 +3,7 @@ from json import loads +from typing import List, Tuple import frappe import frappe.defaults @@ -19,15 +20,24 @@ from erpnext.stock import get_warehouse_account_map from erpnext.stock.utils import get_stock_value_on -class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass -class FiscalYearError(frappe.ValidationError): pass -class PaymentEntryUnlinkError(frappe.ValidationError): pass +class FiscalYearError(frappe.ValidationError): + pass + + +class PaymentEntryUnlinkError(frappe.ValidationError): + pass + @frappe.whitelist() -def get_fiscal_year(date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False): +def get_fiscal_year( + date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False +): return get_fiscal_years(date, fiscal_year, label, verbose, company, as_dict=as_dict)[0] -def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False): + +def get_fiscal_years( + transaction_date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False +): fiscal_years = frappe.cache().hget("fiscal_years", company) or [] if not fiscal_years: @@ -47,7 +57,8 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb ) """ - fiscal_years = frappe.db.sql(""" + fiscal_years = frappe.db.sql( + """ select fy.name, fy.year_start_date, fy.year_end_date from @@ -55,9 +66,12 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb where disabled = 0 {0} order by - fy.year_start_date desc""".format(cond), { - "company": company - }, as_dict=True) + fy.year_start_date desc""".format( + cond + ), + {"company": company}, + as_dict=True, + ) frappe.cache().hset("fiscal_years", company, fiscal_years) @@ -72,8 +86,11 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb if fiscal_year and fy.name == fiscal_year: matched = True - if (transaction_date and getdate(fy.year_start_date) <= transaction_date - and getdate(fy.year_end_date) >= transaction_date): + if ( + transaction_date + and getdate(fy.year_start_date) <= transaction_date + and getdate(fy.year_end_date) >= transaction_date + ): matched = True if matched: @@ -82,30 +99,35 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb else: return ((fy.name, fy.year_start_date, fy.year_end_date),) - error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date)) + error_msg = _("""{0} {1} is not in any active Fiscal Year""").format( + label, formatdate(transaction_date) + ) if company: error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) - if verbose==1: frappe.msgprint(error_msg) + if verbose == 1: + frappe.msgprint(error_msg) raise FiscalYearError(error_msg) + @frappe.whitelist() def get_fiscal_year_filter_field(company=None): - field = { - "fieldtype": "Select", - "options": [], - "operator": "Between", - "query_value": True - } + field = {"fieldtype": "Select", "options": [], "operator": "Between", "query_value": True} fiscal_years = get_fiscal_years(company=company) for fiscal_year in fiscal_years: - field["options"].append({ - "label": fiscal_year.name, - "value": fiscal_year.name, - "query_value": [fiscal_year.year_start_date.strftime("%Y-%m-%d"), fiscal_year.year_end_date.strftime("%Y-%m-%d")] - }) + field["options"].append( + { + "label": fiscal_year.name, + "value": fiscal_year.name, + "query_value": [ + fiscal_year.year_start_date.strftime("%Y-%m-%d"), + fiscal_year.year_end_date.strftime("%Y-%m-%d"), + ], + } + ) return field + def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None): years = [f[0] for f in get_fiscal_years(date, label=_(label), company=company)] if fiscal_year not in years: @@ -114,9 +136,18 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None): else: throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year)) + @frappe.whitelist() -def get_balance_on(account=None, date=None, party_type=None, party=None, company=None, - in_account_currency=True, cost_center=None, ignore_account_permission=False): +def get_balance_on( + account=None, + date=None, + party_type=None, + party=None, + company=None, + in_account_currency=True, + cost_center=None, + ignore_account_permission=False, +): if not account and frappe.form_dict.get("account"): account = frappe.form_dict.get("account") if not date and frappe.form_dict.get("date"): @@ -128,7 +159,6 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company if not cost_center and frappe.form_dict.get("cost_center"): cost_center = frappe.form_dict.get("cost_center") - cond = ["is_cancelled=0"] if date: cond.append("posting_date <= %s" % frappe.db.escape(cstr(date))) @@ -156,45 +186,52 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company else: report_type = "" - if cost_center and report_type == 'Profit and Loss': + if cost_center and report_type == "Profit and Loss": cc = frappe.get_doc("Cost Center", cost_center) if cc.is_group: - cond.append(""" exists ( + cond.append( + """ exists ( select 1 from `tabCost Center` cc where cc.name = gle.cost_center and cc.lft >= %s and cc.rgt <= %s - )""" % (cc.lft, cc.rgt)) + )""" + % (cc.lft, cc.rgt) + ) else: - cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False), )) - + cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),)) if account: - if not (frappe.flags.ignore_account_permission - or ignore_account_permission): + if not (frappe.flags.ignore_account_permission or ignore_account_permission): acc.check_permission("read") - if report_type == 'Profit and Loss': + if report_type == "Profit and Loss": # for pl accounts, get balance within a fiscal year - cond.append("posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" \ - % year_start_date) + cond.append( + "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date + ) # different filter for group and ledger - improved performance if acc.is_group: - cond.append("""exists ( + cond.append( + """exists ( select name from `tabAccount` ac where ac.name = gle.account and ac.lft >= %s and ac.rgt <= %s - )""" % (acc.lft, acc.rgt)) + )""" + % (acc.lft, acc.rgt) + ) # If group and currency same as company, # always return balance based on debit and credit in company currency - if acc.account_currency == frappe.get_cached_value('Company', acc.company, "default_currency"): + if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"): in_account_currency = False else: - cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False), )) + cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),)) if party_type and party: - cond.append("""gle.party_type = %s and gle.party = %s """ % - (frappe.db.escape(party_type), frappe.db.escape(party, percent=False))) + cond.append( + """gle.party_type = %s and gle.party = %s """ + % (frappe.db.escape(party_type), frappe.db.escape(party, percent=False)) + ) if company: cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False))) @@ -204,14 +241,19 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)" else: select_field = "sum(debit) - sum(credit)" - bal = frappe.db.sql(""" + bal = frappe.db.sql( + """ SELECT {0} FROM `tabGL Entry` gle - WHERE {1}""".format(select_field, " and ".join(cond)))[0][0] + WHERE {1}""".format( + select_field, " and ".join(cond) + ) + )[0][0] # if bal is None, return 0 return flt(bal) + def get_count_on(account, fieldname, date): cond = ["is_cancelled=0"] if date: @@ -239,53 +281,71 @@ def get_count_on(account, fieldname, date): acc.check_permission("read") # for pl accounts, get balance within a fiscal year - if acc.report_type == 'Profit and Loss': - cond.append("posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" \ - % year_start_date) + if acc.report_type == "Profit and Loss": + cond.append( + "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date + ) # different filter for group and ledger - improved performance if acc.is_group: - cond.append("""exists ( + cond.append( + """exists ( select name from `tabAccount` ac where ac.name = gle.account and ac.lft >= %s and ac.rgt <= %s - )""" % (acc.lft, acc.rgt)) + )""" + % (acc.lft, acc.rgt) + ) else: - cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False), )) + cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),)) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT name, posting_date, account, party_type, party,debit,credit, voucher_type, voucher_no, against_voucher_type, against_voucher FROM `tabGL Entry` gle - WHERE {0}""".format(" and ".join(cond)), as_dict=True) + WHERE {0}""".format( + " and ".join(cond) + ), + as_dict=True, + ) count = 0 for gle in entries: - if fieldname not in ('invoiced_amount','payables'): + if fieldname not in ("invoiced_amount", "payables"): count += 1 else: dr_or_cr = "debit" if fieldname == "invoiced_amount" else "credit" cr_or_dr = "credit" if fieldname == "invoiced_amount" else "debit" - select_fields = "ifnull(sum(credit-debit),0)" \ - if fieldname == "invoiced_amount" else "ifnull(sum(debit-credit),0)" + select_fields = ( + "ifnull(sum(credit-debit),0)" + if fieldname == "invoiced_amount" + else "ifnull(sum(debit-credit),0)" + ) - if ((not gle.against_voucher) or (gle.against_voucher_type in ["Sales Order", "Purchase Order"]) or - (gle.against_voucher==gle.voucher_no and gle.get(dr_or_cr) > 0)): - payment_amount = frappe.db.sql(""" + if ( + (not gle.against_voucher) + or (gle.against_voucher_type in ["Sales Order", "Purchase Order"]) + or (gle.against_voucher == gle.voucher_no and gle.get(dr_or_cr) > 0) + ): + payment_amount = frappe.db.sql( + """ SELECT {0} FROM `tabGL Entry` gle WHERE docstatus < 2 and posting_date <= %(date)s and against_voucher = %(voucher_no)s - and party = %(party)s and name != %(name)s""" - .format(select_fields), - {"date": date, "voucher_no": gle.voucher_no, - "party": gle.party, "name": gle.name})[0][0] + and party = %(party)s and name != %(name)s""".format( + select_fields + ), + {"date": date, "voucher_no": gle.voucher_no, "party": gle.party, "name": gle.name}, + )[0][0] outstanding_amount = flt(gle.get(dr_or_cr)) - flt(gle.get(cr_or_dr)) - payment_amount currency_precision = get_currency_precision() or 2 - if abs(flt(outstanding_amount)) > 0.1/10**currency_precision: + if abs(flt(outstanding_amount)) > 0.1 / 10**currency_precision: count += 1 return count + @frappe.whitelist() def add_ac(args=None): from frappe.desk.treeview import make_tree_args @@ -317,6 +377,7 @@ def add_ac(args=None): return ac.name + @frappe.whitelist() def add_cc(args=None): from frappe.desk.treeview import make_tree_args @@ -328,8 +389,9 @@ def add_cc(args=None): args = make_tree_args(**args) if args.parent_cost_center == args.company: - args.parent_cost_center = "{0} - {1}".format(args.parent_cost_center, - frappe.get_cached_value('Company', args.company, 'abbr')) + args.parent_cost_center = "{0} - {1}".format( + args.parent_cost_center, frappe.get_cached_value("Company", args.company, "abbr") + ) cc = frappe.new_doc("Cost Center") cc.update(args) @@ -341,9 +403,10 @@ def add_cc(args=None): cc.insert() return cc.name + def reconcile_against_document(args): """ - Cancel PE or JV, Update against document, split if required and resubmit + Cancel PE or JV, Update against document, split if required and resubmit """ # To optimize making GL Entry for PE or JV with multiple references reconciled_entries = {} @@ -375,36 +438,44 @@ def reconcile_against_document(args): doc.save(ignore_permissions=True) # re-submit advance entry doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) - doc.make_gl_entries(cancel = 0, adv_adj =1) + doc.make_gl_entries(cancel=0, adv_adj=1) frappe.flags.ignore_party_validation = False - if entry.voucher_type in ('Payment Entry', 'Journal Entry'): + if entry.voucher_type in ("Payment Entry", "Journal Entry"): doc.update_expense_claim() + def check_if_advance_entry_modified(args): """ - check if there is already a voucher reference - check if amount is same - check if jv is submitted + check if there is already a voucher reference + check if amount is same + check if jv is submitted """ - if not args.get('unreconciled_amount'): - args.update({'unreconciled_amount': args.get('unadjusted_amount')}) + if not args.get("unreconciled_amount"): + args.update({"unreconciled_amount": args.get("unadjusted_amount")}) ret = None if args.voucher_type == "Journal Entry": - ret = frappe.db.sql(""" + ret = frappe.db.sql( + """ select t2.{dr_or_cr} from `tabJournal Entry` t1, `tabJournal Entry Account` t2 where t1.name = t2.parent and t2.account = %(account)s and t2.party_type = %(party_type)s and t2.party = %(party)s and (t2.reference_type is null or t2.reference_type in ("", "Sales Order", "Purchase Order")) and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s - and t1.docstatus=1 """.format(dr_or_cr = args.get("dr_or_cr")), args) + and t1.docstatus=1 """.format( + dr_or_cr=args.get("dr_or_cr") + ), + args, + ) else: - party_account_field = ("paid_from" - if erpnext.get_party_account_type(args.party_type) == 'Receivable' else "paid_to") + party_account_field = ( + "paid_from" if erpnext.get_party_account_type(args.party_type) == "Receivable" else "paid_to" + ) if args.voucher_detail_no: - ret = frappe.db.sql("""select t1.name + ret = frappe.db.sql( + """select t1.name from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 where t1.name = t2.parent and t1.docstatus = 1 @@ -412,37 +483,53 @@ def check_if_advance_entry_modified(args): and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s and t2.reference_doctype in ("", "Sales Order", "Purchase Order") and t2.allocated_amount = %(unreconciled_amount)s - """.format(party_account_field), args) + """.format( + party_account_field + ), + args, + ) else: - ret = frappe.db.sql("""select name from `tabPayment Entry` + ret = frappe.db.sql( + """select name from `tabPayment Entry` where name = %(voucher_no)s and docstatus = 1 and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s and unallocated_amount = %(unreconciled_amount)s - """.format(party_account_field), args) + """.format( + party_account_field + ), + args, + ) if not ret: throw(_("""Payment Entry has been modified after you pulled it. Please pull it again.""")) + def validate_allocated_amount(args): - precision = args.get('precision') or frappe.db.get_single_value("System Settings", "currency_precision") + precision = args.get("precision") or frappe.db.get_single_value( + "System Settings", "currency_precision" + ) if args.get("allocated_amount") < 0: throw(_("Allocated amount cannot be negative")) elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision): throw(_("Allocated amount cannot be greater than unadjusted amount")) + def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): """ - Updates against document, if partial amount splits into rows + Updates against document, if partial amount splits into rows """ jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] - if flt(d['unadjusted_amount']) - flt(d['allocated_amount']) != 0: + if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: # adjust the unreconciled balance - amount_in_account_currency = flt(d['unadjusted_amount']) - flt(d['allocated_amount']) + amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) amount_in_company_currency = amount_in_account_currency * flt(jv_detail.exchange_rate) - jv_detail.set(d['dr_or_cr'], amount_in_account_currency) - jv_detail.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit', amount_in_company_currency) + jv_detail.set(d["dr_or_cr"], amount_in_account_currency) + jv_detail.set( + "debit" if d["dr_or_cr"] == "debit_in_account_currency" else "credit", + amount_in_company_currency, + ) else: journal_entry.remove(jv_detail) @@ -452,12 +539,18 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): new_row.update((frappe.copy_doc(jv_detail)).as_dict()) new_row.set(d["dr_or_cr"], d["allocated_amount"]) - new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit', - d["allocated_amount"] * flt(jv_detail.exchange_rate)) + new_row.set( + "debit" if d["dr_or_cr"] == "debit_in_account_currency" else "credit", + d["allocated_amount"] * flt(jv_detail.exchange_rate), + ) - new_row.set('credit_in_account_currency' if d['dr_or_cr'] == 'debit_in_account_currency' - else 'debit_in_account_currency', 0) - new_row.set('credit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'debit', 0) + new_row.set( + "credit_in_account_currency" + if d["dr_or_cr"] == "debit_in_account_currency" + else "debit_in_account_currency", + 0, + ) + new_row.set("credit" if d["dr_or_cr"] == "debit_in_account_currency" else "debit", 0) new_row.set("reference_type", d["against_voucher_type"]) new_row.set("reference_name", d["against_voucher"]) @@ -471,6 +564,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): if not do_not_save: journal_entry.save(ignore_permissions=True) + def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): reference_details = { "reference_doctype": d.against_voucher_type, @@ -478,8 +572,10 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(), - "exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation + "exchange_rate": d.exchange_rate + if not d.exchange_gain_loss + else payment_entry.get_exchange_rate(), + "exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation } if d.voucher_detail_no: @@ -506,57 +602,75 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): if d.difference_amount and d.difference_account: account_details = { - 'account': d.difference_account, - 'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company', - payment_entry.company, "cost_center") + "account": d.difference_account, + "cost_center": payment_entry.cost_center + or frappe.get_cached_value("Company", payment_entry.company, "cost_center"), } if d.difference_amount: - account_details['amount'] = d.difference_amount + account_details["amount"] = d.difference_amount payment_entry.set_gain_or_loss(account_details=account_details) if not do_not_save: payment_entry.save(ignore_permissions=True) + def unlink_ref_doc_from_payment_entries(ref_doc): remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) - frappe.db.sql("""update `tabGL Entry` + frappe.db.sql( + """update `tabGL Entry` set against_voucher_type=null, against_voucher=null, modified=%s, modified_by=%s where against_voucher_type=%s and against_voucher=%s and voucher_no != ifnull(against_voucher, '')""", - (now(), frappe.session.user, ref_doc.doctype, ref_doc.name)) + (now(), frappe.session.user, ref_doc.doctype, ref_doc.name), + ) if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): ref_doc.set("advances", []) - frappe.db.sql("""delete from `tab{0} Advance` where parent = %s""" - .format(ref_doc.doctype), ref_doc.name) + frappe.db.sql( + """delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name + ) + def remove_ref_doc_link_from_jv(ref_type, ref_no): - linked_jv = frappe.db.sql_list("""select parent from `tabJournal Entry Account` - where reference_type=%s and reference_name=%s and docstatus < 2""", (ref_type, ref_no)) + linked_jv = frappe.db.sql_list( + """select parent from `tabJournal Entry Account` + where reference_type=%s and reference_name=%s and docstatus < 2""", + (ref_type, ref_no), + ) if linked_jv: - frappe.db.sql("""update `tabJournal Entry Account` + frappe.db.sql( + """update `tabJournal Entry Account` set reference_type=null, reference_name = null, modified=%s, modified_by=%s where reference_type=%s and reference_name=%s - and docstatus < 2""", (now(), frappe.session.user, ref_type, ref_no)) + and docstatus < 2""", + (now(), frappe.session.user, ref_type, ref_no), + ) frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv))) + def remove_ref_doc_link_from_pe(ref_type, ref_no): - linked_pe = frappe.db.sql_list("""select parent from `tabPayment Entry Reference` - where reference_doctype=%s and reference_name=%s and docstatus < 2""", (ref_type, ref_no)) + linked_pe = frappe.db.sql_list( + """select parent from `tabPayment Entry Reference` + where reference_doctype=%s and reference_name=%s and docstatus < 2""", + (ref_type, ref_no), + ) if linked_pe: - frappe.db.sql("""update `tabPayment Entry Reference` + frappe.db.sql( + """update `tabPayment Entry Reference` set allocated_amount=0, modified=%s, modified_by=%s where reference_doctype=%s and reference_name=%s - and docstatus < 2""", (now(), frappe.session.user, ref_type, ref_no)) + and docstatus < 2""", + (now(), frappe.session.user, ref_type, ref_no), + ) for pe in linked_pe: try: @@ -566,42 +680,62 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): pe_doc.validate_payment_type_with_outstanding() except Exception as e: msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) - msg += '
' + msg += "
" msg += _("Please cancel payment entry manually first") frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) - frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s, + frappe.db.sql( + """update `tabPayment Entry` set total_allocated_amount=%s, base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s - where name=%s""", (pe_doc.total_allocated_amount, pe_doc.base_total_allocated_amount, - pe_doc.unallocated_amount, now(), frappe.session.user, pe)) + where name=%s""", + ( + pe_doc.total_allocated_amount, + pe_doc.base_total_allocated_amount, + pe_doc.unallocated_amount, + now(), + frappe.session.user, + pe, + ), + ) frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) + @frappe.whitelist() def get_company_default(company, fieldname, ignore_validation=False): - value = frappe.get_cached_value('Company', company, fieldname) + value = frappe.get_cached_value("Company", company, fieldname) if not ignore_validation and not value: - throw(_("Please set default {0} in Company {1}") - .format(frappe.get_meta("Company").get_label(fieldname), company)) + throw( + _("Please set default {0} in Company {1}").format( + frappe.get_meta("Company").get_label(fieldname), company + ) + ) return value + def fix_total_debit_credit(): - vouchers = frappe.db.sql("""select voucher_type, voucher_no, + vouchers = frappe.db.sql( + """select voucher_type, voucher_no, sum(debit) - sum(credit) as diff from `tabGL Entry` group by voucher_type, voucher_no - having sum(debit) != sum(credit)""", as_dict=1) + having sum(debit) != sum(credit)""", + as_dict=1, + ) for d in vouchers: if abs(d.diff) > 0: dr_or_cr = d.voucher_type == "Sales Invoice" and "credit" or "debit" - frappe.db.sql("""update `tabGL Entry` set %s = %s + %s - where voucher_type = %s and voucher_no = %s and %s > 0 limit 1""" % - (dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr), - (d.diff, d.voucher_type, d.voucher_no)) + frappe.db.sql( + """update `tabGL Entry` set %s = %s + %s + where voucher_type = %s and voucher_no = %s and %s > 0 limit 1""" + % (dr_or_cr, dr_or_cr, "%s", "%s", "%s", dr_or_cr), + (d.diff, d.voucher_type, d.voucher_no), + ) + def get_currency_precision(): precision = cint(frappe.db.get_default("currency_precision")) @@ -611,29 +745,41 @@ def get_currency_precision(): return precision -def get_stock_rbnb_difference(posting_date, company): - stock_items = frappe.db.sql_list("""select distinct item_code - from `tabStock Ledger Entry` where company=%s""", company) - pr_valuation_amount = frappe.db.sql(""" +def get_stock_rbnb_difference(posting_date, company): + stock_items = frappe.db.sql_list( + """select distinct item_code + from `tabStock Ledger Entry` where company=%s""", + company, + ) + + pr_valuation_amount = frappe.db.sql( + """ select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor) from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s - and pr.posting_date <= %s and pr_item.item_code in (%s)""" % - ('%s', '%s', ', '.join(['%s']*len(stock_items))), tuple([company, posting_date] + stock_items))[0][0] + and pr.posting_date <= %s and pr_item.item_code in (%s)""" + % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), + tuple([company, posting_date] + stock_items), + )[0][0] - pi_valuation_amount = frappe.db.sql(""" + pi_valuation_amount = frappe.db.sql( + """ select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor) from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s - and pi.posting_date <= %s and pi_item.item_code in (%s)""" % - ('%s', '%s', ', '.join(['%s']*len(stock_items))), tuple([company, posting_date] + stock_items))[0][0] + and pi.posting_date <= %s and pi_item.item_code in (%s)""" + % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), + tuple([company, posting_date] + stock_items), + )[0][0] # Balance should be stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2) # Balance as per system - stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value('Company', company, "abbr") + stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value( + "Company", company, "abbr" + ) sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False) # Amount should be credited @@ -646,12 +792,12 @@ def get_held_invoices(party_type, party): """ held_invoices = None - if party_type == 'Supplier': + if party_type == "Supplier": held_invoices = frappe.db.sql( - 'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()', - as_dict=1 + "select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()", + as_dict=1, ) - held_invoices = set(d['name'] for d in held_invoices) + held_invoices = set(d["name"] for d in held_invoices) return held_invoices @@ -661,13 +807,15 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 if account: - root_type, account_type = frappe.get_cached_value("Account", account, ["root_type", "account_type"]) + root_type, account_type = frappe.get_cached_value( + "Account", account, ["root_type", "account_type"] + ) party_account_type = "Receivable" if root_type == "Asset" else "Payable" party_account_type = account_type or party_account_type else: party_account_type = erpnext.get_party_account_type(party_type) - if party_account_type == 'Receivable': + if party_account_type == "Receivable": dr_or_cr = "debit_in_account_currency - credit_in_account_currency" payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency" else: @@ -676,7 +824,8 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters held_invoices = get_held_invoices(party_type, party) - invoice_list = frappe.db.sql(""" + invoice_list = frappe.db.sql( + """ select voucher_no, voucher_type, posting_date, due_date, ifnull(sum({dr_or_cr}), 0) as invoice_amount, @@ -693,15 +842,18 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters or (voucher_type not in ('Journal Entry', 'Payment Entry'))) group by voucher_type, voucher_no order by posting_date, name""".format( - dr_or_cr=dr_or_cr, - condition=condition or "" - ), { + dr_or_cr=dr_or_cr, condition=condition or "" + ), + { "party_type": party_type, "party": party, "account": account, - }, as_dict=True) + }, + as_dict=True, + ) - payment_entries = frappe.db.sql(""" + payment_entries = frappe.db.sql( + """ select against_voucher_type, against_voucher, ifnull(sum({payment_dr_or_cr}), 0) as payment_amount from `tabGL Entry` @@ -711,11 +863,12 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters and against_voucher is not null and against_voucher != '' and is_cancelled=0 group by against_voucher_type, against_voucher - """.format(payment_dr_or_cr=payment_dr_or_cr), { - "party_type": party_type, - "party": party, - "account": account - }, as_dict=True) + """.format( + payment_dr_or_cr=payment_dr_or_cr + ), + {"party_type": party_type, "party": party, "account": account}, + as_dict=True, + ) pe_map = frappe._dict() for d in payment_entries: @@ -725,73 +878,87 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0) outstanding_amount = flt(d.invoice_amount - payment_amount, precision) if outstanding_amount > 0.5 / (10**precision): - if (filters and filters.get("outstanding_amt_greater_than") and - not (outstanding_amount >= filters.get("outstanding_amt_greater_than") and - outstanding_amount <= filters.get("outstanding_amt_less_than"))): + if ( + filters + and filters.get("outstanding_amt_greater_than") + and not ( + outstanding_amount >= filters.get("outstanding_amt_greater_than") + and outstanding_amount <= filters.get("outstanding_amt_less_than") + ) + ): continue if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices: outstanding_invoices.append( - frappe._dict({ - 'voucher_no': d.voucher_no, - 'voucher_type': d.voucher_type, - 'posting_date': d.posting_date, - 'invoice_amount': flt(d.invoice_amount), - 'payment_amount': payment_amount, - 'outstanding_amount': outstanding_amount, - 'due_date': d.due_date, - 'currency': d.currency - }) + frappe._dict( + { + "voucher_no": d.voucher_no, + "voucher_type": d.voucher_type, + "posting_date": d.posting_date, + "invoice_amount": flt(d.invoice_amount), + "payment_amount": payment_amount, + "outstanding_amount": outstanding_amount, + "due_date": d.due_date, + "currency": d.currency, + } + ) ) - outstanding_invoices = sorted(outstanding_invoices, key=lambda k: k['due_date'] or getdate(nowdate())) + outstanding_invoices = sorted( + outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate()) + ) return outstanding_invoices -def get_account_name(account_type=None, root_type=None, is_group=None, account_currency=None, company=None): +def get_account_name( + account_type=None, root_type=None, is_group=None, account_currency=None, company=None +): """return account based on matching conditions""" - return frappe.db.get_value("Account", { - "account_type": account_type or '', - "root_type": root_type or '', - "is_group": is_group or 0, - "account_currency": account_currency or frappe.defaults.get_defaults().currency, - "company": company or frappe.defaults.get_defaults().company - }, "name") + return frappe.db.get_value( + "Account", + { + "account_type": account_type or "", + "root_type": root_type or "", + "is_group": is_group or 0, + "account_currency": account_currency or frappe.defaults.get_defaults().currency, + "company": company or frappe.defaults.get_defaults().company, + }, + "name", + ) + @frappe.whitelist() def get_companies(): """get a list of companies based on permission""" - return [d.name for d in frappe.get_list("Company", fields=["name"], - order_by="name")] + return [d.name for d in frappe.get_list("Company", fields=["name"], order_by="name")] + @frappe.whitelist() def get_children(doctype, parent, company, is_root=False): from erpnext.accounts.report.financial_statements import sort_accounts - parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_') - fields = [ - 'name as value', - 'is_group as expandable' - ] - filters = [['docstatus', '<', 2]] + parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") + fields = ["name as value", "is_group as expandable"] + filters = [["docstatus", "<", 2]] - filters.append(['ifnull(`{0}`,"")'.format(parent_fieldname), '=', '' if is_root else parent]) + filters.append(['ifnull(`{0}`,"")'.format(parent_fieldname), "=", "" if is_root else parent]) if is_root: - fields += ['root_type', 'report_type', 'account_currency'] if doctype == 'Account' else [] - filters.append(['company', '=', company]) + fields += ["root_type", "report_type", "account_currency"] if doctype == "Account" else [] + filters.append(["company", "=", company]) else: - fields += ['root_type', 'account_currency'] if doctype == 'Account' else [] - fields += [parent_fieldname + ' as parent'] + fields += ["root_type", "account_currency"] if doctype == "Account" else [] + fields += [parent_fieldname + " as parent"] acc = frappe.get_list(doctype, fields=fields, filters=filters) - if doctype == 'Account': + if doctype == "Account": sort_accounts(acc, is_root, key="value") return acc + @frappe.whitelist() def get_account_balances(accounts, company): @@ -801,16 +968,19 @@ def get_account_balances(accounts, company): if not accounts: return [] - company_currency = frappe.get_cached_value("Company", company, "default_currency") + company_currency = frappe.get_cached_value("Company", company, "default_currency") for account in accounts: account["company_currency"] = company_currency - account["balance"] = flt(get_balance_on(account["value"], in_account_currency=False, company=company)) + account["balance"] = flt( + get_balance_on(account["value"], in_account_currency=False, company=company) + ) if account["account_currency"] and account["account_currency"] != company_currency: account["balance_in_account_currency"] = flt(get_balance_on(account["value"], company=company)) return accounts + def create_payment_gateway_account(gateway, payment_channel="Email"): from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account @@ -819,13 +989,21 @@ def create_payment_gateway_account(gateway, payment_channel="Email"): return # NOTE: we translate Payment Gateway account name because that is going to be used by the end user - bank_account = frappe.db.get_value("Account", {"account_name": _(gateway), "company": company}, - ["name", 'account_currency'], as_dict=1) + bank_account = frappe.db.get_value( + "Account", + {"account_name": _(gateway), "company": company}, + ["name", "account_currency"], + as_dict=1, + ) if not bank_account: # check for untranslated one - bank_account = frappe.db.get_value("Account", {"account_name": gateway, "company": company}, - ["name", 'account_currency'], as_dict=1) + bank_account = frappe.db.get_value( + "Account", + {"account_name": gateway, "company": company}, + ["name", "account_currency"], + as_dict=1, + ) if not bank_account: # try creating one @@ -836,30 +1014,35 @@ def create_payment_gateway_account(gateway, payment_channel="Email"): return # if payment gateway account exists, return - if frappe.db.exists("Payment Gateway Account", - {"payment_gateway": gateway, "currency": bank_account.account_currency}): + if frappe.db.exists( + "Payment Gateway Account", + {"payment_gateway": gateway, "currency": bank_account.account_currency}, + ): return try: - frappe.get_doc({ - "doctype": "Payment Gateway Account", - "is_default": 1, - "payment_gateway": gateway, - "payment_account": bank_account.name, - "currency": bank_account.account_currency, - "payment_channel": payment_channel - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Payment Gateway Account", + "is_default": 1, + "payment_gateway": gateway, + "payment_account": bank_account.name, + "currency": bank_account.account_currency, + "payment_channel": payment_channel, + } + ).insert(ignore_permissions=True) except frappe.DuplicateEntryError: # already exists, due to a reinstall? pass + @frappe.whitelist() def update_cost_center(docname, cost_center_name, cost_center_number, company, merge): - ''' - Renames the document by adding the number as a prefix to the current name and updates - all transaction where it was present. - ''' + """ + Renames the document by adding the number as a prefix to the current name and updates + all transaction where it was present. + """ validate_field_number("Cost Center", docname, cost_center_number, company, "cost_center_number") if cost_center_number: @@ -874,8 +1057,9 @@ def update_cost_center(docname, cost_center_name, cost_center_number, company, m frappe.rename_doc("Cost Center", docname, new_name, force=1, merge=merge) return new_name + def validate_field_number(doctype_name, docname, number_value, company, field_name): - ''' Validate if the number entered isn't already assigned to some other document. ''' + """Validate if the number entered isn't already assigned to some other document.""" if number_value: filters = {field_name: number_value, "name": ["!=", docname]} if company: @@ -884,20 +1068,25 @@ def validate_field_number(doctype_name, docname, number_value, company, field_na doctype_with_same_number = frappe.db.get_value(doctype_name, filters) if doctype_with_same_number: - frappe.throw(_("{0} Number {1} is already used in {2} {3}") - .format(doctype_name, number_value, doctype_name.lower(), doctype_with_same_number)) + frappe.throw( + _("{0} Number {1} is already used in {2} {3}").format( + doctype_name, number_value, doctype_name.lower(), doctype_with_same_number + ) + ) + def get_autoname_with_number(number_value, doc_title, name, company): - ''' append title with prefix as number and suffix as company's abbreviation separated by '-' ''' + """append title with prefix as number and suffix as company's abbreviation separated by '-'""" if name: - name_split=name.split("-") - parts = [doc_title.strip(), name_split[len(name_split)-1].strip()] + name_split = name.split("-") + parts = [doc_title.strip(), name_split[len(name_split) - 1].strip()] else: - abbr = frappe.get_cached_value('Company', company, ["abbr"], as_dict=True) + abbr = frappe.get_cached_value("Company", company, ["abbr"], as_dict=True) parts = [doc_title.strip(), abbr.abbr] if cstr(number_value).strip(): parts.insert(0, cstr(number_value).strip()) - return ' - '.join(parts) + return " - ".join(parts) + @frappe.whitelist() def get_coa(doctype, parent, is_root, chart=None): @@ -909,24 +1098,43 @@ def get_coa(doctype, parent, is_root, chart=None): chart = chart if chart else frappe.flags.chart frappe.flags.chart = chart - parent = None if parent==_('All Accounts') else parent - accounts = build_tree_from_json(chart) # returns alist of dict in a tree render-able form + parent = None if parent == _("All Accounts") else parent + accounts = build_tree_from_json(chart) # returns alist of dict in a tree render-able form # filter out to show data for the selected node only - accounts = [d for d in accounts if d['parent_account']==parent] + accounts = [d for d in accounts if d["parent_account"] == parent] return accounts -def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, - warehouse_account=None, company=None): - stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items, company) + +def update_gl_entries_after( + posting_date, + posting_time, + for_warehouses=None, + for_items=None, + warehouse_account=None, + company=None, +): + stock_vouchers = get_future_stock_vouchers( + posting_date, posting_time, for_warehouses, for_items, company + ) repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company, warehouse_account) -def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, warehouse_account=None): +def repost_gle_for_stock_vouchers( + stock_vouchers, posting_date, company=None, warehouse_account=None +): + if not stock_vouchers: + return + def _delete_gl_entries(voucher_type, voucher_no): - frappe.db.sql("""delete from `tabGL Entry` - where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) + frappe.db.sql( + """delete from `tabGL Entry` + where voucher_type=%s and voucher_no=%s""", + (voucher_type, voucher_no), + ) + + stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers) if not warehouse_account: warehouse_account = get_warehouse_account_map(company) @@ -939,13 +1147,39 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no) expected_gle = voucher_obj.get_gl_entries(warehouse_account) if expected_gle: - if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision): + if not existing_gle or not compare_existing_and_expected_gle( + existing_gle, expected_gle, precision + ): _delete_gl_entries(voucher_type, voucher_no) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) else: _delete_gl_entries(voucher_type, voucher_no) -def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None): + +def sort_stock_vouchers_by_posting_date( + stock_vouchers: List[Tuple[str, str]] +) -> List[Tuple[str, str]]: + sle = frappe.qb.DocType("Stock Ledger Entry") + voucher_nos = [v[1] for v in stock_vouchers] + + sles = ( + frappe.qb.from_(sle) + .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation) + .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) + .groupby(sle.voucher_type, sle.voucher_no) + ).run(as_dict=True) + sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] + + unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers) + if unknown_vouchers: + sorted_vouchers.extend(unknown_vouchers) + + return sorted_vouchers + + +def get_future_stock_vouchers( + posting_date, posting_time, for_warehouses=None, for_items=None, company=None +): values = [] condition = "" @@ -961,25 +1195,31 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f condition += " and company = %s" values.append(company) - future_stock_vouchers = frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no + future_stock_vouchers = frappe.db.sql( + """select distinct sle.voucher_type, sle.voucher_no from `tabStock Ledger Entry` sle where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) and is_cancelled = 0 {condition} - order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition), - tuple([posting_date, posting_time] + values), as_dict=True) + order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format( + condition=condition + ), + tuple([posting_date, posting_time] + values), + as_dict=True, + ) return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers] + def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): - """ Get voucherwise list of GL entries. + """Get voucherwise list of GL entries. Only fetches GLE fields required for comparing with new GLE. Check compare_existing_and_expected_gle function below. returns: - Dict[Tuple[voucher_type, voucher_no], List[GL Entries]] + Dict[Tuple[voucher_type, voucher_no], List[GL Entries]] """ gl_entries = {} if not future_stock_vouchers: @@ -987,19 +1227,23 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): voucher_nos = [d[1] for d in future_stock_vouchers] - gles = frappe.db.sql(""" + gles = frappe.db.sql( + """ select name, account, credit, debit, cost_center, project, voucher_type, voucher_no from `tabGL Entry` where - posting_date >= %s and voucher_no in (%s)""" % - ('%s', ', '.join(['%s'] * len(voucher_nos))), - tuple([posting_date] + voucher_nos), as_dict=1) + posting_date >= %s and voucher_no in (%s)""" + % ("%s", ", ".join(["%s"] * len(voucher_nos))), + tuple([posting_date] + voucher_nos), + as_dict=1, + ) for d in gles: gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) return gl_entries + def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): if len(existing_gle) != len(expected_gle): return False @@ -1010,10 +1254,14 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): for e in existing_gle: if entry.account == e.account: account_existed = True - if (entry.account == e.account - and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) - and ( flt(entry.debit, precision) != flt(e.debit, precision) or - flt(entry.credit, precision) != flt(e.credit, precision))): + if ( + entry.account == e.account + and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) + and ( + flt(entry.debit, precision) != flt(e.debit, precision) + or flt(entry.credit, precision) != flt(e.credit, precision) + ) + ): matched = False break if not account_existed: @@ -1021,69 +1269,51 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): break return matched -def check_if_stock_and_account_balance_synced(posting_date, company, voucher_type=None, voucher_no=None): - if not cint(erpnext.is_perpetual_inventory_enabled(company)): - return - - accounts = get_stock_accounts(company, voucher_type, voucher_no) - stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account") - - for account in accounts: - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, - posting_date, company) - - if abs(account_bal - stock_bal) > 0.1: - precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), - currency=frappe.get_cached_value('Company', company, "default_currency")) - - diff = flt(stock_bal - account_bal, precision) - - error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format( - stock_bal, account_bal, frappe.bold(account), posting_date) - error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\ - .format(frappe.bold(diff), frappe.bold(posting_date)) - - frappe.msgprint( - msg="""{0}

{1}

""".format(error_reason, error_resolution), - raise_exception=StockValueAndAccountBalanceOutOfSync, - title=_('Values Out Of Sync'), - primary_action={ - 'label': _('Make Journal Entry'), - 'client_action': 'erpnext.route_to_adjustment_jv', - 'args': get_journal_entry(account, stock_adjustment_account, diff) - }) def get_stock_accounts(company, voucher_type=None, voucher_no=None): - stock_accounts = [d.name for d in frappe.db.get_all("Account", { - "account_type": "Stock", - "company": company, - "is_group": 0 - })] + stock_accounts = [ + d.name + for d in frappe.db.get_all( + "Account", {"account_type": "Stock", "company": company, "is_group": 0} + ) + ] if voucher_type and voucher_no: if voucher_type == "Journal Entry": - stock_accounts = [d.account for d in frappe.db.get_all("Journal Entry Account", { - "parent": voucher_no, - "account": ["in", stock_accounts] - }, "account")] + stock_accounts = [ + d.account + for d in frappe.db.get_all( + "Journal Entry Account", {"parent": voucher_no, "account": ["in", stock_accounts]}, "account" + ) + ] else: - stock_accounts = [d.account for d in frappe.db.get_all("GL Entry", { - "voucher_type": voucher_type, - "voucher_no": voucher_no, - "account": ["in", stock_accounts] - }, "account")] + stock_accounts = [ + d.account + for d in frappe.db.get_all( + "GL Entry", + {"voucher_type": voucher_type, "voucher_no": voucher_no, "account": ["in", stock_accounts]}, + "account", + ) + ] return stock_accounts + def get_stock_and_account_balance(account=None, posting_date=None, company=None): - if not posting_date: posting_date = nowdate() + if not posting_date: + posting_date = nowdate() warehouse_account = get_warehouse_account_map(company) - account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True) + account_balance = get_balance_on( + account, posting_date, in_account_currency=False, ignore_account_permission=True + ) - related_warehouses = [wh for wh, wh_details in warehouse_account.items() - if wh_details.account == account and not wh_details.is_group] + related_warehouses = [ + wh + for wh, wh_details in warehouse_account.items() + if wh_details.account == account and not wh_details.is_group + ] total_stock_value = 0.0 for warehouse in related_warehouses: @@ -1093,27 +1323,26 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None) precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses + def get_journal_entry(account, stock_adjustment_account, amount): - db_or_cr_warehouse_account =('credit_in_account_currency' if amount < 0 else 'debit_in_account_currency') - db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if amount < 0 else 'credit_in_account_currency') + db_or_cr_warehouse_account = ( + "credit_in_account_currency" if amount < 0 else "debit_in_account_currency" + ) + db_or_cr_stock_adjustment_account = ( + "debit_in_account_currency" if amount < 0 else "credit_in_account_currency" + ) return { - 'accounts':[{ - 'account': account, - db_or_cr_warehouse_account: abs(amount) - }, { - 'account': stock_adjustment_account, - db_or_cr_stock_adjustment_account : abs(amount) - }] + "accounts": [ + {"account": account, db_or_cr_warehouse_account: abs(amount)}, + {"account": stock_adjustment_account, db_or_cr_stock_adjustment_account: abs(amount)}, + ] } + def check_and_delete_linked_reports(report): - """ Check if reports are referenced in Desktop Icon """ - icons = frappe.get_all("Desktop Icon", - fields = ['name'], - filters = { - "_report": report - }) + """Check if reports are referenced in Desktop Icon""" + icons = frappe.get_all("Desktop Icon", fields=["name"], filters={"_report": report}) if icons: for icon in icons: frappe.delete_doc("Desktop Icon", icon) diff --git a/erpnext/agriculture/doctype/crop/crop.py b/erpnext/agriculture/doctype/crop/crop.py index ed2073cebf8..b2a193a2c17 100644 --- a/erpnext/agriculture/doctype/crop/crop.py +++ b/erpnext/agriculture/doctype/crop/crop.py @@ -27,5 +27,5 @@ class Crop(Document): @frappe.whitelist() def get_item_details(item_code): - item = frappe.get_doc('Item', item_code) + item = frappe.get_doc("Item", item_code) return {"uom": item.stock_uom, "rate": item.valuation_rate} diff --git a/erpnext/agriculture/doctype/crop/crop_dashboard.py b/erpnext/agriculture/doctype/crop/crop_dashboard.py index 772ed611373..1b975e17a6f 100644 --- a/erpnext/agriculture/doctype/crop/crop_dashboard.py +++ b/erpnext/agriculture/doctype/crop/crop_dashboard.py @@ -1,13 +1,5 @@ - from frappe import _ def get_data(): - return { - 'transactions': [ - { - 'label': _('Crop Cycle'), - 'items': ['Crop Cycle'] - } - ] - } + return {"transactions": [{"label": _("Crop Cycle"), "items": ["Crop Cycle"]}]} diff --git a/erpnext/agriculture/doctype/crop/test_crop.py b/erpnext/agriculture/doctype/crop/test_crop.py index c79a3672199..3859ebc7764 100644 --- a/erpnext/agriculture/doctype/crop/test_crop.py +++ b/erpnext/agriculture/doctype/crop/test_crop.py @@ -7,7 +7,8 @@ import frappe test_dependencies = ["Fertilizer"] + class TestCrop(unittest.TestCase): def test_crop_period(self): - basil = frappe.get_doc('Crop', 'Basil from seed') + basil = frappe.get_doc("Crop", "Basil from seed") self.assertEqual(basil.period, 15) diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py index 43c5bbde82f..470d900310d 100644 --- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py +++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py @@ -22,7 +22,7 @@ class CropCycle(Document): self.create_tasks_for_diseases() def set_missing_values(self): - crop = frappe.get_doc('Crop', self.crop) + crop = frappe.get_doc("Crop", self.crop) if not self.crop_spacing_uom: self.crop_spacing_uom = crop.crop_spacing_uom @@ -31,7 +31,7 @@ class CropCycle(Document): self.row_spacing_uom = crop.row_spacing_uom def create_crop_cycle_project(self): - crop = frappe.get_doc('Crop', self.crop) + crop = frappe.get_doc("Crop", self.crop) self.project = self.create_project(crop.period, crop.agriculture_task) self.create_task(crop.agriculture_task, self.project, self.start_date) @@ -42,49 +42,56 @@ class CropCycle(Document): self.import_disease_tasks(disease.disease, disease.start_date) disease.tasks_created = True - frappe.msgprint(_("Tasks have been created for managing the {0} disease (on row {1})").format(disease.disease, disease.idx)) + frappe.msgprint( + _("Tasks have been created for managing the {0} disease (on row {1})").format( + disease.disease, disease.idx + ) + ) def import_disease_tasks(self, disease, start_date): - disease_doc = frappe.get_doc('Disease', disease) + disease_doc = frappe.get_doc("Disease", disease) self.create_task(disease_doc.treatment_task, self.project, start_date) def create_project(self, period, crop_tasks): - project = frappe.get_doc({ - "doctype": "Project", - "project_name": self.title, - "expected_start_date": self.start_date, - "expected_end_date": add_days(self.start_date, period - 1) - }).insert() + project = frappe.get_doc( + { + "doctype": "Project", + "project_name": self.title, + "expected_start_date": self.start_date, + "expected_end_date": add_days(self.start_date, period - 1), + } + ).insert() return project.name def create_task(self, crop_tasks, project_name, start_date): for crop_task in crop_tasks: - frappe.get_doc({ - "doctype": "Task", - "subject": crop_task.get("task_name"), - "priority": crop_task.get("priority"), - "project": project_name, - "exp_start_date": add_days(start_date, crop_task.get("start_day") - 1), - "exp_end_date": add_days(start_date, crop_task.get("end_day") - 1) - }).insert() + frappe.get_doc( + { + "doctype": "Task", + "subject": crop_task.get("task_name"), + "priority": crop_task.get("priority"), + "project": project_name, + "exp_start_date": add_days(start_date, crop_task.get("start_day") - 1), + "exp_end_date": add_days(start_date, crop_task.get("end_day") - 1), + } + ).insert() @frappe.whitelist() def reload_linked_analysis(self): - linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis'] - required_fields = ['location', 'name', 'collection_datetime'] + linked_doctypes = ["Soil Texture", "Soil Analysis", "Plant Analysis"] + required_fields = ["location", "name", "collection_datetime"] output = {} for doctype in linked_doctypes: output[doctype] = frappe.get_all(doctype, fields=required_fields) - output['Location'] = [] + output["Location"] = [] for location in self.linked_location: - output['Location'].append(frappe.get_doc('Location', location.location)) + output["Location"].append(frappe.get_doc("Location", location.location)) - frappe.publish_realtime("List of Linked Docs", - output, user=frappe.session.user) + frappe.publish_realtime("List of Linked Docs", output, user=frappe.session.user) @frappe.whitelist() def append_to_child(self, obj_to_append): @@ -96,11 +103,11 @@ class CropCycle(Document): def get_coordinates(doc): - return ast.literal_eval(doc.location).get('features')[0].get('geometry').get('coordinates') + return ast.literal_eval(doc.location).get("features")[0].get("geometry").get("coordinates") def get_geometry_type(doc): - return ast.literal_eval(doc.location).get('features')[0].get('geometry').get('type') + return ast.literal_eval(doc.location).get("features")[0].get("geometry").get("type") def is_in_location(point, vs): @@ -114,8 +121,7 @@ def is_in_location(point, vs): xi, yi = vs[i] xj, yj = vs[j] - intersect = ((yi > y) != (yj > y)) and ( - x < (xj - xi) * (y - yi) / (yj - yi) + xi) + intersect = ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi) + xi) if intersect: inside = not inside diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py index e4765a57c0b..2b494f8476e 100644 --- a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py +++ b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py @@ -11,8 +11,8 @@ test_dependencies = ["Crop", "Fertilizer", "Location", "Disease"] class TestCropCycle(unittest.TestCase): def test_crop_cycle_creation(self): - cycle = frappe.get_doc('Crop Cycle', 'Basil from seed 2017') - self.assertTrue(frappe.db.exists('Crop Cycle', 'Basil from seed 2017')) + cycle = frappe.get_doc("Crop Cycle", "Basil from seed 2017") + self.assertTrue(frappe.db.exists("Crop Cycle", "Basil from seed 2017")) # check if the tasks were created self.assertEqual(check_task_creation(), True) @@ -23,45 +23,48 @@ def check_task_creation(): all_task_dict = { "Survey and find the aphid locations": { "exp_start_date": datetime.date(2017, 11, 21), - "exp_end_date": datetime.date(2017, 11, 22) + "exp_end_date": datetime.date(2017, 11, 22), }, "Apply Pesticides": { "exp_start_date": datetime.date(2017, 11, 23), - "exp_end_date": datetime.date(2017, 11, 23) + "exp_end_date": datetime.date(2017, 11, 23), }, "Plough the field": { "exp_start_date": datetime.date(2017, 11, 11), - "exp_end_date": datetime.date(2017, 11, 11) + "exp_end_date": datetime.date(2017, 11, 11), }, "Plant the seeds": { "exp_start_date": datetime.date(2017, 11, 12), - "exp_end_date": datetime.date(2017, 11, 13) + "exp_end_date": datetime.date(2017, 11, 13), }, "Water the field": { "exp_start_date": datetime.date(2017, 11, 14), - "exp_end_date": datetime.date(2017, 11, 14) + "exp_end_date": datetime.date(2017, 11, 14), }, "First harvest": { "exp_start_date": datetime.date(2017, 11, 18), - "exp_end_date": datetime.date(2017, 11, 18) + "exp_end_date": datetime.date(2017, 11, 18), }, "Add the fertilizer": { "exp_start_date": datetime.date(2017, 11, 20), - "exp_end_date": datetime.date(2017, 11, 22) + "exp_end_date": datetime.date(2017, 11, 22), }, "Final cut": { "exp_start_date": datetime.date(2017, 11, 25), - "exp_end_date": datetime.date(2017, 11, 25) - } + "exp_end_date": datetime.date(2017, 11, 25), + }, } - all_tasks = frappe.get_all('Task') + all_tasks = frappe.get_all("Task") for task in all_tasks: - sample_task = frappe.get_doc('Task', task.name) + sample_task = frappe.get_doc("Task", task.name) if sample_task.subject in list(all_task_dict): - if sample_task.exp_start_date != all_task_dict[sample_task.subject]['exp_start_date'] or sample_task.exp_end_date != all_task_dict[sample_task.subject]['exp_end_date']: + if ( + sample_task.exp_start_date != all_task_dict[sample_task.subject]["exp_start_date"] + or sample_task.exp_end_date != all_task_dict[sample_task.subject]["exp_end_date"] + ): return False all_task_dict.pop(sample_task.subject) @@ -69,4 +72,4 @@ def check_task_creation(): def check_project_creation(): - return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False + return True if frappe.db.exists("Project", {"project_name": "Basil from seed 2017"}) else False diff --git a/erpnext/agriculture/doctype/disease/disease.py b/erpnext/agriculture/doctype/disease/disease.py index 30ab298376a..dfab47895ef 100644 --- a/erpnext/agriculture/doctype/disease/disease.py +++ b/erpnext/agriculture/doctype/disease/disease.py @@ -15,5 +15,6 @@ class Disease(Document): if task.start_day > task.end_day: frappe.throw(_("Start day is greater than end day in task '{0}'").format(task.task_name)) # to calculate the period of the Crop Cycle - if task.end_day > max_period: max_period = task.end_day + if task.end_day > max_period: + max_period = task.end_day self.treatment_period = max_period diff --git a/erpnext/agriculture/doctype/disease/test_disease.py b/erpnext/agriculture/doctype/disease/test_disease.py index 6a6f1e70a94..f6eda359983 100644 --- a/erpnext/agriculture/doctype/disease/test_disease.py +++ b/erpnext/agriculture/doctype/disease/test_disease.py @@ -8,5 +8,5 @@ import frappe class TestDisease(unittest.TestCase): def test_treatment_period(self): - disease = frappe.get_doc('Disease', 'Aphids') + disease = frappe.get_doc("Disease", "Aphids") self.assertEqual(disease.treatment_period, 3) diff --git a/erpnext/agriculture/doctype/fertilizer/fertilizer.py b/erpnext/agriculture/doctype/fertilizer/fertilizer.py index 2408302bd18..1796d240ec1 100644 --- a/erpnext/agriculture/doctype/fertilizer/fertilizer.py +++ b/erpnext/agriculture/doctype/fertilizer/fertilizer.py @@ -9,6 +9,6 @@ from frappe.model.document import Document class Fertilizer(Document): @frappe.whitelist() def load_contents(self): - docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'}) + docs = frappe.get_all("Agriculture Analysis Criteria", filters={"linked_doctype": "Fertilizer"}) for doc in docs: - self.append('fertilizer_contents', {'title': str(doc.name)}) + self.append("fertilizer_contents", {"title": str(doc.name)}) diff --git a/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py b/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py index c8630ef1f87..aa6d96ce6cd 100644 --- a/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py +++ b/erpnext/agriculture/doctype/fertilizer/test_fertilizer.py @@ -8,4 +8,4 @@ import frappe class TestFertilizer(unittest.TestCase): def test_fertilizer_creation(self): - self.assertEqual(frappe.db.exists('Fertilizer', 'Urea'), 'Urea') + self.assertEqual(frappe.db.exists("Fertilizer", "Urea"), "Urea") diff --git a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py index 9a939cde0b4..5f2d46fa048 100644 --- a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py +++ b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py @@ -9,6 +9,8 @@ from frappe.model.document import Document class PlantAnalysis(Document): @frappe.whitelist() def load_contents(self): - docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'}) + docs = frappe.get_all( + "Agriculture Analysis Criteria", filters={"linked_doctype": "Plant Analysis"} + ) for doc in docs: - self.append('plant_analysis_criteria', {'title': str(doc.name)}) + self.append("plant_analysis_criteria", {"title": str(doc.name)}) diff --git a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py index 03667fbcae9..0017b56546e 100644 --- a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py +++ b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py @@ -9,6 +9,8 @@ from frappe.model.document import Document class SoilAnalysis(Document): @frappe.whitelist() def load_contents(self): - docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'}) + docs = frappe.get_all( + "Agriculture Analysis Criteria", filters={"linked_doctype": "Soil Analysis"} + ) for doc in docs: - self.append('soil_analysis_criteria', {'title': str(doc.name)}) + self.append("soil_analysis_criteria", {"title": str(doc.name)}) diff --git a/erpnext/agriculture/doctype/soil_texture/soil_texture.py b/erpnext/agriculture/doctype/soil_texture/soil_texture.py index b1fc9a063d0..94e031db052 100644 --- a/erpnext/agriculture/doctype/soil_texture/soil_texture.py +++ b/erpnext/agriculture/doctype/soil_texture/soil_texture.py @@ -10,62 +10,74 @@ from frappe.utils import cint, flt class SoilTexture(Document): soil_edit_order = [2, 1, 0] - soil_types = ['clay_composition', 'sand_composition', 'silt_composition'] + soil_types = ["clay_composition", "sand_composition", "silt_composition"] @frappe.whitelist() def load_contents(self): - docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'}) + docs = frappe.get_all( + "Agriculture Analysis Criteria", filters={"linked_doctype": "Soil Texture"} + ) for doc in docs: - self.append('soil_texture_criteria', {'title': str(doc.name)}) + self.append("soil_texture_criteria", {"title": str(doc.name)}) def validate(self): - self.update_soil_edit('sand_composition') + self.update_soil_edit("sand_composition") for soil_type in self.soil_types: if self.get(soil_type) > 100 or self.get(soil_type) < 0: frappe.throw(_("{0} should be a value between 0 and 100").format(soil_type)) if sum(self.get(soil_type) for soil_type in self.soil_types) != 100: - frappe.throw(_('Soil compositions do not add up to 100')) + frappe.throw(_("Soil compositions do not add up to 100")) @frappe.whitelist() def update_soil_edit(self, soil_type): - self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1 + self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order) + 1 self.soil_type = self.get_soil_type() def get_soil_type(self): # update the last edited soil type - if sum(self.soil_edit_order) < 5: return + if sum(self.soil_edit_order) < 5: + return last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order)) # set composition of the last edited soil - self.set(self.soil_types[last_edit_index], - 100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index]))) + self.set( + self.soil_types[last_edit_index], + 100 + - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + + cint(self.get(self.soil_types[last_edit_index])), + ) # calculate soil type c, sa, si = flt(self.clay_composition), flt(self.sand_composition), flt(self.silt_composition) if si + (1.5 * c) < 15: - return 'Sand' + return "Sand" elif si + 1.5 * c >= 15 and si + 2 * c < 30: - return 'Loamy Sand' - elif ((c >= 7 and c < 20) or (sa > 52) and ((si + 2*c) >= 30) or (c < 7 and si < 50 and (si+2*c) >= 30)): - return 'Sandy Loam' - elif ((c >= 7 and c < 27) and (si >= 28 and si < 50) and (sa <= 52)): - return 'Loam' - elif ((si >= 50 and (c >= 12 and c < 27)) or ((si >= 50 and si < 80) and c < 12)): - return 'Silt Loam' - elif (si >= 80 and c < 12): - return 'Silt' - elif ((c >= 20 and c < 35) and (si < 28) and (sa > 45)): - return 'Sandy Clay Loam' - elif ((c >= 27 and c < 40) and (sa > 20 and sa <= 45)): - return 'Clay Loam' - elif ((c >= 27 and c < 40) and (sa <= 20)): - return 'Silty Clay Loam' - elif (c >= 35 and sa > 45): - return 'Sandy Clay' - elif (c >= 40 and si >= 40): - return 'Silty Clay' - elif (c >= 40 and sa <= 45 and si < 40): - return 'Clay' + return "Loamy Sand" + elif ( + (c >= 7 and c < 20) + or (sa > 52) + and ((si + 2 * c) >= 30) + or (c < 7 and si < 50 and (si + 2 * c) >= 30) + ): + return "Sandy Loam" + elif (c >= 7 and c < 27) and (si >= 28 and si < 50) and (sa <= 52): + return "Loam" + elif (si >= 50 and (c >= 12 and c < 27)) or ((si >= 50 and si < 80) and c < 12): + return "Silt Loam" + elif si >= 80 and c < 12: + return "Silt" + elif (c >= 20 and c < 35) and (si < 28) and (sa > 45): + return "Sandy Clay Loam" + elif (c >= 27 and c < 40) and (sa > 20 and sa <= 45): + return "Clay Loam" + elif (c >= 27 and c < 40) and (sa <= 20): + return "Silty Clay Loam" + elif c >= 35 and sa > 45: + return "Sandy Clay" + elif c >= 40 and si >= 40: + return "Silty Clay" + elif c >= 40 and sa <= 45 and si < 40: + return "Clay" else: - return 'Select' + return "Select" diff --git a/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py b/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py index 45497675cec..7233ea96e2a 100644 --- a/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py +++ b/erpnext/agriculture/doctype/soil_texture/test_soil_texture.py @@ -8,7 +8,9 @@ import frappe class TestSoilTexture(unittest.TestCase): def test_texture_selection(self): - soil_tex = frappe.get_all('Soil Texture', fields=['name'], filters={'collection_datetime': '2017-11-08'}) - doc = frappe.get_doc('Soil Texture', soil_tex[0].name) + soil_tex = frappe.get_all( + "Soil Texture", fields=["name"], filters={"collection_datetime": "2017-11-08"} + ) + doc = frappe.get_doc("Soil Texture", soil_tex[0].name) self.assertEqual(doc.silt_composition, 50) - self.assertEqual(doc.soil_type, 'Silt Loam') + self.assertEqual(doc.soil_type, "Silt Loam") diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.py b/erpnext/agriculture/doctype/water_analysis/water_analysis.py index 434acecc6e3..b5fe48ad5bd 100644 --- a/erpnext/agriculture/doctype/water_analysis/water_analysis.py +++ b/erpnext/agriculture/doctype/water_analysis/water_analysis.py @@ -10,9 +10,11 @@ from frappe.model.document import Document class WaterAnalysis(Document): @frappe.whitelist() def load_contents(self): - docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'}) + docs = frappe.get_all( + "Agriculture Analysis Criteria", filters={"linked_doctype": "Water Analysis"} + ) for doc in docs: - self.append('water_analysis_criteria', {'title': str(doc.name)}) + self.append("water_analysis_criteria", {"title": str(doc.name)}) @frappe.whitelist() def update_lab_result_date(self): @@ -21,6 +23,6 @@ class WaterAnalysis(Document): def validate(self): if self.collection_datetime > self.laboratory_testing_datetime: - frappe.throw(_('Lab testing datetime cannot be before collection datetime')) + frappe.throw(_("Lab testing datetime cannot be before collection datetime")) if self.laboratory_testing_datetime > self.result_datetime: - frappe.throw(_('Lab result datetime cannot be before testing datetime')) + frappe.throw(_("Lab result datetime cannot be before testing datetime")) diff --git a/erpnext/agriculture/doctype/weather/weather.py b/erpnext/agriculture/doctype/weather/weather.py index 8750709c564..082bf8b0fdf 100644 --- a/erpnext/agriculture/doctype/weather/weather.py +++ b/erpnext/agriculture/doctype/weather/weather.py @@ -9,6 +9,6 @@ from frappe.model.document import Document class Weather(Document): @frappe.whitelist() def load_contents(self): - docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'}) + docs = frappe.get_all("Agriculture Analysis Criteria", filters={"linked_doctype": "Weather"}) for doc in docs: - self.append('weather_parameter', {'title': str(doc.name)}) + self.append("weather_parameter", {"title": str(doc.name)}) diff --git a/erpnext/agriculture/setup.py b/erpnext/agriculture/setup.py index 433466bf1b7..ce1ecec0ad7 100644 --- a/erpnext/agriculture/setup.py +++ b/erpnext/agriculture/setup.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ @@ -6,427 +5,493 @@ from erpnext.setup.utils import insert_record def setup_agriculture(): - if frappe.get_all('Agriculture Analysis Criteria'): + if frappe.get_all("Agriculture Analysis Criteria"): # already setup return create_agriculture_data() + def create_agriculture_data(): records = [ dict( - doctype='Item Group', - item_group_name='Fertilizer', + doctype="Item Group", + item_group_name="Fertilizer", is_group=0, - parent_item_group=_('All Item Groups')), + parent_item_group=_("All Item Groups"), + ), dict( - doctype='Item Group', - item_group_name='Seed', + doctype="Item Group", item_group_name="Seed", is_group=0, parent_item_group=_("All Item Groups") + ), + dict( + doctype="Item Group", + item_group_name="By-product", is_group=0, - parent_item_group=_('All Item Groups')), + parent_item_group=_("All Item Groups"), + ), dict( - doctype='Item Group', - item_group_name='By-product', + doctype="Item Group", + item_group_name="Produce", is_group=0, - parent_item_group=_('All Item Groups')), + parent_item_group=_("All Item Groups"), + ), dict( - doctype='Item Group', - item_group_name='Produce', - is_group=0, - parent_item_group=_('All Item Groups')), - dict( - doctype='Agriculture Analysis Criteria', - title='Nitrogen Content', + doctype="Agriculture Analysis Criteria", + title="Nitrogen Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Phosphorous Content', + doctype="Agriculture Analysis Criteria", + title="Phosphorous Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Potassium Content', + doctype="Agriculture Analysis Criteria", + title="Potassium Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Calcium Content', + doctype="Agriculture Analysis Criteria", + title="Calcium Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Sulphur Content', + doctype="Agriculture Analysis Criteria", + title="Sulphur Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Magnesium Content', + doctype="Agriculture Analysis Criteria", + title="Magnesium Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Iron Content', + doctype="Agriculture Analysis Criteria", + title="Iron Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Copper Content', + doctype="Agriculture Analysis Criteria", + title="Copper Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Zinc Content', + doctype="Agriculture Analysis Criteria", + title="Zinc Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Boron Content', + doctype="Agriculture Analysis Criteria", + title="Boron Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Manganese Content', + doctype="Agriculture Analysis Criteria", + title="Manganese Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Chlorine Content', + doctype="Agriculture Analysis Criteria", + title="Chlorine Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Molybdenum Content', + doctype="Agriculture Analysis Criteria", + title="Molybdenum Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Sodium Content', + doctype="Agriculture Analysis Criteria", + title="Sodium Content", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Humic Acid', + doctype="Agriculture Analysis Criteria", + title="Humic Acid", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Fulvic Acid', + doctype="Agriculture Analysis Criteria", + title="Fulvic Acid", standard=1, - linked_doctype='Fertilizer'), + linked_doctype="Fertilizer", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Inert', - standard=1, - linked_doctype='Fertilizer'), + doctype="Agriculture Analysis Criteria", title="Inert", standard=1, linked_doctype="Fertilizer" + ), dict( - doctype='Agriculture Analysis Criteria', - title='Others', - standard=1, - linked_doctype='Fertilizer'), + doctype="Agriculture Analysis Criteria", title="Others", standard=1, linked_doctype="Fertilizer" + ), dict( - doctype='Agriculture Analysis Criteria', - title='Nitrogen', + doctype="Agriculture Analysis Criteria", + title="Nitrogen", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Phosphorous', + doctype="Agriculture Analysis Criteria", + title="Phosphorous", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Potassium', + doctype="Agriculture Analysis Criteria", + title="Potassium", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Calcium', + doctype="Agriculture Analysis Criteria", + title="Calcium", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Magnesium', + doctype="Agriculture Analysis Criteria", + title="Magnesium", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Sulphur', + doctype="Agriculture Analysis Criteria", + title="Sulphur", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Boron', + doctype="Agriculture Analysis Criteria", + title="Boron", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Copper', + doctype="Agriculture Analysis Criteria", + title="Copper", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Iron', + doctype="Agriculture Analysis Criteria", + title="Iron", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Manganese', + doctype="Agriculture Analysis Criteria", + title="Manganese", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Zinc', + doctype="Agriculture Analysis Criteria", + title="Zinc", standard=1, - linked_doctype='Plant Analysis'), + linked_doctype="Plant Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Depth (in cm)', + doctype="Agriculture Analysis Criteria", + title="Depth (in cm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Soil pH', + doctype="Agriculture Analysis Criteria", + title="Soil pH", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Salt Concentration (%)', + doctype="Agriculture Analysis Criteria", + title="Salt Concentration (%)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Organic Matter (%)', + doctype="Agriculture Analysis Criteria", + title="Organic Matter (%)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='CEC (Cation Exchange Capacity) (MAQ/100mL)', + doctype="Agriculture Analysis Criteria", + title="CEC (Cation Exchange Capacity) (MAQ/100mL)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Potassium Saturation (%)', + doctype="Agriculture Analysis Criteria", + title="Potassium Saturation (%)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Calcium Saturation (%)', + doctype="Agriculture Analysis Criteria", + title="Calcium Saturation (%)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Manganese Saturation (%)', + doctype="Agriculture Analysis Criteria", + title="Manganese Saturation (%)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Nirtogen (ppm)', + doctype="Agriculture Analysis Criteria", + title="Nirtogen (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Phosphorous (ppm)', + doctype="Agriculture Analysis Criteria", + title="Phosphorous (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Potassium (ppm)', + doctype="Agriculture Analysis Criteria", + title="Potassium (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Calcium (ppm)', + doctype="Agriculture Analysis Criteria", + title="Calcium (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Magnesium (ppm)', + doctype="Agriculture Analysis Criteria", + title="Magnesium (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Sulphur (ppm)', + doctype="Agriculture Analysis Criteria", + title="Sulphur (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Copper (ppm)', + doctype="Agriculture Analysis Criteria", + title="Copper (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Iron (ppm)', + doctype="Agriculture Analysis Criteria", + title="Iron (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Manganese (ppm)', + doctype="Agriculture Analysis Criteria", + title="Manganese (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Zinc (ppm)', + doctype="Agriculture Analysis Criteria", + title="Zinc (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Aluminium (ppm)', + doctype="Agriculture Analysis Criteria", + title="Aluminium (ppm)", standard=1, - linked_doctype='Soil Analysis'), + linked_doctype="Soil Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Water pH', + doctype="Agriculture Analysis Criteria", + title="Water pH", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Conductivity (mS/cm)', + doctype="Agriculture Analysis Criteria", + title="Conductivity (mS/cm)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Hardness (mg/CaCO3)', + doctype="Agriculture Analysis Criteria", + title="Hardness (mg/CaCO3)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Turbidity (NTU)', + doctype="Agriculture Analysis Criteria", + title="Turbidity (NTU)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Odor', + doctype="Agriculture Analysis Criteria", + title="Odor", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Color', + doctype="Agriculture Analysis Criteria", + title="Color", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Nitrate (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Nitrate (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Nirtite (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Nirtite (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Calcium (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Calcium (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Magnesium (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Magnesium (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Sulphate (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Sulphate (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Boron (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Boron (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Copper (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Copper (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Iron (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Iron (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Manganese (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Manganese (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Zinc (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Zinc (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Chlorine (mg/L)', + doctype="Agriculture Analysis Criteria", + title="Chlorine (mg/L)", standard=1, - linked_doctype='Water Analysis'), + linked_doctype="Water Analysis", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Bulk Density', + doctype="Agriculture Analysis Criteria", + title="Bulk Density", standard=1, - linked_doctype='Soil Texture'), + linked_doctype="Soil Texture", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Field Capacity', + doctype="Agriculture Analysis Criteria", + title="Field Capacity", standard=1, - linked_doctype='Soil Texture'), + linked_doctype="Soil Texture", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Wilting Point', + doctype="Agriculture Analysis Criteria", + title="Wilting Point", standard=1, - linked_doctype='Soil Texture'), + linked_doctype="Soil Texture", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Hydraulic Conductivity', + doctype="Agriculture Analysis Criteria", + title="Hydraulic Conductivity", standard=1, - linked_doctype='Soil Texture'), + linked_doctype="Soil Texture", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Organic Matter', + doctype="Agriculture Analysis Criteria", + title="Organic Matter", standard=1, - linked_doctype='Soil Texture'), + linked_doctype="Soil Texture", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Temperature High', + doctype="Agriculture Analysis Criteria", + title="Temperature High", standard=1, - linked_doctype='Weather'), + linked_doctype="Weather", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Temperature Low', + doctype="Agriculture Analysis Criteria", + title="Temperature Low", standard=1, - linked_doctype='Weather'), + linked_doctype="Weather", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Temperature Average', + doctype="Agriculture Analysis Criteria", + title="Temperature Average", standard=1, - linked_doctype='Weather'), + linked_doctype="Weather", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Dew Point', - standard=1, - linked_doctype='Weather'), + doctype="Agriculture Analysis Criteria", title="Dew Point", standard=1, linked_doctype="Weather" + ), dict( - doctype='Agriculture Analysis Criteria', - title='Precipitation Received', + doctype="Agriculture Analysis Criteria", + title="Precipitation Received", standard=1, - linked_doctype='Weather'), + linked_doctype="Weather", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Humidity', - standard=1, - linked_doctype='Weather'), + doctype="Agriculture Analysis Criteria", title="Humidity", standard=1, linked_doctype="Weather" + ), dict( - doctype='Agriculture Analysis Criteria', - title='Pressure', - standard=1, - linked_doctype='Weather'), + doctype="Agriculture Analysis Criteria", title="Pressure", standard=1, linked_doctype="Weather" + ), dict( - doctype='Agriculture Analysis Criteria', - title='Insolation/ PAR (Photosynthetically Active Radiation)', + doctype="Agriculture Analysis Criteria", + title="Insolation/ PAR (Photosynthetically Active Radiation)", standard=1, - linked_doctype='Weather'), + linked_doctype="Weather", + ), dict( - doctype='Agriculture Analysis Criteria', - title='Degree Days', + doctype="Agriculture Analysis Criteria", + title="Degree Days", standard=1, - linked_doctype='Weather') + linked_doctype="Weather", + ), ] insert_record(records) diff --git a/erpnext/assets/dashboard_fixtures.py b/erpnext/assets/dashboard_fixtures.py index 39f0f1a88be..fc9ba386a38 100644 --- a/erpnext/assets/dashboard_fixtures.py +++ b/erpnext/assets/dashboard_fixtures.py @@ -18,30 +18,36 @@ def get_data(): if not fiscal_year: return frappe._dict() - year_start_date = get_date_str(fiscal_year.get('year_start_date')) - year_end_date = get_date_str(fiscal_year.get('year_end_date')) + year_start_date = get_date_str(fiscal_year.get("year_start_date")) + year_end_date = get_date_str(fiscal_year.get("year_end_date")) + + return frappe._dict( + { + "dashboards": get_dashboards(), + "charts": get_charts(fiscal_year, year_start_date, year_end_date), + "number_cards": get_number_cards(fiscal_year, year_start_date, year_end_date), + } + ) - return frappe._dict({ - "dashboards": get_dashboards(), - "charts": get_charts(fiscal_year, year_start_date, year_end_date), - "number_cards": get_number_cards(fiscal_year, year_start_date, year_end_date), - }) def get_dashboards(): - return [{ - "name": "Asset", - "dashboard_name": "Asset", - "charts": [ - { "chart": "Asset Value Analytics", "width": "Full" }, - { "chart": "Category-wise Asset Value", "width": "Half" }, - { "chart": "Location-wise Asset Value", "width": "Half" }, - ], - "cards": [ - {"card": "Total Assets"}, - {"card": "New Assets (This Year)"}, - {"card": "Asset Value"} - ] - }] + return [ + { + "name": "Asset", + "dashboard_name": "Asset", + "charts": [ + {"chart": "Asset Value Analytics", "width": "Full"}, + {"chart": "Category-wise Asset Value", "width": "Half"}, + {"chart": "Location-wise Asset Value", "width": "Half"}, + ], + "cards": [ + {"card": "Total Assets"}, + {"card": "New Assets (This Year)"}, + {"card": "Asset Value"}, + ], + } + ] + def get_charts(fiscal_year, year_start_date, year_end_date): company = get_company_for_dashboards() @@ -58,26 +64,30 @@ def get_charts(fiscal_year, year_start_date, year_end_date): "timespan": "Last Year", "time_interval": "Yearly", "timeseries": 0, - "filters_json": json.dumps({ - "company": company, - "status": "In Location", - "filter_based_on": "Fiscal Year", - "from_fiscal_year": fiscal_year.get('name'), - "to_fiscal_year": fiscal_year.get('name'), - "period_start_date": year_start_date, - "period_end_date": year_end_date, - "date_based_on": "Purchase Date", - "group_by": "--Select a group--" - }), + "filters_json": json.dumps( + { + "company": company, + "status": "In Location", + "filter_based_on": "Fiscal Year", + "from_fiscal_year": fiscal_year.get("name"), + "to_fiscal_year": fiscal_year.get("name"), + "period_start_date": year_start_date, + "period_end_date": year_end_date, + "date_based_on": "Purchase Date", + "group_by": "--Select a group--", + } + ), "type": "Bar", - "custom_options": json.dumps({ - "type": "bar", - "barOptions": { "stacked": 1 }, - "axisOptions": { "shortenYAxisNumbers": 1 }, - "tooltipOptions": {} - }), + "custom_options": json.dumps( + { + "type": "bar", + "barOptions": {"stacked": 1}, + "axisOptions": {"shortenYAxisNumbers": 1}, + "tooltipOptions": {}, + } + ), "doctype": "Dashboard Chart", - "y_axis": [] + "y_axis": [], }, { "name": "Category-wise Asset Value", @@ -86,12 +96,14 @@ def get_charts(fiscal_year, year_start_date, year_end_date): "report_name": "Fixed Asset Register", "x_field": "asset_category", "timeseries": 0, - "filters_json": json.dumps({ - "company": company, - "status":"In Location", - "group_by":"Asset Category", - "is_existing_asset":0 - }), + "filters_json": json.dumps( + { + "company": company, + "status": "In Location", + "group_by": "Asset Category", + "is_existing_asset": 0, + } + ), "type": "Donut", "doctype": "Dashboard Chart", "y_axis": [ @@ -100,14 +112,12 @@ def get_charts(fiscal_year, year_start_date, year_end_date): "parentfield": "y_axis", "parenttype": "Dashboard Chart", "y_field": "asset_value", - "doctype": "Dashboard Chart Field" + "doctype": "Dashboard Chart Field", } ], - "custom_options": json.dumps({ - "type": "donut", - "height": 300, - "axisOptions": {"shortenYAxisNumbers": 1} - }) + "custom_options": json.dumps( + {"type": "donut", "height": 300, "axisOptions": {"shortenYAxisNumbers": 1}} + ), }, { "name": "Location-wise Asset Value", @@ -116,12 +126,9 @@ def get_charts(fiscal_year, year_start_date, year_end_date): "report_name": "Fixed Asset Register", "x_field": "location", "timeseries": 0, - "filters_json": json.dumps({ - "company": company, - "status":"In Location", - "group_by":"Location", - "is_existing_asset":0 - }), + "filters_json": json.dumps( + {"company": company, "status": "In Location", "group_by": "Location", "is_existing_asset": 0} + ), "type": "Donut", "doctype": "Dashboard Chart", "y_axis": [ @@ -130,17 +137,16 @@ def get_charts(fiscal_year, year_start_date, year_end_date): "parentfield": "y_axis", "parenttype": "Dashboard Chart", "y_field": "asset_value", - "doctype": "Dashboard Chart Field" + "doctype": "Dashboard Chart Field", } ], - "custom_options": json.dumps({ - "type": "donut", - "height": 300, - "axisOptions": {"shortenYAxisNumbers": 1} - }) - } + "custom_options": json.dumps( + {"type": "donut", "height": 300, "axisOptions": {"shortenYAxisNumbers": 1}} + ), + }, ] + def get_number_cards(fiscal_year, year_start_date, year_end_date): return [ { @@ -162,9 +168,9 @@ def get_number_cards(fiscal_year, year_start_date, year_end_date): "is_public": 1, "show_percentage_stats": 1, "stats_time_interval": "Monthly", - "filters_json": json.dumps([ - ['Asset', 'creation', 'between', [year_start_date, year_end_date]] - ]), + "filters_json": json.dumps( + [["Asset", "creation", "between", [year_start_date, year_end_date]]] + ), "doctype": "Number Card", }, { @@ -177,6 +183,6 @@ def get_number_cards(fiscal_year, year_start_date, year_end_date): "show_percentage_stats": 1, "stats_time_interval": "Monthly", "filters_json": "[]", - "doctype": "Number Card" - } + "doctype": "Number Card", + }, ] diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index fb48712c825..c255348a2bf 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -58,21 +58,26 @@ class Asset(AccountsController): self.cancel_movement_entries() self.delete_depreciation_entries() self.set_status() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') - make_reverse_gl_entries(voucher_type='Asset', voucher_no=self.name) - self.db_set('booked_fixed_asset', 0) + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name) + self.db_set("booked_fixed_asset", 0) def validate_asset_and_reference(self): if self.purchase_invoice or self.purchase_receipt: - reference_doc = 'Purchase Invoice' if self.purchase_invoice else 'Purchase Receipt' + reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt" reference_name = self.purchase_invoice or self.purchase_receipt reference_doc = frappe.get_doc(reference_doc, reference_name) - if reference_doc.get('company') != self.company: - frappe.throw(_("Company of asset {0} and purchase document {1} doesn't matches.").format(self.name, reference_doc.get('name'))) - + if reference_doc.get("company") != self.company: + frappe.throw( + _("Company of asset {0} and purchase document {1} doesn't matches.").format( + self.name, reference_doc.get("name") + ) + ) if self.is_existing_asset and self.purchase_invoice: - frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) + frappe.throw( + _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) + ) def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None): if self.calculate_depreciation: @@ -82,12 +87,14 @@ class Asset(AccountsController): self.set_accumulated_depreciation(date_of_sale, date_of_return) else: self.finance_books = [] - self.value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) + self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( + self.opening_accumulated_depreciation + ) def validate_item(self): - item = frappe.get_cached_value("Item", self.item_code, - ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1) + item = frappe.get_cached_value( + "Item", self.item_code, ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1 + ) if not item: frappe.throw(_("Item {0} does not exist").format(self.item_code)) elif item.disabled: @@ -98,16 +105,16 @@ class Asset(AccountsController): frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code)) def validate_cost_center(self): - if not self.cost_center: return + if not self.cost_center: + return - cost_center_company = frappe.db.get_value('Cost Center', self.cost_center, 'company') + cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company") if cost_center_company != self.company: frappe.throw( _("Selected Cost Center {} doesn't belongs to {}").format( - frappe.bold(self.cost_center), - frappe.bold(self.company) + frappe.bold(self.cost_center), frappe.bold(self.company) ), - title=_("Invalid Cost Center") + title=_("Invalid Cost Center"), ) def validate_in_use_date(self): @@ -116,16 +123,20 @@ class Asset(AccountsController): for d in self.finance_books: if d.depreciation_start_date == self.available_for_use_date: - frappe.throw(_("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format(d.idx), - title=_("Incorrect Date")) + frappe.throw( + _("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format( + d.idx + ), + title=_("Incorrect Date"), + ) def set_missing_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") - if self.item_code and not self.get('finance_books'): + if self.item_code and not self.get("finance_books"): finance_books = get_item_details(self.item_code, self.asset_category) - self.set('finance_books', finance_books) + self.set("finance_books", finance_books) def validate_asset_values(self): if not self.asset_category: @@ -136,13 +147,20 @@ class Asset(AccountsController): if is_cwip_accounting_enabled(self.asset_category): if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice): - frappe.throw(_("Please create purchase receipt or purchase invoice for the item {0}"). - format(self.item_code)) + frappe.throw( + _("Please create purchase receipt or purchase invoice for the item {0}").format( + self.item_code + ) + ) - if (not self.purchase_receipt and self.purchase_invoice - and not frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')): - frappe.throw(_("Update stock must be enable for the purchase invoice {0}"). - format(self.purchase_invoice)) + if ( + not self.purchase_receipt + and self.purchase_invoice + and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock") + ): + frappe.throw( + _("Update stock must be enable for the purchase invoice {0}").format(self.purchase_invoice) + ) if not self.calculate_depreciation: return @@ -152,49 +170,63 @@ class Asset(AccountsController): if self.is_existing_asset: return - if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): + if self.available_for_use_date and getdate(self.available_for_use_date) < getdate( + self.purchase_date + ): frappe.throw(_("Available-for-use Date should be after purchase date")) def validate_gross_and_purchase_amount(self): - if self.is_existing_asset: return + if self.is_existing_asset: + return if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount: - error_message = _("Gross Purchase Amount should be equal to purchase amount of one single Asset.") + error_message = _( + "Gross Purchase Amount should be equal to purchase amount of one single Asset." + ) error_message += "
" error_message += _("Please do not book expense of multiple assets against one single Asset.") frappe.throw(error_message, title=_("Invalid Gross Purchase Amount")) def make_asset_movement(self): - reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice' + reference_doctype = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice" reference_docname = self.purchase_receipt or self.purchase_invoice transaction_date = getdate(self.purchase_date) if reference_docname: - posting_date, posting_time = frappe.db.get_value(reference_doctype, reference_docname, ["posting_date", "posting_time"]) + posting_date, posting_time = frappe.db.get_value( + reference_doctype, reference_docname, ["posting_date", "posting_time"] + ) transaction_date = get_datetime("{} {}".format(posting_date, posting_time)) - assets = [{ - 'asset': self.name, - 'asset_name': self.asset_name, - 'target_location': self.location, - 'to_employee': self.custodian - }] - asset_movement = frappe.get_doc({ - 'doctype': 'Asset Movement', - 'assets': assets, - 'purpose': 'Receipt', - 'company': self.company, - 'transaction_date': transaction_date, - 'reference_doctype': reference_doctype, - 'reference_name': reference_docname - }).insert() + assets = [ + { + "asset": self.name, + "asset_name": self.asset_name, + "target_location": self.location, + "to_employee": self.custodian, + } + ] + asset_movement = frappe.get_doc( + { + "doctype": "Asset Movement", + "assets": assets, + "purpose": "Receipt", + "company": self.company, + "transaction_date": transaction_date, + "reference_doctype": reference_doctype, + "reference_name": reference_docname, + } + ).insert() asset_movement.submit() def set_depreciation_rate(self): for d in self.get("finance_books"): - d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True), - d.precision("rate_of_depreciation")) + d.rate_of_depreciation = flt( + self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") + ) def make_depreciation_schedule(self, date_of_sale): - if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.get('schedules'): + if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get( + "schedules" + ): self.schedules = [] if not self.available_for_use_date: @@ -202,20 +234,22 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() - for finance_book in self.get('finance_books'): + for finance_book in self.get("finance_books"): self.validate_asset_finance_books(finance_book) # value_after_depreciation - current Asset value if self.docstatus == 1 and finance_book.value_after_depreciation: value_after_depreciation = flt(finance_book.value_after_depreciation) else: - value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = flt(self.gross_purchase_amount) - flt( + self.opening_accumulated_depreciation + ) finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ - cint(self.number_of_depreciations_booked) + number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint( + self.number_of_depreciations_booked + ) has_pro_rata = self.check_is_pro_rata(finance_book) @@ -224,75 +258,94 @@ class Asset(AccountsController): skip_row = False - for n in range(start[finance_book.idx-1], number_of_pending_depreciations): + for n in range(start[finance_book.idx - 1], number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) - if skip_row: continue + if skip_row: + continue depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(finance_book.depreciation_start_date, - n * cint(finance_book.frequency_of_depreciation)) + schedule_date = add_months( + finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) + ) # schedule date will be a year later from start date # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) + monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1) # if asset is being sold if date_of_sale: from_date = self.get_from_date(finance_book.finance_book) - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, - from_date, date_of_sale) + depreciation_amount, days, months = self.get_pro_rata_amt( + finance_book, depreciation_amount, from_date, date_of_sale + ) if depreciation_amount > 0: - self.append("schedules", { - "schedule_date": date_of_sale, - "depreciation_amount": depreciation_amount, - "depreciation_method": finance_book.depreciation_method, - "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) + self.append( + "schedules", + { + "schedule_date": date_of_sale, + "depreciation_amount": depreciation_amount, + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx, + }, + ) break # For first row - if has_pro_rata and not self.opening_accumulated_depreciation and n==0: - from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, - from_date, finance_book.depreciation_start_date) + if has_pro_rata and not self.opening_accumulated_depreciation and n == 0: + from_date = add_days( + self.available_for_use_date, -1 + ) # needed to calc depr amount for available_for_use_date too + depreciation_amount, days, months = self.get_pro_rata_amt( + finance_book, depreciation_amount, from_date, finance_book.depreciation_start_date + ) # For first depr schedule date will be the start date # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) + monthly_schedule_date = add_months(finance_book.depreciation_start_date, -months + 1) # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission - self.to_date = add_months(self.available_for_use_date, - (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation)) + self.to_date = add_months( + self.available_for_use_date, + (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation), + ) depreciation_amount_without_pro_rata = depreciation_amount - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, - depreciation_amount, schedule_date, self.to_date) + depreciation_amount, days, months = self.get_pro_rata_amt( + finance_book, depreciation_amount, schedule_date, self.to_date + ) - depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, - depreciation_amount, finance_book.finance_book) + depreciation_amount = self.get_adjusted_depreciation_amount( + depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book + ) monthly_schedule_date = add_months(schedule_date, 1) schedule_date = add_days(schedule_date, days) last_schedule_date = schedule_date - if not depreciation_amount: continue - value_after_depreciation -= flt(depreciation_amount, - self.precision("gross_purchase_amount")) + if not depreciation_amount: + continue + value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount")) # Adjust depreciation amount in the last period based on the expected value after useful life - if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != finance_book.expected_value_after_useful_life) - or value_after_depreciation < finance_book.expected_value_after_useful_life): - depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) + if finance_book.expected_value_after_useful_life and ( + ( + n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life + ) + or value_after_depreciation < finance_book.expected_value_after_useful_life + ): + depreciation_amount += ( + value_after_depreciation - finance_book.expected_value_after_useful_life + ) skip_row = True if depreciation_amount > 0: @@ -300,15 +353,18 @@ class Asset(AccountsController): if self.allow_monthly_depreciation: # month range is 1 to 12 # In pro rata case, for first and last depreciation, month range would be different - month_range = months \ - if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ + month_range = ( + months + if (has_pro_rata and n == 0) + or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) else finance_book.frequency_of_depreciation + ) for r in range(month_range): - if (has_pro_rata and n == 0): + if has_pro_rata and n == 0: # For first entry of monthly depr if r == 0: - days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1 per_day_amt = depreciation_amount / days depreciation_amount_for_current_month = per_day_amt * days_until_first_depr depreciation_amount -= depreciation_amount_for_current_month @@ -317,7 +373,9 @@ class Asset(AccountsController): else: date = add_months(monthly_schedule_date, r) amount = depreciation_amount / (month_range - 1) - elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1: + elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint( + month_range + ) - 1: # For last entry of monthly depr date = last_schedule_date amount = depreciation_amount / month_range @@ -325,21 +383,27 @@ class Asset(AccountsController): date = add_months(monthly_schedule_date, r) amount = depreciation_amount / month_range - self.append("schedules", { - "schedule_date": date, - "depreciation_amount": amount, + self.append( + "schedules", + { + "schedule_date": date, + "depreciation_amount": amount, + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx, + }, + ) + else: + self.append( + "schedules", + { + "schedule_date": schedule_date, + "depreciation_amount": depreciation_amount, "depreciation_method": finance_book.depreciation_method, "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) - else: - self.append("schedules", { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": finance_book.depreciation_method, - "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) + "finance_book_id": finance_book.idx, + }, + ) # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales # JE: Journal Entry, FB: Finance Book @@ -348,7 +412,7 @@ class Asset(AccountsController): num_of_depreciations_completed = 0 depr_schedule = [] - for schedule in self.get('schedules'): + for schedule in self.get("schedules"): # to update start when there are JEs linked with all the schedule rows corresponding to an FB if len(start) == (int(schedule.finance_book_id) - 2): @@ -377,14 +441,14 @@ class Asset(AccountsController): return start def get_from_date(self, finance_book): - if not self.get('schedules'): + if not self.get("schedules"): return self.available_for_use_date if len(self.finance_books) == 1: return self.schedules[-1].schedule_date from_date = "" - for schedule in self.get('schedules'): + for schedule in self.get("schedules"): if schedule.finance_book == finance_book: from_date = schedule.schedule_date @@ -413,17 +477,25 @@ class Asset(AccountsController): return has_pro_rata def get_modified_available_for_use_date(self, row): - return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation)) + return add_months( + self.available_for_use_date, + (self.number_of_depreciations_booked * row.frequency_of_depreciation), + ) def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): - frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") - .format(row.idx), title=_("Invalid Schedule")) + frappe.throw( + _("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format( + row.idx + ), + title=_("Invalid Schedule"), + ) if not row.depreciation_start_date: if not self.available_for_use_date: - frappe.throw(_("Row {0}: Depreciation Start Date is required") - .format(row.idx), title=_("Invalid Schedule")) + frappe.throw( + _("Row {0}: Depreciation Start Date is required").format(row.idx), title=_("Invalid Schedule") + ) row.depreciation_start_date = get_last_day(self.available_for_use_date) if not self.is_existing_asset: @@ -432,8 +504,11 @@ class Asset(AccountsController): else: depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) if flt(self.opening_accumulated_depreciation) > depreciable_amount: - frappe.throw(_("Opening Accumulated Depreciation must be less than equal to {0}") - .format(depreciable_amount)) + frappe.throw( + _("Opening Accumulated Depreciation must be less than equal to {0}").format( + depreciable_amount + ) + ) if self.opening_accumulated_depreciation: if not self.number_of_depreciations_booked: @@ -442,24 +517,45 @@ class Asset(AccountsController): self.number_of_depreciations_booked = 0 if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked): - frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked") - .format(row.idx), title=_("Invalid Schedule")) + frappe.throw( + _( + "Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked" + ).format(row.idx), + title=_("Invalid Schedule"), + ) - if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date): - frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date") - .format(row.idx)) + if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate( + self.purchase_date + ): + frappe.throw( + _("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date").format( + row.idx + ) + ) - if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.available_for_use_date): - frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date") - .format(row.idx)) + if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate( + self.available_for_use_date + ): + frappe.throw( + _( + "Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date" + ).format(row.idx) + ) # to ensure that final accumulated depreciation amount is accurate - def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book): + def get_adjusted_depreciation_amount( + self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book + ): if not self.opening_accumulated_depreciation: depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) - if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: - depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + if ( + depreciation_amount_for_first_row + depreciation_amount_for_last_row + != depreciation_amount_without_pro_rata + ): + depreciation_amount_for_last_row = ( + depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + ) return depreciation_amount_for_last_row @@ -475,8 +571,12 @@ class Asset(AccountsController): if len(self.finance_books) == 1: return True - def set_accumulated_depreciation(self, date_of_sale=None, date_of_return=None, ignore_booked_entry = False): - straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line'] + def set_accumulated_depreciation( + self, date_of_sale=None, date_of_return=None, ignore_booked_entry=False + ): + straight_line_idx = [ + d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line" + ] finance_books = [] for i, d in enumerate(self.get("schedules")): @@ -492,41 +592,64 @@ class Asset(AccountsController): value_after_depreciation -= flt(depreciation_amount) # for the last row, if depreciation method = Straight Line - if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale and not date_of_return: - book = self.get('finance_books')[cint(d.finance_book_id) - 1] - depreciation_amount += flt(value_after_depreciation - - flt(book.expected_value_after_useful_life), d.precision("depreciation_amount")) + if ( + straight_line_idx + and i == max(straight_line_idx) - 1 + and not date_of_sale + and not date_of_return + ): + book = self.get("finance_books")[cint(d.finance_book_id) - 1] + depreciation_amount += flt( + value_after_depreciation - flt(book.expected_value_after_useful_life), + d.precision("depreciation_amount"), + ) d.depreciation_amount = depreciation_amount accumulated_depreciation += d.depreciation_amount - d.accumulated_depreciation_amount = flt(accumulated_depreciation, - d.precision("accumulated_depreciation_amount")) + d.accumulated_depreciation_amount = flt( + accumulated_depreciation, d.precision("accumulated_depreciation_amount") + ) def get_value_after_depreciation(self, idx): - return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation) + return flt(self.get("finance_books")[cint(idx) - 1].value_after_depreciation) def validate_expected_value_after_useful_life(self): - for row in self.get('finance_books'): - accumulated_depreciation_after_full_schedule = [d.accumulated_depreciation_amount - for d in self.get("schedules") if cint(d.finance_book_id) == row.idx] + for row in self.get("finance_books"): + accumulated_depreciation_after_full_schedule = [ + d.accumulated_depreciation_amount + for d in self.get("schedules") + if cint(d.finance_book_id) == row.idx + ] if accumulated_depreciation_after_full_schedule: - accumulated_depreciation_after_full_schedule = max(accumulated_depreciation_after_full_schedule) + accumulated_depreciation_after_full_schedule = max( + accumulated_depreciation_after_full_schedule + ) asset_value_after_full_schedule = flt( - flt(self.gross_purchase_amount) - - flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount')) + flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule), + row.precision("expected_value_after_useful_life"), + ) - if (row.expected_value_after_useful_life and - row.expected_value_after_useful_life < asset_value_after_full_schedule): - frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}") - .format(row.idx, asset_value_after_full_schedule)) + if ( + row.expected_value_after_useful_life + and row.expected_value_after_useful_life < asset_value_after_full_schedule + ): + frappe.throw( + _( + "Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}" + ).format(row.idx, asset_value_after_full_schedule) + ) elif not row.expected_value_after_useful_life: row.expected_value_after_useful_life = asset_value_after_full_schedule def validate_cancellation(self): if self.status in ("In Maintenance", "Out of Order"): - frappe.throw(_("There are active maintenance or repairs against the asset. You must complete all of them before cancelling the asset.")) + frappe.throw( + _( + "There are active maintenance or repairs against the asset. You must complete all of them before cancelling the asset." + ) + ) if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"): frappe.throw(_("Asset cannot be cancelled, as it is already {0}").format(self.status)) @@ -534,10 +657,13 @@ class Asset(AccountsController): movements = frappe.db.sql( """SELECT asm.name, asm.docstatus FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item - WHERE asm_item.parent=asm.name and asm_item.asset=%s and asm.docstatus=1""", self.name, as_dict=1) + WHERE asm_item.parent=asm.name and asm_item.asset=%s and asm.docstatus=1""", + self.name, + as_dict=1, + ) for movement in movements: - movement = frappe.get_doc('Asset Movement', movement.get('name')) + movement = frappe.get_doc("Asset Movement", movement.get("name")) movement.cancel() def delete_depreciation_entries(self): @@ -546,17 +672,19 @@ class Asset(AccountsController): frappe.get_doc("Journal Entry", d.journal_entry).cancel() d.db_set("journal_entry", None) - self.db_set("value_after_depreciation", - (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation))) + self.db_set( + "value_after_depreciation", + (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)), + ) def set_status(self, status=None): - '''Get and update status''' + """Get and update status""" if not status: status = self.get_status() self.db_set("status", status) def get_status(self): - '''Returns status based on whether it is draft, submitted, scrapped or depreciated''' + """Returns status based on whether it is draft, submitted, scrapped or depreciated""" if self.docstatus == 0: status = "Draft" elif self.docstatus == 1: @@ -573,17 +701,17 @@ class Asset(AccountsController): if flt(value_after_depreciation) <= expected_value_after_useful_life: status = "Fully Depreciated" elif flt(value_after_depreciation) < flt(self.gross_purchase_amount): - status = 'Partially Depreciated' + status = "Partially Depreciated" elif self.docstatus == 2: status = "Cancelled" return status def get_default_finance_book_idx(self): - if not self.get('default_finance_book') and self.company: + if not self.get("default_finance_book") and self.company: self.default_finance_book = erpnext.get_default_finance_book(self.company) - if self.get('default_finance_book'): - for d in self.get('finance_books'): + if self.get("default_finance_book"): + for d in self.get("finance_books"): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 @@ -592,7 +720,7 @@ class Asset(AccountsController): if not purchase_document: return False - asset_bought_with_invoice = (purchase_document == self.purchase_invoice) + asset_bought_with_invoice = purchase_document == self.purchase_invoice fixed_asset_account = self.get_fixed_asset_account() cwip_enabled = is_cwip_accounting_enabled(self.asset_category) @@ -622,13 +750,17 @@ class Asset(AccountsController): return cwip_booked def get_purchase_document(self): - asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') + asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value( + "Purchase Invoice", self.purchase_invoice, "update_stock" + ) purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt return purchase_document def get_fixed_asset_account(self): - fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + fixed_asset_account = get_asset_category_account( + "fixed_asset_account", None, self.name, None, self.asset_category, self.company + ) if not fixed_asset_account: frappe.throw( _("Set {0} in asset category {1} for company {2}").format( @@ -643,7 +775,9 @@ class Asset(AccountsController): def get_cwip_account(self, cwip_enabled=False): cwip_account = None try: - cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", self.name, self.asset_category, self.company + ) except Exception: # if no cwip account found in category or company and "cwip is enabled" then raise else silently pass if cwip_enabled: @@ -657,33 +791,45 @@ class Asset(AccountsController): purchase_document = self.get_purchase_document() fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() - if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): + if ( + purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate() + ): - gl_entries.append(self.get_gl_dict({ - "account": cwip_account, - "against": fixed_asset_account, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "posting_date": self.available_for_use_date, - "credit": self.purchase_receipt_amount, - "credit_in_account_currency": self.purchase_receipt_amount, - "cost_center": self.cost_center - }, item=self)) + gl_entries.append( + self.get_gl_dict( + { + "account": cwip_account, + "against": fixed_asset_account, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "posting_date": self.available_for_use_date, + "credit": self.purchase_receipt_amount, + "credit_in_account_currency": self.purchase_receipt_amount, + "cost_center": self.cost_center, + }, + item=self, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": fixed_asset_account, - "against": cwip_account, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "posting_date": self.available_for_use_date, - "debit": self.purchase_receipt_amount, - "debit_in_account_currency": self.purchase_receipt_amount, - "cost_center": self.cost_center - }, item=self)) + gl_entries.append( + self.get_gl_dict( + { + "account": fixed_asset_account, + "against": cwip_account, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "posting_date": self.available_for_use_date, + "debit": self.purchase_receipt_amount, + "debit_in_account_currency": self.purchase_receipt_amount, + "cost_center": self.cost_center, + }, + item=self, + ) + ) if gl_entries: from erpnext.accounts.general_ledger import make_gl_entries make_gl_entries(gl_entries) - self.db_set('booked_fixed_asset', 1) + self.db_set("booked_fixed_asset", 1) @frappe.whitelist() def get_depreciation_rate(self, args, on_validate=False): @@ -692,18 +838,21 @@ class Asset(AccountsController): float_precision = cint(frappe.db.get_default("float_precision")) or 2 - if args.get("depreciation_method") == 'Double Declining Balance': + if args.get("depreciation_method") == "Double Declining Balance": return 200.0 / args.get("total_number_of_depreciations") if args.get("depreciation_method") == "Written Down Value": if args.get("rate_of_depreciation") and on_validate: return args.get("rate_of_depreciation") - no_of_years = flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation"))) / 12 + no_of_years = ( + flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation"))) + / 12 + ) value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) # square root of flt(salvage_value) / flt(asset_cost) - depreciation_rate = math.pow(value, 1.0/flt(no_of_years, 2)) + depreciation_rate = math.pow(value, 1.0 / flt(no_of_years, 2)) return 100 * (1 - flt(depreciation_rate, float_precision)) @@ -714,93 +863,104 @@ class Asset(AccountsController): return (depreciation_amount * flt(days)) / flt(total_days), days, months + def update_maintenance_status(): - assets = frappe.get_all( - "Asset", filters={"docstatus": 1, "maintenance_required": 1} - ) + assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1}) for asset in assets: asset = frappe.get_doc("Asset", asset.name) if frappe.db.exists("Asset Repair", {"asset_name": asset.name, "repair_status": "Pending"}): asset.set_status("Out of Order") - elif frappe.db.exists("Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()}): + elif frappe.db.exists( + "Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()} + ): asset.set_status("In Maintenance") else: asset.set_status() + def make_post_gl_entry(): - asset_categories = frappe.db.get_all('Asset Category', fields = ['name', 'enable_cwip_accounting']) + asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"]) for asset_category in asset_categories: if cint(asset_category.enable_cwip_accounting): - assets = frappe.db.sql_list(""" select name from `tabAsset` + assets = frappe.db.sql_list( + """ select name from `tabAsset` where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0 - and available_for_use_date = %s""", (asset_category.name, nowdate())) + and available_for_use_date = %s""", + (asset_category.name, nowdate()), + ) for asset in assets: - doc = frappe.get_doc('Asset', asset) + doc = frappe.get_doc("Asset", asset) doc.make_gl_entries() + def get_asset_naming_series(): - meta = frappe.get_meta('Asset') + meta = frappe.get_meta("Asset") return meta.get_field("naming_series").options + @frappe.whitelist() def make_sales_invoice(asset, item_code, company, serial_no=None): si = frappe.new_doc("Sales Invoice") si.company = company - si.currency = frappe.get_cached_value('Company', company, "default_currency") + si.currency = frappe.get_cached_value("Company", company, "default_currency") disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(company) - si.append("items", { - "item_code": item_code, - "is_fixed_asset": 1, - "asset": asset, - "income_account": disposal_account, - "serial_no": serial_no, - "cost_center": depreciation_cost_center, - "qty": 1 - }) + si.append( + "items", + { + "item_code": item_code, + "is_fixed_asset": 1, + "asset": asset, + "income_account": disposal_account, + "serial_no": serial_no, + "cost_center": depreciation_cost_center, + "qty": 1, + }, + ) si.set_missing_values() return si + @frappe.whitelist() def create_asset_maintenance(asset, item_code, item_name, asset_category, company): asset_maintenance = frappe.new_doc("Asset Maintenance") - asset_maintenance.update({ - "asset_name": asset, - "company": company, - "item_code": item_code, - "item_name": item_name, - "asset_category": asset_category - }) + asset_maintenance.update( + { + "asset_name": asset, + "company": company, + "item_code": item_code, + "item_name": item_name, + "asset_category": asset_category, + } + ) return asset_maintenance + @frappe.whitelist() def create_asset_repair(asset, asset_name): asset_repair = frappe.new_doc("Asset Repair") - asset_repair.update({ - "asset": asset, - "asset_name": asset_name - }) + asset_repair.update({"asset": asset, "asset_name": asset_name}) return asset_repair + @frappe.whitelist() def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") - asset_value_adjustment.update({ - "asset": asset, - "company": company, - "asset_category": asset_category - }) + asset_value_adjustment.update( + {"asset": asset, "company": company, "asset_category": asset_category} + ) return asset_value_adjustment + @frappe.whitelist() def transfer_asset(args): args = json.loads(args) - if args.get('serial_no'): - args['quantity'] = len(args.get('serial_no').split('\n')) + if args.get("serial_no"): + args["quantity"] = len(args.get("serial_no").split("\n")) movement_entry = frappe.new_doc("Asset Movement") movement_entry.update(args) @@ -809,52 +969,73 @@ def transfer_asset(args): frappe.db.commit() - frappe.msgprint(_("Asset Movement record {0} created").format("{0}").format(movement_entry.name)) + frappe.msgprint( + _("Asset Movement record {0} created") + .format("{0}") + .format(movement_entry.name) + ) + @frappe.whitelist() def get_item_details(item_code, asset_category): - asset_category_doc = frappe.get_doc('Asset Category', asset_category) + asset_category_doc = frappe.get_doc("Asset Category", asset_category) books = [] for d in asset_category_doc.finance_books: - books.append({ - 'finance_book': d.finance_book, - 'depreciation_method': d.depreciation_method, - 'total_number_of_depreciations': d.total_number_of_depreciations, - 'frequency_of_depreciation': d.frequency_of_depreciation, - 'start_date': nowdate() - }) + books.append( + { + "finance_book": d.finance_book, + "depreciation_method": d.depreciation_method, + "total_number_of_depreciations": d.total_number_of_depreciations, + "frequency_of_depreciation": d.frequency_of_depreciation, + "start_date": nowdate(), + } + ) return books + def get_asset_account(account_name, asset=None, asset_category=None, company=None): account = None if asset: - account = get_asset_category_account(account_name, asset=asset, - asset_category = asset_category, company = company) + account = get_asset_category_account( + account_name, asset=asset, asset_category=asset_category, company=company + ) if not asset and not account: - account = get_asset_category_account(account_name, asset_category = asset_category, company = company) + account = get_asset_category_account( + account_name, asset_category=asset_category, company=company + ) if not account: - account = frappe.get_cached_value('Company', company, account_name) + account = frappe.get_cached_value("Company", company, account_name) if not account: if not asset_category: - frappe.throw(_("Set {0} in company {1}").format(account_name.replace('_', ' ').title(), company)) + frappe.throw( + _("Set {0} in company {1}").format(account_name.replace("_", " ").title(), company) + ) else: - frappe.throw(_("Set {0} in asset category {1} or company {2}") - .format(account_name.replace('_', ' ').title(), asset_category, company)) + frappe.throw( + _("Set {0} in asset category {1} or company {2}").format( + account_name.replace("_", " ").title(), asset_category, company + ) + ) return account + @frappe.whitelist() def make_journal_entry(asset_name): asset = frappe.get_doc("Asset", asset_name) - fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account = \ - get_depreciation_accounts(asset) + ( + fixed_asset_account, + accumulated_depreciation_account, + depreciation_expense_account, + ) = get_depreciation_accounts(asset) - depreciation_cost_center, depreciation_series = frappe.db.get_value("Company", asset.company, - ["depreciation_cost_center", "series_for_depreciation_entry"]) + depreciation_cost_center, depreciation_series = frappe.db.get_value( + "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] + ) depreciation_cost_center = asset.cost_center or depreciation_cost_center je = frappe.new_doc("Journal Entry") @@ -863,21 +1044,28 @@ def make_journal_entry(asset_name): je.company = asset.company je.remark = "Depreciation Entry against asset {0}".format(asset_name) - je.append("accounts", { - "account": depreciation_expense_account, - "reference_type": "Asset", - "reference_name": asset.name, - "cost_center": depreciation_cost_center - }) + je.append( + "accounts", + { + "account": depreciation_expense_account, + "reference_type": "Asset", + "reference_name": asset.name, + "cost_center": depreciation_cost_center, + }, + ) - je.append("accounts", { - "account": accumulated_depreciation_account, - "reference_type": "Asset", - "reference_name": asset.name - }) + je.append( + "accounts", + { + "account": accumulated_depreciation_account, + "reference_type": "Asset", + "reference_name": asset.name, + }, + ) return je + @frappe.whitelist() def make_asset_movement(assets, purpose=None): import json @@ -888,43 +1076,50 @@ def make_asset_movement(assets, purpose=None): assets = json.loads(assets) if len(assets) == 0: - frappe.throw(_('Atleast one asset has to be selected.')) + frappe.throw(_("Atleast one asset has to be selected.")) asset_movement = frappe.new_doc("Asset Movement") asset_movement.quantity = len(assets) for asset in assets: - asset = frappe.get_doc('Asset', asset.get('name')) - asset_movement.company = asset.get('company') - asset_movement.append("assets", { - 'asset': asset.get('name'), - 'source_location': asset.get('location'), - 'from_employee': asset.get('custodian') - }) + asset = frappe.get_doc("Asset", asset.get("name")) + asset_movement.company = asset.get("company") + asset_movement.append( + "assets", + { + "asset": asset.get("name"), + "source_location": asset.get("location"), + "from_employee": asset.get("custodian"), + }, + ) - if asset_movement.get('assets'): + if asset_movement.get("assets"): return asset_movement.as_dict() + def is_cwip_accounting_enabled(asset_category): return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) + def get_total_days(date, frequency): - period_start_date = add_months(date, - cint(frequency) * -1) + period_start_date = add_months(date, cint(frequency) * -1) return date_diff(date, period_start_date) + @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - - flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) + depreciation_amount = ( + flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + depreciation_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) else: depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py index c9efe3d0848..9a45bcb0f9a 100644 --- a/erpnext/assets/doctype/asset/asset_dashboard.py +++ b/erpnext/assets/doctype/asset/asset_dashboard.py @@ -1,14 +1,8 @@ +from frappe import _ def get_data(): return { - 'non_standard_fieldnames': { - 'Asset Movement': 'asset' - }, - 'transactions': [ - { - 'label': ['Movement'], - 'items': ['Asset Movement'] - } - ] + "non_standard_fieldnames": {"Asset Movement": "asset"}, + "transactions": [{"label": _("Movement"), "items": ["Asset Movement"]}], } diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 874fb630f87..3f7e9459943 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -13,7 +13,9 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( def post_depreciation_entries(date=None): # Return if automatic booking of asset depreciation is disabled - if not cint(frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically")): + if not cint( + frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically") + ): return if not date: @@ -22,26 +24,35 @@ def post_depreciation_entries(date=None): make_depreciation_entry(asset, date) frappe.db.commit() + def get_depreciable_assets(date): - return frappe.db.sql_list("""select a.name + return frappe.db.sql_list( + """select distinct a.name from tabAsset a, `tabDepreciation Schedule` ds where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 and a.status in ('Submitted', 'Partially Depreciated') - and ifnull(ds.journal_entry, '')=''""", date) + and ifnull(ds.journal_entry, '')=''""", + date, + ) + @frappe.whitelist() def make_depreciation_entry(asset_name, date=None): - frappe.has_permission('Journal Entry', throw=True) + frappe.has_permission("Journal Entry", throw=True) if not date: date = today() asset = frappe.get_doc("Asset", asset_name) - fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account = \ - get_depreciation_accounts(asset) + ( + fixed_asset_account, + accumulated_depreciation_account, + depreciation_expense_account, + ) = get_depreciation_accounts(asset) - depreciation_cost_center, depreciation_series = frappe.get_cached_value('Company', asset.company, - ["depreciation_cost_center", "series_for_depreciation_entry"]) + depreciation_cost_center, depreciation_series = frappe.get_cached_value( + "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] + ) depreciation_cost_center = asset.cost_center or depreciation_cost_center @@ -57,14 +68,16 @@ def make_depreciation_entry(asset_name, date=None): je.finance_book = d.finance_book je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) - credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account) + credit_account, debit_account = get_credit_and_debit_accounts( + accumulated_depreciation_account, depreciation_expense_account + ) credit_entry = { "account": credit_account, "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, - "cost_center": depreciation_cost_center + "cost_center": depreciation_cost_center, } debit_entry = { @@ -72,19 +85,25 @@ def make_depreciation_entry(asset_name, date=None): "debit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, - "cost_center": depreciation_cost_center + "cost_center": depreciation_cost_center, } for dimension in accounting_dimensions: - if (asset.get(dimension['fieldname']) or dimension.get('mandatory_for_bs')): - credit_entry.update({ - dimension['fieldname']: asset.get(dimension['fieldname']) or dimension.get('default_dimension') - }) + if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"): + credit_entry.update( + { + dimension["fieldname"]: asset.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) - if (asset.get(dimension['fieldname']) or dimension.get('mandatory_for_pl')): - debit_entry.update({ - dimension['fieldname']: asset.get(dimension['fieldname']) or dimension.get('default_dimension') - }) + if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"): + debit_entry.update( + { + dimension["fieldname"]: asset.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) je.append("accounts", credit_entry) @@ -98,7 +117,7 @@ def make_depreciation_entry(asset_name, date=None): d.db_set("journal_entry", je.name) idx = cint(d.finance_book_id) - finance_books = asset.get('finance_books')[idx - 1] + finance_books = asset.get("finance_books")[idx - 1] finance_books.value_after_depreciation -= d.depreciation_amount finance_books.db_update() @@ -106,13 +125,20 @@ def make_depreciation_entry(asset_name, date=None): return asset + def get_depreciation_accounts(asset): fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None - accounts = frappe.db.get_value("Asset Category Account", - filters={'parent': asset.asset_category, 'company_name': asset.company}, - fieldname = ['fixed_asset_account', 'accumulated_depreciation_account', - 'depreciation_expense_account'], as_dict=1) + accounts = frappe.db.get_value( + "Asset Category Account", + filters={"parent": asset.asset_category, "company_name": asset.company}, + fieldname=[ + "fixed_asset_account", + "accumulated_depreciation_account", + "depreciation_expense_account", + ], + as_dict=1, + ) if accounts: fixed_asset_account = accounts.fixed_asset_account @@ -120,20 +146,29 @@ def get_depreciation_accounts(asset): depreciation_expense_account = accounts.depreciation_expense_account if not accumulated_depreciation_account or not depreciation_expense_account: - accounts = frappe.get_cached_value('Company', asset.company, - ["accumulated_depreciation_account", "depreciation_expense_account"]) + accounts = frappe.get_cached_value( + "Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"] + ) if not accumulated_depreciation_account: accumulated_depreciation_account = accounts[0] if not depreciation_expense_account: depreciation_expense_account = accounts[1] - if not fixed_asset_account or not accumulated_depreciation_account or not depreciation_expense_account: - frappe.throw(_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}") - .format(asset.asset_category, asset.company)) + if ( + not fixed_asset_account + or not accumulated_depreciation_account + or not depreciation_expense_account + ): + frappe.throw( + _("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format( + asset.asset_category, asset.company + ) + ) return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account + def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") @@ -148,6 +183,7 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation return credit_account, debit_account + @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) @@ -155,9 +191,13 @@ def scrap_asset(asset_name): if asset.docstatus != 1: frappe.throw(_("Asset {0} must be submitted").format(asset.name)) elif asset.status in ("Cancelled", "Sold", "Scrapped"): - frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)) + frappe.throw( + _("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status) + ) - depreciation_series = frappe.get_cached_value('Company', asset.company, "series_for_depreciation_entry") + depreciation_series = frappe.get_cached_value( + "Company", asset.company, "series_for_depreciation_entry" + ) je = frappe.new_doc("Journal Entry") je.voucher_type = "Journal Entry" @@ -167,10 +207,7 @@ def scrap_asset(asset_name): je.remark = "Scrap Entry for asset {0}".format(asset_name) for entry in get_gl_entries_on_asset_disposal(asset): - entry.update({ - "reference_type": "Asset", - "reference_name": asset_name - }) + entry.update({"reference_type": "Asset", "reference_name": asset_name}) je.append("accounts", entry) je.flags.ignore_permissions = True @@ -182,6 +219,7 @@ def scrap_asset(asset_name): frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name)) + @frappe.whitelist() def restore_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) @@ -195,23 +233,31 @@ def restore_asset(asset_name): asset.set_status() + def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): - fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \ - get_asset_details(asset, finance_book) + ( + fixed_asset_account, + asset, + depreciation_cost_center, + accumulated_depr_account, + accumulated_depr_amount, + disposal_account, + value_after_depreciation, + ) = get_asset_details(asset, finance_book) gl_entries = [ { "account": fixed_asset_account, "debit_in_account_currency": asset.gross_purchase_amount, "debit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center + "cost_center": depreciation_cost_center, }, { "account": accumulated_depr_account, "credit_in_account_currency": accumulated_depr_amount, "credit": accumulated_depr_amount, - "cost_center": depreciation_cost_center - } + "cost_center": depreciation_cost_center, + }, ] profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) @@ -220,23 +266,31 @@ def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): return gl_entries + def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): - fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \ - get_asset_details(asset, finance_book) + ( + fixed_asset_account, + asset, + depreciation_cost_center, + accumulated_depr_account, + accumulated_depr_amount, + disposal_account, + value_after_depreciation, + ) = get_asset_details(asset, finance_book) gl_entries = [ { "account": fixed_asset_account, "credit_in_account_currency": asset.gross_purchase_amount, "credit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center + "cost_center": depreciation_cost_center, }, { "account": accumulated_depr_account, "debit_in_account_currency": accumulated_depr_amount, "debit": accumulated_depr_amount, - "cost_center": depreciation_cost_center - } + "cost_center": depreciation_cost_center, + }, ] profit_amount = flt(selling_amount) - flt(value_after_depreciation) @@ -245,8 +299,11 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) return gl_entries + def get_asset_details(asset, finance_book=None): - fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset) + fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts( + asset + ) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center @@ -257,28 +314,46 @@ def get_asset_details(asset, finance_book=None): idx = d.idx break - value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation - if asset.calculate_depreciation else asset.value_after_depreciation) + value_after_depreciation = ( + asset.finance_books[idx - 1].value_after_depreciation + if asset.calculate_depreciation + else asset.value_after_depreciation + ) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) - return fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation + return ( + fixed_asset_account, + asset, + depreciation_cost_center, + accumulated_depr_account, + accumulated_depr_amount, + disposal_account, + value_after_depreciation, + ) + def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center): debit_or_credit = "debit" if profit_amount < 0 else "credit" - gl_entries.append({ - "account": disposal_account, - "cost_center": depreciation_cost_center, - debit_or_credit: abs(profit_amount), - debit_or_credit + "_in_account_currency": abs(profit_amount) - }) + gl_entries.append( + { + "account": disposal_account, + "cost_center": depreciation_cost_center, + debit_or_credit: abs(profit_amount), + debit_or_credit + "_in_account_currency": abs(profit_amount), + } + ) + @frappe.whitelist() def get_disposal_account_and_cost_center(company): - disposal_account, depreciation_cost_center = frappe.get_cached_value('Company', company, - ["disposal_account", "depreciation_cost_center"]) + disposal_account, depreciation_cost_center = frappe.get_cached_value( + "Company", company, ["disposal_account", "depreciation_cost_center"] + ) if not disposal_account: - frappe.throw(_("Please set 'Gain/Loss Account on Asset Disposal' in Company {0}").format(company)) + frappe.throw( + _("Please set 'Gain/Loss Account on Asset Disposal' in Company {0}").format(company) + ) if not depreciation_cost_center: frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company)) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 6afd6dab264..79455bb1b4e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -32,6 +32,7 @@ class AssetSetup(unittest.TestCase): def tearDownClass(cls): frappe.db.rollback() + class TestAsset(AssetSetup): def test_asset_category_is_fetched(self): """Tests if the Item's Asset Category value is assigned to the Asset, if the field is empty.""" @@ -52,7 +53,7 @@ class TestAsset(AssetSetup): """Tests if either PI or PR is present if CWIP is enabled and is_existing_asset=0.""" asset = create_asset(item_code="Macbook Pro", do_not_save=1) - asset.is_existing_asset=0 + asset.is_existing_asset = 0 self.assertRaises(frappe.ValidationError, asset.save) @@ -67,7 +68,7 @@ class TestAsset(AssetSetup): def test_item_exists(self): asset = create_asset(item_code="MacBook", do_not_save=1) - self.assertRaises(frappe.DoesNotExistError, asset.save) + self.assertRaises(frappe.ValidationError, asset.save) def test_validate_item(self): asset = create_asset(item_code="MacBook Pro", do_not_save=1) @@ -86,11 +87,12 @@ class TestAsset(AssetSetup): self.assertRaises(frappe.ValidationError, asset.save) def test_purchase_asset(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + ) - asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - asset = frappe.get_doc('Asset', asset_name) + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset = frappe.get_doc("Asset", asset_name) asset.calculate_depreciation = 1 month_end_date = get_last_day(nowdate()) @@ -98,13 +100,16 @@ class TestAsset(AssetSetup): asset.available_for_use_date = purchase_date asset.purchase_date = purchase_date - asset.append("finance_books", { - "expected_value_after_useful_life": 10000, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date - }) + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date, + }, + ) asset.submit() pi = make_invoice(pr.name) @@ -119,12 +124,15 @@ class TestAsset(AssetSetup): expected_gle = ( ("Asset Received But Not Billed - _TC", 100000.0, 0.0), - ("Creditors - _TC", 0.0, 100000.0) + ("Creditors - _TC", 0.0, 100000.0), ) - gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", pi.name) + order by account""", + pi.name, + ) self.assertEqual(gle, expected_gle) pi.cancel() @@ -135,30 +143,26 @@ class TestAsset(AssetSetup): self.assertEqual(asset.docstatus, 2) def test_is_fixed_asset_set(self): - asset = create_asset(is_existing_asset = 1) - doc = frappe.new_doc('Purchase Invoice') - doc.supplier = '_Test Supplier' - doc.append('items', { - 'item_code': 'Macbook Pro', - 'qty': 1, - 'asset': asset.name - }) + asset = create_asset(is_existing_asset=1) + doc = frappe.new_doc("Purchase Invoice") + doc.supplier = "_Test Supplier" + doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name}) doc.set_missing_values() self.assertEqual(doc.items[0].is_fixed_asset, 1) def test_scrap_asset(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = '2020-01-01', - purchase_date = '2020-01-01', - expected_value_after_useful_life = 10000, - total_number_of_depreciations = 10, - frequency_of_depreciation = 1, - submit = 1 + calculate_depreciation=1, + available_for_use_date="2020-01-01", + purchase_date="2020-01-01", + expected_value_after_useful_life=10000, + total_number_of_depreciations=10, + frequency_of_depreciation=1, + submit=1, ) - post_depreciation_entries(date=add_months('2020-01-01', 4)) + post_depreciation_entries(date=add_months("2020-01-01", 4)) scrap_asset(asset.name) @@ -169,12 +173,15 @@ class TestAsset(AssetSetup): expected_gle = ( ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0) + ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0), ) - gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no = %s - order by account""", asset.journal_entry_for_scrap) + order by account""", + asset.journal_entry_for_scrap, + ) self.assertEqual(gle, expected_gle) restore_asset(asset.name) @@ -185,14 +192,14 @@ class TestAsset(AssetSetup): def test_gle_made_by_asset_sale(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = '2020-06-06', - purchase_date = '2020-01-01', - expected_value_after_useful_life = 10000, - total_number_of_depreciations = 3, - frequency_of_depreciation = 10, - depreciation_start_date = '2020-12-31', - submit = 1 + calculate_depreciation=1, + available_for_use_date="2020-06-06", + purchase_date="2020-01-01", + expected_value_after_useful_life=10000, + total_number_of_depreciations=3, + frequency_of_depreciation=10, + depreciation_start_date="2020-12-31", + submit=1, ) post_depreciation_entries(date="2021-01-01") @@ -210,12 +217,15 @@ class TestAsset(AssetSetup): ("_Test Accumulated Depreciations - _TC", 20490.2, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0), - ("Debtors - _TC", 25000.0, 0.0) + ("Debtors - _TC", 25000.0, 0.0), ) - gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", si.name) + order by account""", + si.name, + ) self.assertEqual(gle, expected_gle) @@ -223,45 +233,56 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") def test_expense_head(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=2, rate=200000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location" + ) doc = make_invoice(pr.name) - self.assertEqual('Asset Received But Not Billed - _TC', doc.items[0].expense_account) + self.assertEqual("Asset Received But Not Billed - _TC", doc.items[0].expense_account) # CWIP: Capital Work In Progress def test_cwip_accounting(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=5000, do_not_submit=True, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location" + ) - pr.set('taxes', [{ - 'category': 'Total', - 'add_deduct_tax': 'Add', - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Service Tax - _TC', - 'description': '_Test Account Service Tax', - 'cost_center': 'Main - _TC', - 'rate': 5.0 - }, { - 'category': 'Valuation and Total', - 'add_deduct_tax': 'Add', - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Shipping Charges - _TC', - 'description': '_Test Account Shipping Charges', - 'cost_center': 'Main - _TC', - 'rate': 5.0 - }]) + pr.set( + "taxes", + [ + { + "category": "Total", + "add_deduct_tax": "Add", + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "description": "_Test Account Service Tax", + "cost_center": "Main - _TC", + "rate": 5.0, + }, + { + "category": "Valuation and Total", + "add_deduct_tax": "Add", + "charge_type": "On Net Total", + "account_head": "_Test Account Shipping Charges - _TC", + "description": "_Test Account Shipping Charges", + "cost_center": "Main - _TC", + "rate": 5.0, + }, + ], + ) pr.submit() expected_gle = ( ("Asset Received But Not Billed - _TC", 0.0, 5250.0), - ("CWIP Account - _TC", 5250.0, 0.0) + ("CWIP Account - _TC", 5250.0, 0.0), ) - pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + pr_gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no = %s - order by account""", pr.name) + order by account""", + pr.name, + ) self.assertEqual(pr_gle, expected_gle) @@ -276,45 +297,53 @@ class TestAsset(AssetSetup): ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), ) - pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + pi_gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", pi.name) + order by account""", + pi.name, + ) self.assertEqual(pi_gle, expected_gle) - asset = frappe.db.get_value('Asset', - {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name") - asset_doc = frappe.get_doc('Asset', asset) + asset_doc = frappe.get_doc("Asset", asset) month_end_date = get_last_day(nowdate()) - asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) + asset_doc.available_for_use_date = ( + nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) + ) self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) - asset_doc.append("finance_books", { - "expected_value_after_useful_life": 200, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date - }) + asset_doc.append( + "finance_books", + { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date, + }, + ) asset_doc.submit() - expected_gle = ( - ("_Test Fixed Asset - _TC", 5250.0, 0.0), - ("CWIP Account - _TC", 0.0, 5250.0) - ) + expected_gle = (("_Test Fixed Asset - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 0.0, 5250.0)) - gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s - order by account""", asset_doc.name) - + order by account""", + asset_doc.name, + ) self.assertEqual(gle, expected_gle) 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"]) + name = frappe.db.get_value( + "Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"] + ) cwip_acc = "CWIP Account - _TC" frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) @@ -322,197 +351,231 @@ class TestAsset(AssetSetup): frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", "") # case 0 -- PI with cwip disable, Asset with cwip disabled, No cwip account set - pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) - asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') - asset_doc = frappe.get_doc('Asset', asset) + pi = make_purchase_invoice( + item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1 + ) + asset = frappe.db.get_value("Asset", {"purchase_invoice": pi.name, "docstatus": 0}, "name") + asset_doc = frappe.get_doc("Asset", asset) asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", + asset_doc.name, + ) self.assertFalse(gle) # case 1 -- PR with cwip disabled, Asset with cwip enabled - pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location" + ) frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) - asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') - asset_doc = frappe.get_doc('Asset', asset) + asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name") + asset_doc = frappe.get_doc("Asset", asset) asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", + asset_doc.name, + ) self.assertFalse(gle) # case 2 -- PR with cwip enabled, Asset with cwip disabled - pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location" + ) frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) - asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') - asset_doc = frappe.get_doc('Asset', asset) + asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name") + asset_doc = frappe.get_doc("Asset", asset) asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", + asset_doc.name, + ) self.assertTrue(gle) # case 3 -- PI with cwip disabled, Asset with cwip enabled - pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) + pi = make_purchase_invoice( + item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1 + ) frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) - asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') - asset_doc = frappe.get_doc('Asset', asset) + asset = frappe.db.get_value("Asset", {"purchase_invoice": pi.name, "docstatus": 0}, "name") + asset_doc = frappe.get_doc("Asset", asset) asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", + asset_doc.name, + ) self.assertFalse(gle) # case 4 -- PI with cwip enabled, Asset with cwip disabled - pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) + pi = make_purchase_invoice( + item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1 + ) frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) - asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') - asset_doc = frappe.get_doc('Asset', asset) + asset = frappe.db.get_value("Asset", {"purchase_invoice": pi.name, "docstatus": 0}, "name") + asset_doc = frappe.get_doc("Asset", asset) asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", + asset_doc.name, + ) self.assertTrue(gle) frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip) frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc) + class TestDepreciationMethods(AssetSetup): def test_schedule_for_straight_line_method(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = "2030-01-01", - purchase_date = "2030-01-01", - expected_value_after_useful_life = 10000, - depreciation_start_date = "2030-12-31", - total_number_of_depreciations = 3, - frequency_of_depreciation = 12 + calculate_depreciation=1, + available_for_use_date="2030-01-01", + purchase_date="2030-01-01", + expected_value_after_useful_life=10000, + depreciation_start_date="2030-12-31", + total_number_of_depreciations=3, + frequency_of_depreciation=12, ) self.assertEqual(asset.status, "Draft") expected_schedules = [ ["2030-12-31", 30000.00, 30000.00], ["2031-12-31", 30000.00, 60000.00], - ["2032-12-31", 30000.00, 90000.00] + ["2032-12-31", 30000.00, 90000.00], ] - schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules")] + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules") + ] self.assertEqual(schedules, expected_schedules) def test_schedule_for_straight_line_method_for_existing_asset(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = "2030-06-06", - is_existing_asset = 1, - number_of_depreciations_booked = 2, - opening_accumulated_depreciation = 47095.89, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2032-12-31", - total_number_of_depreciations = 3, - frequency_of_depreciation = 12 + calculate_depreciation=1, + available_for_use_date="2030-06-06", + is_existing_asset=1, + number_of_depreciations_booked=2, + opening_accumulated_depreciation=47095.89, + expected_value_after_useful_life=10000, + depreciation_start_date="2032-12-31", + total_number_of_depreciations=3, + frequency_of_depreciation=12, ) self.assertEqual(asset.status, "Draft") - expected_schedules = [ - ["2032-12-31", 30000.0, 77095.89], - ["2033-06-06", 12904.11, 90000.0] + expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]] + schedules = [ + [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] + for d in asset.get("schedules") ] - schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] - for d in asset.get("schedules")] self.assertEqual(schedules, expected_schedules) def test_schedule_for_double_declining_method(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = "2030-01-01", - purchase_date = "2030-01-01", - depreciation_method = "Double Declining Balance", - expected_value_after_useful_life = 10000, - depreciation_start_date = "2030-12-31", - total_number_of_depreciations = 3, - frequency_of_depreciation = 12 + calculate_depreciation=1, + available_for_use_date="2030-01-01", + purchase_date="2030-01-01", + depreciation_method="Double Declining Balance", + expected_value_after_useful_life=10000, + depreciation_start_date="2030-12-31", + total_number_of_depreciations=3, + frequency_of_depreciation=12, ) self.assertEqual(asset.status, "Draft") expected_schedules = [ - ['2030-12-31', 66667.00, 66667.00], - ['2031-12-31', 22222.11, 88889.11], - ['2032-12-31', 1110.89, 90000.0] + ["2030-12-31", 66667.00, 66667.00], + ["2031-12-31", 22222.11, 88889.11], + ["2032-12-31", 1110.89, 90000.0], ] - schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules")] + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules") + ] self.assertEqual(schedules, expected_schedules) def test_schedule_for_double_declining_method_for_existing_asset(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = "2030-01-01", - is_existing_asset = 1, - depreciation_method = "Double Declining Balance", - number_of_depreciations_booked = 1, - opening_accumulated_depreciation = 50000, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2030-12-31", - total_number_of_depreciations = 3, - frequency_of_depreciation = 12 + calculate_depreciation=1, + available_for_use_date="2030-01-01", + is_existing_asset=1, + depreciation_method="Double Declining Balance", + number_of_depreciations_booked=1, + opening_accumulated_depreciation=50000, + expected_value_after_useful_life=10000, + depreciation_start_date="2030-12-31", + total_number_of_depreciations=3, + frequency_of_depreciation=12, ) self.assertEqual(asset.status, "Draft") - expected_schedules = [ - ["2030-12-31", 33333.50, 83333.50], - ["2031-12-31", 6666.50, 90000.0] - ] + expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]] - schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules")] + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules") + ] self.assertEqual(schedules, expected_schedules) def test_schedule_for_prorated_straight_line_method(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = "2030-01-30", - purchase_date = "2030-01-30", - depreciation_method = "Straight Line", - expected_value_after_useful_life = 10000, - depreciation_start_date = "2030-12-31", - total_number_of_depreciations = 3, - frequency_of_depreciation = 12 + calculate_depreciation=1, + available_for_use_date="2030-01-30", + purchase_date="2030-01-30", + depreciation_method="Straight Line", + expected_value_after_useful_life=10000, + depreciation_start_date="2030-12-31", + total_number_of_depreciations=3, + frequency_of_depreciation=12, ) expected_schedules = [ - ['2030-12-31', 27616.44, 27616.44], - ['2031-12-31', 30000.0, 57616.44], - ['2032-12-31', 30000.0, 87616.44], - ['2033-01-30', 2383.56, 90000.0] + ["2030-12-31", 27616.44, 27616.44], + ["2031-12-31", 30000.0, 57616.44], + ["2032-12-31", 30000.0, 87616.44], + ["2033-01-30", 2383.56, 90000.0], ] - schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - for d in asset.get("schedules")] + schedules = [ + [ + cstr(d.schedule_date), + flt(d.depreciation_amount, 2), + flt(d.accumulated_depreciation_amount, 2), + ] + for d in asset.get("schedules") + ] self.assertEqual(schedules, expected_schedules) # WDV: Written Down Value method def test_depreciation_entry_for_wdv_without_pro_rata(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = "2030-01-01", - purchase_date = "2030-01-01", - depreciation_method = "Written Down Value", - expected_value_after_useful_life = 12500, - depreciation_start_date = "2030-12-31", - total_number_of_depreciations = 3, - frequency_of_depreciation = 12 + calculate_depreciation=1, + available_for_use_date="2030-01-01", + purchase_date="2030-01-01", + depreciation_method="Written Down Value", + expected_value_after_useful_life=12500, + depreciation_start_date="2030-12-31", + total_number_of_depreciations=3, + frequency_of_depreciation=12, ) self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) @@ -523,35 +586,47 @@ class TestDepreciationMethods(AssetSetup): ["2032-12-31", 12500.0, 87500.0], ] - schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - for d in asset.get("schedules")] + schedules = [ + [ + cstr(d.schedule_date), + flt(d.depreciation_amount, 2), + flt(d.accumulated_depreciation_amount, 2), + ] + for d in asset.get("schedules") + ] self.assertEqual(schedules, expected_schedules) # WDV: Written Down Value method def test_pro_rata_depreciation_entry_for_wdv(self): asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = "2030-06-06", - purchase_date = "2030-01-01", - depreciation_method = "Written Down Value", - expected_value_after_useful_life = 12500, - depreciation_start_date = "2030-12-31", - total_number_of_depreciations = 3, - frequency_of_depreciation = 12 + calculate_depreciation=1, + available_for_use_date="2030-06-06", + purchase_date="2030-01-01", + depreciation_method="Written Down Value", + expected_value_after_useful_life=12500, + depreciation_start_date="2030-12-31", + total_number_of_depreciations=3, + frequency_of_depreciation=12, ) self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) expected_schedules = [ - ['2030-12-31', 28630.14, 28630.14], - ['2031-12-31', 35684.93, 64315.07], - ['2032-12-31', 17842.47, 82157.54], - ['2033-06-06', 5342.46, 87500.0] + ["2030-12-31", 28630.14, 28630.14], + ["2031-12-31", 35684.93, 64315.07], + ["2032-12-31", 17842.47, 82157.54], + ["2033-06-06", 5342.46, 87500.0], ] - schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - for d in asset.get("schedules")] + schedules = [ + [ + cstr(d.schedule_date), + flt(d.depreciation_amount, 2), + flt(d.accumulated_depreciation_amount, 2), + ] + for d in asset.get("schedules") + ] self.assertEqual(schedules, expected_schedules) @@ -563,18 +638,18 @@ class TestDepreciationMethods(AssetSetup): finance_book = frappe.new_doc("Finance Book") finance_book.finance_book_name = "Income Tax" finance_book.for_income_tax = 1 - finance_book.insert(ignore_if_duplicate = True) + finance_book.insert(ignore_if_duplicate=True) asset = create_asset( - calculate_depreciation = 1, - available_for_use_date = "2030-07-12", - purchase_date = "2030-01-01", - finance_book = finance_book.name, - depreciation_method = "Written Down Value", - expected_value_after_useful_life = 12500, - depreciation_start_date = "2030-12-31", - total_number_of_depreciations = 3, - frequency_of_depreciation = 12 + calculate_depreciation=1, + available_for_use_date="2030-07-12", + purchase_date="2030-01-01", + finance_book=finance_book.name, + depreciation_method="Written Down Value", + expected_value_after_useful_life=12500, + depreciation_start_date="2030-12-31", + total_number_of_depreciations=3, + frequency_of_depreciation=12, ) self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) @@ -583,11 +658,17 @@ class TestDepreciationMethods(AssetSetup): ["2030-12-31", 11849.32, 11849.32], ["2031-12-31", 44075.34, 55924.66], ["2032-12-31", 22037.67, 77962.33], - ["2033-07-12", 9537.67, 87500.0] + ["2033-07-12", 9537.67, 87500.0], ] - schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - for d in asset.get("schedules")] + schedules = [ + [ + cstr(d.schedule_date), + flt(d.depreciation_amount, 2), + flt(d.accumulated_depreciation_amount, 2), + ] + for d in asset.get("schedules") + ] self.assertEqual(schedules, expected_schedules) @@ -596,8 +677,8 @@ class TestDepreciationMethods(AssetSetup): def test_expected_value_change(self): """ - tests if changing `expected_value_after_useful_life` - affects `value_after_depreciation` + tests if changing `expected_value_after_useful_life` + affects `value_after_depreciation` """ asset = create_asset(calculate_depreciation=1) @@ -615,22 +696,23 @@ class TestDepreciationMethods(AssetSetup): asset.reload() self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) + class TestDepreciationBasics(AssetSetup): def test_depreciation_without_pro_rata(self): asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = getdate("2019-12-31"), - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = getdate("2020-12-31"), - submit = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date=getdate("2019-12-31"), + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date=getdate("2020-12-31"), + submit=1, ) expected_values = [ ["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000], - ["2022-12-31", 30000, 90000] + ["2022-12-31", 30000, 90000], ] for i, schedule in enumerate(asset.schedules): @@ -640,20 +722,20 @@ class TestDepreciationBasics(AssetSetup): def test_depreciation_with_pro_rata(self): asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = getdate("2020-01-01"), - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = getdate("2020-07-01"), - submit = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date=getdate("2020-01-01"), + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date=getdate("2020-07-01"), + submit=1, ) expected_values = [ ["2020-07-01", 15000, 15000], ["2021-07-01", 30000, 45000], ["2022-07-01", 30000, 75000], - ["2023-01-01", 15000, 90000] + ["2023-01-01", 15000, 90000], ] for i, schedule in enumerate(asset.schedules): @@ -666,19 +748,19 @@ class TestDepreciationBasics(AssetSetup): from erpnext.assets.doctype.asset.asset import get_depreciation_amount - asset = create_asset( - item_code = "Macbook Pro", - available_for_use_date = "2019-12-31" - ) + asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31") asset.calculate_depreciation = 1 - asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, - "total_number_of_depreciations": 3, - "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" - }) + asset.append( + "finance_books", + { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31", + }, + ) depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) self.assertEqual(depreciation_amount, 30000) @@ -687,21 +769,17 @@ class TestDepreciationBasics(AssetSetup): """Tests if make_depreciation_schedule() returns the right values.""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - depreciation_method = "Straight Line", - frequency_of_depreciation = 12, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2020-12-31" + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + depreciation_method="Straight Line", + frequency_of_depreciation=12, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date="2020-12-31", ) - expected_values = [ - ['2020-12-31', 30000.0], - ['2021-12-31', 30000.0], - ['2022-12-31', 30000.0] - ] + expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]] for i, schedule in enumerate(asset.schedules): self.assertEqual(expected_values[i][0], schedule.schedule_date) @@ -711,14 +789,14 @@ class TestDepreciationBasics(AssetSetup): """Tests if set_accumulated_depreciation() returns the right values.""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - depreciation_method = "Straight Line", - frequency_of_depreciation = 12, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2020-12-31" + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + depreciation_method="Straight Line", + frequency_of_depreciation=12, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date="2020-12-31", ) expected_values = [30000.0, 60000.0, 90000.0] @@ -729,32 +807,34 @@ class TestDepreciationBasics(AssetSetup): def test_check_is_pro_rata(self): """Tests if check_is_pro_rata() returns the right value(i.e. checks if has_pro_rata is accurate).""" - asset = create_asset( - item_code = "Macbook Pro", - available_for_use_date = "2019-12-31", - do_not_save = 1 - ) + asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1) asset.calculate_depreciation = 1 - asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, - "total_number_of_depreciations": 3, - "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" - }) + asset.append( + "finance_books", + { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31", + }, + ) has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) self.assertFalse(has_pro_rata) asset.finance_books = [] - asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, - "total_number_of_depreciations": 3, - "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-07-01" - }) + asset.append( + "finance_books", + { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-07-01", + }, + ) has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) self.assertTrue(has_pro_rata) @@ -763,13 +843,13 @@ class TestDepreciationBasics(AssetSetup): """Tests if an error is raised when expected_value_after_useful_life(110,000) > gross_purchase_amount(100,000).""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - total_number_of_depreciations = 3, - expected_value_after_useful_life = 110000, - depreciation_start_date = "2020-07-01", - do_not_save = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + total_number_of_depreciations=3, + expected_value_after_useful_life=110000, + depreciation_start_date="2020-07-01", + do_not_save=1, ) self.assertRaises(frappe.ValidationError, asset.save) @@ -778,11 +858,11 @@ class TestDepreciationBasics(AssetSetup): """Tests if an error is raised when neither depreciation_start_date nor available_for_use_date are specified.""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 110000, - do_not_save = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + total_number_of_depreciations=3, + expected_value_after_useful_life=110000, + do_not_save=1, ) self.assertRaises(frappe.ValidationError, asset.save) @@ -791,14 +871,14 @@ class TestDepreciationBasics(AssetSetup): """Tests if an error is raised when opening_accumulated_depreciation > (gross_purchase_amount - expected_value_after_useful_life).""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2020-07-01", - opening_accumulated_depreciation = 100000, - do_not_save = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date="2020-07-01", + opening_accumulated_depreciation=100000, + do_not_save=1, ) self.assertRaises(frappe.ValidationError, asset.save) @@ -807,14 +887,14 @@ class TestDepreciationBasics(AssetSetup): """Tests if an error is raised when number_of_depreciations_booked is not specified when opening_accumulated_depreciation is.""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2020-07-01", - opening_accumulated_depreciation = 10000, - do_not_save = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date="2020-07-01", + opening_accumulated_depreciation=10000, + do_not_save=1, ) self.assertRaises(frappe.ValidationError, asset.save) @@ -824,56 +904,56 @@ class TestDepreciationBasics(AssetSetup): # number_of_depreciations_booked > total_number_of_depreciations asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2020-07-01", - opening_accumulated_depreciation = 10000, - number_of_depreciations_booked = 5, - do_not_save = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date="2020-07-01", + opening_accumulated_depreciation=10000, + number_of_depreciations_booked=5, + do_not_save=1, ) self.assertRaises(frappe.ValidationError, asset.save) # number_of_depreciations_booked = total_number_of_depreciations asset_2 = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - total_number_of_depreciations = 5, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2020-07-01", - opening_accumulated_depreciation = 10000, - number_of_depreciations_booked = 5, - do_not_save = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + total_number_of_depreciations=5, + expected_value_after_useful_life=10000, + depreciation_start_date="2020-07-01", + opening_accumulated_depreciation=10000, + number_of_depreciations_booked=5, + do_not_save=1, ) self.assertRaises(frappe.ValidationError, asset_2.save) def test_depreciation_start_date_is_before_purchase_date(self): asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2014-07-01", - do_not_save = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date="2014-07-01", + do_not_save=1, ) self.assertRaises(frappe.ValidationError, asset.save) def test_depreciation_start_date_is_before_available_for_use_date(self): asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - depreciation_start_date = "2018-07-01", - do_not_save = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date="2018-07-01", + do_not_save=1, ) self.assertRaises(frappe.ValidationError, asset.save) @@ -888,14 +968,14 @@ class TestDepreciationBasics(AssetSetup): """Tests if post_depreciation_entries() works as expected.""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - depreciation_start_date = "2020-12-31", - frequency_of_depreciation = 12, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - submit = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + depreciation_start_date="2020-12-31", + frequency_of_depreciation=12, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + submit=1, ) post_depreciation_entries(date="2021-06-01") @@ -909,21 +989,24 @@ class TestDepreciationBasics(AssetSetup): """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account.""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - depreciation_start_date = "2020-12-31", - frequency_of_depreciation = 12, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - submit = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + depreciation_start_date="2020-12-31", + frequency_of_depreciation=12, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + submit=1, ) post_depreciation_entries(date="2021-06-01") asset.load_from_db() je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) - accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + accounting_entries = [ + {"account": entry.account, "debit": entry.debit, "credit": entry.credit} + for entry in je.accounts + ] for entry in accounting_entries: if entry["account"] == "_Test Depreciations - _TC": @@ -942,21 +1025,24 @@ class TestDepreciationBasics(AssetSetup): depr_expense_account.save() asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - depreciation_start_date = "2020-12-31", - frequency_of_depreciation = 12, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - submit = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + depreciation_start_date="2020-12-31", + frequency_of_depreciation=12, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + submit=1, ) post_depreciation_entries(date="2021-06-01") asset.load_from_db() je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) - accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + accounting_entries = [ + {"account": entry.account, "debit": entry.debit, "credit": entry.credit} + for entry in je.accounts + ] for entry in accounting_entries: if entry["account"] == "_Test Depreciations - _TC": @@ -975,14 +1061,14 @@ class TestDepreciationBasics(AssetSetup): """Tests if clear_depreciation_schedule() works as expected.""" asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2019-12-31", - depreciation_start_date = "2020-12-31", - frequency_of_depreciation = 12, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - submit = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2019-12-31", + depreciation_start_date="2020-12-31", + frequency_of_depreciation=12, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + submit=1, ) post_depreciation_entries(date="2021-06-01") @@ -993,34 +1079,39 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) def test_clear_depreciation_schedule_for_multiple_finance_books(self): - asset = create_asset( - item_code = "Macbook Pro", - available_for_use_date = "2019-12-31", - do_not_save = 1 - ) + asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1) asset.calculate_depreciation = 1 - asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 1, - "total_number_of_depreciations": 3, - "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-01-31" - }) - asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 1, - "total_number_of_depreciations": 6, - "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-01-31" - }) - asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, - "total_number_of_depreciations": 3, - "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" - }) + asset.append( + "finance_books", + { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 1, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-01-31", + }, + ) + asset.append( + "finance_books", + { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 1, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-01-31", + }, + ) + asset.append( + "finance_books", + { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31", + }, + ) asset.submit() post_depreciation_entries(date="2020-04-01") @@ -1037,27 +1128,29 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(schedule.finance_book_id, "2") def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): - asset = create_asset( - item_code = "Macbook Pro", - available_for_use_date = "2019-12-31", - do_not_save = 1 - ) + asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1) asset.calculate_depreciation = 1 - asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, - "total_number_of_depreciations": 3, - "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" - }) - asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, - "total_number_of_depreciations": 6, - "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" - }) + asset.append( + "finance_books", + { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31", + }, + ) + asset.append( + "finance_books", + { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31", + }, + ) asset.save() self.assertEqual(len(asset.schedules), 9) @@ -1070,15 +1163,15 @@ class TestDepreciationBasics(AssetSetup): def test_depreciation_entry_cancellation(self): asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - purchase_date = "2020-06-06", - available_for_use_date = "2020-06-06", - depreciation_start_date = "2020-12-31", - frequency_of_depreciation = 10, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - submit = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + purchase_date="2020-06-06", + available_for_use_date="2020-06-06", + depreciation_start_date="2020-12-31", + frequency_of_depreciation=10, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + submit=1, ) post_depreciation_entries(date="2021-01-01") @@ -1096,34 +1189,38 @@ class TestDepreciationBasics(AssetSetup): def test_asset_expected_value_after_useful_life(self): asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - available_for_use_date = "2020-06-06", - purchase_date = "2020-06-06", - frequency_of_depreciation = 10, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000 + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date="2020-06-06", + purchase_date="2020-06-06", + frequency_of_depreciation=10, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, ) - accumulated_depreciation_after_full_schedule = \ - max(d.accumulated_depreciation_amount for d in asset.get("schedules")) + accumulated_depreciation_after_full_schedule = max( + d.accumulated_depreciation_amount for d in asset.get("schedules") + ) - asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - - flt(accumulated_depreciation_after_full_schedule)) + asset_value_after_full_schedule = flt(asset.gross_purchase_amount) - flt( + accumulated_depreciation_after_full_schedule + ) - self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule) + self.assertTrue( + asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule + ) def test_gle_made_by_depreciation_entries(self): asset = create_asset( - item_code = "Macbook Pro", - calculate_depreciation = 1, - purchase_date = "2020-01-30", - available_for_use_date = "2020-01-30", - depreciation_start_date = "2020-12-31", - frequency_of_depreciation = 10, - total_number_of_depreciations = 3, - expected_value_after_useful_life = 10000, - submit = 1 + item_code="Macbook Pro", + calculate_depreciation=1, + purchase_date="2020-01-30", + available_for_use_date="2020-01-30", + depreciation_start_date="2020-12-31", + frequency_of_depreciation=10, + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + submit=1, ) self.assertEqual(asset.status, "Submitted") @@ -1137,18 +1234,21 @@ class TestDepreciationBasics(AssetSetup): expected_gle = ( ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), - ("_Test Depreciations - _TC", 30000.0, 0.0) + ("_Test Depreciations - _TC", 30000.0, 0.0), ) - gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where against_voucher_type='Asset' and against_voucher = %s - order by account""", asset.name) + order by account""", + asset.name, + ) self.assertEqual(gle, expected_gle) self.assertEqual(asset.get("value_after_depreciation"), 0) def test_asset_cost_center(self): - asset = create_asset(is_existing_asset = 1, do_not_save=1) + asset = create_asset(is_existing_asset=1, do_not_save=1) asset.cost_center = "Main - WP" self.assertRaises(frappe.ValidationError, asset.submit) @@ -1156,6 +1256,7 @@ class TestDepreciationBasics(AssetSetup): asset.cost_center = "Main - _TC" asset.submit() + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): create_asset_category() @@ -1164,44 +1265,47 @@ def create_asset_data(): create_fixed_asset_item() if not frappe.db.exists("Location", "Test Location"): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() + def create_asset(**args): args = frappe._dict(args) create_asset_data() - asset = frappe.get_doc({ - "doctype": "Asset", - "asset_name": args.asset_name or "Macbook Pro 1", - "asset_category": args.asset_category or "Computers", - "item_code": args.item_code or "Macbook Pro", - "company": args.company or "_Test Company", - "purchase_date": args.purchase_date or "2015-01-01", - "calculate_depreciation": args.calculate_depreciation or 0, - "opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0, - "number_of_depreciations_booked": args.number_of_depreciations_booked or 0, - "gross_purchase_amount": args.gross_purchase_amount or 100000, - "purchase_receipt_amount": args.purchase_receipt_amount or 100000, - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "available_for_use_date": args.available_for_use_date or "2020-06-06", - "location": args.location or "Test Location", - "asset_owner": args.asset_owner or "Company", - "is_existing_asset": args.is_existing_asset or 1 - }) + asset = frappe.get_doc( + { + "doctype": "Asset", + "asset_name": args.asset_name or "Macbook Pro 1", + "asset_category": args.asset_category or "Computers", + "item_code": args.item_code or "Macbook Pro", + "company": args.company or "_Test Company", + "purchase_date": args.purchase_date or "2015-01-01", + "calculate_depreciation": args.calculate_depreciation or 0, + "opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0, + "number_of_depreciations_booked": args.number_of_depreciations_booked or 0, + "gross_purchase_amount": args.gross_purchase_amount or 100000, + "purchase_receipt_amount": args.purchase_receipt_amount or 100000, + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "available_for_use_date": args.available_for_use_date or "2020-06-06", + "location": args.location or "Test Location", + "asset_owner": args.asset_owner or "Company", + "is_existing_asset": args.is_existing_asset or 1, + } + ) if asset.calculate_depreciation: - asset.append("finance_books", { - "finance_book": args.finance_book, - "depreciation_method": args.depreciation_method or "Straight Line", - "frequency_of_depreciation": args.frequency_of_depreciation or 12, - "total_number_of_depreciations": args.total_number_of_depreciations or 5, - "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, - "depreciation_start_date": args.depreciation_start_date - }) + asset.append( + "finance_books", + { + "finance_book": args.finance_book, + "depreciation_method": args.depreciation_method or "Straight Line", + "frequency_of_depreciation": args.frequency_of_depreciation or 12, + "total_number_of_depreciations": args.total_number_of_depreciations or 5, + "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, + "depreciation_start_date": args.depreciation_start_date, + }, + ) if not args.do_not_save: try: @@ -1214,40 +1318,48 @@ def create_asset(**args): return asset + def create_asset_category(): asset_category = frappe.new_doc("Asset Category") asset_category.asset_category_name = "Computers" asset_category.total_number_of_depreciations = 3 asset_category.frequency_of_depreciation = 3 asset_category.enable_cwip_accounting = 1 - asset_category.append("accounts", { - "company_name": "_Test Company", - "fixed_asset_account": "_Test Fixed Asset - _TC", - "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", - "depreciation_expense_account": "_Test Depreciations - _TC" - }) + asset_category.append( + "accounts", + { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC", + }, + ) asset_category.insert() + def create_fixed_asset_item(): - meta = frappe.get_meta('Asset') - naming_series = meta.get_field("naming_series").options.splitlines()[0] or 'ACC-ASS-.YYYY.-' + meta = frappe.get_meta("Asset") + naming_series = meta.get_field("naming_series").options.splitlines()[0] or "ACC-ASS-.YYYY.-" try: - frappe.get_doc({ - "doctype": "Item", - "item_code": "Macbook Pro", - "item_name": "Macbook Pro", - "description": "Macbook Pro Retina Display", - "asset_category": "Computers", - "item_group": "All Item Groups", - "stock_uom": "Nos", - "is_stock_item": 0, - "is_fixed_asset": 1, - "auto_create_assets": 1, - "asset_naming_series": naming_series - }).insert() + frappe.get_doc( + { + "doctype": "Item", + "item_code": "Macbook Pro", + "item_name": "Macbook Pro", + "description": "Macbook Pro Retina Display", + "asset_category": "Computers", + "item_group": "All Item Groups", + "stock_uom": "Nos", + "is_stock_item": 0, + "is_fixed_asset": 1, + "auto_create_assets": 1, + "asset_naming_series": naming_series, + } + ).insert() except frappe.DuplicateEntryError: pass + def set_depreciation_settings_in_company(): company = frappe.get_doc("Company", "_Test Company") company.accumulated_depreciation_account = "_Test Accumulated Depreciations - _TC" @@ -1259,5 +1371,6 @@ def set_depreciation_settings_in_company(): # Enable booking asset depreciation entry automatically frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) + def enable_cwip_accounting(asset_category, enable=1): frappe.db.set_value("Asset Category", asset_category, "enable_cwip_accounting", enable) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index bd573bf479d..7291daf2b33 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -18,79 +18,104 @@ class AssetCategory(Document): def validate_finance_books(self): for d in self.finance_books: for field in ("Total Number of Depreciations", "Frequency of Depreciation"): - if cint(d.get(frappe.scrub(field)))<1: - frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError) + if cint(d.get(frappe.scrub(field))) < 1: + frappe.throw( + _("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError + ) def validate_account_currency(self): account_types = [ - 'fixed_asset_account', 'accumulated_depreciation_account', 'depreciation_expense_account', 'capital_work_in_progress_account' + "fixed_asset_account", + "accumulated_depreciation_account", + "depreciation_expense_account", + "capital_work_in_progress_account", ] invalid_accounts = [] for d in self.accounts: - company_currency = frappe.get_value('Company', d.get('company_name'), 'default_currency') + company_currency = frappe.get_value("Company", d.get("company_name"), "default_currency") for type_of_account in account_types: if d.get(type_of_account): account_currency = frappe.get_value("Account", d.get(type_of_account), "account_currency") if account_currency != company_currency: - invalid_accounts.append(frappe._dict({ 'type': type_of_account, 'idx': d.idx, 'account': d.get(type_of_account) })) + invalid_accounts.append( + frappe._dict({"type": type_of_account, "idx": d.idx, "account": d.get(type_of_account)}) + ) for d in invalid_accounts: - frappe.throw(_("Row #{}: Currency of {} - {} doesn't matches company currency.") - .format(d.idx, frappe.bold(frappe.unscrub(d.type)), frappe.bold(d.account)), - title=_("Invalid Account")) - + frappe.throw( + _("Row #{}: Currency of {} - {} doesn't matches company currency.").format( + d.idx, frappe.bold(frappe.unscrub(d.type)), frappe.bold(d.account) + ), + title=_("Invalid Account"), + ) def validate_account_types(self): account_type_map = { - 'fixed_asset_account': {'account_type': ['Fixed Asset']}, - 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']}, - 'depreciation_expense_account': {'root_type': ['Expense', 'Income']}, - 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']} + "fixed_asset_account": {"account_type": ["Fixed Asset"]}, + "accumulated_depreciation_account": {"account_type": ["Accumulated Depreciation"]}, + "depreciation_expense_account": {"root_type": ["Expense", "Income"]}, + "capital_work_in_progress_account": {"account_type": ["Capital Work in Progress"]}, } for d in self.accounts: for fieldname in account_type_map.keys(): if d.get(fieldname): selected_account = d.get(fieldname) - key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type - selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) + key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type + selected_key_type = frappe.db.get_value("Account", selected_account, key_to_match) expected_key_types = account_type_map[fieldname][key_to_match] if selected_key_type not in expected_key_types: - frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") - .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)), - title=_("Invalid Account")) + frappe.throw( + _( + "Row #{}: {} of {} should be {}. Please modify the account or select a different account." + ).format( + d.idx, + frappe.unscrub(key_to_match), + frappe.bold(selected_account), + frappe.bold(expected_key_types), + ), + title=_("Invalid Account"), + ) def valide_cwip_account(self): if self.enable_cwip_accounting: missing_cwip_accounts_for_company = [] for d in self.accounts: - if (not d.capital_work_in_progress_account and - not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")): + if not d.capital_work_in_progress_account and not frappe.db.get_value( + "Company", d.company_name, "capital_work_in_progress_account" + ): missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name)) if missing_cwip_accounts_for_company: msg = _("""To enable Capital Work in Progress Accounting, """) msg += _("""you must select Capital Work in Progress Account in accounts table""") msg += "

" - msg += _("You can also set default CWIP account in Company {}").format(", ".join(missing_cwip_accounts_for_company)) + msg += _("You can also set default CWIP account in Company {}").format( + ", ".join(missing_cwip_accounts_for_company) + ) frappe.throw(msg, title=_("Missing Account")) @frappe.whitelist() -def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None): +def get_asset_category_account( + fieldname, item=None, asset=None, account=None, asset_category=None, company=None +): if item and frappe.db.get_value("Item", item, "is_fixed_asset"): asset_category = frappe.db.get_value("Item", item, ["asset_category"]) elif not asset_category or not company: if account: if frappe.db.get_value("Account", account, "account_type") != "Fixed Asset": - account=None + account = None if not account: asset_details = frappe.db.get_value("Asset", asset, ["asset_category", "company"]) asset_category, company = asset_details or [None, None] - account = frappe.db.get_value("Asset Category Account", - filters={"parent": asset_category, "company_name": company}, fieldname=fieldname) + account = frappe.db.get_value( + "Asset Category Account", + filters={"parent": asset_category, "company_name": company}, + fieldname=fieldname, + ) return account diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py index 3d19fa39d1e..e221bfabb01 100644 --- a/erpnext/assets/doctype/asset_category/test_asset_category.py +++ b/erpnext/assets/doctype/asset_category/test_asset_category.py @@ -15,12 +15,15 @@ class TestAssetCategory(unittest.TestCase): asset_category.total_number_of_depreciations = 3 asset_category.frequency_of_depreciation = 3 - asset_category.append("accounts", { - "company_name": "_Test Company", - "fixed_asset_account": "_Test Fixed Asset - _TC", - "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", - "depreciation_expense_account": "_Test Depreciations - _TC" - }) + asset_category.append( + "accounts", + { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC", + }, + ) try: asset_category.insert() @@ -28,7 +31,9 @@ class TestAssetCategory(unittest.TestCase): pass def test_cwip_accounting(self): - company_cwip_acc = frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account") + company_cwip_acc = frappe.db.get_value( + "Company", "_Test Company", "capital_work_in_progress_account" + ) frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "") asset_category = frappe.new_doc("Asset Category") @@ -37,11 +42,14 @@ class TestAssetCategory(unittest.TestCase): asset_category.total_number_of_depreciations = 3 asset_category.frequency_of_depreciation = 3 - asset_category.append("accounts", { - "company_name": "_Test Company", - "fixed_asset_account": "_Test Fixed Asset - _TC", - "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", - "depreciation_expense_account": "_Test Depreciations - _TC" - }) + asset_category.append( + "accounts", + { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC", + }, + ) self.assertRaises(frappe.ValidationError, asset_category.insert) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 52996e93475..5c03b98873b 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -48,7 +48,7 @@ frappe.ui.form.on('Asset Maintenance', {
- ${d.maintenance_status} ${d.count} + ${__(d.maintenance_status)} ${d.count}
`).appendTo(rows); diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 4fc4c4cb993..e603d346266 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -11,7 +11,7 @@ from frappe.utils import add_days, add_months, add_years, getdate, nowdate class AssetMaintenance(Document): def validate(self): - for task in self.get('asset_maintenance_tasks'): + for task in self.get("asset_maintenance_tasks"): if task.end_date and (getdate(task.start_date) >= getdate(task.end_date)): throw(_("Start date should be less than end date for task {0}").format(task.maintenance_task)) if getdate(task.next_due_date) < getdate(nowdate()): @@ -20,83 +20,109 @@ class AssetMaintenance(Document): throw(_("Row #{}: Please asign task to a member.").format(task.idx)) def on_update(self): - for task in self.get('asset_maintenance_tasks'): + for task in self.get("asset_maintenance_tasks"): assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) self.sync_maintenance_tasks() def sync_maintenance_tasks(self): tasks_names = [] - for task in self.get('asset_maintenance_tasks'): + for task in self.get("asset_maintenance_tasks"): tasks_names.append(task.name) - update_maintenance_log(asset_maintenance = self.name, item_code = self.item_code, item_name = self.item_name, task = task) - asset_maintenance_logs = frappe.get_all("Asset Maintenance Log", fields=["name"], filters = {"asset_maintenance": self.name, - "task": ("not in", tasks_names)}) + update_maintenance_log( + asset_maintenance=self.name, item_code=self.item_code, item_name=self.item_name, task=task + ) + asset_maintenance_logs = frappe.get_all( + "Asset Maintenance Log", + fields=["name"], + filters={"asset_maintenance": self.name, "task": ("not in", tasks_names)}, + ) if asset_maintenance_logs: for asset_maintenance_log in asset_maintenance_logs: - maintenance_log = frappe.get_doc('Asset Maintenance Log', asset_maintenance_log.name) - maintenance_log.db_set('maintenance_status', 'Cancelled') + maintenance_log = frappe.get_doc("Asset Maintenance Log", asset_maintenance_log.name) + maintenance_log.db_set("maintenance_status", "Cancelled") + @frappe.whitelist() def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, next_due_date): - team_member = frappe.db.get_value('User', assign_to_member, "email") + team_member = frappe.db.get_value("User", assign_to_member, "email") args = { - 'doctype' : 'Asset Maintenance', - 'assign_to' : [team_member], - 'name' : asset_maintenance_name, - 'description' : maintenance_task, - 'date' : next_due_date + "doctype": "Asset Maintenance", + "assign_to": [team_member], + "name": asset_maintenance_name, + "description": maintenance_task, + "date": next_due_date, } - if not frappe.db.sql("""select owner from `tabToDo` + if not frappe.db.sql( + """select owner from `tabToDo` where reference_type=%(doctype)s and reference_name=%(name)s and status="Open" - and owner=%(assign_to)s""", args): + and owner=%(assign_to)s""", + args, + ): assign_to.add(args) + @frappe.whitelist() -def calculate_next_due_date(periodicity, start_date = None, end_date = None, last_completion_date = None, next_due_date = None): +def calculate_next_due_date( + periodicity, start_date=None, end_date=None, last_completion_date=None, next_due_date=None +): if not start_date and not last_completion_date: start_date = frappe.utils.now() - if last_completion_date and ((start_date and last_completion_date > start_date) or not start_date): + if last_completion_date and ( + (start_date and last_completion_date > start_date) or not start_date + ): start_date = last_completion_date - if periodicity == 'Daily': + if periodicity == "Daily": next_due_date = add_days(start_date, 1) - if periodicity == 'Weekly': + if periodicity == "Weekly": next_due_date = add_days(start_date, 7) - if periodicity == 'Monthly': + if periodicity == "Monthly": next_due_date = add_months(start_date, 1) - if periodicity == 'Yearly': + if periodicity == "Yearly": next_due_date = add_years(start_date, 1) - if periodicity == '2 Yearly': + if periodicity == "2 Yearly": next_due_date = add_years(start_date, 2) - if periodicity == 'Quarterly': + if periodicity == "Quarterly": next_due_date = add_months(start_date, 3) - if end_date and ((start_date and start_date >= end_date) or (last_completion_date and last_completion_date >= end_date) or next_due_date): + if end_date and ( + (start_date and start_date >= end_date) + or (last_completion_date and last_completion_date >= end_date) + or next_due_date + ): next_due_date = "" return next_due_date def update_maintenance_log(asset_maintenance, item_code, item_name, task): - asset_maintenance_log = frappe.get_value("Asset Maintenance Log", {"asset_maintenance": asset_maintenance, - "task": task.name, "maintenance_status": ('in',['Planned','Overdue'])}) + asset_maintenance_log = frappe.get_value( + "Asset Maintenance Log", + { + "asset_maintenance": asset_maintenance, + "task": task.name, + "maintenance_status": ("in", ["Planned", "Overdue"]), + }, + ) if not asset_maintenance_log: - asset_maintenance_log = frappe.get_doc({ - "doctype": "Asset Maintenance Log", - "asset_maintenance": asset_maintenance, - "asset_name": asset_maintenance, - "item_code": item_code, - "item_name": item_name, - "task": task.name, - "has_certificate": task.certificate_required, - "description": task.description, - "assign_to_name": task.assign_to_name, - "periodicity": str(task.periodicity), - "maintenance_type": task.maintenance_type, - "due_date": task.next_due_date - }) + asset_maintenance_log = frappe.get_doc( + { + "doctype": "Asset Maintenance Log", + "asset_maintenance": asset_maintenance, + "asset_name": asset_maintenance, + "item_code": item_code, + "item_name": item_name, + "task": task.name, + "has_certificate": task.certificate_required, + "description": task.description, + "assign_to_name": task.assign_to_name, + "periodicity": str(task.periodicity), + "maintenance_type": task.maintenance_type, + "due_date": task.next_due_date, + } + ) asset_maintenance_log.insert() else: - maintenance_log = frappe.get_doc('Asset Maintenance Log', asset_maintenance_log) + maintenance_log = frappe.get_doc("Asset Maintenance Log", asset_maintenance_log) maintenance_log.assign_to_name = task.assign_to_name maintenance_log.has_certificate = task.certificate_required maintenance_log.description = task.description @@ -105,15 +131,22 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): maintenance_log.due_date = task.next_due_date maintenance_log.save() + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_team_members(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }, "team_member") + return frappe.db.get_values( + "Maintenance Team Member", {"parent": filters.get("maintenance_team")}, "team_member" + ) + @frappe.whitelist() def get_maintenance_log(asset_name): - return frappe.db.sql(""" + return frappe.db.sql( + """ select maintenance_status, count(asset_name) as count, asset_name from `tabAsset Maintenance Log` where asset_name=%s group by maintenance_status""", - (asset_name), as_dict=1) + (asset_name), + as_dict=1, + ) diff --git a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py index 8acb61b9671..e40a5519eb2 100644 --- a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py @@ -17,11 +17,12 @@ class TestAssetMaintenance(unittest.TestCase): create_maintenance_team() def test_create_asset_maintenance(self): - pr = make_purchase_receipt(item_code="Photocopier", - qty=1, rate=100000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Photocopier", qty=1, rate=100000.0, location="Test Location" + ) - asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - asset_doc = frappe.get_doc('Asset', asset_name) + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset_doc = frappe.get_doc("Asset", asset_name) month_end_date = get_last_day(nowdate()) purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) @@ -30,66 +31,74 @@ class TestAssetMaintenance(unittest.TestCase): asset_doc.purchase_date = purchase_date asset_doc.calculate_depreciation = 1 - asset_doc.append("finance_books", { - "expected_value_after_useful_life": 200, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date - }) + asset_doc.append( + "finance_books", + { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date, + }, + ) asset_doc.save() if not frappe.db.exists("Asset Maintenance", "Photocopier"): - asset_maintenance = frappe.get_doc({ + asset_maintenance = frappe.get_doc( + { "doctype": "Asset Maintenance", "asset_name": "Photocopier", "maintenance_team": "Team Awesome", "company": "_Test Company", - "asset_maintenance_tasks": get_maintenance_tasks() - }).insert() + "asset_maintenance_tasks": get_maintenance_tasks(), + } + ).insert() next_due_date = calculate_next_due_date(nowdate(), "Monthly") self.assertEqual(asset_maintenance.asset_maintenance_tasks[0].next_due_date, next_due_date) def test_create_asset_maintenance_log(self): if not frappe.db.exists("Asset Maintenance Log", "Photocopier"): - asset_maintenance_log = frappe.get_doc({ + asset_maintenance_log = frappe.get_doc( + { "doctype": "Asset Maintenance Log", "asset_maintenance": "Photocopier", "task": "Change Oil", "completion_date": add_days(nowdate(), 2), - "maintenance_status": "Completed" - }).insert() + "maintenance_status": "Completed", + } + ).insert() asset_maintenance = frappe.get_doc("Asset Maintenance", "Photocopier") next_due_date = calculate_next_due_date(asset_maintenance_log.completion_date, "Monthly") self.assertEqual(asset_maintenance.asset_maintenance_tasks[0].next_due_date, next_due_date) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Equipment"): create_asset_category() if not frappe.db.exists("Location", "Test Location"): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() if not frappe.db.exists("Item", "Photocopier"): - meta = frappe.get_meta('Asset') + meta = frappe.get_meta("Asset") naming_series = meta.get_field("naming_series").options - frappe.get_doc({ - "doctype": "Item", - "item_code": "Photocopier", - "item_name": "Photocopier", - "item_group": "All Item Groups", - "company": "_Test Company", - "is_fixed_asset": 1, - "is_stock_item": 0, - "asset_category": "Equipment", - "auto_create_assets": 1, - "asset_naming_series": naming_series - }).insert() + frappe.get_doc( + { + "doctype": "Item", + "item_code": "Photocopier", + "item_name": "Photocopier", + "item_group": "All Item Groups", + "company": "_Test Company", + "is_fixed_asset": 1, + "is_stock_item": 0, + "asset_category": "Equipment", + "auto_create_assets": 1, + "asset_naming_series": naming_series, + } + ).insert() + def create_maintenance_team(): user_list = ["marcus@abc.com", "thalia@abc.com", "mathias@abc.com"] @@ -97,60 +106,73 @@ def create_maintenance_team(): frappe.get_doc({"doctype": "Role", "role_name": "Technician"}).insert() for user in user_list: if not frappe.db.get_value("User", user): - frappe.get_doc({ - "doctype": "User", - "email": user, - "first_name": user, - "new_password": "password", - "roles": [{"doctype": "Has Role", "role": "Technician"}] - }).insert() + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": user, + "new_password": "password", + "roles": [{"doctype": "Has Role", "role": "Technician"}], + } + ).insert() if not frappe.db.exists("Asset Maintenance Team", "Team Awesome"): - frappe.get_doc({ - "doctype": "Asset Maintenance Team", - "maintenance_manager": "marcus@abc.com", - "maintenance_team_name": "Team Awesome", - "company": "_Test Company", - "maintenance_team_members": get_maintenance_team(user_list) - }).insert() + frappe.get_doc( + { + "doctype": "Asset Maintenance Team", + "maintenance_manager": "marcus@abc.com", + "maintenance_team_name": "Team Awesome", + "company": "_Test Company", + "maintenance_team_members": get_maintenance_team(user_list), + } + ).insert() + def get_maintenance_team(user_list): - return [{"team_member": user, - "full_name": user, - "maintenance_role": "Technician" - } - for user in user_list[1:]] + return [ + {"team_member": user, "full_name": user, "maintenance_role": "Technician"} + for user in user_list[1:] + ] + def get_maintenance_tasks(): - return [{"maintenance_task": "Change Oil", + return [ + { + "maintenance_task": "Change Oil", "start_date": nowdate(), "periodicity": "Monthly", "maintenance_type": "Preventive Maintenance", "maintenance_status": "Planned", - "assign_to": "marcus@abc.com" - }, - {"maintenance_task": "Check Gears", + "assign_to": "marcus@abc.com", + }, + { + "maintenance_task": "Check Gears", "start_date": nowdate(), "periodicity": "Yearly", "maintenance_type": "Calibration", "maintenance_status": "Planned", - "assign_to": "thalia@abc.com" - } - ] + "assign_to": "thalia@abc.com", + }, + ] + def create_asset_category(): asset_category = frappe.new_doc("Asset Category") asset_category.asset_category_name = "Equipment" asset_category.total_number_of_depreciations = 3 asset_category.frequency_of_depreciation = 3 - asset_category.append("accounts", { - "company_name": "_Test Company", - "fixed_asset_account": "_Test Fixed Asset - _TC", - "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", - "depreciation_expense_account": "_Test Depreciations - _TC" - }) + asset_category.append( + "accounts", + { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC", + }, + ) asset_category.insert() + def set_depreciation_settings_in_company(): company = frappe.get_doc("Company", "_Test Company") company.accumulated_depreciation_account = "_Test Accumulated Depreciations - _TC" diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py index 7d3453fc982..ff791b27549 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py @@ -12,7 +12,10 @@ from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate class AssetMaintenanceLog(Document): def validate(self): - if getdate(self.due_date) < getdate(nowdate()) and self.maintenance_status not in ["Completed", "Cancelled"]: + if getdate(self.due_date) < getdate(nowdate()) and self.maintenance_status not in [ + "Completed", + "Cancelled", + ]: self.maintenance_status = "Overdue" if self.maintenance_status == "Completed" and not self.completion_date: @@ -22,15 +25,17 @@ class AssetMaintenanceLog(Document): frappe.throw(_("Please select Maintenance Status as Completed or remove Completion Date")) def on_submit(self): - if self.maintenance_status not in ['Completed', 'Cancelled']: + if self.maintenance_status not in ["Completed", "Cancelled"]: frappe.throw(_("Maintenance Status has to be Cancelled or Completed to Submit")) self.update_maintenance_task() def update_maintenance_task(self): - asset_maintenance_doc = frappe.get_doc('Asset Maintenance Task', self.task) + asset_maintenance_doc = frappe.get_doc("Asset Maintenance Task", self.task) if self.maintenance_status == "Completed": if asset_maintenance_doc.last_completion_date != self.completion_date: - next_due_date = calculate_next_due_date(periodicity = self.periodicity, last_completion_date = self.completion_date) + next_due_date = calculate_next_due_date( + periodicity=self.periodicity, last_completion_date=self.completion_date + ) asset_maintenance_doc.last_completion_date = self.completion_date asset_maintenance_doc.next_due_date = next_due_date asset_maintenance_doc.maintenance_status = "Planned" @@ -38,11 +43,14 @@ class AssetMaintenanceLog(Document): if self.maintenance_status == "Cancelled": asset_maintenance_doc.maintenance_status = "Cancelled" asset_maintenance_doc.save() - asset_maintenance_doc = frappe.get_doc('Asset Maintenance', self.asset_maintenance) + asset_maintenance_doc = frappe.get_doc("Asset Maintenance", self.asset_maintenance) asset_maintenance_doc.save() + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_maintenance_tasks(doctype, txt, searchfield, start, page_len, filters): - asset_maintenance_tasks = frappe.db.get_values('Asset Maintenance Task', {'parent':filters.get("asset_maintenance")}, 'maintenance_task') + asset_maintenance_tasks = frappe.db.get_values( + "Asset Maintenance Task", {"parent": filters.get("asset_maintenance")}, "maintenance_task" + ) return asset_maintenance_tasks diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 07bea616da6..e61efadb123 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -16,7 +16,7 @@ class AssetMovement(Document): def validate_asset(self): for d in self.assets: status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"]) - if self.purpose == 'Transfer' and status in ("Draft", "Scrapped", "Sold"): + if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"): frappe.throw(_("{0} asset cannot be transferred").format(status)) if company != self.company: @@ -27,7 +27,7 @@ class AssetMovement(Document): def validate_location(self): for d in self.assets: - if self.purpose in ['Transfer', 'Issue']: + if self.purpose in ["Transfer", "Issue"]: if not d.source_location: d.source_location = frappe.db.get_value("Asset", d.asset, "location") @@ -38,52 +38,76 @@ class AssetMovement(Document): current_location = frappe.db.get_value("Asset", d.asset, "location") if current_location != d.source_location: - frappe.throw(_("Asset {0} does not belongs to the location {1}"). - format(d.asset, d.source_location)) + frappe.throw( + _("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location) + ) - if self.purpose == 'Issue': + if self.purpose == "Issue": if d.target_location: - frappe.throw(_("Issuing cannot be done to a location. \ - Please enter employee who has issued Asset {0}").format(d.asset), title="Incorrect Movement Purpose") + frappe.throw( + _( + "Issuing cannot be done to a location. \ + Please enter employee who has issued Asset {0}" + ).format(d.asset), + title="Incorrect Movement Purpose", + ) if not d.to_employee: frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset)) - if self.purpose == 'Transfer': + if self.purpose == "Transfer": if d.to_employee: - frappe.throw(_("Transferring cannot be done to an Employee. \ - Please enter location where Asset {0} has to be transferred").format( - d.asset), title="Incorrect Movement Purpose") + frappe.throw( + _( + "Transferring cannot be done to an Employee. \ + Please enter location where Asset {0} has to be transferred" + ).format(d.asset), + title="Incorrect Movement Purpose", + ) if not d.target_location: frappe.throw(_("Target Location is required while transferring Asset {0}").format(d.asset)) if d.source_location == d.target_location: frappe.throw(_("Source and Target Location cannot be same")) - if self.purpose == 'Receipt': + if self.purpose == "Receipt": # only when asset is bought and first entry is made if not d.source_location and not (d.target_location or d.to_employee): - frappe.throw(_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset)) + frappe.throw( + _("Target Location or To Employee is required while receiving Asset {0}").format(d.asset) + ) elif d.source_location: # when asset is received from an employee if d.target_location and not d.from_employee: - frappe.throw(_("From employee is required while receiving Asset {0} to a target location").format(d.asset)) + frappe.throw( + _("From employee is required while receiving Asset {0} to a target location").format( + d.asset + ) + ) if d.from_employee and not d.target_location: - frappe.throw(_("Target Location is required while receiving Asset {0} from an employee").format(d.asset)) + frappe.throw( + _("Target Location is required while receiving Asset {0} from an employee").format(d.asset) + ) if d.to_employee and d.target_location: - frappe.throw(_("Asset {0} cannot be received at a location and \ - given to employee in a single movement").format(d.asset)) + frappe.throw( + _( + "Asset {0} cannot be received at a location and \ + given to employee in a single movement" + ).format(d.asset) + ) def validate_employee(self): for d in self.assets: if d.from_employee: - current_custodian = frappe.db.get_value("Asset", d.asset, "custodian") + current_custodian = frappe.db.get_value("Asset", d.asset, "custodian") - if current_custodian != d.from_employee: - frappe.throw(_("Asset {0} does not belongs to the custodian {1}"). - format(d.asset, d.from_employee)) + if current_custodian != d.from_employee: + frappe.throw( + _("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee) + ) if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company: - frappe.throw(_("Employee {0} does not belongs to the company {1}"). - format(d.to_employee, self.company)) + frappe.throw( + _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company) + ) def on_submit(self): self.set_latest_location_in_asset() @@ -92,14 +116,11 @@ class AssetMovement(Document): self.set_latest_location_in_asset() def set_latest_location_in_asset(self): - current_location, current_employee = '', '' + current_location, current_employee = "", "" cond = "1=1" for d in self.assets: - args = { - 'asset': d.asset, - 'company': self.company - } + args = {"asset": d.asset, "company": self.company} # latest entry corresponds to current document's location, employee when transaction date > previous dates # In case of cancellation it corresponds to previous latest document's location, employee @@ -114,10 +135,14 @@ class AssetMovement(Document): asm.docstatus=1 and {0} ORDER BY asm.transaction_date desc limit 1 - """.format(cond), args) + """.format( + cond + ), + args, + ) if latest_movement_entry: current_location = latest_movement_entry[0][0] current_employee = latest_movement_entry[0][1] - frappe.db.set_value('Asset', d.asset, 'location', current_location) - frappe.db.set_value('Asset', d.asset, 'custodian', current_employee) + frappe.db.set_value("Asset", d.asset, "location", current_location) + frappe.db.set_value("Asset", d.asset, "custodian", current_employee) diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 025facc4fda..a5fe52cefa2 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -13,95 +13,122 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu class TestAssetMovement(unittest.TestCase): def setUp(self): - frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC") + frappe.db.set_value( + "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" + ) create_asset_data() make_location() def test_movement(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + ) - asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - asset = frappe.get_doc('Asset', asset_name) + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset = frappe.get_doc("Asset", asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = '2020-06-06' - asset.purchase_date = '2020-06-06' - asset.append("finance_books", { - "expected_value_after_useful_life": 10000, - "next_depreciation_date": "2020-12-31", - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10 - }) + asset.available_for_use_date = "2020-06-06" + asset.purchase_date = "2020-06-06" + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10000, + "next_depreciation_date": "2020-12-31", + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + }, + ) if asset.docstatus == 0: asset.submit() # check asset movement is created if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location 2' - }).insert() + frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company, - assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'target_location': 'Test Location 2'}], - reference_doctype = 'Purchase Receipt', reference_name = pr.name) + movement1 = create_asset_movement( + purpose="Transfer", + company=asset.company, + assets=[ + {"asset": asset.name, "source_location": "Test Location", "target_location": "Test Location 2"} + ], + reference_doctype="Purchase Receipt", + reference_name=pr.name, + ) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") - create_asset_movement(purpose = 'Transfer', company = asset.company, - assets = [{ 'asset': asset.name , 'source_location': 'Test Location 2', 'target_location': 'Test Location'}], - reference_doctype = 'Purchase Receipt', reference_name = pr.name) + create_asset_movement( + purpose="Transfer", + company=asset.company, + assets=[ + {"asset": asset.name, "source_location": "Test Location 2", "target_location": "Test Location"} + ], + reference_doctype="Purchase Receipt", + reference_name=pr.name, + ) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") movement1.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") employee = make_employee("testassetmovemp@example.com", company="_Test Company") - create_asset_movement(purpose = 'Issue', company = asset.company, - assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'to_employee': employee}], - reference_doctype = 'Purchase Receipt', reference_name = pr.name) + create_asset_movement( + purpose="Issue", + company=asset.company, + assets=[{"asset": asset.name, "source_location": "Test Location", "to_employee": employee}], + reference_doctype="Purchase Receipt", + reference_name=pr.name, + ) # after issuing asset should belong to an employee not at a location self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None) self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee) def test_last_movement_cancellation(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + ) - asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - asset = frappe.get_doc('Asset', asset_name) + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset = frappe.get_doc("Asset", asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = '2020-06-06' - asset.purchase_date = '2020-06-06' - asset.append("finance_books", { - "expected_value_after_useful_life": 10000, - "next_depreciation_date": "2020-12-31", - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10 - }) + asset.available_for_use_date = "2020-06-06" + asset.purchase_date = "2020-06-06" + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10000, + "next_depreciation_date": "2020-12-31", + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + }, + ) if asset.docstatus == 0: asset.submit() if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location 2' - }).insert() + frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - movement = frappe.get_doc({'doctype': 'Asset Movement', 'reference_name': pr.name }) + movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name}) self.assertRaises(frappe.ValidationError, movement.cancel) - movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company, - assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'target_location': 'Test Location 2'}], - reference_doctype = 'Purchase Receipt', reference_name = pr.name) + movement1 = create_asset_movement( + purpose="Transfer", + company=asset.company, + assets=[ + {"asset": asset.name, "source_location": "Test Location", "target_location": "Test Location 2"} + ], + reference_doctype="Purchase Receipt", + reference_name=pr.name, + ) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") movement1.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") + def create_asset_movement(**args): args = frappe._dict(args) @@ -109,24 +136,26 @@ def create_asset_movement(**args): args.transaction_date = now() movement = frappe.new_doc("Asset Movement") - movement.update({ - "assets": args.assets, - "transaction_date": args.transaction_date, - "company": args.company, - 'purpose': args.purpose or 'Receipt', - 'reference_doctype': args.reference_doctype, - 'reference_name': args.reference_name - }) + movement.update( + { + "assets": args.assets, + "transaction_date": args.transaction_date, + "company": args.company, + "purpose": args.purpose or "Receipt", + "reference_doctype": args.reference_doctype, + "reference_name": args.reference_name, + } + ) movement.insert() movement.submit() return movement + def make_location(): - for location in ['Pune', 'Mumbai', 'Nagpur']: - if not frappe.db.exists('Location', location): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': location - }).insert(ignore_permissions = True) + for location in ["Pune", "Mumbai", "Nagpur"]: + if not frappe.db.exists("Location", location): + frappe.get_doc({"doctype": "Location", "location_name": location}).insert( + ignore_permissions=True + ) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 36848e9f15c..94b4c641333 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -13,21 +13,21 @@ from erpnext.controllers.accounts_controller import AccountsController class AssetRepair(AccountsController): def validate(self): - self.asset_doc = frappe.get_doc('Asset', self.asset) + self.asset_doc = frappe.get_doc("Asset", self.asset) self.update_status() - if self.get('stock_items'): + if self.get("stock_items"): self.set_total_value() self.calculate_total_repair_cost() def update_status(self): - if self.repair_status == 'Pending': - frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') + if self.repair_status == "Pending": + frappe.db.set_value("Asset", self.asset, "status", "Out of Order") else: self.asset_doc.set_status() def set_total_value(self): - for item in self.get('stock_items'): + for item in self.get("stock_items"): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) def calculate_total_repair_cost(self): @@ -39,46 +39,63 @@ class AssetRepair(AccountsController): def before_submit(self): self.check_repair_status() - if self.get('stock_consumption') or self.get('capitalize_repair_cost'): + if self.get("stock_consumption") or self.get("capitalize_repair_cost"): self.increase_asset_value() - if self.get('stock_consumption'): - self.check_for_stock_items_and_warehouse() - self.decrease_stock_quantity() - if self.get('capitalize_repair_cost'): - self.make_gl_entries() - if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: - self.modify_depreciation_schedule() - self.asset_doc.flags.ignore_validate_update_after_submit = True - self.asset_doc.prepare_depreciation_data() - self.asset_doc.save() + if self.get("stock_consumption"): + self.check_for_stock_items_and_warehouse() + self.decrease_stock_quantity() + + if self.get("capitalize_repair_cost"): + self.make_gl_entries() + + if ( + frappe.db.get_value("Asset", self.asset, "calculate_depreciation") + and self.increase_in_asset_life + ): + self.modify_depreciation_schedule() + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() def before_cancel(self): - self.asset_doc = frappe.get_doc('Asset', self.asset) + self.asset_doc = frappe.get_doc("Asset", self.asset) - if self.get('stock_consumption') or self.get('capitalize_repair_cost'): + if self.get("stock_consumption") or self.get("capitalize_repair_cost"): self.decrease_asset_value() - if self.get('stock_consumption'): - self.increase_stock_quantity() - if self.get('capitalize_repair_cost'): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') - self.make_gl_entries(cancel=True) - if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: - self.revert_depreciation_schedule_on_cancellation() - self.asset_doc.flags.ignore_validate_update_after_submit = True - self.asset_doc.prepare_depreciation_data() - self.asset_doc.save() + if self.get("stock_consumption"): + self.increase_stock_quantity() + + if self.get("capitalize_repair_cost"): + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.make_gl_entries(cancel=True) + + if ( + frappe.db.get_value("Asset", self.asset, "calculate_depreciation") + and self.increase_in_asset_life + ): + self.revert_depreciation_schedule_on_cancellation() + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) def check_for_stock_items_and_warehouse(self): - if not self.get('stock_items'): - frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) + if not self.get("stock_items"): + frappe.throw( + _("Please enter Stock Items consumed during the Repair."), title=_("Missing Items") + ) if not self.warehouse: - frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) + frappe.throw( + _("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), + title=_("Missing Warehouse"), + ) def increase_asset_value(self): total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() @@ -102,35 +119,36 @@ class AssetRepair(AccountsController): def get_total_value_of_stock_consumed(self): total_value_of_stock_consumed = 0 - if self.get('stock_consumption'): - for item in self.get('stock_items'): + if self.get("stock_consumption"): + for item in self.get("stock_items"): total_value_of_stock_consumed += item.total_value return total_value_of_stock_consumed def decrease_stock_quantity(self): - stock_entry = frappe.get_doc({ - "doctype": "Stock Entry", - "stock_entry_type": "Material Issue", - "company": self.company - }) + stock_entry = frappe.get_doc( + {"doctype": "Stock Entry", "stock_entry_type": "Material Issue", "company": self.company} + ) - for stock_item in self.get('stock_items'): - stock_entry.append('items', { - "s_warehouse": self.warehouse, - "item_code": stock_item.item_code, - "qty": stock_item.consumed_quantity, - "basic_rate": stock_item.valuation_rate, - "serial_no": stock_item.serial_no - }) + for stock_item in self.get("stock_items"): + stock_entry.append( + "items", + { + "s_warehouse": self.warehouse, + "item_code": stock_item.item_code, + "qty": stock_item.consumed_quantity, + "basic_rate": stock_item.valuation_rate, + "serial_no": stock_item.serial_no, + }, + ) stock_entry.insert() stock_entry.submit() - self.db_set('stock_entry', stock_entry.name) + self.db_set("stock_entry", stock_entry.name) def increase_stock_quantity(self): - stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) + stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) stock_entry.flags.ignore_links = True stock_entry.cancel() @@ -141,63 +159,78 @@ class AssetRepair(AccountsController): def get_gl_entries(self): gl_entries = [] - repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') - fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company) - expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account - - gl_entries.append( - self.get_gl_dict({ - "account": expense_account, - "credit": self.repair_cost, - "credit_in_account_currency": self.repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company - }, item=self) + repair_and_maintenance_account = frappe.db.get_value( + "Company", self.company, "repair_and_maintenance_account" + ) + fixed_asset_account = get_asset_account( + "fixed_asset_account", asset=self.asset, company=self.company + ) + expense_account = ( + frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account ) - if self.get('stock_consumption'): + gl_entries.append( + self.get_gl_dict( + { + "account": expense_account, + "credit": self.repair_cost, + "credit_in_account_currency": self.repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company, + }, + item=self, + ) + ) + + if self.get("stock_consumption"): # creating GL Entries for each row in Stock Items based on the Stock Entry created for it - stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) + stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) for item in stock_entry.items: gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "credit": item.amount, - "credit_in_account_currency": item.amount, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company - }, item=self) + self.get_gl_dict( + { + "account": item.expense_account, + "credit": item.amount, + "credit_in_account_currency": item.amount, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company, + }, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": fixed_asset_account, - "debit": self.total_repair_cost, - "debit_in_account_currency": self.total_repair_cost, - "against": expense_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "against_voucher_type": "Purchase Invoice", - "against_voucher": self.purchase_invoice, - "company": self.company - }, item=self) + self.get_gl_dict( + { + "account": fixed_asset_account, + "debit": self.total_repair_cost, + "debit_in_account_currency": self.total_repair_cost, + "against": expense_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "against_voucher_type": "Purchase Invoice", + "against_voucher": self.purchase_invoice, + "company": self.company, + }, + item=self, + ) ) return gl_entries def modify_depreciation_schedule(self): for row in self.asset_doc.finance_books: - row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation + row.total_number_of_depreciations += self.increase_in_asset_life / row.frequency_of_depreciation self.asset_doc.flags.increase_in_asset_life = False extra_months = self.increase_in_asset_life % row.frequency_of_depreciation @@ -207,26 +240,29 @@ class AssetRepair(AccountsController): # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation def calculate_last_schedule_date(self, asset, row, extra_months): asset.flags.increase_in_asset_life = True - number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ - cint(asset.number_of_depreciations_booked) + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( + asset.number_of_depreciations_booked + ) # the Schedule Date in the final row of the old Depreciation Schedule - last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date # the Schedule Date in the final row of the new Depreciation Schedule asset.to_date = add_months(last_schedule_date, extra_months) # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... - schedule_date = add_months(row.depreciation_start_date, - number_of_pending_depreciations * cint(row.frequency_of_depreciation)) + schedule_date = add_months( + row.depreciation_start_date, + number_of_pending_depreciations * cint(row.frequency_of_depreciation), + ) if asset.to_date > schedule_date: row.total_number_of_depreciations += 1 def revert_depreciation_schedule_on_cancellation(self): for row in self.asset_doc.finance_books: - row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation + row.total_number_of_depreciations -= self.increase_in_asset_life / row.frequency_of_depreciation self.asset_doc.flags.increase_in_asset_life = False extra_months = self.increase_in_asset_life % row.frequency_of_depreciation @@ -235,23 +271,27 @@ class AssetRepair(AccountsController): def calculate_last_schedule_date_before_modification(self, asset, row, extra_months): asset.flags.increase_in_asset_life = True - number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ - cint(asset.number_of_depreciations_booked) + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( + asset.number_of_depreciations_booked + ) # the Schedule Date in the final row of the modified Depreciation Schedule - last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date # the Schedule Date in the final row of the original Depreciation Schedule asset.to_date = add_months(last_schedule_date, -extra_months) # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... - schedule_date = add_months(row.depreciation_start_date, - (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation)) + schedule_date = add_months( + row.depreciation_start_date, + (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation), + ) if asset.to_date < schedule_date: row.total_number_of_depreciations -= 1 + @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 7c0d05748e1..4e7cf78090b 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -25,7 +25,7 @@ class TestAssetRepair(unittest.TestCase): def test_update_status(self): asset = create_asset(submit=1) initial_status = asset.status - asset_repair = create_asset_repair(asset = asset) + asset_repair = create_asset_repair(asset=asset) if asset_repair.repair_status == "Pending": asset.reload() @@ -37,14 +37,14 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(asset_status, initial_status) def test_stock_item_total_value(self): - asset_repair = create_asset_repair(stock_consumption = 1) + asset_repair = create_asset_repair(stock_consumption=1) for item in asset_repair.stock_items: total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) self.assertEqual(item.total_value, total_value) def test_total_repair_cost(self): - asset_repair = create_asset_repair(stock_consumption = 1) + asset_repair = create_asset_repair(stock_consumption=1) total_repair_cost = asset_repair.repair_cost self.assertEqual(total_repair_cost, asset_repair.repair_cost) @@ -54,22 +54,22 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(total_repair_cost, asset_repair.total_repair_cost) def test_repair_status_after_submit(self): - asset_repair = create_asset_repair(submit = 1) + asset_repair = create_asset_repair(submit=1) self.assertNotEqual(asset_repair.repair_status, "Pending") def test_stock_items(self): - asset_repair = create_asset_repair(stock_consumption = 1) + asset_repair = create_asset_repair(stock_consumption=1) self.assertTrue(asset_repair.stock_consumption) self.assertTrue(asset_repair.stock_items) def test_warehouse(self): - asset_repair = create_asset_repair(stock_consumption = 1) + asset_repair = create_asset_repair(stock_consumption=1) self.assertTrue(asset_repair.stock_consumption) self.assertTrue(asset_repair.warehouse) def test_decrease_stock_quantity(self): - asset_repair = create_asset_repair(stock_consumption = 1, submit = 1) - stock_entry = frappe.get_last_doc('Stock Entry') + asset_repair = create_asset_repair(stock_consumption=1, submit=1) + stock_entry = frappe.get_last_doc("Stock Entry") self.assertEqual(stock_entry.stock_entry_type, "Material Issue") self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) @@ -85,58 +85,72 @@ class TestAssetRepair(unittest.TestCase): serial_no = serial_nos.split("\n")[0] # should not raise any error - create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code, - warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1) + create_asset_repair( + stock_consumption=1, + item_code=stock_entry.get("items")[0].item_code, + warehouse="_Test Warehouse - _TC", + serial_no=serial_no, + submit=1, + ) # should raise error - asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC", - item_code = stock_entry.get("items")[0].item_code) + asset_repair = create_asset_repair( + stock_consumption=1, + warehouse="_Test Warehouse - _TC", + item_code=stock_entry.get("items")[0].item_code, + ) asset_repair.repair_status = "Completed" self.assertRaises(SerialNoRequiredError, asset_repair.submit) def test_increase_in_asset_value_due_to_stock_consumption(self): - asset = create_asset(calculate_depreciation = 1, submit=1) + asset = create_asset(calculate_depreciation=1, submit=1) initial_asset_value = get_asset_value(asset) - asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1) + asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1) asset.reload() increase_in_asset_value = get_asset_value(asset) - initial_asset_value self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): - asset = create_asset(calculate_depreciation = 1, submit=1) + asset = create_asset(calculate_depreciation=1, submit=1) initial_asset_value = get_asset_value(asset) - asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) + asset_repair = create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) asset.reload() increase_in_asset_value = get_asset_value(asset) - initial_asset_value self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) def test_purchase_invoice(self): - asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) + asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) self.assertTrue(asset_repair.purchase_invoice) def test_gl_entries(self): - asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) - gl_entry = frappe.get_last_doc('GL Entry') + asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) + gl_entry = frappe.get_last_doc("GL Entry") self.assertEqual(asset_repair.name, gl_entry.voucher_no) def test_increase_in_asset_life(self): - asset = create_asset(calculate_depreciation = 1, submit=1) + asset = create_asset(calculate_depreciation=1, submit=1) initial_num_of_depreciations = num_of_depreciations(asset) - create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) + create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) asset.reload() self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) - self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation) + self.assertEqual( + asset.schedules[-1].accumulated_depreciation_amount, + asset.finance_books[0].value_after_depreciation, + ) + def get_asset_value(asset): return asset.finance_books[0].value_after_depreciation + def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations + def create_asset_repair(**args): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -146,26 +160,33 @@ def create_asset_repair(**args): if args.asset: asset = args.asset else: - asset = create_asset(is_existing_asset = 1, submit=1) + asset = create_asset(is_existing_asset=1, submit=1) asset_repair = frappe.new_doc("Asset Repair") - asset_repair.update({ - "asset": asset.name, - "asset_name": asset.asset_name, - "failure_date": nowdate(), - "description": "Test Description", - "repair_cost": 0, - "company": asset.company - }) + asset_repair.update( + { + "asset": asset.name, + "asset_name": asset.asset_name, + "failure_date": nowdate(), + "description": "Test Description", + "repair_cost": 0, + "company": asset.company, + } + ) if args.stock_consumption: asset_repair.stock_consumption = 1 - asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company) - asset_repair.append("stock_items", { - "item_code": args.item_code or "_Test Stock Item", - "valuation_rate": args.rate if args.get("rate") is not None else 100, - "consumed_quantity": args.qty or 1, - "serial_no": args.serial_no - }) + asset_repair.warehouse = args.warehouse or create_warehouse( + "Test Warehouse", company=asset.company + ) + asset_repair.append( + "stock_items", + { + "item_code": args.item_code or "_Test Stock Item", + "valuation_rate": args.rate if args.get("rate") is not None else 100, + "consumed_quantity": args.qty or 1, + "serial_no": args.serial_no, + }, + ) asset_repair.insert(ignore_if_duplicate=True) @@ -174,16 +195,17 @@ def create_asset_repair(**args): asset_repair.cost_center = "_Test Cost Center - _TC" if args.stock_consumption: - stock_entry = frappe.get_doc({ - "doctype": "Stock Entry", - "stock_entry_type": "Material Receipt", - "company": asset.company - }) - stock_entry.append('items', { - "t_warehouse": asset_repair.warehouse, - "item_code": asset_repair.stock_items[0].item_code, - "qty": asset_repair.stock_items[0].consumed_quantity - }) + stock_entry = frappe.get_doc( + {"doctype": "Stock Entry", "stock_entry_type": "Material Receipt", "company": asset.company} + ) + stock_entry.append( + "items", + { + "t_warehouse": asset_repair.warehouse, + "item_code": asset_repair.stock_items[0].item_code, + "qty": asset_repair.stock_items[0].consumed_quantity, + }, + ) stock_entry.submit() if args.capitalize_repair_cost: diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 0b646ed4ede..9953c61a811 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -31,10 +31,14 @@ class AssetValueAdjustment(Document): self.reschedule_depreciations(self.current_asset_value) def validate_date(self): - asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date') + asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date") if getdate(self.date) < getdate(asset_purchase_date): - frappe.throw(_("Asset Value Adjustment cannot be posted before Asset's purchase date {0}.") - .format(formatdate(asset_purchase_date)), title="Incorrect Date") + frappe.throw( + _("Asset Value Adjustment cannot be posted before Asset's purchase date {0}.").format( + formatdate(asset_purchase_date) + ), + title="Incorrect Date", + ) def set_difference_amount(self): self.difference_amount = flt(self.current_asset_value - self.new_asset_value) @@ -45,11 +49,15 @@ class AssetValueAdjustment(Document): def make_depreciation_entry(self): asset = frappe.get_doc("Asset", self.asset) - fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account = \ - get_depreciation_accounts(asset) + ( + fixed_asset_account, + accumulated_depreciation_account, + depreciation_expense_account, + ) = get_depreciation_accounts(asset) - depreciation_cost_center, depreciation_series = frappe.get_cached_value('Company', asset.company, - ["depreciation_cost_center", "series_for_depreciation_entry"]) + depreciation_cost_center, depreciation_series = frappe.get_cached_value( + "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] + ) je = frappe.new_doc("Journal Entry") je.voucher_type = "Depreciation Entry" @@ -62,27 +70,33 @@ class AssetValueAdjustment(Document): credit_entry = { "account": accumulated_depreciation_account, "credit_in_account_currency": self.difference_amount, - "cost_center": depreciation_cost_center or self.cost_center + "cost_center": depreciation_cost_center or self.cost_center, } debit_entry = { "account": depreciation_expense_account, "debit_in_account_currency": self.difference_amount, - "cost_center": depreciation_cost_center or self.cost_center + "cost_center": depreciation_cost_center or self.cost_center, } accounting_dimensions = get_checks_for_pl_and_bs_accounts() for dimension in accounting_dimensions: - if dimension.get('mandatory_for_bs'): - credit_entry.update({ - dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension') - }) + if dimension.get("mandatory_for_bs"): + credit_entry.update( + { + dimension["fieldname"]: self.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) - if dimension.get('mandatory_for_pl'): - debit_entry.update({ - dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension') - }) + if dimension.get("mandatory_for_pl"): + debit_entry.update( + { + dimension["fieldname"]: self.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) je.append("accounts", credit_entry) je.append("accounts", debit_entry) @@ -93,8 +107,8 @@ class AssetValueAdjustment(Document): self.db_set("journal_entry", je.name) def reschedule_depreciations(self, asset_value): - asset = frappe.get_doc('Asset', self.asset) - country = frappe.get_value('Company', self.company, 'country') + asset = frappe.get_doc("Asset", self.asset) + country = frappe.get_value("Company", self.company, "country") for d in asset.finance_books: d.value_after_depreciation = asset_value @@ -105,8 +119,11 @@ class AssetValueAdjustment(Document): rate_per_day = flt(d.value_after_depreciation) / flt(total_days) from_date = self.date else: - no_of_depreciations = len([s.name for s in asset.schedules - if (cint(s.finance_book_id) == d.idx and not s.journal_entry)]) + no_of_depreciations = len( + [ + s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry) + ] + ) value_after_depreciation = d.value_after_depreciation for data in asset.schedules: @@ -132,10 +149,11 @@ class AssetValueAdjustment(Document): if not asset_data.journal_entry: asset_data.db_update() + @frappe.whitelist() def get_current_asset_value(asset, finance_book=None): - cond = {'parent': asset, 'parenttype': 'Asset'} + cond = {"parent": asset, "parenttype": "Asset"} if finance_book: - cond.update({'finance_book': finance_book}) + cond.update({"finance_book": finance_book}) - return frappe.db.get_value('Asset Finance Book', cond, 'value_after_depreciation') + return frappe.db.get_value("Asset Finance Book", cond, "value_after_depreciation") diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index ef13c5617f5..ebeb174d135 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -18,11 +18,12 @@ class TestAssetValueAdjustment(unittest.TestCase): create_asset_data() def test_current_asset_value(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + ) - asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - asset_doc = frappe.get_doc('Asset', asset_name) + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset_doc = frappe.get_doc("Asset", asset_name) month_end_date = get_last_day(nowdate()) purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) @@ -30,24 +31,28 @@ class TestAssetValueAdjustment(unittest.TestCase): asset_doc.available_for_use_date = purchase_date asset_doc.purchase_date = purchase_date asset_doc.calculate_depreciation = 1 - asset_doc.append("finance_books", { - "expected_value_after_useful_life": 200, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date - }) + asset_doc.append( + "finance_books", + { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date, + }, + ) asset_doc.submit() current_value = get_current_asset_value(asset_doc.name) self.assertEqual(current_value, 100000.0) def test_asset_depreciation_value_adjustment(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.0, location="Test Location") + pr = make_purchase_receipt( + item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + ) - asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - asset_doc = frappe.get_doc('Asset', asset_name) + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") + asset_doc = frappe.get_doc("Asset", asset_name) asset_doc.calculate_depreciation = 1 month_end_date = get_last_day(nowdate()) @@ -56,42 +61,52 @@ class TestAssetValueAdjustment(unittest.TestCase): asset_doc.available_for_use_date = purchase_date asset_doc.purchase_date = purchase_date asset_doc.calculate_depreciation = 1 - asset_doc.append("finance_books", { - "expected_value_after_useful_life": 200, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date - }) + asset_doc.append( + "finance_books", + { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date, + }, + ) asset_doc.submit() current_value = get_current_asset_value(asset_doc.name) - adj_doc = make_asset_value_adjustment(asset = asset_doc.name, - current_asset_value = current_value, new_asset_value = 50000.0) + adj_doc = make_asset_value_adjustment( + asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 + ) adj_doc.submit() expected_gle = ( ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0), - ("_Test Depreciations - _TC", 50000.0, 0.0) + ("_Test Depreciations - _TC", 50000.0, 0.0), ) - gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no = %s - order by account""", adj_doc.journal_entry) + order by account""", + adj_doc.journal_entry, + ) self.assertEqual(gle, expected_gle) + def make_asset_value_adjustment(**args): args = frappe._dict(args) - doc = frappe.get_doc({ - "doctype": "Asset Value Adjustment", - "company": args.company or "_Test Company", - "asset": args.asset, - "date": args.date or nowdate(), - "new_asset_value": args.new_asset_value, - "current_asset_value": args.current_asset_value, - "cost_center": args.cost_center or "Main - _TC" - }).insert() + doc = frappe.get_doc( + { + "doctype": "Asset Value Adjustment", + "company": args.company or "_Test Company", + "asset": args.asset, + "date": args.date or nowdate(), + "new_asset_value": args.new_asset_value, + "current_asset_value": args.current_asset_value, + "cost_center": args.cost_center or "Main - _TC", + } + ).insert() return doc diff --git a/erpnext/assets/doctype/location/location.py b/erpnext/assets/doctype/location/location.py index abc7325cf6c..0d87bb2bf4d 100644 --- a/erpnext/assets/doctype/location/location.py +++ b/erpnext/assets/doctype/location/location.py @@ -13,12 +13,12 @@ EARTH_RADIUS = 6378137 class Location(NestedSet): - nsm_parent_field = 'parent_location' + nsm_parent_field = "parent_location" def validate(self): self.calculate_location_area() - if not self.is_new() and self.get('parent_location'): + if not self.is_new() and self.get("parent_location"): self.update_ancestor_location_features() def on_update(self): @@ -42,7 +42,7 @@ class Location(NestedSet): if not self.location: return [] - features = json.loads(self.location).get('features') + features = json.loads(self.location).get("features") if not isinstance(features, list): features = json.loads(features) @@ -54,15 +54,15 @@ class Location(NestedSet): self.location = '{"type":"FeatureCollection","features":[]}' location = json.loads(self.location) - location['features'] = features + location["features"] = features - self.db_set('location', json.dumps(location), commit=True) + self.db_set("location", json.dumps(location), commit=True) def update_ancestor_location_features(self): self_features = set(self.add_child_property()) for ancestor in self.get_ancestors(): - ancestor_doc = frappe.get_doc('Location', ancestor) + ancestor_doc = frappe.get_doc("Location", ancestor) child_features, ancestor_features = ancestor_doc.feature_seperator(child_feature=self.name) ancestor_features = list(set(ancestor_features)) @@ -84,25 +84,27 @@ class Location(NestedSet): ancestor_features[index] = json.loads(feature) ancestor_doc.set_location_features(features=ancestor_features) - ancestor_doc.db_set('area', ancestor_doc.area + self.area_difference, commit=True) + ancestor_doc.db_set("area", ancestor_doc.area + self.area_difference, commit=True) def remove_ancestor_location_features(self): for ancestor in self.get_ancestors(): - ancestor_doc = frappe.get_doc('Location', ancestor) + ancestor_doc = frappe.get_doc("Location", ancestor) child_features, ancestor_features = ancestor_doc.feature_seperator(child_feature=self.name) for index, feature in enumerate(ancestor_features): ancestor_features[index] = json.loads(feature) ancestor_doc.set_location_features(features=ancestor_features) - ancestor_doc.db_set('area', ancestor_doc.area - self.area, commit=True) + ancestor_doc.db_set("area", ancestor_doc.area - self.area, commit=True) def add_child_property(self): features = self.get_location_features() - filter_features = [feature for feature in features if not feature.get('properties').get('child_feature')] + filter_features = [ + feature for feature in features if not feature.get("properties").get("child_feature") + ] for index, feature in enumerate(filter_features): - feature['properties'].update({'child_feature': True, 'feature_of': self.location_name}) + feature["properties"].update({"child_feature": True, "feature_of": self.location_name}) filter_features[index] = json.dumps(filter_features[index]) return filter_features @@ -112,7 +114,7 @@ class Location(NestedSet): features = self.get_location_features() for feature in features: - if feature.get('properties').get('feature_of') == child_feature: + if feature.get("properties").get("feature_of") == child_feature: child_features.extend([json.dumps(feature)]) else: non_child_features.extend([json.dumps(feature)]) @@ -126,22 +128,22 @@ def compute_area(features): Reference from https://github.com/scisco/area. Args: - `features` (list of dict): Features marked on the map as - GeoJSON data + `features` (list of dict): Features marked on the map as + GeoJSON data Returns: - float: The approximate signed geodesic area (in sq. meters) + float: The approximate signed geodesic area (in sq. meters) """ layer_area = 0.0 for feature in features: - feature_type = feature.get('geometry', {}).get('type') + feature_type = feature.get("geometry", {}).get("type") - if feature_type == 'Polygon': - layer_area += _polygon_area(coords=feature.get('geometry').get('coordinates')) - elif feature_type == 'Point' and feature.get('properties').get('point_type') == 'circle': - layer_area += math.pi * math.pow(feature.get('properties').get('radius'), 2) + if feature_type == "Polygon": + layer_area += _polygon_area(coords=feature.get("geometry").get("coordinates")) + elif feature_type == "Point" and feature.get("properties").get("point_type") == "circle": + layer_area += math.pi * math.pow(feature.get("properties").get("radius"), 2) return layer_area @@ -192,7 +194,8 @@ def get_children(doctype, parent=None, location=None, is_root=False): if parent is None or parent == "All Locations": parent = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name as value, is_group as expandable @@ -201,17 +204,20 @@ def get_children(doctype, parent=None, location=None, is_root=False): where ifnull(parent_location, "")={parent} """.format( - doctype=doctype, - parent=frappe.db.escape(parent) - ), as_dict=1) + doctype=doctype, parent=frappe.db.escape(parent) + ), + as_dict=1, + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = frappe.form_dict args = make_tree_args(**args) - if args.parent_location == 'All Locations': + if args.parent_location == "All Locations": args.parent_location = None frappe.get_doc(args).insert() diff --git a/erpnext/assets/doctype/location/test_location.py b/erpnext/assets/doctype/location/test_location.py index 36e1dd4ce4b..b8563cb0a29 100644 --- a/erpnext/assets/doctype/location/test_location.py +++ b/erpnext/assets/doctype/location/test_location.py @@ -6,29 +6,34 @@ import unittest import frappe -test_records = frappe.get_test_records('Location') +test_records = frappe.get_test_records("Location") + class TestLocation(unittest.TestCase): def runTest(self): - locations = ['Basil Farm', 'Division 1', 'Field 1', 'Block 1'] + locations = ["Basil Farm", "Division 1", "Field 1", "Block 1"] area = 0 formatted_locations = [] for location in locations: - doc = frappe.get_doc('Location', location) + doc = frappe.get_doc("Location", location) doc.save() area += doc.area temp = json.loads(doc.location) - temp['features'][0]['properties']['child_feature'] = True - temp['features'][0]['properties']['feature_of'] = location - formatted_locations.extend(temp['features']) + temp["features"][0]["properties"]["child_feature"] = True + temp["features"][0]["properties"]["feature_of"] = location + formatted_locations.extend(temp["features"]) - test_location = frappe.get_doc('Location', 'Test Location Area') + test_location = frappe.get_doc("Location", "Test Location Area") test_location.save() - test_location_features = json.loads(test_location.get('location'))['features'] - ordered_test_location_features = sorted(test_location_features, key=lambda x: x['properties']['feature_of']) - ordered_formatted_locations = sorted(formatted_locations, key=lambda x: x['properties']['feature_of']) + test_location_features = json.loads(test_location.get("location"))["features"] + ordered_test_location_features = sorted( + test_location_features, key=lambda x: x["properties"]["feature_of"] + ) + ordered_formatted_locations = sorted( + formatted_locations, key=lambda x: x["properties"]["feature_of"] + ) self.assertEqual(ordered_formatted_locations, ordered_test_location_features) - self.assertEqual(area, test_location.get('area')) + self.assertEqual(area, test_location.get("area")) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index db513364f49..6b14dce084e 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -17,16 +17,21 @@ def execute(filters=None): filters = frappe._dict(filters or {}) columns = get_columns(filters) data = get_data(filters) - chart = prepare_chart_data(data, filters) if filters.get("group_by") not in ("Asset Category", "Location") else {} + chart = ( + prepare_chart_data(data, filters) + if filters.get("group_by") not in ("Asset Category", "Location") + else {} + ) return columns, data, None, chart + def get_conditions(filters): - conditions = { 'docstatus': 1 } + conditions = {"docstatus": 1} status = filters.status date_field = frappe.scrub(filters.date_based_on or "Purchase Date") - if filters.get('company'): + if filters.get("company"): conditions["company"] = filters.company if filters.filter_based_on == "Date Range": conditions[date_field] = ["between", [filters.from_date, filters.to_date]] @@ -37,23 +42,24 @@ def get_conditions(filters): filters.year_end_date = getdate(fiscal_year.year_end_date) conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]] - if filters.get('is_existing_asset'): - conditions["is_existing_asset"] = filters.get('is_existing_asset') - if filters.get('asset_category'): - conditions["asset_category"] = filters.get('asset_category') - if filters.get('cost_center'): - conditions["cost_center"] = filters.get('cost_center') + if filters.get("is_existing_asset"): + conditions["is_existing_asset"] = filters.get("is_existing_asset") + if filters.get("asset_category"): + conditions["asset_category"] = filters.get("asset_category") + if filters.get("cost_center"): + conditions["cost_center"] = filters.get("cost_center") if status: # In Store assets are those that are not sold or scrapped - operand = 'not in' - if status not in 'In Location': - operand = 'in' + operand = "not in" + if status not in "In Location": + operand = "in" - conditions['status'] = (operand, ['Sold', 'Scrapped']) + conditions["status"] = (operand, ["Sold", "Scrapped"]) return conditions + def get_data(filters): data = [] @@ -74,21 +80,37 @@ def get_data(filters): assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by) else: - fields = ["name as asset_id", "asset_name", "status", "department", "cost_center", "purchase_receipt", - "asset_category", "purchase_date", "gross_purchase_amount", "location", - "available_for_use_date", "purchase_invoice", "opening_accumulated_depreciation"] + fields = [ + "name as asset_id", + "asset_name", + "status", + "department", + "cost_center", + "purchase_receipt", + "asset_category", + "purchase_date", + "gross_purchase_amount", + "location", + "available_for_use_date", + "purchase_invoice", + "opening_accumulated_depreciation", + ] assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) for asset in assets_record: - asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ + asset_value = ( + asset.gross_purchase_amount + - flt(asset.opening_accumulated_depreciation) - flt(depreciation_amount_map.get(asset.name)) + ) row = { "asset_id": asset.asset_id, "asset_name": asset.asset_name, "status": asset.status, "department": asset.department, "cost_center": asset.cost_center, - "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), + "vendor_name": pr_supplier_map.get(asset.purchase_receipt) + or pi_supplier_map.get(asset.purchase_invoice), "gross_purchase_amount": asset.gross_purchase_amount, "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, @@ -96,21 +118,31 @@ def get_data(filters): "location": asset.location, "asset_category": asset.asset_category, "purchase_date": asset.purchase_date, - "asset_value": asset_value + "asset_value": asset_value, } data.append(row) return data + def prepare_chart_data(data, filters): labels_values_map = {} date_field = frappe.scrub(filters.date_based_on) - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.from_date, filters.to_date, filters.filter_based_on, "Monthly", company=filters.company) + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.from_date, + filters.to_date, + filters.filter_based_on, + "Monthly", + company=filters.company, + ) for d in period_list: - labels_values_map.setdefault(d.get('label'), frappe._dict({'asset_value': 0, 'depreciated_amount': 0})) + labels_values_map.setdefault( + d.get("label"), frappe._dict({"asset_value": 0, "depreciated_amount": 0}) + ) for d in data: date = d.get(date_field) @@ -120,23 +152,30 @@ def prepare_chart_data(data, filters): labels_values_map[belongs_to_month].depreciated_amount += d.get("depreciated_amount") return { - "data" : { + "data": { "labels": labels_values_map.keys(), "datasets": [ - { 'name': _('Asset Value'), 'values': [d.get("asset_value") for d in labels_values_map.values()] }, - { 'name': _('Depreciatied Amount'), 'values': [d.get("depreciated_amount") for d in labels_values_map.values()] } - ] + { + "name": _("Asset Value"), + "values": [d.get("asset_value") for d in labels_values_map.values()], + }, + { + "name": _("Depreciatied Amount"), + "values": [d.get("depreciated_amount") for d in labels_values_map.values()], + }, + ], }, "type": "bar", - "barOptions": { - "stacked": 1 - }, + "barOptions": {"stacked": 1}, } + def get_finance_book_value_map(filters): date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date - return frappe._dict(frappe.db.sql(''' Select + return frappe._dict( + frappe.db.sql( + """ Select parent, SUM(depreciation_amount) FROM `tabDepreciation Schedule` WHERE @@ -144,27 +183,41 @@ def get_finance_book_value_map(filters): AND schedule_date<=%s AND journal_entry IS NOT NULL AND ifnull(finance_book, '')=%s - GROUP BY parent''', (date, cstr(filters.finance_book or '')))) + GROUP BY parent""", + (date, cstr(filters.finance_book or "")), + ) + ) + def get_purchase_receipt_supplier_map(): - return frappe._dict(frappe.db.sql(''' Select + return frappe._dict( + frappe.db.sql( + """ Select pr.name, pr.supplier FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri WHERE pri.parent = pr.name AND pri.is_fixed_asset=1 AND pr.docstatus=1 - AND pr.is_return=0''')) + AND pr.is_return=0""" + ) + ) + def get_purchase_invoice_supplier_map(): - return frappe._dict(frappe.db.sql(''' Select + return frappe._dict( + frappe.db.sql( + """ Select pi.name, pi.supplier FROM `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pii WHERE pii.parent = pi.name AND pii.is_fixed_asset=1 AND pi.docstatus=1 - AND pi.is_return=0''')) + AND pi.is_return=0""" + ) + ) + def get_columns(filters): if filters.get("group_by") in ["Asset Category", "Location"]: @@ -174,36 +227,36 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": frappe.scrub(filters.get("group_by")), "options": filters.get("group_by"), - "width": 120 + "width": 120, }, { "label": _("Gross Purchase Amount"), "fieldname": "gross_purchase_amount", "fieldtype": "Currency", "options": "company:currency", - "width": 100 + "width": 100, }, { "label": _("Opening Accumulated Depreciation"), "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", "options": "company:currency", - "width": 90 + "width": 90, }, { "label": _("Depreciated Amount"), "fieldname": "depreciated_amount", "fieldtype": "Currency", "options": "company:currency", - "width": 100 + "width": 100, }, { "label": _("Asset Value"), "fieldname": "asset_value", "fieldtype": "Currency", "options": "company:currency", - "width": 100 - } + "width": 100, + }, ] return [ @@ -212,92 +265,72 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "asset_id", "options": "Asset", - "width": 60 - }, - { - "label": _("Asset Name"), - "fieldtype": "Data", - "fieldname": "asset_name", - "width": 140 + "width": 60, }, + {"label": _("Asset Name"), "fieldtype": "Data", "fieldname": "asset_name", "width": 140}, { "label": _("Asset Category"), "fieldtype": "Link", "fieldname": "asset_category", "options": "Asset Category", - "width": 100 - }, - { - "label": _("Status"), - "fieldtype": "Data", - "fieldname": "status", - "width": 80 - }, - { - "label": _("Purchase Date"), - "fieldtype": "Date", - "fieldname": "purchase_date", - "width": 90 + "width": 100, }, + {"label": _("Status"), "fieldtype": "Data", "fieldname": "status", "width": 80}, + {"label": _("Purchase Date"), "fieldtype": "Date", "fieldname": "purchase_date", "width": 90}, { "label": _("Available For Use Date"), "fieldtype": "Date", "fieldname": "available_for_use_date", - "width": 90 + "width": 90, }, { "label": _("Gross Purchase Amount"), "fieldname": "gross_purchase_amount", "fieldtype": "Currency", "options": "company:currency", - "width": 100 + "width": 100, }, { "label": _("Asset Value"), "fieldname": "asset_value", "fieldtype": "Currency", "options": "company:currency", - "width": 100 + "width": 100, }, { "label": _("Opening Accumulated Depreciation"), "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", "options": "company:currency", - "width": 90 + "width": 90, }, { "label": _("Depreciated Amount"), "fieldname": "depreciated_amount", "fieldtype": "Currency", "options": "company:currency", - "width": 100 + "width": 100, }, { "label": _("Cost Center"), "fieldtype": "Link", "fieldname": "cost_center", "options": "Cost Center", - "width": 100 + "width": 100, }, { "label": _("Department"), "fieldtype": "Link", "fieldname": "department", "options": "Department", - "width": 100 - }, - { - "label": _("Vendor Name"), - "fieldtype": "Data", - "fieldname": "vendor_name", - "width": 100 + "width": 100, }, + {"label": _("Vendor Name"), "fieldtype": "Data", "fieldname": "vendor_name", "width": 100}, { "label": _("Location"), "fieldtype": "Link", "fieldname": "location", "options": "Location", - "width": 100 + "width": 100, }, ] diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 2b6ff43530f..5507254bbc2 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -14,5 +14,10 @@ class BuyingSettings(Document): frappe.db.set_default(key, self.get(key, "")) from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Supplier", "supplier_name", - self.get("supp_master_name")=="Naming Series", hide_name_field=False) + + set_by_naming_series( + "Supplier", + "supplier_name", + self.get("supp_master_name") == "Naming Series", + hide_name_field=False, + ) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 896208f25e1..239f4988303 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -23,6 +23,10 @@ "order_confirmation_no", "order_confirmation_date", "amended_from", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "drop_ship", "customer", "customer_name", @@ -1138,16 +1142,39 @@ "fieldtype": "Link", "label": "Tax Withholding Category", "options": "Tax Withholding Category" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions " + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:10:47.955401", + "modified": "2022-04-26 12:16:38.694276", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -1194,6 +1221,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "supplier_name", "track_changes": 1 diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 1b5f35efbb4..582bd8d1db8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -26,24 +26,25 @@ from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty from erpnext.stock.utils import get_bin -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class PurchaseOrder(BuyingController): def __init__(self, *args, **kwargs): super(PurchaseOrder, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Purchase Order Item', - 'target_dt': 'Material Request Item', - 'join_field': 'material_request_item', - 'target_field': 'ordered_qty', - 'target_parent_dt': 'Material Request', - 'target_parent_field': 'per_ordered', - 'target_ref_field': 'stock_qty', - 'source_field': 'stock_qty', - 'percent_join_field': 'material_request' - }] + self.status_updater = [ + { + "source_dt": "Purchase Order Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "ordered_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_ordered", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + "percent_join_field": "material_request", + } + ] def onload(self): supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") @@ -71,35 +72,44 @@ class PurchaseOrder(BuyingController): self.validate_bom_for_subcontracting_items() self.create_raw_materials_supplied("supplied_items") self.set_received_qty_for_drop_ship_items() - validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference) + validate_inter_company_party( + self.doctype, self.supplier, self.company, self.inter_company_order_reference + ) self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): - super(PurchaseOrder, self).validate_with_previous_doc({ - "Supplier Quotation": { - "ref_dn_field": "supplier_quotation", - "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], - }, - "Supplier Quotation Item": { - "ref_dn_field": "supplier_quotation_item", - "compare_fields": [["project", "="], ["item_code", "="], - ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True - }, - "Material Request": { - "ref_dn_field": "material_request", - "compare_fields": [["company", "="]], - }, - "Material Request Item": { - "ref_dn_field": "material_request_item", - "compare_fields": [["project", "="], ["item_code", "="]], - "is_child_table": True + super(PurchaseOrder, self).validate_with_previous_doc( + { + "Supplier Quotation": { + "ref_dn_field": "supplier_quotation", + "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], + }, + "Supplier Quotation Item": { + "ref_dn_field": "supplier_quotation_item", + "compare_fields": [ + ["project", "="], + ["item_code", "="], + ["uom", "="], + ["conversion_factor", "="], + ], + "is_child_table": True, + }, + "Material Request": { + "ref_dn_field": "material_request", + "compare_fields": [["company", "="]], + }, + "Material Request Item": { + "ref_dn_field": "material_request_item", + "compare_fields": [["project", "="], ["item_code", "="]], + "is_child_table": True, + }, } - }) + ) - - if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')): - self.validate_rate_with_reference_doc([["Supplier Quotation", "supplier_quotation", "supplier_quotation_item"]]) + if cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")): + self.validate_rate_with_reference_doc( + [["Supplier Quotation", "supplier_quotation", "supplier_quotation_item"]] + ) def set_tax_withholding(self): if not self.apply_tds: @@ -119,8 +129,11 @@ class PurchaseOrder(BuyingController): if not accounts or tax_withholding_details.get("account_head") not in accounts: self.append("taxes", tax_withholding_details) - to_remove = [d for d in self.taxes - if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")] + to_remove = [ + d + for d in self.taxes + if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head") + ] for d in to_remove: self.remove(d) @@ -129,26 +142,43 @@ class PurchaseOrder(BuyingController): self.calculate_taxes_and_totals() def validate_supplier(self): - prevent_po = frappe.db.get_value("Supplier", self.supplier, 'prevent_pos') + prevent_po = frappe.db.get_value("Supplier", self.supplier, "prevent_pos") if prevent_po: - standing = frappe.db.get_value("Supplier Scorecard", self.supplier, 'status') + standing = frappe.db.get_value("Supplier Scorecard", self.supplier, "status") if standing: - frappe.throw(_("Purchase Orders are not allowed for {0} due to a scorecard standing of {1}.") - .format(self.supplier, standing)) + frappe.throw( + _("Purchase Orders are not allowed for {0} due to a scorecard standing of {1}.").format( + self.supplier, standing + ) + ) - warn_po = frappe.db.get_value("Supplier", self.supplier, 'warn_pos') + warn_po = frappe.db.get_value("Supplier", self.supplier, "warn_pos") if warn_po: - standing = frappe.db.get_value("Supplier Scorecard",self.supplier, 'status') - frappe.msgprint(_("{0} currently has a {1} Supplier Scorecard standing, and Purchase Orders to this supplier should be issued with caution.").format(self.supplier, standing), title=_("Caution"), indicator='orange') + standing = frappe.db.get_value("Supplier Scorecard", self.supplier, "status") + frappe.msgprint( + _( + "{0} currently has a {1} Supplier Scorecard standing, and Purchase Orders to this supplier should be issued with caution." + ).format(self.supplier, standing), + title=_("Caution"), + indicator="orange", + ) self.party_account_currency = get_party_account_currency("Supplier", self.supplier, self.company) def validate_minimum_order_qty(self): - if not self.get("items"): return + if not self.get("items"): + return items = list(set(d.item_code for d in self.get("items"))) - itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty - from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items)) + itemwise_min_order_qty = frappe._dict( + frappe.db.sql( + """select name, min_order_qty + from tabItem where name in ({0})""".format( + ", ".join(["%s"] * len(items)) + ), + items, + ) + ) itemwise_qty = frappe._dict() for d in self.get("items"): @@ -157,36 +187,43 @@ class PurchaseOrder(BuyingController): for item_code, qty in itemwise_qty.items(): if flt(qty) < flt(itemwise_min_order_qty.get(item_code)): - frappe.throw(_("Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item).").format(item_code, - qty, itemwise_min_order_qty.get(item_code))) + frappe.throw( + _( + "Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item)." + ).format(item_code, qty, itemwise_min_order_qty.get(item_code)) + ) def validate_bom_for_subcontracting_items(self): if self.is_subcontracted == "Yes": for item in self.items: if not item.bom: - frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}") - .format(item.item_code, item.idx)) + frappe.throw( + _("BOM is not specified for subcontracting item {0} at row {1}").format( + item.item_code, item.idx + ) + ) def get_schedule_dates(self): - for d in self.get('items'): + for d in self.get("items"): if d.material_request_item and not d.schedule_date: - d.schedule_date = frappe.db.get_value("Material Request Item", - d.material_request_item, "schedule_date") - + d.schedule_date = frappe.db.get_value( + "Material Request Item", d.material_request_item, "schedule_date" + ) @frappe.whitelist() def get_last_purchase_rate(self): """get last purchase rates for all items""" - conversion_rate = flt(self.get('conversion_rate')) or 1.0 + conversion_rate = flt(self.get("conversion_rate")) or 1.0 for d in self.get("items"): if d.item_code: last_purchase_details = get_last_purchase_details(d.item_code, self.name) if last_purchase_details: - d.base_price_list_rate = (last_purchase_details['base_price_list_rate'] * - (flt(d.conversion_factor) or 1.0)) - d.discount_percentage = last_purchase_details['discount_percentage'] - d.base_rate = last_purchase_details['base_rate'] * (flt(d.conversion_factor) or 1.0) + d.base_price_list_rate = last_purchase_details["base_price_list_rate"] * ( + flt(d.conversion_factor) or 1.0 + ) + d.discount_percentage = last_purchase_details["discount_percentage"] + d.base_rate = last_purchase_details["base_rate"] * (flt(d.conversion_factor) or 1.0) d.price_list_rate = d.base_price_list_rate / conversion_rate d.rate = d.base_rate / conversion_rate d.last_purchase_rate = d.rate @@ -194,16 +231,21 @@ class PurchaseOrder(BuyingController): item_last_purchase_rate = frappe.get_cached_value("Item", d.item_code, "last_purchase_rate") if item_last_purchase_rate: - d.base_price_list_rate = d.base_rate = d.price_list_rate \ - = d.rate = d.last_purchase_rate = item_last_purchase_rate + d.base_price_list_rate = ( + d.base_rate + ) = d.price_list_rate = d.rate = d.last_purchase_rate = item_last_purchase_rate # Check for Closed status def check_on_hold_or_closed_status(self): - check_list =[] - for d in self.get('items'): - if d.meta.get_field('material_request') and d.material_request and d.material_request not in check_list: + check_list = [] + for d in self.get("items"): + if ( + d.meta.get_field("material_request") + and d.material_request + and d.material_request not in check_list + ): check_list.append(d.material_request) - check_on_hold_or_closed_status('Material Request', d.material_request) + check_on_hold_or_closed_status("Material Request", d.material_request) def update_requested_qty(self): material_request_map = {} @@ -216,7 +258,9 @@ class PurchaseOrder(BuyingController): mr_obj = frappe.get_doc("Material Request", mr) if mr_obj.status in ["Stopped", "Cancelled"]: - frappe.throw(_("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError) + frappe.throw( + _("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError + ) mr_obj.update_requested_qty(mr_item_rows) @@ -224,24 +268,26 @@ class PurchaseOrder(BuyingController): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] for d in self.get("items"): - if (not po_item_rows or d.name in po_item_rows) \ - and [d.item_code, d.warehouse] not in item_wh_list \ - and frappe.get_cached_value("Item", d.item_code, "is_stock_item") \ - and d.warehouse and not d.delivered_by_supplier: - item_wh_list.append([d.item_code, d.warehouse]) + if ( + (not po_item_rows or d.name in po_item_rows) + and [d.item_code, d.warehouse] not in item_wh_list + and frappe.get_cached_value("Item", d.item_code, "is_stock_item") + and d.warehouse + and not d.delivered_by_supplier + ): + item_wh_list.append([d.item_code, d.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, { - "ordered_qty": get_ordered_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)}) def check_modified_date(self): - mod_db = frappe.db.sql("select modified from `tabPurchase Order` where name = %s", - self.name) + mod_db = frappe.db.sql("select modified from `tabPurchase Order` where name = %s", self.name) date_diff = frappe.db.sql("select '%s' - '%s' " % (mod_db[0][0], cstr(self.modified))) if date_diff and date_diff[0][0]: - msgprint(_("{0} {1} has been modified. Please refresh.").format(self.doctype, self.name), - raise_exception=True) + msgprint( + _("{0} {1} has been modified. Please refresh.").format(self.doctype, self.name), + raise_exception=True, + ) def update_status(self, status): self.check_modified_date() @@ -268,8 +314,9 @@ class PurchaseOrder(BuyingController): if self.is_subcontracted == "Yes": self.update_reserved_qty_for_subcontract() - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total + ) self.update_blanket_order() @@ -289,7 +336,7 @@ class PurchaseOrder(BuyingController): self.check_on_hold_or_closed_status() - frappe.db.set(self,'status','Cancelled') + frappe.db.set(self, "status", "Cancelled") self.update_prevdoc_status() @@ -306,16 +353,30 @@ class PurchaseOrder(BuyingController): pass def update_status_updater(self): - self.status_updater.append({ - 'source_dt': 'Purchase Order Item', - 'target_dt': 'Sales Order Item', - 'target_field': 'ordered_qty', - 'target_parent_dt': 'Sales Order', - 'target_parent_field': '', - 'join_field': 'sales_order_item', - 'target_ref_field': 'stock_qty', - 'source_field': 'stock_qty' - }) + self.status_updater.append( + { + "source_dt": "Purchase Order Item", + "target_dt": "Sales Order Item", + "target_field": "ordered_qty", + "target_parent_dt": "Sales Order", + "target_parent_field": "", + "join_field": "sales_order_item", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + } + ) + self.status_updater.append( + { + "source_dt": "Purchase Order Item", + "target_dt": "Packed Item", + "target_field": "ordered_qty", + "target_parent_dt": "Sales Order", + "target_parent_field": "", + "join_field": "sales_order_packed_item", + "target_ref_field": "qty", + "source_field": "stock_qty", + } + ) def update_delivered_qty_in_sales_order(self): """Update delivered qty in Sales Order for drop ship""" @@ -354,24 +415,28 @@ class PurchaseOrder(BuyingController): received_qty += item.received_qty total_qty += item.qty if total_qty: - self.db_set("per_received", flt(received_qty/total_qty) * 100, update_modified=False) + self.db_set("per_received", flt(received_qty / total_qty) * 100, update_modified=False) else: self.db_set("per_received", 0, update_modified=False) -def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor= 1.0): + +def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" conversion_rate = flt(conversion_rate) or 1.0 - last_purchase_details = get_last_purchase_details(item_code, name) + last_purchase_details = get_last_purchase_details(item_code, name) if last_purchase_details: - last_purchase_rate = (last_purchase_details['base_net_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate + last_purchase_rate = ( + last_purchase_details["base_net_rate"] * (flt(conversion_factor) or 1.0) + ) / conversion_rate return last_purchase_rate else: item_last_purchase_rate = frappe.get_cached_value("Item", item_code, "last_purchase_rate") if item_last_purchase_rate: return item_last_purchase_rate + @frappe.whitelist() def close_or_unclose_purchase_orders(names, status): if not frappe.has_permission("Purchase Order", "write"): @@ -382,7 +447,7 @@ def close_or_unclose_purchase_orders(names, status): po = frappe.get_doc("Purchase Order", name) if po.docstatus == 1: if status == "Closed": - if po.status not in ( "Cancelled", "Closed") and (po.per_received < 100 or po.per_billed < 100): + if po.status not in ("Cancelled", "Closed") and (po.per_received < 100 or po.per_billed < 100): po.update_status(status) else: if po.status == "Closed": @@ -391,69 +456,78 @@ def close_or_unclose_purchase_orders(names, status): frappe.local.message_log = [] + def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") + @frappe.whitelist() def make_purchase_receipt(source_name, target_doc=None): def update_item(obj, target, source_parent): target.qty = flt(obj.qty) - flt(obj.received_qty) target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor) target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) - target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \ - flt(obj.rate) * flt(source_parent.conversion_rate) + target.base_amount = ( + (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate) + ) - doc = get_mapped_doc("Purchase Order", source_name, { - "Purchase Order": { - "doctype": "Purchase Receipt", - "field_map": { - "supplier_warehouse":"supplier_warehouse" + doc = get_mapped_doc( + "Purchase Order", + source_name, + { + "Purchase Order": { + "doctype": "Purchase Receipt", + "field_map": {"supplier_warehouse": "supplier_warehouse"}, + "validation": { + "docstatus": ["=", 1], + }, }, - "validation": { - "docstatus": ["=", 1], - } - }, - "Purchase Order Item": { - "doctype": "Purchase Receipt Item", - "field_map": { - "name": "purchase_order_item", - "parent": "purchase_order", - "bom": "bom", - "material_request": "material_request", - "material_request_item": "material_request_item" + "Purchase Order Item": { + "doctype": "Purchase Receipt Item", + "field_map": { + "name": "purchase_order_item", + "parent": "purchase_order", + "bom": "bom", + "material_request": "material_request", + "material_request_item": "material_request_item", + }, + "postprocess": update_item, + "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1, }, - "postprocess": update_item, - "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges", - "add_if_empty": True - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) + + doc.set_onload("ignore_price_list", True) return doc + @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): return get_mapped_purchase_invoice(source_name, target_doc) + @frappe.whitelist() def make_purchase_invoice_from_portal(purchase_order_name): doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True) if doc.contact_email != frappe.session.user: - frappe.throw(_('Not Permitted'), frappe.PermissionError) + frappe.throw(_("Not Permitted"), frappe.PermissionError) doc.save() frappe.db.commit() - frappe.response['type'] = 'redirect' - frappe.response.location = '/purchase-invoices/' + doc.name + frappe.response["type"] = "redirect" + frappe.response.location = "/purchase-invoices/" + doc.name + def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False): def postprocess(source, target): target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) - #Get the advance paid Journal Entries in Purchase Invoice Advance + # Get the advance paid Journal Entries in Purchase Invoice Advance if target.get("allocate_advances_automatically"): target.set_advances() @@ -462,26 +536,30 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions def update_item(obj, target, source_parent): target.amount = flt(obj.amount) - flt(obj.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) - target.qty = target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty) + target.qty = ( + target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty) + ) item = get_item_defaults(target.item_code, source_parent.company) item_group = get_item_group_defaults(target.item_code, source_parent.company) - target.cost_center = (obj.cost_center + target.cost_center = ( + obj.cost_center or frappe.db.get_value("Project", obj.project, "cost_center") or item.get("buying_cost_center") - or item_group.get("buying_cost_center")) + or item_group.get("buying_cost_center") + ) fields = { "Purchase Order": { "doctype": "Purchase Invoice", "field_map": { "party_account_currency": "party_account_currency", - "supplier_warehouse":"supplier_warehouse" + "supplier_warehouse": "supplier_warehouse", }, - "field_no_map" : ["payment_terms_template"], + "field_no_map": ["payment_terms_template"], "validation": { "docstatus": ["=", 1], - } + }, }, "Purchase Order Item": { "doctype": "Purchase Invoice Item", @@ -490,19 +568,24 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "parent": "purchase_order", }, "postprocess": update_item, - "condition": lambda doc: (doc.base_amount==0 or abs(doc.billed_amt) < abs(doc.amount)) - }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges", - "add_if_empty": True + "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), }, + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, } - doc = get_mapped_doc("Purchase Order", source_name, fields, - target_doc, postprocess, ignore_permissions=ignore_permissions) + doc = get_mapped_doc( + "Purchase Order", + source_name, + fields, + target_doc, + postprocess, + ignore_permissions=ignore_permissions, + ) + doc.set_onload("ignore_price_list", True) return doc + @frappe.whitelist() def make_rm_stock_entry(purchase_order, rm_items): rm_items_list = rm_items @@ -543,14 +626,14 @@ def make_rm_stock_entry(purchase_order, rm_items): rm_item_code: { "po_detail": rm_item_data.get("name"), "item_name": rm_item_data["item_name"], - "description": item_wh.get(rm_item_code, {}).get('description', ""), - 'qty': rm_item_data["qty"], - 'from_warehouse': rm_item_data["warehouse"], - 'stock_uom': rm_item_data["stock_uom"], - 'serial_no': rm_item_data.get('serial_no'), - 'batch_no': rm_item_data.get('batch_no'), - 'main_item_code': rm_item_data["item_code"], - 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item') + "description": item_wh.get(rm_item_code, {}).get("description", ""), + "qty": rm_item_data["qty"], + "from_warehouse": rm_item_data["warehouse"], + "stock_uom": rm_item_data["stock_uom"], + "serial_no": rm_item_data.get("serial_no"), + "batch_no": rm_item_data.get("batch_no"), + "main_item_code": rm_item_data["item_code"], + "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), } } stock_entry.add_to_stock_entry_detail(items_dict) @@ -559,55 +642,72 @@ def make_rm_stock_entry(purchase_order, rm_items): frappe.throw(_("No Items selected for transfer")) return purchase_order.name + def get_item_details(items): item_details = {} - for d in frappe.db.sql("""select item_code, description, allow_alternative_item from `tabItem` - where name in ({0})""".format(", ".join(["%s"] * len(items))), items, as_dict=1): + for d in frappe.db.sql( + """select item_code, description, allow_alternative_item from `tabItem` + where name in ({0})""".format( + ", ".join(["%s"] * len(items)) + ), + items, + as_dict=1, + ): item_details[d.item_code] = d return item_details + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Purchase Orders'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Purchase Orders"), + } + ) return list_context + @frappe.whitelist() def update_status(status, name): po = frappe.get_doc("Purchase Order", name) po.update_status(status) po.update_delivered_qty_in_sales_order() + @frappe.whitelist() def make_inter_company_sales_order(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction + return make_inter_company_transaction("Purchase Order", source_name, target_doc) + @frappe.whitelist() def get_materials_from_supplier(purchase_order, po_details): if isinstance(po_details, str): po_details = json.loads(po_details) - doc = frappe.get_cached_doc('Purchase Order', purchase_order) + doc = frappe.get_cached_doc("Purchase Order", purchase_order) doc.initialized_fields() doc.purchase_orders = [doc.name] doc.get_available_materials() if not doc.available_materials: - frappe.throw(_('Materials are already received against the purchase order {0}') - .format(purchase_order)) + frappe.throw( + _("Materials are already received against the purchase order {0}").format(purchase_order) + ) return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details) + def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details): - ste_doc = frappe.new_doc('Stock Entry') - ste_doc.purpose = 'Material Transfer' + ste_doc = frappe.new_doc("Stock Entry") + ste_doc.purpose = "Material Transfer" ste_doc.purchase_order = po_doc.name ste_doc.company = po_doc.company ste_doc.is_return = 1 @@ -628,18 +728,21 @@ def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_deta return ste_doc + def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None): - item = ste_doc.append('items', row.item_details) + item = ste_doc.append("items", row.item_details) po_detail = list(set(row.po_details).intersection(po_details)) - item.update({ - 'qty': qty, - 'batch_no': batch_no, - 'basic_rate': row.item_details['rate'], - 'po_detail': po_detail[0] if po_detail else '', - 's_warehouse': row.item_details['t_warehouse'], - 't_warehouse': row.item_details['s_warehouse'], - 'item_code': row.item_details['rm_item_code'], - 'subcontracted_item': row.item_details['main_item_code'], - 'serial_no': '\n'.join(row.serial_no) if row.serial_no else '' - }) + item.update( + { + "qty": qty, + "batch_no": batch_no, + "basic_rate": row.item_details["rate"], + "po_detail": po_detail[0] if po_detail else "", + "s_warehouse": row.item_details["t_warehouse"], + "t_warehouse": row.item_details["s_warehouse"], + "item_code": row.item_details["rm_item_code"], + "subcontracted_item": row.item_details["main_item_code"], + "serial_no": "\n".join(row.serial_no) if row.serial_no else "", + } + ) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 8588c002d5e..e656fa1071d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -1,36 +1,26 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'purchase_order', - 'non_standard_fieldnames': { - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', - 'Auto Repeat': 'reference_document' + "fieldname": "purchase_order", + "non_standard_fieldnames": { + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", + "Auto Repeat": "reference_document", }, - 'internal_links': { - 'Material Request': ['items', 'material_request'], - 'Supplier Quotation': ['items', 'supplier_quotation'], - 'Project': ['items', 'project'], + "internal_links": { + "Material Request": ["items", "material_request"], + "Supplier Quotation": ["items", "supplier_quotation"], + "Project": ["items", "project"], }, - 'transactions': [ + "transactions": [ + {"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice"]}, + {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}, { - 'label': _('Related'), - 'items': ['Purchase Receipt', 'Purchase Invoice'] + "label": _("Reference"), + "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], }, - { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Journal Entry'] - }, - { - 'label': _('Reference'), - 'items': ['Material Request', 'Supplier Quotation', 'Project', 'Auto Repeat'] - }, - { - 'label': _('Sub-contracting'), - 'items': ['Stock Entry'] - }, - ] + {"label": _("Sub-contracting"), "items": ["Stock Entry"]}, + ], } diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index efa2ab12685..e4fb970c3f7 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -51,7 +51,7 @@ class TestPurchaseOrder(FrappeTestCase): po.load_from_db() self.assertEqual(po.get("items")[0].received_qty, 4) - frappe.db.set_value('Item', '_Test Item', 'over_delivery_receipt_allowance', 50) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50) pr = create_pr_against_po(po.name, received_qty=8) self.assertEqual(get_ordered_qty(), existing_ordered_qty) @@ -71,8 +71,8 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(get_ordered_qty(), existing_ordered_qty + 10) - frappe.db.set_value('Item', '_Test Item', 'over_delivery_receipt_allowance', 50) - frappe.db.set_value('Item', '_Test Item', 'over_billing_allowance', 20) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50) + frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 20) pi = make_pi_from_po(po.name) pi.update_stock = 1 @@ -91,8 +91,8 @@ class TestPurchaseOrder(FrappeTestCase): po.load_from_db() self.assertEqual(po.get("items")[0].received_qty, 0) - frappe.db.set_value('Item', '_Test Item', 'over_delivery_receipt_allowance', 0) - frappe.db.set_value('Item', '_Test Item', 'over_billing_allowance', 0) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 0) + frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 0) frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) def test_update_remove_child_linked_to_mr(self): @@ -104,41 +104,41 @@ class TestPurchaseOrder(FrappeTestCase): po.submit() first_item_of_po = po.get("items")[0] - existing_ordered_qty = get_ordered_qty() # 10 - existing_requested_qty = get_requested_qty() # 0 + existing_ordered_qty = get_ordered_qty() # 10 + existing_requested_qty = get_requested_qty() # 0 # decrease ordered qty by 3 (10 -> 7) and add item - trans_item = json.dumps([ - { - 'item_code': first_item_of_po.item_code, - 'rate': first_item_of_po.rate, - 'qty': 7, - 'docname': first_item_of_po.name - }, - {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2} - ]) - update_child_qty_rate('Purchase Order', trans_item, po.name) + trans_item = json.dumps( + [ + { + "item_code": first_item_of_po.item_code, + "rate": first_item_of_po.rate, + "qty": 7, + "docname": first_item_of_po.name, + }, + {"item_code": "_Test Item 2", "rate": 200, "qty": 2}, + ] + ) + update_child_qty_rate("Purchase Order", trans_item, po.name) mr.reload() # requested qty increases as ordered qty decreases - self.assertEqual(get_requested_qty(), existing_requested_qty + 3) # 3 + self.assertEqual(get_requested_qty(), existing_requested_qty + 3) # 3 self.assertEqual(mr.items[0].ordered_qty, 7) - self.assertEqual(get_ordered_qty(), existing_ordered_qty - 3) # 7 + self.assertEqual(get_ordered_qty(), existing_ordered_qty - 3) # 7 # delete first item linked to Material Request - trans_item = json.dumps([ - {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2} - ]) - update_child_qty_rate('Purchase Order', trans_item, po.name) + trans_item = json.dumps([{"item_code": "_Test Item 2", "rate": 200, "qty": 2}]) + update_child_qty_rate("Purchase Order", trans_item, po.name) mr.reload() # requested qty increases as ordered qty is 0 (deleted row) - self.assertEqual(get_requested_qty(), existing_requested_qty + 10) # 10 + self.assertEqual(get_requested_qty(), existing_requested_qty + 10) # 10 self.assertEqual(mr.items[0].ordered_qty, 0) # ordered qty decreases as ordered qty is 0 (deleted row) - self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0 + self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0 def test_update_child(self): mr = make_material_request(qty=10) @@ -155,8 +155,10 @@ class TestPurchaseOrder(FrappeTestCase): existing_ordered_qty = get_ordered_qty() existing_requested_qty = get_requested_qty() - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.items[0].name}]) - update_child_qty_rate('Purchase Order', trans_item, po.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": po.items[0].name}] + ) + update_child_qty_rate("Purchase Order", trans_item, po.name) mr.reload() self.assertEqual(mr.items[0].ordered_qty, 7) @@ -180,20 +182,22 @@ class TestPurchaseOrder(FrappeTestCase): existing_ordered_qty = get_ordered_qty() first_item_of_po = po.get("items")[0] - trans_item = json.dumps([ - { - 'item_code': first_item_of_po.item_code, - 'rate': first_item_of_po.rate, - 'qty': first_item_of_po.qty, - 'docname': first_item_of_po.name - }, - {'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7} - ]) - update_child_qty_rate('Purchase Order', trans_item, po.name) + trans_item = json.dumps( + [ + { + "item_code": first_item_of_po.item_code, + "rate": first_item_of_po.rate, + "qty": first_item_of_po.qty, + "docname": first_item_of_po.name, + }, + {"item_code": "_Test Item", "rate": 200, "qty": 7}, + ] + ) + update_child_qty_rate("Purchase Order", trans_item, po.name) po.reload() - self.assertEqual(len(po.get('items')), 2) - self.assertEqual(po.status, 'To Receive and Bill') + self.assertEqual(len(po.get("items")), 2) + self.assertEqual(po.status, "To Receive and Bill") # ordered qty should increase on row addition self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7) @@ -208,15 +212,18 @@ class TestPurchaseOrder(FrappeTestCase): first_item_of_po = po.get("items")[0] existing_ordered_qty = get_ordered_qty() # add an item - trans_item = json.dumps([ - { - 'item_code': first_item_of_po.item_code, - 'rate': first_item_of_po.rate, - 'qty': first_item_of_po.qty, - 'docname': first_item_of_po.name - }, - {'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7}]) - update_child_qty_rate('Purchase Order', trans_item, po.name) + trans_item = json.dumps( + [ + { + "item_code": first_item_of_po.item_code, + "rate": first_item_of_po.rate, + "qty": first_item_of_po.qty, + "docname": first_item_of_po.name, + }, + {"item_code": "_Test Item", "rate": 200, "qty": 7}, + ] + ) + update_child_qty_rate("Purchase Order", trans_item, po.name) po.reload() @@ -224,115 +231,145 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7) # check if can remove received item - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": po.get("items")[1].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Purchase Order", trans_item, po.name + ) first_item_of_po = po.get("items")[0] - trans_item = json.dumps([ - { - 'item_code': first_item_of_po.item_code, - 'rate': first_item_of_po.rate, - 'qty': first_item_of_po.qty, - 'docname': first_item_of_po.name - } - ]) - update_child_qty_rate('Purchase Order', trans_item, po.name) + trans_item = json.dumps( + [ + { + "item_code": first_item_of_po.item_code, + "rate": first_item_of_po.rate, + "qty": first_item_of_po.qty, + "docname": first_item_of_po.name, + } + ] + ) + update_child_qty_rate("Purchase Order", trans_item, po.name) po.reload() - self.assertEqual(len(po.get('items')), 1) - self.assertEqual(po.status, 'To Receive and Bill') + self.assertEqual(len(po.get("items")), 1) + self.assertEqual(po.status, "To Receive and Bill") # ordered qty should decrease (back to initial) on row deletion self.assertEqual(get_ordered_qty(), existing_ordered_qty) def test_update_child_perm(self): - po = create_purchase_order(item_code= "_Test Item", qty=4) + po = create_purchase_order(item_code="_Test Item", qty=4) - user = 'test@example.com' - test_user = frappe.get_doc('User', user) + user = "test@example.com" + test_user = frappe.get_doc("User", user) test_user.add_roles("Accounts User") frappe.set_user(user) # update qty - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": po.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Purchase Order", trans_item, po.name + ) # add new item - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) + trans_item = json.dumps([{"item_code": "_Test Item", "rate": 100, "qty": 2}]) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Purchase Order", trans_item, po.name + ) frappe.set_user("Administrator") def test_update_child_with_tax_template(self): """ - Test Action: Create a PO with one item having its tax account head already in the PO. - Add the same item + new item with tax template via Update Items. - Expected result: First Item's tax row is updated. New tax row is added for second Item. + Test Action: Create a PO with one item having its tax account head already in the PO. + Add the same item + new item with tax template via Update Items. + Expected result: First Item's tax row is updated. New tax row is added for second Item. """ if not frappe.db.exists("Item", "Test Item with Tax"): - make_item("Test Item with Tax", { - 'is_stock_item': 1, - }) + make_item( + "Test Item with Tax", + { + "is_stock_item": 1, + }, + ) - if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): - frappe.get_doc({ - 'doctype': 'Item Tax Template', - 'title': 'Test Update Items Template', - 'company': '_Test Company', - 'taxes': [ - { - 'tax_type': "_Test Account Service Tax - _TC", - 'tax_rate': 10, - } - ] - }).insert() + if not frappe.db.exists("Item Tax Template", {"title": "Test Update Items Template"}): + frappe.get_doc( + { + "doctype": "Item Tax Template", + "title": "Test Update Items Template", + "company": "_Test Company", + "taxes": [ + { + "tax_type": "_Test Account Service Tax - _TC", + "tax_rate": 10, + } + ], + } + ).insert() new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") - if not frappe.db.exists("Item Tax", - {"item_tax_template": "Test Update Items Template - _TC", "parent": "Test Item with Tax"}): - new_item_with_tax.append("taxes", { - "item_tax_template": "Test Update Items Template - _TC", - "valid_from": nowdate() - }) + if not frappe.db.exists( + "Item Tax", + {"item_tax_template": "Test Update Items Template - _TC", "parent": "Test Item with Tax"}, + ): + new_item_with_tax.append( + "taxes", {"item_tax_template": "Test Update Items Template - _TC", "valid_from": nowdate()} + ) new_item_with_tax.save() tax_template = "_Test Account Excise Duty @ 10 - _TC" - item = "_Test Item Home Desktop 100" - if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item = "_Test Item Home Desktop 100" + if not frappe.db.exists("Item Tax", {"parent": item, "item_tax_template": tax_template}): item_doc = frappe.get_doc("Item", item) - item_doc.append("taxes", { - "item_tax_template": tax_template, - "valid_from": nowdate() - }) + item_doc.append("taxes", {"item_tax_template": tax_template, "valid_from": nowdate()}) item_doc.save() else: # update valid from - frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = CURDATE() where parent = %(item)s and item_tax_template = %(tax)s""", - {"item": item, "tax": tax_template}) + {"item": item, "tax": tax_template}, + ) po = create_purchase_order(item_code=item, qty=1, do_not_save=1) - po.append("taxes", { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Purchase Taxes and Charges", - "rate": 10 - }) + po.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Purchase Taxes and Charges", + "rate": 10, + }, + ) po.insert() po.submit() self.assertEqual(po.taxes[0].tax_amount, 50) self.assertEqual(po.taxes[0].total, 550) - items = json.dumps([ - {'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name}, - {'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO - {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO - ]) - update_child_qty_rate('Purchase Order', items, po.name) + items = json.dumps( + [ + {"item_code": item, "rate": 500, "qty": 1, "docname": po.items[0].name}, + { + "item_code": item, + "rate": 100, + "qty": 1, + }, # added item whose tax account head already exists in PO + { + "item_code": new_item_with_tax.name, + "rate": 100, + "qty": 1, + }, # added item whose tax account head is missing in PO + ] + ) + update_child_qty_rate("Purchase Order", items, po.name) po.reload() self.assertEqual(po.taxes[0].tax_amount, 70) @@ -342,8 +379,11 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(po.taxes[1].total, 840) # teardown - frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL - where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}, + ) po.cancel() po.delete() new_item_with_tax.delete() @@ -353,18 +393,24 @@ class TestPurchaseOrder(FrappeTestCase): po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) - trans_item = json.dumps([{ - 'item_code': po.get("items")[0].item_code, - 'rate': po.get("items")[0].rate, - 'qty': po.get("items")[0].qty, - 'uom': "_Test UOM 1", - 'conversion_factor': 2, - 'docname': po.get("items")[0].name - }]) - update_child_qty_rate('Purchase Order', trans_item, po.name) + trans_item = json.dumps( + [ + { + "item_code": po.get("items")[0].item_code, + "rate": po.get("items")[0].rate, + "qty": po.get("items")[0].qty, + "uom": "_Test UOM 1", + "conversion_factor": 2, + "docname": po.get("items")[0].name, + } + ] + ) + update_child_qty_rate("Purchase Order", trans_item, po.name) po.reload() - total_reqd_qty_after_change = sum(d.get("required_qty") for d in po.as_dict().get("supplied_items")) + total_reqd_qty_after_change = sum( + d.get("required_qty") for d in po.as_dict().get("supplied_items") + ) self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty) @@ -401,8 +447,6 @@ class TestPurchaseOrder(FrappeTestCase): po.load_from_db() self.assertEqual(po.get("items")[0].received_qty, 6) - - def test_return_against_purchase_order(self): po = create_purchase_order() @@ -428,17 +472,20 @@ class TestPurchaseOrder(FrappeTestCase): make_purchase_receipt as make_purchase_receipt_return, ) - pr1 = make_purchase_receipt_return(is_return=1, return_against=pr.name, qty=-3, do_not_submit=True) + pr1 = make_purchase_receipt_return( + is_return=1, return_against=pr.name, qty=-3, do_not_submit=True + ) pr1.items[0].purchase_order = po.name pr1.items[0].purchase_order_item = po.items[0].name pr1.submit() - pi1= make_purchase_invoice_return(is_return=1, return_against=pi2.name, qty=-1, update_stock=1, do_not_submit=True) + pi1 = make_purchase_invoice_return( + is_return=1, return_against=pi2.name, qty=-1, update_stock=1, do_not_submit=True + ) pi1.items[0].purchase_order = po.name pi1.items[0].po_detail = po.items[0].name pi1.submit() - po.load_from_db() self.assertEqual(po.get("items")[0].received_qty, 5) @@ -484,13 +531,12 @@ class TestPurchaseOrder(FrappeTestCase): def test_purchase_order_on_hold(self): po = create_purchase_order(item_code="_Test Product Bundle Item") - po.db_set('Status', "On Hold") + po.db_set("Status", "On Hold") pi = make_pi_from_po(po.name) pr = make_purchase_receipt(po.name) self.assertRaises(frappe.ValidationError, pr.submit) self.assertRaises(frappe.ValidationError, pi.submit) - def test_make_purchase_invoice_with_terms(self): from erpnext.selling.doctype.sales_order.test_sales_order import ( automatically_fetch_payment_terms, @@ -501,9 +547,7 @@ class TestPurchaseOrder(FrappeTestCase): self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name) - po.update( - {"payment_terms_template": "_Test Payment Term Template"} - ) + po.update({"payment_terms_template": "_Test Payment Term Template"}) po.save() po.submit() @@ -511,7 +555,9 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(po.payment_schedule[0].payment_amount, 2500.0) self.assertEqual(getdate(po.payment_schedule[0].due_date), getdate(po.transaction_date)) self.assertEqual(po.payment_schedule[1].payment_amount, 2500.0) - self.assertEqual(getdate(po.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30)) + self.assertEqual( + getdate(po.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30) + ) pi = make_pi_from_po(po.name) pi.save() @@ -521,7 +567,9 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(pi.payment_schedule[0].payment_amount, 2500.0) self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date)) self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0) - self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30)) + self.assertEqual( + getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30) + ) automatically_fetch_payment_terms(enable=0) def test_subcontracting(self): @@ -530,66 +578,78 @@ class TestPurchaseOrder(FrappeTestCase): def test_warehouse_company_validation(self): from erpnext.stock.utils import InvalidWarehouseCompany + po = create_purchase_order(company="_Test Company 1", do_not_save=True) self.assertRaises(InvalidWarehouseCompany, po.insert) def test_uom_integer_validation(self): from erpnext.utilities.transaction_base import UOMMustBeIntegerError + po = create_purchase_order(qty=3.4, do_not_save=True) self.assertRaises(UOMMustBeIntegerError, po.insert) def test_ordered_qty_for_closing_po(self): - bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty"]) + bin = frappe.get_all( + "Bin", + filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty"], + ) existing_ordered_qty = bin[0].ordered_qty if bin else 0.0 - po = create_purchase_order(item_code= "_Test Item", qty=1) + po = create_purchase_order(item_code="_Test Item", qty=1) - self.assertEqual(get_ordered_qty(item_code= "_Test Item", warehouse="_Test Warehouse - _TC"), existing_ordered_qty+1) + self.assertEqual( + get_ordered_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), + existing_ordered_qty + 1, + ) po.update_status("Closed") - self.assertEqual(get_ordered_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), existing_ordered_qty) + self.assertEqual( + get_ordered_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), existing_ordered_qty + ) def test_group_same_items(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) - frappe.get_doc({ - "doctype": "Purchase Order", - "company": "_Test Company", - "supplier" : "_Test Supplier", - "is_subcontracted" : "No", - "schedule_date": add_days(nowdate(), 1), - "currency" : frappe.get_cached_value('Company', "_Test Company", "default_currency"), - "conversion_factor" : 1, - "items" : get_same_items(), - "group_same_items": 1 - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Purchase Order", + "company": "_Test Company", + "supplier": "_Test Supplier", + "is_subcontracted": "No", + "schedule_date": add_days(nowdate(), 1), + "currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), + "conversion_factor": 1, + "items": get_same_items(), + "group_same_items": 1, + } + ).insert(ignore_permissions=True) def test_make_po_without_terms(self): po = create_purchase_order(do_not_save=1) - self.assertFalse(po.get('payment_schedule')) + self.assertFalse(po.get("payment_schedule")) po.insert() - self.assertTrue(po.get('payment_schedule')) + self.assertTrue(po.get("payment_schedule")) def test_po_for_blocked_supplier_all(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 supplier.save() - self.assertEqual(supplier.hold_type, 'All') + self.assertEqual(supplier.hold_type, "All") self.assertRaises(frappe.ValidationError, create_purchase_order) supplier.on_hold = 0 supplier.save() def test_po_for_blocked_supplier_invoices(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Invoices' + supplier.hold_type = "Invoices" supplier.save() self.assertRaises(frappe.ValidationError, create_purchase_order) @@ -598,30 +658,40 @@ class TestPurchaseOrder(FrappeTestCase): supplier.save() def test_po_for_blocked_supplier_payments(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' + supplier.hold_type = "Payments" supplier.save() po = create_purchase_order() self.assertRaises( - frappe.ValidationError, get_payment_entry, dt='Purchase Order', dn=po.name, bank_account="_Test Bank - _TC") + frappe.ValidationError, + get_payment_entry, + dt="Purchase Order", + dn=po.name, + bank_account="_Test Bank - _TC", + ) supplier.on_hold = 0 supplier.save() def test_po_for_blocked_supplier_payments_with_today_date(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 supplier.release_date = nowdate() - supplier.hold_type = 'Payments' + supplier.hold_type = "Payments" supplier.save() po = create_purchase_order() self.assertRaises( - frappe.ValidationError, get_payment_entry, dt='Purchase Order', dn=po.name, bank_account="_Test Bank - _TC") + frappe.ValidationError, + get_payment_entry, + dt="Purchase Order", + dn=po.name, + bank_account="_Test Bank - _TC", + ) supplier.on_hold = 0 supplier.save() @@ -630,14 +700,14 @@ class TestPurchaseOrder(FrappeTestCase): # this test is meant to fail only if something fails in the try block with self.assertRaises(Exception): try: - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' - supplier.release_date = '2018-03-01' + supplier.hold_type = "Payments" + supplier.release_date = "2018-03-01" supplier.save() po = create_purchase_order() - get_payment_entry('Purchase Order', po.name, bank_account='_Test Bank - _TC') + get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC") supplier.on_hold = 0 supplier.save() @@ -648,88 +718,124 @@ class TestPurchaseOrder(FrappeTestCase): def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): po = create_purchase_order(do_not_save=1) - po.payment_terms_template = '_Test Payment Term Template' + po.payment_terms_template = "_Test Payment Term Template" po.save() po.submit() - frappe.db.set_value('Company', '_Test Company', 'payment_terms', '_Test Payment Term Template 1') + frappe.db.set_value("Company", "_Test Company", "payment_terms", "_Test Payment Term Template 1") pi = make_pi_from_po(po.name) pi.save() - self.assertEqual(pi.get('payment_terms_template'), '_Test Payment Term Template 1') - frappe.db.set_value('Company', '_Test Company', 'payment_terms', '') + self.assertEqual(pi.get("payment_terms_template"), "_Test Payment Term Template 1") + frappe.db.set_value("Company", "_Test Company", "payment_terms", "") def test_terms_copied(self): po = create_purchase_order(do_not_save=1) - po.payment_terms_template = '_Test Payment Term Template' + po.payment_terms_template = "_Test Payment Term Template" po.insert() po.submit() - self.assertTrue(po.get('payment_schedule')) + self.assertTrue(po.get("payment_schedule")) pi = make_pi_from_po(po.name) pi.insert() - self.assertTrue(pi.get('payment_schedule')) + self.assertTrue(pi.get("payment_schedule")) def test_reserved_qty_subcontract_po(self): # Make stock available for raw materials make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) - make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", - qty=20, basic_rate=100) - make_stock_entry(target="_Test Warehouse 1 - _TC", item_code="_Test Item", - qty=30, basic_rate=100) - make_stock_entry(target="_Test Warehouse 1 - _TC", item_code="_Test Item Home Desktop 100", - qty=30, basic_rate=100) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code="_Test Item Home Desktop 100", + qty=30, + basic_rate=100, + ) - bin1 = frappe.db.get_value("Bin", + bin1 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1) + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], + as_dict=1, + ) # Submit PO po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") - bin2 = frappe.db.get_value("Bin", + bin2 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1) + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], + as_dict=1, + ) self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) self.assertNotEqual(bin1.modified, bin2.modified) # Create stock transfer - rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item", - "qty":6,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":600,"stock_uom":"Nos"}] + rm_item = [ + { + "item_code": "_Test FG Item", + "rm_item_code": "_Test Item", + "item_name": "_Test Item", + "qty": 6, + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 600, + "stock_uom": "Nos", + } + ] rm_item_string = json.dumps(rm_item) se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) se.to_warehouse = "_Test Warehouse 1 - _TC" se.save() se.submit() - bin3 = frappe.db.get_value("Bin", + bin3 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) # close PO po.update_status("Closed") - bin4 = frappe.db.get_value("Bin", + bin4 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) # Re-open PO po.update_status("Submitted") - bin5 = frappe.db.get_value("Bin", + bin5 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - make_stock_entry(target="_Test Warehouse 1 - _TC", item_code="_Test Item", - qty=40, basic_rate=100) - make_stock_entry(target="_Test Warehouse 1 - _TC", item_code="_Test Item Home Desktop 100", - qty=40, basic_rate=100) + make_stock_entry( + target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code="_Test Item Home Desktop 100", + qty=40, + basic_rate=100, + ) # make Purchase Receipt against PO pr = make_purchase_receipt(po.name) @@ -737,17 +843,23 @@ class TestPurchaseOrder(FrappeTestCase): pr.save() pr.submit() - bin6 = frappe.db.get_value("Bin", + bin6 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) # Cancel PR pr.cancel() - bin7 = frappe.db.get_value("Bin", + bin7 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) @@ -757,34 +869,46 @@ class TestPurchaseOrder(FrappeTestCase): pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.insert() pi.submit() - bin8 = frappe.db.get_value("Bin", + bin8 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) # Cancel PR pi.cancel() - bin9 = frappe.db.get_value("Bin", + bin9 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) # Cancel Stock Entry se.cancel() - bin10 = frappe.db.get_value("Bin", + bin10 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) # Cancel PO po.reload() po.cancel() - bin11 = frappe.db.get_value("Bin", + bin11 = frappe.db.get_value( + "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", as_dict=1) + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) @@ -792,56 +916,101 @@ class TestPurchaseOrder(FrappeTestCase): item_code = "_Test Subcontracted FG Item 11" make_subcontracted_item(item_code=item_code) - po = create_purchase_order(item_code=item_code, qty=1, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=1) + po = create_purchase_order( + item_code=item_code, + qty=1, + is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC", + include_exploded_items=1, + ) - name = frappe.db.get_value('BOM', {'item': item_code}, 'name') - bom = frappe.get_doc('BOM', name) + name = frappe.db.get_value("BOM", {"item": item_code}, "name") + bom = frappe.get_doc("BOM", name) - exploded_items = sorted([d.item_code for d in bom.exploded_items if not d.get('sourced_by_supplier')]) + exploded_items = sorted( + [d.item_code for d in bom.exploded_items if not d.get("sourced_by_supplier")] + ) supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(exploded_items, supplied_items) - po1 = create_purchase_order(item_code=item_code, qty=1, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=0) + po1 = create_purchase_order( + item_code=item_code, + qty=1, + is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC", + include_exploded_items=0, + ) supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items]) - bom_items = sorted([d.item_code for d in bom.items if not d.get('sourced_by_supplier')]) + bom_items = sorted([d.item_code for d in bom.items if not d.get("sourced_by_supplier")]) self.assertEqual(supplied_items1, bom_items) def test_backflush_based_on_stock_entry(self): item_code = "_Test Subcontracted FG Item 1" make_subcontracted_item(item_code=item_code) - make_item('Sub Contracted Raw Material 1', { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1 - }) + make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1}) update_backflush_based_on("Material Transferred for Subcontract") order_qty = 5 - po = create_purchase_order(item_code=item_code, qty=order_qty, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + item_code=item_code, + qty=order_qty, + is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC", + ) - make_stock_entry(target="_Test Warehouse - _TC", - item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100) - make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Test Extra Item 1", qty=100, basic_rate=100) - make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Test Extra Item 2", qty=10, basic_rate=100) - make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 1", qty=10, basic_rate=100) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 1", + qty=10, + basic_rate=100, + ) rm_items = [ - {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 1","item_name":"_Test Item", - "qty":10,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, - {"item_code":item_code,"rm_item_code":"_Test Item Home Desktop 100","item_name":"_Test Item Home Desktop 100", - "qty":20,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, - {"item_code":item_code,"rm_item_code":"Test Extra Item 1","item_name":"Test Extra Item 1", - "qty":10,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, - {'item_code': item_code, 'rm_item_code': 'Test Extra Item 2', 'stock_uom':'Nos', - 'qty': 10, 'warehouse': '_Test Warehouse - _TC', 'item_name':'Test Extra Item 2'}] + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 1", + "item_name": "_Test Item", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "qty": 20, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "Test Extra Item 1", + "item_name": "Test Extra Item 1", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "Test Extra Item 2", + "stock_uom": "Nos", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "item_name": "Test Extra Item 2", + }, + ] rm_item_string = json.dumps(rm_items) se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) @@ -851,61 +1020,80 @@ class TestPurchaseOrder(FrappeTestCase): received_qty = 2 # partial receipt - pr.get('items')[0].qty = received_qty + pr.get("items")[0].qty = received_qty pr.save() pr.submit() - transferred_items = sorted([d.item_code for d in se.get('items') if se.purchase_order == po.name]) - issued_items = sorted([d.rm_item_code for d in pr.get('supplied_items')]) + transferred_items = sorted( + [d.item_code for d in se.get("items") if se.purchase_order == po.name] + ) + issued_items = sorted([d.rm_item_code for d in pr.get("supplied_items")]) self.assertEqual(transferred_items, issued_items) - self.assertEqual(pr.get('items')[0].rm_supp_cost, 2000) - + self.assertEqual(pr.get("items")[0].rm_supp_cost, 2000) transferred_rm_map = frappe._dict() for item in rm_items: - transferred_rm_map[item.get('rm_item_code')] = item + transferred_rm_map[item.get("rm_item_code")] = item update_backflush_based_on("BOM") def test_supplied_qty_against_subcontracted_po(self): item_code = "_Test Subcontracted FG Item 5" - make_item('Sub Contracted Raw Material 4', { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1 - }) + make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1}) make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"]) update_backflush_based_on("Material Transferred for Subcontract") order_qty = 250 - po = create_purchase_order(item_code=item_code, qty=order_qty, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True) + po = create_purchase_order( + item_code=item_code, + qty=order_qty, + is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC", + do_not_save=True, + ) # Add same subcontracted items multiple times - po.append("items", { - "item_code": item_code, - "qty": order_qty, - "schedule_date": add_days(nowdate(), 1), - "warehouse": "_Test Warehouse - _TC" - }) + po.append( + "items", + { + "item_code": item_code, + "qty": order_qty, + "schedule_date": add_days(nowdate(), 1), + "warehouse": "_Test Warehouse - _TC", + }, + ) po.set_missing_values() po.submit() # Material receipt entry for the raw materials which will be send to supplier - make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100) + make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 4", + qty=500, + basic_rate=100, + ) rm_items = [ { - "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item", - "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 4", + "item_name": "_Test Item", + "qty": 250, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": po.supplied_items[0].name, }, { - "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item", - "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos" + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 4", + "item_name": "_Test Item", + "qty": 250, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", }, ] @@ -927,8 +1115,10 @@ class TestPurchaseOrder(FrappeTestCase): def test_advance_payment_entry_unlink_against_purchase_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 1) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 1 + ) po_doc = create_purchase_order() @@ -943,24 +1133,23 @@ class TestPurchaseOrder(FrappeTestCase): pe.save(ignore_permissions=True) pe.submit() - po_doc = frappe.get_doc('Purchase Order', po_doc.name) + po_doc = frappe.get_doc("Purchase Order", po_doc.name) po_doc.cancel() - pe_doc = frappe.get_doc('Payment Entry', pe.name) + pe_doc = frappe.get_doc("Payment Entry", pe.name) pe_doc.cancel() - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 0) + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 + ) def test_schedule_date(self): po = create_purchase_order(do_not_submit=True) po.schedule_date = None - po.append("items", { - "item_code": "_Test Item", - "qty": 1, - "rate": 100, - "schedule_date": add_days(nowdate(), 5) - }) + po.append( + "items", + {"item_code": "_Test Item", "qty": 1, "rate": 100, "schedule_date": add_days(nowdate(), 5)}, + ) po.save() self.assertEqual(po.schedule_date, add_days(nowdate(), 1)) @@ -968,22 +1157,21 @@ class TestPurchaseOrder(FrappeTestCase): po.save() self.assertEqual(po.schedule_date, add_days(nowdate(), 2)) - def test_po_optional_blanket_order(self): """ - Expected result: Blanket order Ordered Quantity should only be affected on Purchase Order with against_blanket_order = 1. - Second Purchase Order should not add on to Blanket Orders Ordered Quantity. + Expected result: Blanket order Ordered Quantity should only be affected on Purchase Order with against_blanket_order = 1. + Second Purchase Order should not add on to Blanket Orders Ordered Quantity. """ - bo = make_blanket_order(blanket_order_type = "Purchasing", quantity = 10, rate = 10) + bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10) - po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) - po_doc = frappe.get_doc('Purchase Order', po.get('name')) + po = create_purchase_order(item_code="_Test Item", qty=5, against_blanket_order=1) + po_doc = frappe.get_doc("Purchase Order", po.get("name")) # To test if the PO has a Blanket Order self.assertTrue(po_doc.items[0].blanket_order) - po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) - po_doc = frappe.get_doc('Purchase Order', po.get('name')) + po = create_purchase_order(item_code="_Test Item", qty=5, against_blanket_order=0) + po_doc = frappe.get_doc("Purchase Order", po.get("name")) # To test if the PO does NOT have a Blanket Order self.assertEqual(po_doc.items[0].blanket_order, None) @@ -1001,7 +1189,7 @@ class TestPurchaseOrder(FrappeTestCase): po = create_purchase_order(qty=10, rate=100, do_not_save=1) create_payment_terms_template() - po.payment_terms_template = 'Test Receivable Template' + po.payment_terms_template = "Test Receivable Template" po.submit() pi = make_purchase_invoice(qty=10, rate=100, do_not_save=1) @@ -1014,6 +1202,7 @@ class TestPurchaseOrder(FrappeTestCase): automatically_fetch_payment_terms(enable=0) + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) pr.get("items")[0].qty = received_qty or 5 @@ -1021,39 +1210,51 @@ def make_pr_against_po(po, received_qty=0): pr.submit() return pr + def make_subcontracted_item(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom args = frappe._dict(args) - if not frappe.db.exists('Item', args.item_code): - make_item(args.item_code, { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'has_batch_no': args.get("has_batch_no") or 0 - }) + if not frappe.db.exists("Item", args.item_code): + make_item( + args.item_code, + { + "is_stock_item": 1, + "is_sub_contracted_item": 1, + "has_batch_no": args.get("has_batch_no") or 0, + }, + ) if not args.raw_materials: - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item 1"): + make_item( + "Test Extra Item 1", + { + "is_stock_item": 1, + }, + ) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item 2"): + make_item( + "Test Extra Item 2", + { + "is_stock_item": 1, + }, + ) - args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] + + if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): + make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) - if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): - make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) def update_backflush_based_on(based_on): - doc = frappe.get_doc('Buying Settings') + doc = frappe.get_doc("Buying Settings") doc.backflush_raw_materials_of_subcontract_based_on = based_on doc.save() + def get_same_items(): return [ { @@ -1061,17 +1262,18 @@ def get_same_items(): "warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 500, - "schedule_date": add_days(nowdate(), 1) + "schedule_date": add_days(nowdate(), 1), }, { "item_code": "_Test FG Item", "warehouse": "_Test Warehouse - _TC", "qty": 4, "rate": 500, - "schedule_date": add_days(nowdate(), 1) - } + "schedule_date": add_days(nowdate(), 1), + }, ] + def create_purchase_order(**args): po = frappe.new_doc("Purchase Order") args = frappe._dict(args) @@ -1082,7 +1284,7 @@ def create_purchase_order(**args): po.company = args.company or "_Test Company" po.supplier = args.supplier or "_Test Supplier" po.is_subcontracted = args.is_subcontracted or "No" - po.currency = args.currency or frappe.get_cached_value('Company', po.company, "default_currency") + po.currency = args.currency or frappe.get_cached_value("Company", po.company, "default_currency") po.conversion_factor = args.conversion_factor or 1 po.supplier_warehouse = args.supplier_warehouse or None @@ -1090,15 +1292,18 @@ def create_purchase_order(**args): for row in args.rm_items: po.append("items", row) else: - po.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 10, - "rate": args.rate or 500, - "schedule_date": add_days(nowdate(), 1), - "include_exploded_items": args.get('include_exploded_items', 1), - "against_blanket_order": args.against_blanket_order - }) + po.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 10, + "rate": args.rate or 500, + "schedule_date": add_days(nowdate(), 1), + "include_exploded_items": args.get("include_exploded_items", 1), + "against_blanket_order": args.against_blanket_order, + }, + ) po.set_missing_values() if not args.do_not_save: @@ -1113,6 +1318,7 @@ def create_purchase_order(**args): return po + def create_pr_against_po(po, received_qty=4): pr = make_purchase_receipt(po) pr.get("items")[0].qty = received_qty @@ -1120,14 +1326,19 @@ def create_pr_against_po(po, received_qty=4): pr.submit() return pr + def get_ordered_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): - return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - "ordered_qty")) + return flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "ordered_qty") + ) + def get_requested_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): - return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - "indented_qty")) + return flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty") + ) + test_dependencies = ["BOM", "Item Price"] -test_records = frappe.get_test_records('Purchase Order') +test_records = frappe.get_test_records("Purchase Order") diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 87cd57517e2..a18c527644e 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -63,6 +63,7 @@ "material_request_item", "sales_order", "sales_order_item", + "sales_order_packed_item", "supplier_quotation", "supplier_quotation_item", "col_break5", @@ -837,21 +838,30 @@ "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "fieldname": "sales_order_packed_item", + "fieldtype": "Data", + "label": "Sales Order Packed Item", + "no_copy": 1, + "print_hide": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-30 20:06:26.712097", + "modified": "2022-02-02 13:10:18.398976", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "quick_entry": 1, "search_fields": "item_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py index 0cef0deee55..a8bafda0029 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py @@ -9,5 +9,6 @@ from frappe.model.document import Document class PurchaseOrderItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Purchase Order Item", ["item_code", "warehouse"]) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index b670bd58d48..1122e7fc573 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -21,6 +21,7 @@ from erpnext.stock.doctype.material_request.material_request import set_missing_ STANDARD_USERS = ("Guest", "Administrator") + class RequestforQuotation(BuyingController): def validate(self): self.validate_duplicate_supplier() @@ -31,7 +32,7 @@ class RequestforQuotation(BuyingController): if self.docstatus < 1: # after amend and save, status still shows as cancelled, until submit - frappe.db.set(self, 'status', 'Draft') + frappe.db.set(self, "status", "Draft") def validate_duplicate_supplier(self): supplier_list = [d.supplier for d in self.suppliers] @@ -40,14 +41,24 @@ class RequestforQuotation(BuyingController): def validate_supplier_list(self): for d in self.suppliers: - prevent_rfqs = frappe.db.get_value("Supplier", d.supplier, 'prevent_rfqs') + prevent_rfqs = frappe.db.get_value("Supplier", d.supplier, "prevent_rfqs") if prevent_rfqs: - standing = frappe.db.get_value("Supplier Scorecard",d.supplier, 'status') - frappe.throw(_("RFQs are not allowed for {0} due to a scorecard standing of {1}").format(d.supplier, standing)) - warn_rfqs = frappe.db.get_value("Supplier", d.supplier, 'warn_rfqs') + standing = frappe.db.get_value("Supplier Scorecard", d.supplier, "status") + frappe.throw( + _("RFQs are not allowed for {0} due to a scorecard standing of {1}").format( + d.supplier, standing + ) + ) + warn_rfqs = frappe.db.get_value("Supplier", d.supplier, "warn_rfqs") if warn_rfqs: - standing = frappe.db.get_value("Supplier Scorecard",d.supplier, 'status') - frappe.msgprint(_("{0} currently has a {1} Supplier Scorecard standing, and RFQs to this supplier should be issued with caution.").format(d.supplier, standing), title=_("Caution"), indicator='orange') + standing = frappe.db.get_value("Supplier Scorecard", d.supplier, "status") + frappe.msgprint( + _( + "{0} currently has a {1} Supplier Scorecard standing, and RFQs to this supplier should be issued with caution." + ).format(d.supplier, standing), + title=_("Caution"), + indicator="orange", + ) def update_email_id(self): for rfq_supplier in self.suppliers: @@ -56,17 +67,21 @@ class RequestforQuotation(BuyingController): def validate_email_id(self, args): if not args.email_id: - frappe.throw(_("Row {0}: For Supplier {1}, Email Address is Required to send an email").format(args.idx, frappe.bold(args.supplier))) + frappe.throw( + _("Row {0}: For Supplier {1}, Email Address is Required to send an email").format( + args.idx, frappe.bold(args.supplier) + ) + ) def on_submit(self): - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") for supplier in self.suppliers: supplier.email_sent = 0 - supplier.quote_status = 'Pending' + supplier.quote_status = "Pending" self.send_to_supplier() def on_cancel(self): - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") @frappe.whitelist() def get_supplier_email_preview(self, supplier): @@ -76,7 +91,7 @@ class RequestforQuotation(BuyingController): self.validate_email_id(rfq_supplier) - message = self.supplier_rfq_mail(rfq_supplier, '', self.get_link(), True) + message = self.supplier_rfq_mail(rfq_supplier, "", self.get_link(), True) return message @@ -103,12 +118,13 @@ class RequestforQuotation(BuyingController): def update_supplier_part_no(self, supplier): self.vendor = supplier for item in self.items: - item.supplier_part_no = frappe.db.get_value('Item Supplier', - {'parent': item.item_code, 'supplier': supplier}, 'supplier_part_no') + item.supplier_part_no = frappe.db.get_value( + "Item Supplier", {"parent": item.item_code, "supplier": supplier}, "supplier_part_no" + ) def update_supplier_contact(self, rfq_supplier, link): - '''Create a new user for the supplier if not set in contact''' - update_password_link, contact = '', '' + """Create a new user for the supplier if not set in contact""" + update_password_link, contact = "", "" if frappe.db.exists("User", rfq_supplier.email_id): user = frappe.get_doc("User", rfq_supplier.email_id) @@ -126,14 +142,8 @@ class RequestforQuotation(BuyingController): else: contact = frappe.new_doc("Contact") contact.first_name = rfq_supplier.supplier_name or rfq_supplier.supplier - contact.append('links', { - 'link_doctype': 'Supplier', - 'link_name': rfq_supplier.supplier - }) - contact.append('email_ids', { - 'email_id': user.name, - 'is_primary': 1 - }) + contact.append("links", {"link_doctype": "Supplier", "link_name": rfq_supplier.supplier}) + contact.append("email_ids", {"email_id": user.name, "is_primary": 1}) if not contact.email_id and not contact.user: contact.email_id = user.name @@ -146,39 +156,38 @@ class RequestforQuotation(BuyingController): return contact.name def create_user(self, rfq_supplier, link): - user = frappe.get_doc({ - 'doctype': 'User', - 'send_welcome_email': 0, - 'email': rfq_supplier.email_id, - 'first_name': rfq_supplier.supplier_name or rfq_supplier.supplier, - 'user_type': 'Website User', - 'redirect_url': link - }) + user = frappe.get_doc( + { + "doctype": "User", + "send_welcome_email": 0, + "email": rfq_supplier.email_id, + "first_name": rfq_supplier.supplier_name or rfq_supplier.supplier, + "user_type": "Website User", + "redirect_url": link, + } + ) user.save(ignore_permissions=True) update_password_link = user.reset_password() return user, update_password_link def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False): - full_name = get_user_fullname(frappe.session['user']) + full_name = get_user_fullname(frappe.session["user"]) if full_name == "Guest": full_name = "Administrator" # send document dict and some important data from suppliers row # to render message_for_supplier from any template doc_args = self.as_dict() - doc_args.update({ - 'supplier': data.get('supplier'), - 'supplier_name': data.get('supplier_name') - }) + doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")}) args = { - 'update_password_link': update_password_link, - 'message': frappe.render_template(self.message_for_supplier, doc_args), - 'rfq_link': rfq_link, - 'user_fullname': full_name, - 'supplier_name' : data.get('supplier_name'), - 'supplier_salutation' : self.salutation or 'Dear Mx.', + "update_password_link": update_password_link, + "message": frappe.render_template(self.message_for_supplier, doc_args), + "rfq_link": rfq_link, + "user_fullname": full_name, + "supplier_name": data.get("supplier_name"), + "supplier_salutation": self.salutation or "Dear Mx.", } subject = self.subject or _("Request for Quotation") @@ -194,9 +203,16 @@ class RequestforQuotation(BuyingController): self.send_email(data, sender, subject, message, attachments) def send_email(self, data, sender, subject, message, attachments): - make(subject = subject, content=message,recipients=data.email_id, - sender=sender,attachments = attachments, send_email=True, - doctype=self.doctype, name=self.name)["name"] + make( + subject=subject, + content=message, + recipients=data.email_id, + sender=sender, + attachments=attachments, + send_email=True, + doctype=self.doctype, + name=self.name, + )["name"] frappe.msgprint(_("Email Sent to Supplier {0}").format(data.supplier)) @@ -208,9 +224,10 @@ class RequestforQuotation(BuyingController): def update_rfq_supplier_status(self, sup_name=None): for supplier in self.suppliers: if sup_name == None or supplier.supplier == sup_name: - quote_status = _('Received') + quote_status = _("Received") for item in self.items: - sqi_count = frappe.db.sql(""" + sqi_count = frappe.db.sql( + """ SELECT COUNT(sqi.name) as count FROM @@ -220,42 +237,59 @@ class RequestforQuotation(BuyingController): AND sqi.docstatus = 1 AND sqi.request_for_quotation_item = %(rqi)s AND sqi.parent = sq.name""", - {"supplier": supplier.supplier, "rqi": item.name}, as_dict=1)[0] + {"supplier": supplier.supplier, "rqi": item.name}, + as_dict=1, + )[0] if (sqi_count.count) == 0: - quote_status = _('Pending') + quote_status = _("Pending") supplier.quote_status = quote_status @frappe.whitelist() def send_supplier_emails(rfq_name): - check_portal_enabled('Request for Quotation') + check_portal_enabled("Request for Quotation") rfq = frappe.get_doc("Request for Quotation", rfq_name) - if rfq.docstatus==1: + if rfq.docstatus == 1: rfq.send_to_supplier() + def check_portal_enabled(reference_doctype): - if not frappe.db.get_value('Portal Menu Item', - {'reference_doctype': reference_doctype}, 'enabled'): - frappe.throw(_("The Access to Request for Quotation From Portal is Disabled. To Allow Access, Enable it in Portal Settings.")) + if not frappe.db.get_value( + "Portal Menu Item", {"reference_doctype": reference_doctype}, "enabled" + ): + frappe.throw( + _( + "The Access to Request for Quotation From Portal is Disabled. To Allow Access, Enable it in Portal Settings." + ) + ) + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Request for Quotation'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Request for Quotation"), + } + ) return list_context + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select `tabContact`.name from `tabContact`, `tabDynamic Link` + return frappe.db.sql( + """select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent - limit %(start)s, %(page_len)s""", {"start": start, "page_len":page_len, "txt": "%%%s%%" % txt, "name": filters.get('supplier')}) + limit %(start)s, %(page_len)s""", + {"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")}, + ) + @frappe.whitelist() def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None): @@ -263,28 +297,34 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier= if for_supplier: target_doc.supplier = for_supplier args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True) - target_doc.currency = args.currency or get_party_account_currency('Supplier', for_supplier, source.company) - target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value('Buying Settings', None, 'buying_price_list') + target_doc.currency = args.currency or get_party_account_currency( + "Supplier", for_supplier, source.company + ) + target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value( + "Buying Settings", None, "buying_price_list" + ) set_missing_values(source, target_doc) - doclist = get_mapped_doc("Request for Quotation", source_name, { - "Request for Quotation": { - "doctype": "Supplier Quotation", - "validation": { - "docstatus": ["=", 1] - } - }, - "Request for Quotation Item": { - "doctype": "Supplier Quotation Item", - "field_map": { - "name": "request_for_quotation_item", - "parent": "request_for_quotation" + doclist = get_mapped_doc( + "Request for Quotation", + source_name, + { + "Request for Quotation": { + "doctype": "Supplier Quotation", + "validation": {"docstatus": ["=", 1]}, }, - } - }, target_doc, postprocess) + "Request for Quotation Item": { + "doctype": "Supplier Quotation Item", + "field_map": {"name": "request_for_quotation_item", "parent": "request_for_quotation"}, + }, + }, + target_doc, + postprocess, + ) return doclist + # This method is used to make supplier quotation from supplier's portal. @frappe.whitelist() def create_supplier_quotation(doc): @@ -292,15 +332,19 @@ def create_supplier_quotation(doc): doc = json.loads(doc) try: - sq_doc = frappe.get_doc({ - "doctype": "Supplier Quotation", - "supplier": doc.get('supplier'), - "terms": doc.get("terms"), - "company": doc.get("company"), - "currency": doc.get('currency') or get_party_account_currency('Supplier', doc.get('supplier'), doc.get('company')), - "buying_price_list": doc.get('buying_price_list') or frappe.db.get_value('Buying Settings', None, 'buying_price_list') - }) - add_items(sq_doc, doc.get('supplier'), doc.get('items')) + sq_doc = frappe.get_doc( + { + "doctype": "Supplier Quotation", + "supplier": doc.get("supplier"), + "terms": doc.get("terms"), + "company": doc.get("company"), + "currency": doc.get("currency") + or get_party_account_currency("Supplier", doc.get("supplier"), doc.get("company")), + "buying_price_list": doc.get("buying_price_list") + or frappe.db.get_value("Buying Settings", None, "buying_price_list"), + } + ) + add_items(sq_doc, doc.get("supplier"), doc.get("items")) sq_doc.flags.ignore_permissions = True sq_doc.run_method("set_missing_values") sq_doc.save() @@ -309,6 +353,7 @@ def create_supplier_quotation(doc): except Exception: return None + def add_items(sq_doc, supplier, items): for data in items: if data.get("qty") > 0: @@ -317,21 +362,36 @@ def add_items(sq_doc, supplier, items): create_rfq_items(sq_doc, supplier, data) + def create_rfq_items(sq_doc, supplier, data): args = {} - for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor', - 'warehouse', 'material_request', 'material_request_item', 'stock_qty']: + for field in [ + "item_code", + "item_name", + "description", + "qty", + "rate", + "conversion_factor", + "warehouse", + "material_request", + "material_request_item", + "stock_qty", + ]: args[field] = data.get(field) - args.update({ - "request_for_quotation_item": data.name, - "request_for_quotation": data.parent, - "supplier_part_no": frappe.db.get_value("Item Supplier", - {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no") - }) + args.update( + { + "request_for_quotation_item": data.name, + "request_for_quotation": data.parent, + "supplier_part_no": frappe.db.get_value( + "Item Supplier", {"parent": data.item_code, "supplier": supplier}, "supplier_part_no" + ), + } + ) + + sq_doc.append("items", args) - sq_doc.append('items', args) @frappe.whitelist() def get_pdf(doctype, name, supplier): @@ -339,15 +399,18 @@ def get_pdf(doctype, name, supplier): if doc: download_pdf(doctype, name, doc=doc) + def get_rfq_doc(doctype, name, supplier): if supplier: doc = frappe.get_doc(doctype, name) doc.update_supplier_part_no(supplier) return doc + @frappe.whitelist() -def get_item_from_material_requests_based_on_supplier(source_name, target_doc = None): - mr_items_list = frappe.db.sql(""" +def get_item_from_material_requests_based_on_supplier(source_name, target_doc=None): + mr_items_list = frappe.db.sql( + """ SELECT mr.name, mr_item.item_code FROM @@ -362,52 +425,65 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc = AND mr.status != "Stopped" AND mr.material_request_type = "Purchase" AND mr.docstatus = 1 - AND mr.per_ordered < 99.99""", {"supplier": source_name}, as_dict=1) + AND mr.per_ordered < 99.99""", + {"supplier": source_name}, + as_dict=1, + ) material_requests = {} for d in mr_items_list: material_requests.setdefault(d.name, []).append(d.item_code) for mr, items in material_requests.items(): - target_doc = get_mapped_doc("Material Request", mr, { - "Material Request": { - "doctype": "Request for Quotation", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"], - } + target_doc = get_mapped_doc( + "Material Request", + mr, + { + "Material Request": { + "doctype": "Request for Quotation", + "validation": { + "docstatus": ["=", 1], + "material_request_type": ["=", "Purchase"], + }, + }, + "Material Request Item": { + "doctype": "Request for Quotation Item", + "condition": lambda row: row.item_code in items, + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "uom"], + ], + }, }, - "Material Request Item": { - "doctype": "Request for Quotation Item", - "condition": lambda row: row.item_code in items, - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "uom"] - ] - } - }, target_doc) + target_doc, + ) return target_doc + @frappe.whitelist() def get_supplier_tag(): filters = {"document_type": "Supplier"} - tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag)) + tags = list( + set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag) + ) return tags + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters): conditions = "" if txt: - conditions += "and rfq.name like '%%"+txt+"%%' " + conditions += "and rfq.name like '%%" + txt + "%%' " if filters.get("transaction_date"): conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date")) - rfq_data = frappe.db.sql(""" + rfq_data = frappe.db.sql( + """ select distinct rfq.name, rfq.transaction_date, rfq.company @@ -420,8 +496,11 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt and rfq.company = '{1}' {2} order by rfq.transaction_date ASC - limit %(page_len)s offset %(start)s """ \ - .format(filters.get("supplier"), filters.get("company"), conditions), - {"page_len": page_len, "start": start}, as_dict=1) + limit %(page_len)s offset %(start)s """.format( + filters.get("supplier"), filters.get("company"), conditions + ), + {"page_len": page_len, "start": start}, + as_dict=1, + ) return rfq_data diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation_dashboard.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation_dashboard.py index 21ef33294e3..505e3e0f674 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation_dashboard.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation_dashboard.py @@ -1,12 +1,8 @@ - - def get_data(): return { - 'docstatus': 1, - 'fieldname': 'request_for_quotation', - 'transactions': [ - { - 'items': ['Supplier Quotation'] - }, - ] + "docstatus": 1, + "fieldname": "request_for_quotation", + "transactions": [ + {"items": ["Supplier Quotation"]}, + ], } diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 5b2112424c9..dcdba095fbf 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -20,36 +20,36 @@ class TestRequestforQuotation(FrappeTestCase): def test_quote_status(self): rfq = make_request_for_quotation() - self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Pending') - self.assertEqual(rfq.get('suppliers')[1].quote_status, 'Pending') + self.assertEqual(rfq.get("suppliers")[0].quote_status, "Pending") + self.assertEqual(rfq.get("suppliers")[1].quote_status, "Pending") # Submit the first supplier quotation - sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier) + sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[0].supplier) sq.submit() - rfq.update_rfq_supplier_status() #rfq.get('suppliers')[1].supplier) + rfq.update_rfq_supplier_status() # rfq.get('suppliers')[1].supplier) - self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Received') - self.assertEqual(rfq.get('suppliers')[1].quote_status, 'Pending') + self.assertEqual(rfq.get("suppliers")[0].quote_status, "Received") + self.assertEqual(rfq.get("suppliers")[1].quote_status, "Pending") def test_make_supplier_quotation(self): rfq = make_request_for_quotation() - sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier) + sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[0].supplier) sq.submit() - sq1 = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[1].supplier) + sq1 = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[1].supplier) sq1.submit() - self.assertEqual(sq.supplier, rfq.get('suppliers')[0].supplier) - self.assertEqual(sq.get('items')[0].request_for_quotation, rfq.name) - self.assertEqual(sq.get('items')[0].item_code, "_Test Item") - self.assertEqual(sq.get('items')[0].qty, 5) + self.assertEqual(sq.supplier, rfq.get("suppliers")[0].supplier) + self.assertEqual(sq.get("items")[0].request_for_quotation, rfq.name) + self.assertEqual(sq.get("items")[0].item_code, "_Test Item") + self.assertEqual(sq.get("items")[0].qty, 5) - self.assertEqual(sq1.supplier, rfq.get('suppliers')[1].supplier) - self.assertEqual(sq1.get('items')[0].request_for_quotation, rfq.name) - self.assertEqual(sq1.get('items')[0].item_code, "_Test Item") - self.assertEqual(sq1.get('items')[0].qty, 5) + self.assertEqual(sq1.supplier, rfq.get("suppliers")[1].supplier) + self.assertEqual(sq1.get("items")[0].request_for_quotation, rfq.name) + self.assertEqual(sq1.get("items")[0].item_code, "_Test Item") + self.assertEqual(sq1.get("items")[0].qty, 5) def test_make_supplier_quotation_with_special_characters(self): frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1) @@ -60,52 +60,50 @@ class TestRequestforQuotation(FrappeTestCase): rfq = make_request_for_quotation(supplier_data=supplier_wt_appos) - sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier_wt_appos[0].get("supplier")) + sq = make_supplier_quotation_from_rfq( + rfq.name, for_supplier=supplier_wt_appos[0].get("supplier") + ) sq.submit() frappe.form_dict = frappe.local("form_dict") frappe.form_dict.name = rfq.name - self.assertEqual( - check_supplier_has_docname_access(supplier_wt_appos[0].get('supplier')), - True - ) + self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True) # reset form_dict frappe.form_dict.name = None def test_make_supplier_quotation_from_portal(self): rfq = make_request_for_quotation() - rfq.get('items')[0].rate = 100 + rfq.get("items")[0].rate = 100 rfq.supplier = rfq.suppliers[0].supplier supplier_quotation_name = create_supplier_quotation(rfq) - supplier_quotation_doc = frappe.get_doc('Supplier Quotation', supplier_quotation_name) + supplier_quotation_doc = frappe.get_doc("Supplier Quotation", supplier_quotation_name) - self.assertEqual(supplier_quotation_doc.supplier, rfq.get('suppliers')[0].supplier) - self.assertEqual(supplier_quotation_doc.get('items')[0].request_for_quotation, rfq.name) - self.assertEqual(supplier_quotation_doc.get('items')[0].item_code, "_Test Item") - self.assertEqual(supplier_quotation_doc.get('items')[0].qty, 5) - self.assertEqual(supplier_quotation_doc.get('items')[0].amount, 500) + self.assertEqual(supplier_quotation_doc.supplier, rfq.get("suppliers")[0].supplier) + self.assertEqual(supplier_quotation_doc.get("items")[0].request_for_quotation, rfq.name) + self.assertEqual(supplier_quotation_doc.get("items")[0].item_code, "_Test Item") + self.assertEqual(supplier_quotation_doc.get("items")[0].qty, 5) + self.assertEqual(supplier_quotation_doc.get("items")[0].amount, 500) def test_make_multi_uom_supplier_quotation(self): item_code = "_Test Multi UOM RFQ Item" - if not frappe.db.exists('Item', item_code): - item = make_item(item_code, {'stock_uom': '_Test UOM'}) - row = item.append('uoms', { - 'uom': 'Kg', - 'conversion_factor': 2 - }) + if not frappe.db.exists("Item", item_code): + item = make_item(item_code, {"stock_uom": "_Test UOM"}) + row = item.append("uoms", {"uom": "Kg", "conversion_factor": 2}) row.db_update() - rfq = make_request_for_quotation(item_code="_Test Multi UOM RFQ Item", uom="Kg", conversion_factor=2) - rfq.get('items')[0].rate = 100 + rfq = make_request_for_quotation( + item_code="_Test Multi UOM RFQ Item", uom="Kg", conversion_factor=2 + ) + rfq.get("items")[0].rate = 100 rfq.supplier = rfq.suppliers[0].supplier self.assertEqual(rfq.items[0].stock_qty, 10) supplier_quotation_name = create_supplier_quotation(rfq) - supplier_quotation = frappe.get_doc('Supplier Quotation', supplier_quotation_name) + supplier_quotation = frappe.get_doc("Supplier Quotation", supplier_quotation_name) self.assertEqual(supplier_quotation.items[0].qty, 5) self.assertEqual(supplier_quotation.items[0].stock_qty, 10) @@ -116,58 +114,62 @@ class TestRequestforQuotation(FrappeTestCase): rfq = make_rfq(opportunity.name) self.assertEqual(len(rfq.get("items")), len(opportunity.get("items"))) - rfq.message_for_supplier = 'Please supply the specified items at the best possible rates.' + rfq.message_for_supplier = "Please supply the specified items at the best possible rates." for item in rfq.items: item.warehouse = "_Test Warehouse - _TC" for data in supplier_data: - rfq.append('suppliers', data) + rfq.append("suppliers", data) - rfq.status = 'Draft' + rfq.status = "Draft" rfq.submit() + def make_request_for_quotation(**args): """ :param supplier_data: List containing supplier data """ args = frappe._dict(args) supplier_data = args.get("supplier_data") if args.get("supplier_data") else get_supplier_data() - rfq = frappe.new_doc('Request for Quotation') + rfq = frappe.new_doc("Request for Quotation") rfq.transaction_date = nowdate() - rfq.status = 'Draft' - rfq.company = '_Test Company' - rfq.message_for_supplier = 'Please supply the specified items at the best possible rates.' + rfq.status = "Draft" + rfq.company = "_Test Company" + rfq.message_for_supplier = "Please supply the specified items at the best possible rates." for data in supplier_data: - rfq.append('suppliers', data) + rfq.append("suppliers", data) - rfq.append("items", { - "item_code": args.item_code or "_Test Item", - "description": "_Test Item", - "uom": args.uom or "_Test UOM", - "stock_uom": args.stock_uom or "_Test UOM", - "qty": args.qty or 5, - "conversion_factor": args.conversion_factor or 1.0, - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "schedule_date": nowdate() - }) + rfq.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "description": "_Test Item", + "uom": args.uom or "_Test UOM", + "stock_uom": args.stock_uom or "_Test UOM", + "qty": args.qty or 5, + "conversion_factor": args.conversion_factor or 1.0, + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "schedule_date": nowdate(), + }, + ) rfq.submit() return rfq -def get_supplier_data(): - return [{ - "supplier": "_Test Supplier", - "supplier_name": "_Test Supplier" - }, - { - "supplier": "_Test Supplier 1", - "supplier_name": "_Test Supplier 1" - }] -supplier_wt_appos = [{ - "supplier": "_Test Supplier '1", - "supplier_name": "_Test Supplier '1", -}] +def get_supplier_data(): + return [ + {"supplier": "_Test Supplier", "supplier_name": "_Test Supplier"}, + {"supplier": "_Test Supplier 1", "supplier_name": "_Test Supplier 1"}, + ] + + +supplier_wt_appos = [ + { + "supplier": "_Test Supplier '1", + "supplier_name": "_Test Supplier '1", + } +] diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 14d2ccdb50d..6ddc2809c7f 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -30,27 +30,27 @@ class Supplier(TransactionBase): def before_save(self): if not self.on_hold: - self.hold_type = '' - self.release_date = '' + self.hold_type = "" + self.release_date = "" elif self.on_hold and not self.hold_type: - self.hold_type = 'All' + self.hold_type = "All" def load_dashboard_info(self): info = get_dashboard_info(self.doctype, self.name) - self.set_onload('dashboard_info', info) + self.set_onload("dashboard_info", info) def autoname(self): - supp_master_name = frappe.defaults.get_global_default('supp_master_name') - if supp_master_name == 'Supplier Name': + supp_master_name = frappe.defaults.get_global_default("supp_master_name") + if supp_master_name == "Supplier Name": self.name = self.supplier_name - elif supp_master_name == 'Naming Series': + elif supp_master_name == "Naming Series": set_name_by_naming_series(self) else: self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) def on_update(self): if not self.naming_series: - self.naming_series = '' + self.naming_series = "" self.create_primary_contact() self.create_primary_address() @@ -59,7 +59,7 @@ class Supplier(TransactionBase): self.flags.is_new_doc = self.is_new() # validation for Naming Series mandatory field... - if frappe.defaults.get_global_default('supp_master_name') == 'Naming Series': + if frappe.defaults.get_global_default("supp_master_name") == "Naming Series": if not self.naming_series: msgprint(_("Series is mandatory"), raise_exception=1) @@ -68,13 +68,13 @@ class Supplier(TransactionBase): @frappe.whitelist() def get_supplier_group_details(self): - doc = frappe.get_doc('Supplier Group', self.supplier_group) + doc = frappe.get_doc("Supplier Group", self.supplier_group) self.payment_terms = "" self.accounts = [] if doc.accounts: for account in doc.accounts: - child = self.append('accounts') + child = self.append("accounts") child.company = account.company child.account = account.account @@ -84,12 +84,22 @@ class Supplier(TransactionBase): self.save() def validate_internal_supplier(self): - internal_supplier = frappe.db.get_value("Supplier", - {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + internal_supplier = frappe.db.get_value( + "Supplier", + { + "is_internal_supplier": 1, + "represents_company": self.represents_company, + "name": ("!=", self.name), + }, + "name", + ) if internal_supplier: - frappe.throw(_("Internal Supplier for company {0} already exists").format( - frappe.bold(self.represents_company))) + frappe.throw( + _("Internal Supplier for company {0} already exists").format( + frappe.bold(self.represents_company) + ) + ) def create_primary_contact(self): from erpnext.selling.doctype.customer.customer import make_contact @@ -97,16 +107,16 @@ class Supplier(TransactionBase): if not self.supplier_primary_contact: if self.mobile_no or self.email_id: contact = make_contact(self) - self.db_set('supplier_primary_contact', contact.name) - self.db_set('mobile_no', self.mobile_no) - self.db_set('email_id', self.email_id) + self.db_set("supplier_primary_contact", contact.name) + self.db_set("mobile_no", self.mobile_no) + self.db_set("email_id", self.email_id) def create_primary_address(self): from frappe.contacts.doctype.address.address import get_address_display from erpnext.selling.doctype.customer.customer import make_address - if self.flags.is_new_doc and self.get('address_line1'): + if self.flags.is_new_doc and self.get("address_line1"): address = make_address(self) address_display = get_address_display(address.name) @@ -115,7 +125,8 @@ class Supplier(TransactionBase): def on_trash(self): if self.supplier_primary_contact: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSupplier` SET supplier_primary_contact=null, @@ -123,41 +134,48 @@ class Supplier(TransactionBase): mobile_no=null, email_id=null, primary_address=null - WHERE name=%(name)s""", {"name": self.name}) + WHERE name=%(name)s""", + {"name": self.name}, + ) - delete_contact_and_address('Supplier', self.name) + delete_contact_and_address("Supplier", self.name) def after_rename(self, olddn, newdn, merge=False): - if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name': + if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name": frappe.db.set(self, "supplier_name", newdn) def create_onboarding_docs(self, args): - company = frappe.defaults.get_defaults().get('company') or \ - frappe.db.get_single_value('Global Defaults', 'default_company') + company = frappe.defaults.get_defaults().get("company") or frappe.db.get_single_value( + "Global Defaults", "default_company" + ) - for i in range(1, args.get('max_count')): - supplier = args.get('supplier_name_' + str(i)) + for i in range(1, args.get("max_count")): + supplier = args.get("supplier_name_" + str(i)) if supplier: try: - doc = frappe.get_doc({ - 'doctype': self.doctype, - 'supplier_name': supplier, - 'supplier_group': _('Local'), - 'company': company - }).insert() + doc = frappe.get_doc( + { + "doctype": self.doctype, + "supplier_name": supplier, + "supplier_group": _("Local"), + "company": company, + } + ).insert() - if args.get('supplier_email_' + str(i)): + if args.get("supplier_email_" + str(i)): from erpnext.selling.doctype.customer.customer import create_contact - create_contact(supplier, 'Supplier', - doc.name, args.get('supplier_email_' + str(i))) + + create_contact(supplier, "Supplier", doc.name, args.get("supplier_email_" + str(i))) except frappe.NameError: pass + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): supplier = filters.get("supplier") - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `tabContact`.name from `tabContact`, `tabDynamic Link` @@ -166,7 +184,6 @@ def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, fil and `tabDynamic Link`.link_name = %(supplier)s and `tabDynamic Link`.link_doctype = 'Supplier' and `tabContact`.name like %(txt)s - """, { - 'supplier': supplier, - 'txt': '%%%s%%' % txt - }) + """, + {"supplier": supplier, "txt": "%%%s%%" % txt}, + ) diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index cfa0375e2ed..11bb06e0caa 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -1,32 +1,18 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Supplier. See timeline below for details'), - 'fieldname': 'supplier', - 'non_standard_fieldnames': { - 'Payment Entry': 'party_name', - 'Bank Account': 'party' - }, - 'transactions': [ - { - 'label': _('Procurement'), - 'items': ['Request for Quotation', 'Supplier Quotation'] - }, - { - 'label': _('Orders'), - 'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] - }, - { - 'label': _('Payments'), - 'items': ['Payment Entry', 'Bank Account'] - }, - { - 'label': _('Pricing'), - 'items': ['Pricing Rule'] - } - ] + "heatmap": True, + "heatmap_message": _( + "This is based on transactions against this Supplier. See timeline below for details" + ), + "fieldname": "supplier", + "non_standard_fieldnames": {"Payment Entry": "party_name", "Bank Account": "party"}, + "transactions": [ + {"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]}, + {"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, + {"label": _("Payments"), "items": ["Payment Entry", "Bank Account"]}, + {"label": _("Pricing"), "items": ["Pricing Rule"]}, + ], } diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 662a758751c..b3cb0e82bdd 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -9,156 +9,164 @@ from frappe.tests.utils import FrappeTestCase from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled -test_dependencies = ['Payment Term', 'Payment Terms Template'] -test_records = frappe.get_test_records('Supplier') +test_dependencies = ["Payment Term", "Payment Terms Template"] +test_records = frappe.get_test_records("Supplier") class TestSupplier(FrappeTestCase): - def test_get_supplier_group_details(self): - doc = frappe.new_doc("Supplier Group") - doc.supplier_group_name = "_Testing Supplier Group" - doc.payment_terms = "_Test Payment Term Template 3" - doc.accounts = [] - test_account_details = { - "company": "_Test Company", - "account": "Creditors - _TC", - } - doc.append("accounts", test_account_details) - doc.save() - s_doc = frappe.new_doc("Supplier") - s_doc.supplier_name = "Testing Supplier" - s_doc.supplier_group = "_Testing Supplier Group" - s_doc.payment_terms = "" - s_doc.accounts = [] - s_doc.insert() - s_doc.get_supplier_group_details() - self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") - self.assertEqual(s_doc.accounts[0].company, "_Test Company") - self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") - s_doc.delete() - doc.delete() + def test_get_supplier_group_details(self): + doc = frappe.new_doc("Supplier Group") + doc.supplier_group_name = "_Testing Supplier Group" + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + doc.append("accounts", test_account_details) + doc.save() + s_doc = frappe.new_doc("Supplier") + s_doc.supplier_name = "Testing Supplier" + s_doc.supplier_group = "_Testing Supplier Group" + s_doc.payment_terms = "" + s_doc.accounts = [] + s_doc.insert() + s_doc.get_supplier_group_details() + self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(s_doc.accounts[0].company, "_Test Company") + self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") + s_doc.delete() + doc.delete() - def test_supplier_default_payment_terms(self): - # Payment Term based on Days after invoice date - frappe.db.set_value( - "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3") + def test_supplier_default_payment_terms(self): + # Payment Term based on Days after invoice date + frappe.db.set_value( + "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3" + ) - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2016-02-21") + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2016-02-21") - due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2017-02-21") + due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2017-02-21") - # Payment Term based on last day of month - frappe.db.set_value( - "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1") + # Payment Term based on last day of month + frappe.db.set_value( + "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1" + ) - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2016-02-29") + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2016-02-29") - due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2017-02-28") + due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2017-02-28") - frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "") + frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "") - # Set credit limit for the supplier group instead of supplier and evaluate the due date - frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3") + # Set credit limit for the supplier group instead of supplier and evaluate the due date + frappe.db.set_value( + "Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3" + ) - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2016-02-21") + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2016-02-21") - # Payment terms for Supplier Group instead of supplier and evaluate the due date - frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1") + # Payment terms for Supplier Group instead of supplier and evaluate the due date + frappe.db.set_value( + "Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1" + ) - # Leap year - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2016-02-29") - # # Non Leap year - due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2017-02-28") + # Leap year + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2016-02-29") + # # Non Leap year + due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2017-02-28") - # Supplier with no default Payment Terms Template - frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "") - frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "") + # Supplier with no default Payment Terms Template + frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "") + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "") - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier") - self.assertEqual(due_date, "2016-01-22") - # # Non Leap year - due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier") - self.assertEqual(due_date, "2017-01-22") + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier") + self.assertEqual(due_date, "2016-01-22") + # # Non Leap year + due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier") + self.assertEqual(due_date, "2017-01-22") - def test_supplier_disabled(self): - make_test_records("Item") + def test_supplier_disabled(self): + make_test_records("Item") - frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1) + frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1) - from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order - po = create_purchase_order(do_not_save=True) + po = create_purchase_order(do_not_save=True) - self.assertRaises(PartyDisabled, po.save) + self.assertRaises(PartyDisabled, po.save) - frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0) + frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0) - po.save() + po.save() - def test_supplier_country(self): - # Test that country field exists in Supplier DocType - supplier = frappe.get_doc('Supplier', '_Test Supplier with Country') - self.assertTrue('country' in supplier.as_dict()) + def test_supplier_country(self): + # Test that country field exists in Supplier DocType + supplier = frappe.get_doc("Supplier", "_Test Supplier with Country") + self.assertTrue("country" in supplier.as_dict()) - # Test if test supplier field record is 'Greece' - self.assertEqual(supplier.country, "Greece") + # Test if test supplier field record is 'Greece' + self.assertEqual(supplier.country, "Greece") - # Test update Supplier instance country value - supplier = frappe.get_doc('Supplier', '_Test Supplier') - supplier.country = 'Greece' - supplier.save() - self.assertEqual(supplier.country, "Greece") + # Test update Supplier instance country value + supplier = frappe.get_doc("Supplier", "_Test Supplier") + supplier.country = "Greece" + supplier.save() + self.assertEqual(supplier.country, "Greece") - def test_party_details_tax_category(self): - from erpnext.accounts.party import get_party_details + def test_party_details_tax_category(self): + from erpnext.accounts.party import get_party_details - frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing") + frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing") - # Tax Category without Address - details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") - self.assertEqual(details.tax_category, "_Test Tax Category 1") + # Tax Category without Address + details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") + self.assertEqual(details.tax_category, "_Test Tax Category 1") - address = frappe.get_doc(dict( - doctype='Address', - address_title='_Test Address With Tax Category', - tax_category='_Test Tax Category 2', - address_type='Billing', - address_line1='Station Road', - city='_Test City', - country='India', - links=[dict( - link_doctype='Supplier', - link_name='_Test Supplier With Tax Category' - )] - )).insert() + address = frappe.get_doc( + dict( + doctype="Address", + address_title="_Test Address With Tax Category", + tax_category="_Test Tax Category 2", + address_type="Billing", + address_line1="Station Road", + city="_Test City", + country="India", + links=[dict(link_doctype="Supplier", link_name="_Test Supplier With Tax Category")], + ) + ).insert() - # Tax Category with Address - details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") - self.assertEqual(details.tax_category, "_Test Tax Category 2") + # Tax Category with Address + details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") + self.assertEqual(details.tax_category, "_Test Tax Category 2") + + # Rollback + address.delete() - # Rollback - address.delete() def create_supplier(**args): - args = frappe._dict(args) + args = frappe._dict(args) - try: - doc = frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": args.supplier_name, - "supplier_group": args.supplier_group or "Services", - "supplier_type": args.supplier_type or "Company", - "tax_withholding_category": args.tax_withholding_category - }).insert() + try: + doc = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": args.supplier_name, + "supplier_group": args.supplier_group or "Services", + "supplier_type": args.supplier_type or "Company", + "tax_withholding_category": args.tax_withholding_category, + } + ).insert() - return doc + return doc - except frappe.DuplicateEntryError: - return frappe.get_doc("Supplier", args.supplier_name) + except frappe.DuplicateEntryError: + return frappe.get_doc("Supplier", args.supplier_name) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 023c95d697d..567e41fb61f 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -72,8 +72,8 @@ "section_break_46", "base_grand_total", "base_rounding_adjustment", - "base_in_words", "base_rounded_total", + "base_in_words", "column_break4", "grand_total", "rounding_adjustment", @@ -635,6 +635,7 @@ "fieldname": "rounded_total", "fieldtype": "Currency", "label": "Rounded Total", + "options": "currency", "read_only": 1 }, { @@ -810,7 +811,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-11 06:43:20.924080", + "modified": "2022-03-14 16:13:20.284572", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", @@ -875,6 +876,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title" } \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index d65ab94a6d3..78f3317bb42 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -10,9 +10,8 @@ from frappe.utils import flt, getdate, nowdate from erpnext.buying.utils import validate_for_items from erpnext.controllers.buying_controller import BuyingController -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class SupplierQuotation(BuyingController): def validate(self): @@ -22,8 +21,8 @@ class SupplierQuotation(BuyingController): self.status = "Draft" from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Draft", "Submitted", "Stopped", - "Cancelled"]) + + validate_status(self.status, ["Draft", "Submitted", "Stopped", "Cancelled"]) validate_for_items(self) self.validate_with_previous_doc() @@ -42,17 +41,19 @@ class SupplierQuotation(BuyingController): pass def validate_with_previous_doc(self): - super(SupplierQuotation, self).validate_with_previous_doc({ - "Material Request": { - "ref_dn_field": "prevdoc_docname", - "compare_fields": [["company", "="]], - }, - "Material Request Item": { - "ref_dn_field": "prevdoc_detail_docname", - "compare_fields": [["item_code", "="], ["uom", "="]], - "is_child_table": True + super(SupplierQuotation, self).validate_with_previous_doc( + { + "Material Request": { + "ref_dn_field": "prevdoc_docname", + "compare_fields": [["company", "="]], + }, + "Material Request Item": { + "ref_dn_field": "prevdoc_detail_docname", + "compare_fields": [["item_code", "="], ["uom", "="]], + "is_child_table": True, + }, } - }) + ) def validate_valid_till(self): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): @@ -64,18 +65,28 @@ class SupplierQuotation(BuyingController): if item.request_for_quotation: rfq_list.add(item.request_for_quotation) for rfq in rfq_list: - doc = frappe.get_doc('Request for Quotation', rfq) - doc_sup = frappe.get_all('Request for Quotation Supplier', filters= - {'parent': doc.name, 'supplier': self.supplier}, fields=['name', 'quote_status']) + doc = frappe.get_doc("Request for Quotation", rfq) + doc_sup = frappe.get_all( + "Request for Quotation Supplier", + filters={"parent": doc.name, "supplier": self.supplier}, + fields=["name", "quote_status"], + ) doc_sup = doc_sup[0] if doc_sup else None if not doc_sup: - frappe.throw(_("Supplier {0} not found in {1}").format(self.supplier, - " Request for Quotation {0} ".format(doc.name))) + frappe.throw( + _("Supplier {0} not found in {1}").format( + self.supplier, + " Request for Quotation {0} ".format( + doc.name + ), + ) + ) - quote_status = _('Received') + quote_status = _("Received") for item in doc.items: - sqi_count = frappe.db.sql(""" + sqi_count = frappe.db.sql( + """ SELECT COUNT(sqi.name) as count FROM @@ -86,30 +97,41 @@ class SupplierQuotation(BuyingController): AND sq.name != %(me)s AND sqi.request_for_quotation_item = %(rqi)s AND sqi.parent = sq.name""", - {"supplier": self.supplier, "rqi": item.name, 'me': self.name}, as_dict=1)[0] - self_count = sum(my_item.request_for_quotation_item == item.name - for my_item in self.items) if include_me else 0 + {"supplier": self.supplier, "rqi": item.name, "me": self.name}, + as_dict=1, + )[0] + self_count = ( + sum(my_item.request_for_quotation_item == item.name for my_item in self.items) + if include_me + else 0 + ) if (sqi_count.count + self_count) == 0: - quote_status = _('Pending') + quote_status = _("Pending") + + frappe.db.set_value( + "Request for Quotation Supplier", doc_sup.name, "quote_status", quote_status + ) - frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'quote_status', quote_status) def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Supplier Quotation'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Supplier Quotation"), + } + ) return list_context + @frappe.whitelist() def make_purchase_order(source_name, target_doc=None): def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("get_schedule_dates") target.run_method("calculate_taxes_and_totals") @@ -117,53 +139,70 @@ def make_purchase_order(source_name, target_doc=None): def update_item(obj, target, source_parent): target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) - doclist = get_mapped_doc("Supplier Quotation", source_name, { - "Supplier Quotation": { - "doctype": "Purchase Order", - "validation": { - "docstatus": ["=", 1], - } + doclist = get_mapped_doc( + "Supplier Quotation", + source_name, + { + "Supplier Quotation": { + "doctype": "Purchase Order", + "validation": { + "docstatus": ["=", 1], + }, + }, + "Supplier Quotation Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "supplier_quotation_item"], + ["parent", "supplier_quotation"], + ["material_request", "material_request"], + ["material_request_item", "material_request_item"], + ["sales_order", "sales_order"], + ], + "postprocess": update_item, + }, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges", + }, }, - "Supplier Quotation Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "supplier_quotation_item"], - ["parent", "supplier_quotation"], - ["material_request", "material_request"], - ["material_request_item", "material_request_item"], - ["sales_order", "sales_order"] - ], - "postprocess": update_item - }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges", - }, - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) + doclist.set_onload("ignore_price_list", True) return doclist + @frappe.whitelist() def make_quotation(source_name, target_doc=None): - doclist = get_mapped_doc("Supplier Quotation", source_name, { - "Supplier Quotation": { - "doctype": "Quotation", - "field_map": { - "name": "supplier_quotation", - } + doclist = get_mapped_doc( + "Supplier Quotation", + source_name, + { + "Supplier Quotation": { + "doctype": "Quotation", + "field_map": { + "name": "supplier_quotation", + }, + }, + "Supplier Quotation Item": { + "doctype": "Quotation Item", + "condition": lambda doc: frappe.db.get_value("Item", doc.item_code, "is_sales_item") == 1, + "add_if_empty": True, + }, }, - "Supplier Quotation Item": { - "doctype": "Quotation Item", - "condition": lambda doc: frappe.db.get_value("Item", doc.item_code, "is_sales_item")==1, - "add_if_empty": True - } - }, target_doc) + target_doc, + ) return doclist + def set_expired_status(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSupplier Quotation` SET `status` = 'Expired' WHERE `status` not in ('Cancelled', 'Stopped') AND `valid_till` < %s - """, (nowdate())) + """, + (nowdate()), + ) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py index 1680efc53c7..369fc94deee 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py @@ -1,31 +1,18 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'supplier_quotation', - 'non_standard_fieldnames': { - 'Auto Repeat': 'reference_document' + "fieldname": "supplier_quotation", + "non_standard_fieldnames": {"Auto Repeat": "reference_document"}, + "internal_links": { + "Material Request": ["items", "material_request"], + "Request for Quotation": ["items", "request_for_quotation"], + "Project": ["items", "project"], }, - 'internal_links': { - 'Material Request': ['items', 'material_request'], - 'Request for Quotation': ['items', 'request_for_quotation'], - 'Project': ['items', 'project'], - }, - 'transactions': [ - { - 'label': _('Related'), - 'items': ['Purchase Order', 'Quotation'] - }, - { - 'label': _('Reference'), - 'items': ['Material Request', 'Request for Quotation', 'Project'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] - + "transactions": [ + {"label": _("Related"), "items": ["Purchase Order", "Quotation"]}, + {"label": _("Reference"), "items": ["Material Request", "Request for Quotation", "Project"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index a4d45975c30..13c851c7353 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -2,8 +2,6 @@ # License: GNU General Public License v3. See license.txt - - import frappe from frappe.tests.utils import FrappeTestCase @@ -14,8 +12,7 @@ class TestPurchaseOrder(FrappeTestCase): sq = frappe.copy_doc(test_records[0]).insert() - self.assertRaises(frappe.ValidationError, make_purchase_order, - sq.name) + self.assertRaises(frappe.ValidationError, make_purchase_order, sq.name) sq = frappe.get_doc("Supplier Quotation", sq.name) sq.submit() @@ -32,4 +29,5 @@ class TestPurchaseOrder(FrappeTestCase): po.insert() -test_records = frappe.get_test_records('Supplier Quotation') + +test_records = frappe.get_test_records("Supplier Quotation") diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py index 3bcc0debae9..992bc805a55 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py @@ -16,7 +16,6 @@ from erpnext.buying.doctype.supplier_scorecard_period.supplier_scorecard_period class SupplierScorecard(Document): - def validate(self): self.validate_standings() self.validate_criteria_weights() @@ -34,12 +33,16 @@ class SupplierScorecard(Document): for c1 in self.standings: for c2 in self.standings: if c1 != c2: - if (c1.max_grade > c2.min_grade and c1.min_grade < c2.max_grade): - throw(_('Overlap in scoring between {0} and {1}').format(c1.standing_name,c2.standing_name)) + if c1.max_grade > c2.min_grade and c1.min_grade < c2.max_grade: + throw(_("Overlap in scoring between {0} and {1}").format(c1.standing_name, c2.standing_name)) if c2.min_grade == score: score = c2.max_grade if score < 100: - throw(_('Unable to find score starting at {0}. You need to have standing scores covering 0 to 100').format(score)) + throw( + _( + "Unable to find score starting at {0}. You need to have standing scores covering 0 to 100" + ).format(score) + ) def validate_criteria_weights(self): @@ -48,10 +51,11 @@ class SupplierScorecard(Document): weight += c.weight if weight != 100: - throw(_('Criteria weights must add up to 100%')) + throw(_("Criteria weights must add up to 100%")) def calculate_total_score(self): - scorecards = frappe.db.sql(""" + scorecards = frappe.db.sql( + """ SELECT scp.name FROM @@ -61,18 +65,20 @@ class SupplierScorecard(Document): AND scp.docstatus = 1 ORDER BY scp.end_date DESC""", - {"sc": self.name}, as_dict=1) + {"sc": self.name}, + as_dict=1, + ) period = 0 total_score = 0 total_max_score = 0 for scp in scorecards: - my_sc = frappe.get_doc('Supplier Scorecard Period', scp.name) + my_sc = frappe.get_doc("Supplier Scorecard Period", scp.name) my_scp_weight = self.weighting_function - my_scp_weight = my_scp_weight.replace('{period_number}', str(period)) + my_scp_weight = my_scp_weight.replace("{period_number}", str(period)) - my_scp_maxweight = my_scp_weight.replace('{total_score}', '100') - my_scp_weight = my_scp_weight.replace('{total_score}', str(my_sc.total_score)) + my_scp_maxweight = my_scp_weight.replace("{total_score}", "100") + my_scp_weight = my_scp_weight.replace("{total_score}", str(my_sc.total_score)) max_score = my_sc.calculate_weighted_score(my_scp_maxweight) score = my_sc.calculate_weighted_score(my_scp_weight) @@ -81,24 +87,25 @@ class SupplierScorecard(Document): total_max_score += max_score period += 1 if total_max_score > 0: - self.supplier_score = round(100.0 * (total_score / total_max_score) ,1) + self.supplier_score = round(100.0 * (total_score / total_max_score), 1) else: - self.supplier_score = 100 + self.supplier_score = 100 def update_standing(self): # Get the setup document for standing in self.standings: - if (not standing.min_grade or (standing.min_grade <= self.supplier_score)) and \ - (not standing.max_grade or (standing.max_grade > self.supplier_score)): + if (not standing.min_grade or (standing.min_grade <= self.supplier_score)) and ( + not standing.max_grade or (standing.max_grade > self.supplier_score) + ): self.status = standing.standing_name self.indicator_color = standing.standing_color self.notify_supplier = standing.notify_supplier self.notify_employee = standing.notify_employee self.employee_link = standing.employee_link - #Update supplier standing info - for fieldname in ('prevent_pos', 'prevent_rfqs','warn_rfqs','warn_pos'): + # Update supplier standing info + for fieldname in ("prevent_pos", "prevent_rfqs", "warn_rfqs", "warn_pos"): self.set(fieldname, standing.get(fieldname)) frappe.db.set_value("Supplier", self.supplier, fieldname, self.get(fieldname)) @@ -109,7 +116,8 @@ def get_timeline_data(doctype, name): scs = frappe.get_doc(doctype, name) out = {} timeline_data = {} - scorecards = frappe.db.sql(""" + scorecards = frappe.db.sql( + """ SELECT sc.name FROM @@ -117,39 +125,48 @@ def get_timeline_data(doctype, name): WHERE sc.scorecard = %(scs)s AND sc.docstatus = 1""", - {"scs": scs.name}, as_dict=1) + {"scs": scs.name}, + as_dict=1, + ) for sc in scorecards: - start_date, end_date, total_score = frappe.db.get_value('Supplier Scorecard Period', sc.name, ['start_date', 'end_date', 'total_score']) + start_date, end_date, total_score = frappe.db.get_value( + "Supplier Scorecard Period", sc.name, ["start_date", "end_date", "total_score"] + ) for single_date in daterange(start_date, end_date): - timeline_data[time.mktime(single_date.timetuple())] = total_score + timeline_data[time.mktime(single_date.timetuple())] = total_score - out['timeline_data'] = timeline_data + out["timeline_data"] = timeline_data return out + def daterange(start_date, end_date): - for n in range(int ((end_date - start_date).days)+1): - yield start_date + timedelta(n) + for n in range(int((end_date - start_date).days) + 1): + yield start_date + timedelta(n) + def refresh_scorecards(): - scorecards = frappe.db.sql(""" + scorecards = frappe.db.sql( + """ SELECT sc.name FROM `tabSupplier Scorecard` sc""", - {}, as_dict=1) + {}, + as_dict=1, + ) for sc in scorecards: # Check to see if any new scorecard periods are created if make_all_scorecards(sc.name) > 0: # Save the scorecard to update the score and standings - frappe.get_doc('Supplier Scorecard', sc.name).save() + frappe.get_doc("Supplier Scorecard", sc.name).save() @frappe.whitelist() def make_all_scorecards(docname): - sc = frappe.get_doc('Supplier Scorecard', docname) - supplier = frappe.get_doc('Supplier',sc.supplier) + sc = frappe.get_doc("Supplier Scorecard", docname) + supplier = frappe.get_doc("Supplier", sc.supplier) start_date = getdate(supplier.creation) end_date = get_scorecard_date(sc.period, start_date) @@ -161,7 +178,8 @@ def make_all_scorecards(docname): while (start_date < todays) and (end_date <= todays): # check to make sure there is no scorecard period already created - scorecards = frappe.db.sql(""" + scorecards = frappe.db.sql( + """ SELECT scp.name FROM @@ -177,7 +195,9 @@ def make_all_scorecards(docname): AND scp.end_date > %(start_date)s)) ORDER BY scp.end_date DESC""", - {"sc": docname, "start_date": start_date, "end_date": end_date}, as_dict=1) + {"sc": docname, "start_date": start_date, "end_date": end_date}, + as_dict=1, + ) if len(scorecards) == 0: period_card = make_supplier_scorecard(docname, None) period_card.start_date = start_date @@ -189,82 +209,178 @@ def make_all_scorecards(docname): first_start_date = start_date last_end_date = end_date - start_date = getdate(add_days(end_date,1)) + start_date = getdate(add_days(end_date, 1)) end_date = get_scorecard_date(sc.period, start_date) if scp_count > 0: - frappe.msgprint(_("Created {0} scorecards for {1} between: ").format(scp_count, sc.supplier) + str(first_start_date) + " - " + str(last_end_date)) + frappe.msgprint( + _("Created {0} scorecards for {1} between: ").format(scp_count, sc.supplier) + + str(first_start_date) + + " - " + + str(last_end_date) + ) return scp_count + def get_scorecard_date(period, start_date): - if period == 'Per Week': - end_date = getdate(add_days(start_date,7)) - elif period == 'Per Month': + if period == "Per Week": + end_date = getdate(add_days(start_date, 7)) + elif period == "Per Month": end_date = get_last_day(start_date) - elif period == 'Per Year': - end_date = add_days(add_years(start_date,1), -1) + elif period == "Per Year": + end_date = add_days(add_years(start_date, 1), -1) return end_date + def make_default_records(): install_variable_docs = [ - {"param_name": "total_accepted_items", "variable_label": "Total Accepted Items", \ - "path": "get_total_accepted_items"}, - {"param_name": "total_accepted_amount", "variable_label": "Total Accepted Amount", \ - "path": "get_total_accepted_amount"}, - {"param_name": "total_rejected_items", "variable_label": "Total Rejected Items", \ - "path": "get_total_rejected_items"}, - {"param_name": "total_rejected_amount", "variable_label": "Total Rejected Amount", \ - "path": "get_total_rejected_amount"}, - {"param_name": "total_received_items", "variable_label": "Total Received Items", \ - "path": "get_total_received_items"}, - {"param_name": "total_received_amount", "variable_label": "Total Received Amount", \ - "path": "get_total_received_amount"}, - {"param_name": "rfq_response_days", "variable_label": "RFQ Response Days", \ - "path": "get_rfq_response_days"}, - {"param_name": "sq_total_items", "variable_label": "SQ Total Items", \ - "path": "get_sq_total_items"}, - {"param_name": "sq_total_number", "variable_label": "SQ Total Number", \ - "path": "get_sq_total_number"}, - {"param_name": "rfq_total_number", "variable_label": "RFQ Total Number", \ - "path": "get_rfq_total_number"}, - {"param_name": "rfq_total_items", "variable_label": "RFQ Total Items", \ - "path": "get_rfq_total_items"}, - {"param_name": "tot_item_days", "variable_label": "Total Item Days", \ - "path": "get_item_workdays"}, - {"param_name": "on_time_shipment_num", "variable_label": "# of On Time Shipments", "path": \ - "get_on_time_shipments"}, - {"param_name": "cost_of_delayed_shipments", "variable_label": "Cost of Delayed Shipments", \ - "path": "get_cost_of_delayed_shipments"}, - {"param_name": "cost_of_on_time_shipments", "variable_label": "Cost of On Time Shipments", \ - "path": "get_cost_of_on_time_shipments"}, - {"param_name": "total_working_days", "variable_label": "Total Working Days", \ - "path": "get_total_workdays"}, - {"param_name": "tot_cost_shipments", "variable_label": "Total Cost of Shipments", \ - "path": "get_total_cost_of_shipments"}, - {"param_name": "tot_days_late", "variable_label": "Total Days Late", \ - "path": "get_total_days_late"}, - {"param_name": "total_shipments", "variable_label": "Total Shipments", \ - "path": "get_total_shipments"} + { + "param_name": "total_accepted_items", + "variable_label": "Total Accepted Items", + "path": "get_total_accepted_items", + }, + { + "param_name": "total_accepted_amount", + "variable_label": "Total Accepted Amount", + "path": "get_total_accepted_amount", + }, + { + "param_name": "total_rejected_items", + "variable_label": "Total Rejected Items", + "path": "get_total_rejected_items", + }, + { + "param_name": "total_rejected_amount", + "variable_label": "Total Rejected Amount", + "path": "get_total_rejected_amount", + }, + { + "param_name": "total_received_items", + "variable_label": "Total Received Items", + "path": "get_total_received_items", + }, + { + "param_name": "total_received_amount", + "variable_label": "Total Received Amount", + "path": "get_total_received_amount", + }, + { + "param_name": "rfq_response_days", + "variable_label": "RFQ Response Days", + "path": "get_rfq_response_days", + }, + { + "param_name": "sq_total_items", + "variable_label": "SQ Total Items", + "path": "get_sq_total_items", + }, + { + "param_name": "sq_total_number", + "variable_label": "SQ Total Number", + "path": "get_sq_total_number", + }, + { + "param_name": "rfq_total_number", + "variable_label": "RFQ Total Number", + "path": "get_rfq_total_number", + }, + { + "param_name": "rfq_total_items", + "variable_label": "RFQ Total Items", + "path": "get_rfq_total_items", + }, + { + "param_name": "tot_item_days", + "variable_label": "Total Item Days", + "path": "get_item_workdays", + }, + { + "param_name": "on_time_shipment_num", + "variable_label": "# of On Time Shipments", + "path": "get_on_time_shipments", + }, + { + "param_name": "cost_of_delayed_shipments", + "variable_label": "Cost of Delayed Shipments", + "path": "get_cost_of_delayed_shipments", + }, + { + "param_name": "cost_of_on_time_shipments", + "variable_label": "Cost of On Time Shipments", + "path": "get_cost_of_on_time_shipments", + }, + { + "param_name": "total_working_days", + "variable_label": "Total Working Days", + "path": "get_total_workdays", + }, + { + "param_name": "tot_cost_shipments", + "variable_label": "Total Cost of Shipments", + "path": "get_total_cost_of_shipments", + }, + { + "param_name": "tot_days_late", + "variable_label": "Total Days Late", + "path": "get_total_days_late", + }, + { + "param_name": "total_shipments", + "variable_label": "Total Shipments", + "path": "get_total_shipments", + }, ] install_standing_docs = [ - {"min_grade": 0.0, "prevent_rfqs": 1, "notify_supplier": 0, "max_grade": 30.0, "prevent_pos": 1, \ - "standing_color": "Red", "notify_employee": 0, "standing_name": "Very Poor"}, - {"min_grade": 30.0, "prevent_rfqs": 1, "notify_supplier": 0, "max_grade": 50.0, "prevent_pos": 0, \ - "standing_color": "Red", "notify_employee": 0, "standing_name": "Poor"}, - {"min_grade": 50.0, "prevent_rfqs": 0, "notify_supplier": 0, "max_grade": 80.0, "prevent_pos": 0, \ - "standing_color": "Green", "notify_employee": 0, "standing_name": "Average"}, - {"min_grade": 80.0, "prevent_rfqs": 0, "notify_supplier": 0, "max_grade": 100.0, "prevent_pos": 0, \ - "standing_color": "Blue", "notify_employee": 0, "standing_name": "Excellent"}, + { + "min_grade": 0.0, + "prevent_rfqs": 1, + "notify_supplier": 0, + "max_grade": 30.0, + "prevent_pos": 1, + "standing_color": "Red", + "notify_employee": 0, + "standing_name": "Very Poor", + }, + { + "min_grade": 30.0, + "prevent_rfqs": 1, + "notify_supplier": 0, + "max_grade": 50.0, + "prevent_pos": 0, + "standing_color": "Red", + "notify_employee": 0, + "standing_name": "Poor", + }, + { + "min_grade": 50.0, + "prevent_rfqs": 0, + "notify_supplier": 0, + "max_grade": 80.0, + "prevent_pos": 0, + "standing_color": "Green", + "notify_employee": 0, + "standing_name": "Average", + }, + { + "min_grade": 80.0, + "prevent_rfqs": 0, + "notify_supplier": 0, + "max_grade": 100.0, + "prevent_pos": 0, + "standing_color": "Blue", + "notify_employee": 0, + "standing_name": "Excellent", + }, ] for d in install_variable_docs: try: - d['doctype'] = "Supplier Scorecard Variable" + d["doctype"] = "Supplier Scorecard Variable" frappe.get_doc(d).insert() except frappe.NameError: pass for d in install_standing_docs: try: - d['doctype'] = "Supplier Scorecard Standing" + d["doctype"] = "Supplier Scorecard Standing" frappe.get_doc(d).insert() except frappe.NameError: pass diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py index e021c9c6be1..e3557bd0d81 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_dashboard.py @@ -1,17 +1,11 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This covers all scorecards tied to this Setup'), - 'fieldname': 'supplier', - 'method' : 'erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.get_timeline_data', - 'transactions': [ - { - 'label': _('Scorecards'), - 'items': ['Supplier Scorecard Period'] - } - ] + "heatmap": True, + "heatmap_message": _("This covers all scorecards tied to this Setup"), + "fieldname": "supplier", + "method": "erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.get_timeline_data", + "transactions": [{"label": _("Scorecards"), "items": ["Supplier Scorecard Period"]}], } diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py index 8ecc2cd4667..2694f96fbe3 100644 --- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py @@ -7,7 +7,6 @@ from frappe.tests.utils import FrappeTestCase class TestSupplierScorecard(FrappeTestCase): - def test_create_scorecard(self): doc = make_supplier_scorecard().insert() self.assertEqual(doc.name, valid_scorecard[0].get("supplier")) @@ -17,7 +16,8 @@ class TestSupplierScorecard(FrappeTestCase): my_doc = make_supplier_scorecard() for d in my_doc.criteria: d.weight = 0 - self.assertRaises(frappe.ValidationError,my_doc.insert) + self.assertRaises(frappe.ValidationError, my_doc.insert) + def make_supplier_scorecard(): my_doc = frappe.get_doc(valid_scorecard[0]) @@ -36,95 +36,106 @@ def delete_test_scorecards(): my_doc = make_supplier_scorecard() if frappe.db.exists("Supplier Scorecard", my_doc.name): # Delete all the periods, then delete the scorecard - frappe.db.sql("""delete from `tabSupplier Scorecard Period` where scorecard = %(scorecard)s""", {'scorecard': my_doc.name}) - frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'""") - frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'""") - frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'""") + frappe.db.sql( + """delete from `tabSupplier Scorecard Period` where scorecard = %(scorecard)s""", + {"scorecard": my_doc.name}, + ) + frappe.db.sql( + """delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'""" + ) + frappe.db.sql( + """delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'""" + ) + frappe.db.sql( + """delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'""" + ) frappe.delete_doc(my_doc.doctype, my_doc.name) + valid_scorecard = [ { - "standings":[ + "standings": [ { - "min_grade":0.0,"name":"Very Poor", - "prevent_rfqs":1, - "notify_supplier":0, - "doctype":"Supplier Scorecard Scoring Standing", - "max_grade":30.0, - "prevent_pos":1, - "warn_pos":0, - "warn_rfqs":0, - "standing_color":"Red", - "notify_employee":0, - "standing_name":"Very Poor", - "parenttype":"Supplier Scorecard", - "parentfield":"standings" + "min_grade": 0.0, + "name": "Very Poor", + "prevent_rfqs": 1, + "notify_supplier": 0, + "doctype": "Supplier Scorecard Scoring Standing", + "max_grade": 30.0, + "prevent_pos": 1, + "warn_pos": 0, + "warn_rfqs": 0, + "standing_color": "Red", + "notify_employee": 0, + "standing_name": "Very Poor", + "parenttype": "Supplier Scorecard", + "parentfield": "standings", }, { - "min_grade":30.0, - "name":"Poor", - "prevent_rfqs":1, - "notify_supplier":0, - "doctype":"Supplier Scorecard Scoring Standing", - "max_grade":50.0, - "prevent_pos":0, - "warn_pos":0, - "warn_rfqs":0, - "standing_color":"Red", - "notify_employee":0, - "standing_name":"Poor", - "parenttype":"Supplier Scorecard", - "parentfield":"standings" + "min_grade": 30.0, + "name": "Poor", + "prevent_rfqs": 1, + "notify_supplier": 0, + "doctype": "Supplier Scorecard Scoring Standing", + "max_grade": 50.0, + "prevent_pos": 0, + "warn_pos": 0, + "warn_rfqs": 0, + "standing_color": "Red", + "notify_employee": 0, + "standing_name": "Poor", + "parenttype": "Supplier Scorecard", + "parentfield": "standings", }, { - "min_grade":50.0, - "name":"Average", - "prevent_rfqs":0, - "notify_supplier":0, - "doctype":"Supplier Scorecard Scoring Standing", - "max_grade":80.0, - "prevent_pos":0, - "warn_pos":0, - "warn_rfqs":0, - "standing_color":"Green", - "notify_employee":0, - "standing_name":"Average", - "parenttype":"Supplier Scorecard", - "parentfield":"standings" + "min_grade": 50.0, + "name": "Average", + "prevent_rfqs": 0, + "notify_supplier": 0, + "doctype": "Supplier Scorecard Scoring Standing", + "max_grade": 80.0, + "prevent_pos": 0, + "warn_pos": 0, + "warn_rfqs": 0, + "standing_color": "Green", + "notify_employee": 0, + "standing_name": "Average", + "parenttype": "Supplier Scorecard", + "parentfield": "standings", }, { - "min_grade":80.0, - "name":"Excellent", - "prevent_rfqs":0, - "notify_supplier":0, - "doctype":"Supplier Scorecard Scoring Standing", - "max_grade":100.0, - "prevent_pos":0, - "warn_pos":0, - "warn_rfqs":0, - "standing_color":"Blue", - "notify_employee":0, - "standing_name":"Excellent", - "parenttype":"Supplier Scorecard", - "parentfield":"standings" + "min_grade": 80.0, + "name": "Excellent", + "prevent_rfqs": 0, + "notify_supplier": 0, + "doctype": "Supplier Scorecard Scoring Standing", + "max_grade": 100.0, + "prevent_pos": 0, + "warn_pos": 0, + "warn_rfqs": 0, + "standing_color": "Blue", + "notify_employee": 0, + "standing_name": "Excellent", + "parenttype": "Supplier Scorecard", + "parentfield": "standings", + }, + ], + "prevent_pos": 0, + "period": "Per Month", + "doctype": "Supplier Scorecard", + "warn_pos": 0, + "warn_rfqs": 0, + "notify_supplier": 0, + "criteria": [ + { + "weight": 100.0, + "doctype": "Supplier Scorecard Scoring Criteria", + "criteria_name": "Delivery", + "formula": "100", } ], - "prevent_pos":0, - "period":"Per Month", - "doctype":"Supplier Scorecard", - "warn_pos":0, - "warn_rfqs":0, - "notify_supplier":0, - "criteria":[ - { - "weight":100.0, - "doctype":"Supplier Scorecard Scoring Criteria", - "criteria_name":"Delivery", - "formula": "100" - } - ], - "supplier":"_Test Supplier", - "name":"_Test Supplier", - "weighting_function":"{total_score} * max( 0, min ( 1 , (12 - {period_number}) / 12) )" + "supplier": "_Test Supplier", + "name": "_Test Supplier", + "weighting_function": "{total_score} * max( 0, min ( 1 , (12 - {period_number}) / 12) )", } ] diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py index 7cd18c31e8b..130adc97d40 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py @@ -9,7 +9,9 @@ from frappe import _ from frappe.model.document import Document -class InvalidFormulaVariable(frappe.ValidationError): pass +class InvalidFormulaVariable(frappe.ValidationError): + pass + class SupplierScorecardCriteria(Document): def validate(self): @@ -29,28 +31,34 @@ class SupplierScorecardCriteria(Document): mylist = re.finditer(regex, test_formula, re.MULTILINE | re.DOTALL) for dummy1, match in enumerate(mylist): for dummy2 in range(0, len(match.groups())): - test_formula = test_formula.replace('{' + match.group(1) + '}', "0") + test_formula = test_formula.replace("{" + match.group(1) + "}", "0") try: - frappe.safe_eval(test_formula, None, {'max':max, 'min': min}) + frappe.safe_eval(test_formula, None, {"max": max, "min": min}) except Exception: frappe.throw(_("Error evaluating the criteria formula")) + @frappe.whitelist() def get_criteria_list(): - criteria = frappe.db.sql(""" + criteria = frappe.db.sql( + """ SELECT scs.name FROM `tabSupplier Scorecard Criteria` scs""", - {}, as_dict=1) + {}, + as_dict=1, + ) return criteria + def get_variables(criteria_name): criteria = frappe.get_doc("Supplier Scorecard Criteria", criteria_name) return _get_variables(criteria) + def _get_variables(criteria): my_variables = [] regex = r"\{(.*?)\}" @@ -59,16 +67,19 @@ def _get_variables(criteria): for dummy1, match in enumerate(mylist): for dummy2 in range(0, len(match.groups())): try: - var = frappe.db.sql(""" + var = frappe.db.sql( + """ SELECT scv.variable_label, scv.description, scv.param_name, scv.path FROM `tabSupplier Scorecard Variable` scv WHERE param_name=%(param)s""", - {'param':match.group(1)}, as_dict=1)[0] + {"param": match.group(1)}, + as_dict=1, + )[0] my_variables.append(var) except Exception: - frappe.throw(_('Unable to find variable: ') + str(match.group(1)), InvalidFormulaVariable) + frappe.throw(_("Unable to find variable: ") + str(match.group(1)), InvalidFormulaVariable) return my_variables diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py index 7ff84c15e52..90468d6c168 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py @@ -12,19 +12,26 @@ class TestSupplierScorecardCriteria(FrappeTestCase): for d in test_good_criteria: frappe.get_doc(d).insert() - self.assertRaises(frappe.ValidationError,frappe.get_doc(test_bad_criteria[0]).insert) + self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[0]).insert) def test_formula_validate(self): delete_test_scorecards() - self.assertRaises(frappe.ValidationError,frappe.get_doc(test_bad_criteria[1]).insert) - self.assertRaises(frappe.ValidationError,frappe.get_doc(test_bad_criteria[2]).insert) + self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[1]).insert) + self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[2]).insert) + def delete_test_scorecards(): # Delete all the periods so we can delete all the criteria frappe.db.sql("""delete from `tabSupplier Scorecard Period`""") - frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'""") - frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'""") - frappe.db.sql("""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'""") + frappe.db.sql( + """delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'""" + ) + frappe.db.sql( + """delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'""" + ) + frappe.db.sql( + """delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'""" + ) for d in test_good_criteria: if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")): @@ -36,40 +43,41 @@ def delete_test_scorecards(): # Delete all the periods, then delete the scorecard frappe.delete_doc(d.get("doctype"), d.get("name")) + test_good_criteria = [ { - "name":"Delivery", - "weight":40.0, - "doctype":"Supplier Scorecard Criteria", - "formula":"(({cost_of_on_time_shipments} / {tot_cost_shipments}) if {tot_cost_shipments} > 0 else 1 )* 100", - "criteria_name":"Delivery", - "max_score":100.0 + "name": "Delivery", + "weight": 40.0, + "doctype": "Supplier Scorecard Criteria", + "formula": "(({cost_of_on_time_shipments} / {tot_cost_shipments}) if {tot_cost_shipments} > 0 else 1 )* 100", + "criteria_name": "Delivery", + "max_score": 100.0, }, ] test_bad_criteria = [ { - "name":"Fake Criteria 1", - "weight":40.0, - "doctype":"Supplier Scorecard Criteria", - "formula":"(({fake_variable} / {tot_cost_shipments}) if {tot_cost_shipments} > 0 else 1 )* 100", # Invalid variable name - "criteria_name":"Fake Criteria 1", - "max_score":100.0 + "name": "Fake Criteria 1", + "weight": 40.0, + "doctype": "Supplier Scorecard Criteria", + "formula": "(({fake_variable} / {tot_cost_shipments}) if {tot_cost_shipments} > 0 else 1 )* 100", # Invalid variable name + "criteria_name": "Fake Criteria 1", + "max_score": 100.0, }, { - "name":"Fake Criteria 2", - "weight":40.0, - "doctype":"Supplier Scorecard Criteria", - "formula":"(({cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Force 0 divided by 0 - "criteria_name":"Fake Criteria 2", - "max_score":100.0 + "name": "Fake Criteria 2", + "weight": 40.0, + "doctype": "Supplier Scorecard Criteria", + "formula": "(({cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Force 0 divided by 0 + "criteria_name": "Fake Criteria 2", + "max_score": 100.0, }, { - "name":"Fake Criteria 3", - "weight":40.0, - "doctype":"Supplier Scorecard Criteria", - "formula":"(({cost_of_on_time_shipments} {cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Two variables beside eachother - "criteria_name":"Fake Criteria 3", - "max_score":100.0 + "name": "Fake Criteria 3", + "weight": 40.0, + "doctype": "Supplier Scorecard Criteria", + "formula": "(({cost_of_on_time_shipments} {cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Two variables beside eachother + "criteria_name": "Fake Criteria 3", + "max_score": 100.0, }, ] diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py index c247241cf35..a8b76db0931 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -14,7 +14,6 @@ from erpnext.buying.doctype.supplier_scorecard_criteria.supplier_scorecard_crite class SupplierScorecardPeriod(Document): - def validate(self): self.validate_criteria_weights() self.calculate_variables() @@ -28,69 +27,84 @@ class SupplierScorecardPeriod(Document): weight += c.weight if weight != 100: - throw(_('Criteria weights must add up to 100%')) + throw(_("Criteria weights must add up to 100%")) def calculate_variables(self): for var in self.variables: - if '.' in var.path: + if "." in var.path: method_to_call = import_string_path(var.path) var.value = method_to_call(self) else: method_to_call = getattr(variable_functions, var.path) var.value = method_to_call(self) - - def calculate_criteria(self): for crit in self.criteria: try: - crit.score = min(crit.max_score, max( 0 ,frappe.safe_eval(self.get_eval_statement(crit.formula), None, {'max':max, 'min': min}))) + crit.score = min( + crit.max_score, + max( + 0, frappe.safe_eval(self.get_eval_statement(crit.formula), None, {"max": max, "min": min}) + ), + ) except Exception: - frappe.throw(_("Could not solve criteria score function for {0}. Make sure the formula is valid.").format(crit.criteria_name),frappe.ValidationError) + frappe.throw( + _("Could not solve criteria score function for {0}. Make sure the formula is valid.").format( + crit.criteria_name + ), + frappe.ValidationError, + ) crit.score = 0 def calculate_score(self): myscore = 0 for crit in self.criteria: - myscore += crit.score * crit.weight/100.0 + myscore += crit.score * crit.weight / 100.0 self.total_score = myscore def calculate_weighted_score(self, weighing_function): try: - weighed_score = frappe.safe_eval(self.get_eval_statement(weighing_function), None, {'max':max, 'min': min}) + weighed_score = frappe.safe_eval( + self.get_eval_statement(weighing_function), None, {"max": max, "min": min} + ) except Exception: - frappe.throw(_("Could not solve weighted score function. Make sure the formula is valid."),frappe.ValidationError) + frappe.throw( + _("Could not solve weighted score function. Make sure the formula is valid."), + frappe.ValidationError, + ) weighed_score = 0 return weighed_score - def get_eval_statement(self, formula): my_eval_statement = formula.replace("\r", "").replace("\n", "") for var in self.variables: - if var.value: - if var.param_name in my_eval_statement: - my_eval_statement = my_eval_statement.replace('{' + var.param_name + '}', "{:.2f}".format(var.value)) - else: - if var.param_name in my_eval_statement: - my_eval_statement = my_eval_statement.replace('{' + var.param_name + '}', '0.0') + if var.value: + if var.param_name in my_eval_statement: + my_eval_statement = my_eval_statement.replace( + "{" + var.param_name + "}", "{:.2f}".format(var.value) + ) + else: + if var.param_name in my_eval_statement: + my_eval_statement = my_eval_statement.replace("{" + var.param_name + "}", "0.0") return my_eval_statement def import_string_path(path): - components = path.split('.') - mod = __import__(components[0]) - for comp in components[1:]: - mod = getattr(mod, comp) - return mod + components = path.split(".") + mod = __import__(components[0]) + for comp in components[1:]: + mod = getattr(mod, comp) + return mod @frappe.whitelist() def make_supplier_scorecard(source_name, target_doc=None): def update_criteria_fields(obj, target, source_parent): - target.max_score, target.formula = frappe.db.get_value('Supplier Scorecard Criteria', - obj.criteria_name, ['max_score', 'formula']) + target.max_score, target.formula = frappe.db.get_value( + "Supplier Scorecard Criteria", obj.criteria_name, ["max_score", "formula"] + ) def post_process(source, target): variables = [] @@ -99,16 +113,21 @@ def make_supplier_scorecard(source_name, target_doc=None): if var not in variables: variables.append(var) - target.extend('variables', variables) + target.extend("variables", variables) - doc = get_mapped_doc("Supplier Scorecard", source_name, { - "Supplier Scorecard": { - "doctype": "Supplier Scorecard Period" + doc = get_mapped_doc( + "Supplier Scorecard", + source_name, + { + "Supplier Scorecard": {"doctype": "Supplier Scorecard Period"}, + "Supplier Scorecard Scoring Criteria": { + "doctype": "Supplier Scorecard Scoring Criteria", + "postprocess": update_criteria_fields, + }, }, - "Supplier Scorecard Scoring Criteria": { - "doctype": "Supplier Scorecard Scoring Criteria", - "postprocess": update_criteria_fields, - } - }, target_doc, post_process, ignore_permissions=True) + target_doc, + post_process, + ignore_permissions=True, + ) return doc diff --git a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py index 11ebe6da13c..929e8a363fd 100644 --- a/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py +++ b/erpnext/buying/doctype/supplier_scorecard_standing/supplier_scorecard_standing.py @@ -19,11 +19,14 @@ def get_scoring_standing(standing_name): @frappe.whitelist() def get_standings_list(): - standings = frappe.db.sql(""" + standings = frappe.db.sql( + """ SELECT scs.name FROM `tabSupplier Scorecard Standing` scs""", - {}, as_dict=1) + {}, + as_dict=1, + ) return standings diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py index 217aadba6bd..fb8819eaf81 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py @@ -10,18 +10,21 @@ from frappe.model.document import Document from frappe.utils import getdate -class VariablePathNotFound(frappe.ValidationError): pass +class VariablePathNotFound(frappe.ValidationError): + pass + class SupplierScorecardVariable(Document): def validate(self): self.validate_path_exists() def validate_path_exists(self): - if '.' in self.path: + if "." in self.path: try: from erpnext.buying.doctype.supplier_scorecard_period.supplier_scorecard_period import ( import_string_path, ) + import_string_path(self.path) except AttributeError: frappe.throw(_("Could not find path for " + self.path), VariablePathNotFound) @@ -30,15 +33,18 @@ class SupplierScorecardVariable(Document): if not hasattr(sys.modules[__name__], self.path): frappe.throw(_("Could not find path for " + self.path), VariablePathNotFound) + def get_total_workdays(scorecard): - """ Gets the number of days in this period""" + """Gets the number of days in this period""" delta = getdate(scorecard.end_date) - getdate(scorecard.start_date) return delta.days + def get_item_workdays(scorecard): - """ Gets the number of days in this period""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) - total_item_days = frappe.db.sql(""" + """Gets the number of days in this period""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) + total_item_days = frappe.db.sql( + """ SELECT SUM(DATEDIFF( %(end_date)s, po_item.schedule_date) * (po_item.qty)) FROM @@ -49,20 +55,22 @@ def get_item_workdays(scorecard): AND po_item.received_qty < po_item.qty AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s AND po_item.parent = po.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not total_item_days: total_item_days = 0 return total_item_days - def get_total_cost_of_shipments(scorecard): - """ Gets the total cost of all shipments in the period (based on Purchase Orders)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total cost of all shipments in the period (based on Purchase Orders)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT SUM(po_item.base_amount) FROM @@ -73,24 +81,29 @@ def get_total_cost_of_shipments(scorecard): AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s AND po_item.docstatus = 1 AND po_item.parent = po.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if data: return data else: return 0 + def get_cost_of_delayed_shipments(scorecard): - """ Gets the total cost of all delayed shipments in the period (based on Purchase Receipts - POs)""" + """Gets the total cost of all delayed shipments in the period (based on Purchase Receipts - POs)""" return get_total_cost_of_shipments(scorecard) - get_cost_of_on_time_shipments(scorecard) + def get_cost_of_on_time_shipments(scorecard): - """ Gets the total cost of all on_time shipments in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total cost of all on_time shipments in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - total_delivered_on_time_costs = frappe.db.sql(""" + total_delivered_on_time_costs = frappe.db.sql( + """ SELECT SUM(pr_item.base_amount) FROM @@ -106,7 +119,9 @@ def get_cost_of_on_time_shipments(scorecard): AND pr_item.purchase_order_item = po_item.name AND po_item.parent = po.name AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if total_delivered_on_time_costs: return total_delivered_on_time_costs @@ -115,9 +130,10 @@ def get_cost_of_on_time_shipments(scorecard): def get_total_days_late(scorecard): - """ Gets the number of item days late in the period (based on Purchase Receipts vs POs)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) - total_delivered_late_days = frappe.db.sql(""" + """Gets the number of item days late in the period (based on Purchase Receipts vs POs)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) + total_delivered_late_days = frappe.db.sql( + """ SELECT SUM(DATEDIFF(pr.posting_date,po_item.schedule_date)* pr_item.qty) FROM @@ -133,11 +149,14 @@ def get_total_days_late(scorecard): AND pr_item.purchase_order_item = po_item.name AND po_item.parent = po.name AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not total_delivered_late_days: total_delivered_late_days = 0 - total_missed_late_days = frappe.db.sql(""" + total_missed_late_days = frappe.db.sql( + """ SELECT SUM(DATEDIFF( %(end_date)s, po_item.schedule_date) * (po_item.qty - po_item.received_qty)) FROM @@ -148,19 +167,23 @@ def get_total_days_late(scorecard): AND po_item.received_qty < po_item.qty AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s AND po_item.parent = po.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not total_missed_late_days: total_missed_late_days = 0 return total_missed_late_days + total_delivered_late_days -def get_on_time_shipments(scorecard): - """ Gets the number of late shipments (counting each item) in the period (based on Purchase Receipts vs POs)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) +def get_on_time_shipments(scorecard): + """Gets the number of late shipments (counting each item) in the period (based on Purchase Receipts vs POs)""" + + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - total_items_delivered_on_time = frappe.db.sql(""" + total_items_delivered_on_time = frappe.db.sql( + """ SELECT COUNT(pr_item.qty) FROM @@ -177,22 +200,27 @@ def get_on_time_shipments(scorecard): AND pr_item.purchase_order_item = po_item.name AND po_item.parent = po.name AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not total_items_delivered_on_time: total_items_delivered_on_time = 0 return total_items_delivered_on_time + def get_late_shipments(scorecard): - """ Gets the number of late shipments (counting each item) in the period (based on Purchase Receipts vs POs)""" + """Gets the number of late shipments (counting each item) in the period (based on Purchase Receipts vs POs)""" return get_total_shipments(scorecard) - get_on_time_shipments(scorecard) + def get_total_received(scorecard): - """ Gets the total number of received shipments in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of received shipments in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT COUNT(pr_item.base_amount) FROM @@ -203,18 +231,22 @@ def get_total_received(scorecard): AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s AND pr_item.docstatus = 1 AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_total_received_amount(scorecard): - """ Gets the total amount (in company currency) received in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total amount (in company currency) received in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT SUM(pr_item.received_qty * pr_item.base_rate) FROM @@ -225,18 +257,22 @@ def get_total_received_amount(scorecard): AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s AND pr_item.docstatus = 1 AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_total_received_items(scorecard): - """ Gets the total number of received shipments in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of received shipments in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT SUM(pr_item.received_qty) FROM @@ -247,18 +283,22 @@ def get_total_received_items(scorecard): AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s AND pr_item.docstatus = 1 AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_total_rejected_amount(scorecard): - """ Gets the total amount (in company currency) rejected in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total amount (in company currency) rejected in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT SUM(pr_item.rejected_qty * pr_item.base_rate) FROM @@ -269,18 +309,22 @@ def get_total_rejected_amount(scorecard): AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s AND pr_item.docstatus = 1 AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_total_rejected_items(scorecard): - """ Gets the total number of rejected items in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of rejected items in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT SUM(pr_item.rejected_qty) FROM @@ -291,18 +335,22 @@ def get_total_rejected_items(scorecard): AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s AND pr_item.docstatus = 1 AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_total_accepted_amount(scorecard): - """ Gets the total amount (in company currency) accepted in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total amount (in company currency) accepted in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT SUM(pr_item.qty * pr_item.base_rate) FROM @@ -313,18 +361,22 @@ def get_total_accepted_amount(scorecard): AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s AND pr_item.docstatus = 1 AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_total_accepted_items(scorecard): - """ Gets the total number of rejected items in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of rejected items in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT SUM(pr_item.qty) FROM @@ -335,18 +387,22 @@ def get_total_accepted_items(scorecard): AND pr.posting_date BETWEEN %(start_date)s AND %(end_date)s AND pr_item.docstatus = 1 AND pr_item.parent = pr.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_total_shipments(scorecard): - """ Gets the total number of ordered shipments to arrive in the period (based on Purchase Receipts)""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of ordered shipments to arrive in the period (based on Purchase Receipts)""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT COUNT(po_item.base_amount) FROM @@ -357,18 +413,22 @@ def get_total_shipments(scorecard): AND po_item.schedule_date BETWEEN %(start_date)s AND %(end_date)s AND po_item.docstatus = 1 AND po_item.parent = po.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_rfq_total_number(scorecard): - """ Gets the total number of RFQs sent to supplier""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of RFQs sent to supplier""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT COUNT(rfq.name) as total_rfqs FROM @@ -381,18 +441,22 @@ def get_rfq_total_number(scorecard): AND rfq_item.docstatus = 1 AND rfq_item.parent = rfq.name AND rfq_sup.parent = rfq.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_rfq_total_items(scorecard): - """ Gets the total number of RFQ items sent to supplier""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of RFQ items sent to supplier""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT COUNT(rfq_item.name) as total_rfqs FROM @@ -405,18 +469,21 @@ def get_rfq_total_items(scorecard): AND rfq_item.docstatus = 1 AND rfq_item.parent = rfq.name AND rfq_sup.parent = rfq.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data def get_sq_total_number(scorecard): - """ Gets the total number of RFQ items sent to supplier""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of RFQ items sent to supplier""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT COUNT(sq.name) as total_sqs FROM @@ -435,17 +502,21 @@ def get_sq_total_number(scorecard): AND sq_item.parent = sq.name AND rfq_item.parent = rfq.name AND rfq_sup.parent = rfq.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_sq_total_items(scorecard): - """ Gets the total number of RFQ items sent to supplier""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) + """Gets the total number of RFQ items sent to supplier""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) # Look up all PO Items with delivery dates between our dates - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT COUNT(sq_item.name) as total_sqs FROM @@ -464,15 +535,19 @@ def get_sq_total_items(scorecard): AND rfq_item.docstatus = 1 AND rfq_item.parent = rfq.name AND rfq_sup.parent = rfq.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not data: data = 0 return data + def get_rfq_response_days(scorecard): - """ Gets the total number of days it has taken a supplier to respond to rfqs in the period""" - supplier = frappe.get_doc('Supplier', scorecard.supplier) - total_sq_days = frappe.db.sql(""" + """Gets the total number of days it has taken a supplier to respond to rfqs in the period""" + supplier = frappe.get_doc("Supplier", scorecard.supplier) + total_sq_days = frappe.db.sql( + """ SELECT SUM(DATEDIFF(sq.transaction_date, rfq.transaction_date)) FROM @@ -491,9 +566,10 @@ def get_rfq_response_days(scorecard): AND rfq_item.docstatus = 1 AND rfq_item.parent = rfq.name AND rfq_sup.parent = rfq.name""", - {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, as_dict=0)[0][0] + {"supplier": supplier.name, "start_date": scorecard.start_date, "end_date": scorecard.end_date}, + as_dict=0, + )[0][0] if not total_sq_days: total_sq_days = 0 - return total_sq_days diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py index 32005a37dc7..60d84644cf5 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py @@ -14,9 +14,9 @@ class TestSupplierScorecardVariable(FrappeTestCase): def test_variable_exist(self): for d in test_existing_variables: my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name")) - self.assertEqual(my_doc.param_name, d.get('param_name')) - self.assertEqual(my_doc.variable_label, d.get('variable_label')) - self.assertEqual(my_doc.path, d.get('path')) + self.assertEqual(my_doc.param_name, d.get("param_name")) + self.assertEqual(my_doc.variable_label, d.get("variable_label")) + self.assertEqual(my_doc.path, d.get("path")) def test_path_exists(self): for d in test_good_variables: @@ -25,34 +25,35 @@ class TestSupplierScorecardVariable(FrappeTestCase): frappe.get_doc(d).insert() for d in test_bad_variables: - self.assertRaises(VariablePathNotFound,frappe.get_doc(d).insert) + self.assertRaises(VariablePathNotFound, frappe.get_doc(d).insert) + test_existing_variables = [ { - "param_name":"total_accepted_items", - "name":"Total Accepted Items", - "doctype":"Supplier Scorecard Variable", - "variable_label":"Total Accepted Items", - "path":"get_total_accepted_items" + "param_name": "total_accepted_items", + "name": "Total Accepted Items", + "doctype": "Supplier Scorecard Variable", + "variable_label": "Total Accepted Items", + "path": "get_total_accepted_items", }, ] test_good_variables = [ { - "param_name":"good_variable1", - "name":"Good Variable 1", - "doctype":"Supplier Scorecard Variable", - "variable_label":"Good Variable 1", - "path":"get_total_accepted_items" + "param_name": "good_variable1", + "name": "Good Variable 1", + "doctype": "Supplier Scorecard Variable", + "variable_label": "Good Variable 1", + "path": "get_total_accepted_items", }, ] test_bad_variables = [ { - "param_name":"fake_variable1", - "name":"Fake Variable 1", - "doctype":"Supplier Scorecard Variable", - "variable_label":"Fake Variable 1", - "path":"get_fake_variable1" + "param_name": "fake_variable1", + "name": "Fake Variable 1", + "doctype": "Supplier Scorecard Variable", + "variable_label": "Fake Variable 1", + "path": "get_fake_variable1", }, ] diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 295a19d052e..e0b02ee4e2a 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -12,152 +12,143 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): columns = [ { "label": _("Material Request Date"), "fieldname": "material_request_date", "fieldtype": "Date", - "width": 140 + "width": 140, }, { "label": _("Material Request No"), "options": "Material Request", "fieldname": "material_request_no", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Cost Center"), "options": "Cost Center", "fieldname": "cost_center", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Project"), "options": "Project", "fieldname": "project", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Requesting Site"), "options": "Warehouse", "fieldname": "requesting_site", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Requestor"), "options": "Employee", "fieldname": "requestor", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 150 - }, - { - "label": _("Quantity"), - "fieldname": "quantity", - "fieldtype": "Float", - "width": 140 + "width": 150, }, + {"label": _("Quantity"), "fieldname": "quantity", "fieldtype": "Float", "width": 140}, { "label": _("Unit of Measure"), "options": "UOM", "fieldname": "unit_of_measurement", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Status"), - "fieldname": "status", - "fieldtype": "data", - "width": 140 + "width": 140, }, + {"label": _("Status"), "fieldname": "status", "fieldtype": "data", "width": 140}, { "label": _("Purchase Order Date"), "fieldname": "purchase_order_date", "fieldtype": "Date", - "width": 140 + "width": 140, }, { "label": _("Purchase Order"), "options": "Purchase Order", "fieldname": "purchase_order", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Supplier"), "options": "Supplier", "fieldname": "supplier", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Estimated Cost"), "fieldname": "estimated_cost", "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Actual Cost"), - "fieldname": "actual_cost", - "fieldtype": "Float", - "width": 140 + "width": 140, }, + {"label": _("Actual Cost"), "fieldname": "actual_cost", "fieldtype": "Float", "width": 140}, { "label": _("Purchase Order Amount"), "fieldname": "purchase_order_amt", "fieldtype": "Float", - "width": 140 + "width": 140, }, { "label": _("Purchase Order Amount(Company Currency)"), "fieldname": "purchase_order_amt_in_company_currency", "fieldtype": "Float", - "width": 140 + "width": 140, }, { "label": _("Expected Delivery Date"), "fieldname": "expected_delivery_date", "fieldtype": "Date", - "width": 140 + "width": 140, }, { "label": _("Actual Delivery Date"), "fieldname": "actual_delivery_date", "fieldtype": "Date", - "width": 140 + "width": 140, }, ] return columns + def get_conditions(filters): conditions = "" if filters.get("company"): - conditions += " AND parent.company=%s" % frappe.db.escape(filters.get('company')) + conditions += " AND parent.company=%s" % frappe.db.escape(filters.get("company")) if filters.get("cost_center") or filters.get("project"): conditions += """ AND (child.`cost_center`=%s OR child.`project`=%s) - """ % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) + """ % ( + frappe.db.escape(filters.get("cost_center")), + frappe.db.escape(filters.get("project")), + ) if filters.get("from_date"): - conditions += " AND parent.transaction_date>='%s'" % filters.get('from_date') + conditions += " AND parent.transaction_date>='%s'" % filters.get("from_date") if filters.get("to_date"): - conditions += " AND parent.transaction_date<='%s'" % filters.get('to_date') + conditions += " AND parent.transaction_date<='%s'" % filters.get("to_date") return conditions + def get_data(filters): conditions = get_conditions(filters) purchase_order_entry = get_po_entries(conditions) @@ -165,14 +156,14 @@ def get_data(filters): pr_records = get_mapped_pr_records() pi_records = get_mapped_pi_records() - procurement_record=[] + procurement_record = [] if procurement_record_against_mr: procurement_record += procurement_record_against_mr for po in purchase_order_entry: # fetch material records linked to the purchase order item mr_record = mr_records.get(po.material_request_item, [{}])[0] procurement_detail = { - "material_request_date": mr_record.get('transaction_date'), + "material_request_date": mr_record.get("transaction_date"), "cost_center": po.cost_center, "project": po.project, "requesting_site": po.warehouse, @@ -185,19 +176,21 @@ def get_data(filters): "purchase_order_date": po.transaction_date, "purchase_order": po.parent, "supplier": po.supplier, - "estimated_cost": flt(mr_record.get('amount')), + "estimated_cost": flt(mr_record.get("amount")), "actual_cost": flt(pi_records.get(po.name)), "purchase_order_amt": flt(po.amount), "purchase_order_amt_in_company_currency": flt(po.base_amount), "expected_delivery_date": po.schedule_date, - "actual_delivery_date": pr_records.get(po.name) + "actual_delivery_date": pr_records.get(po.name), } procurement_record.append(procurement_detail) return procurement_record + def get_mapped_mr_details(conditions): mr_records = {} - mr_details = frappe.db.sql(""" + mr_details = frappe.db.sql( + """ SELECT parent.transaction_date, parent.per_ordered, @@ -217,7 +210,11 @@ def get_mapped_mr_details(conditions): AND parent.name=child.parent AND parent.docstatus=1 {conditions} - """.format(conditions=conditions), as_dict=1) #nosec + """.format( + conditions=conditions + ), + as_dict=1, + ) # nosec procurement_record_against_mr = [] for record in mr_details: @@ -236,14 +233,17 @@ def get_mapped_mr_details(conditions): actual_cost=0, purchase_order_amt=0, purchase_order_amt_in_company_currency=0, - project = record.project, - cost_center = record.cost_center + project=record.project, + cost_center=record.cost_center, ) procurement_record_against_mr.append(procurement_record_details) return mr_records, procurement_record_against_mr + def get_mapped_pi_records(): - return frappe._dict(frappe.db.sql(""" + return frappe._dict( + frappe.db.sql( + """ SELECT pi_item.po_detail, pi_item.base_amount @@ -254,10 +254,15 @@ def get_mapped_pi_records(): pi_item.docstatus = 1 AND po.status not in ("Closed","Completed","Cancelled") AND pi_item.po_detail IS NOT NULL - """)) + """ + ) + ) + def get_mapped_pr_records(): - return frappe._dict(frappe.db.sql(""" + return frappe._dict( + frappe.db.sql( + """ SELECT pr_item.purchase_order_item, pr.posting_date @@ -267,10 +272,14 @@ def get_mapped_pr_records(): AND pr.name=pr_item.parent AND pr_item.purchase_order_item IS NOT NULL AND pr.status not in ("Closed","Completed","Cancelled") - """)) + """ + ) + ) + def get_po_entries(conditions): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT child.name, child.parent, @@ -297,4 +306,8 @@ def get_po_entries(conditions): {conditions} GROUP BY parent.name, child.item_code - """.format(conditions=conditions), as_dict=1) #nosec + """.format( + conditions=conditions + ), + as_dict=1, + ) # nosec diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index 44524527e3a..47a66ad46f2 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -16,27 +16,28 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse class TestProcurementTracker(FrappeTestCase): def test_result_for_procurement_tracker(self): - filters = { - 'company': '_Test Procurement Company', - 'cost_center': 'Main - _TPC' - } + filters = {"company": "_Test Procurement Company", "cost_center": "Main - _TPC"} expected_data = self.generate_expected_data() report = execute(filters) length = len(report[1]) - self.assertEqual(expected_data, report[1][length-1]) + self.assertEqual(expected_data, report[1][length - 1]) def generate_expected_data(self): if not frappe.db.exists("Company", "_Test Procurement Company"): - frappe.get_doc(dict( - doctype="Company", - company_name="_Test Procurement Company", - abbr="_TPC", - default_currency="INR", - country="Pakistan" - )).insert() + frappe.get_doc( + dict( + doctype="Company", + company_name="_Test Procurement Company", + abbr="_TPC", + default_currency="INR", + country="Pakistan", + ) + ).insert() warehouse = create_warehouse("_Test Procurement Warehouse", company="_Test Procurement Company") - mr = make_material_request(company="_Test Procurement Company", warehouse=warehouse, cost_center="Main - _TPC") + mr = make_material_request( + company="_Test Procurement Company", warehouse=warehouse, cost_center="Main - _TPC" + ) po = make_purchase_order(mr.name) po.supplier = "_Test Supplier" po.get("items")[0].cost_center = "Main - _TPC" @@ -55,7 +56,7 @@ class TestProcurementTracker(FrappeTestCase): "requesting_site": "_Test Procurement Warehouse - _TPC", "requestor": "Administrator", "material_request_no": mr.name, - "item_code": '_Test Item', + "item_code": "_Test Item", "quantity": 10.0, "unit_of_measurement": "_Test UOM", "status": "To Bill", @@ -67,7 +68,7 @@ class TestProcurementTracker(FrappeTestCase): "purchase_order_amt": po.net_total, "purchase_order_amt_in_company_currency": po.base_net_total, "expected_delivery_date": date_obj, - "actual_delivery_date": date_obj + "actual_delivery_date": date_obj, } return expected_data diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index 9dd912118ff..a5c464910de 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -27,6 +27,7 @@ def execute(filters=None): return columns, data, None, chart_data + def validate_filters(filters): from_date, to_date = filters.get("from_date"), filters.get("to_date") @@ -35,25 +36,28 @@ def validate_filters(filters): elif date_diff(to_date, from_date) < 0: frappe.throw(_("To Date cannot be before From Date.")) + def get_conditions(filters): conditions = "" if filters.get("from_date") and filters.get("to_date"): conditions += " and po.transaction_date between %(from_date)s and %(to_date)s" - for field in ['company', 'name']: + for field in ["company", "name"]: if filters.get(field): conditions += f" and po.{field} = %({field})s" - if filters.get('status'): + if filters.get("status"): conditions += " and po.status in %(status)s" - if filters.get('project'): + if filters.get("project"): conditions += " and poi.project = %(project)s" return conditions + def get_data(conditions, filters): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT po.transaction_date as date, poi.schedule_date as required_date, @@ -81,13 +85,19 @@ def get_data(conditions, filters): {0} GROUP BY poi.name ORDER BY po.transaction_date ASC - """.format(conditions), filters, as_dict=1) + """.format( + conditions + ), + filters, + as_dict=1, + ) return data + def prepare_data(data, filters): completed, pending = 0, 0 - pending_field = "pending_amount" + pending_field = "pending_amount" completed_field = "billed_amount" if filters.get("group_by_po"): @@ -114,8 +124,17 @@ def prepare_data(data, filters): po_row["required_date"] = min(getdate(po_row["required_date"]), getdate(row["required_date"])) # sum numeric columns - fields = ["qty", "received_qty", "pending_qty", "billed_qty", "qty_to_bill", "amount", - "received_qty_amount", "billed_amount", "pending_amount"] + fields = [ + "qty", + "received_qty", + "pending_qty", + "billed_qty", + "qty_to_bill", + "amount", + "received_qty_amount", + "billed_amount", + "pending_amount", + ] for field in fields: po_row[field] = flt(row[field]) + flt(po_row[field]) @@ -129,152 +148,140 @@ def prepare_data(data, filters): return data, chart_data + def prepare_chart_data(pending, completed): labels = ["Amount to Bill", "Billed Amount"] return { - "data" : { - "labels": labels, - "datasets": [ - {"values": [pending, completed]} - ] - }, - "type": 'donut', - "height": 300 + "data": {"labels": labels, "datasets": [{"values": [pending, completed]}]}, + "type": "donut", + "height": 300, } + def get_columns(filters): columns = [ - { - "label":_("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 90 - }, - { - "label":_("Required By"), - "fieldname": "required_date", - "fieldtype": "Date", - "width": 90 - }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 90}, + {"label": _("Required By"), "fieldname": "required_date", "fieldtype": "Date", "width": 90}, { "label": _("Purchase Order"), "fieldname": "purchase_order", "fieldtype": "Link", "options": "Purchase Order", - "width": 160 - }, - { - "label":_("Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 130 + "width": 160, }, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 130}, { "label": _("Supplier"), "fieldname": "supplier", "fieldtype": "Link", "options": "Supplier", - "width": 130 - },{ + "width": 130, + }, + { "label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", - "width": 130 - }] + "width": 130, + }, + ] if not filters.get("group_by_po"): - columns.append({ - "label":_("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100 - }) + columns.append( + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + } + ) - columns.extend([ - { - "label": _("Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Received Qty"), - "fieldname": "received_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Pending Qty"), - "fieldname": "pending_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Billed Qty"), - "fieldname": "billed_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Qty to Bill"), - "fieldname": "qty_to_bill", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Billed Amount"), - "fieldname": "billed_amount", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Pending Amount"), - "fieldname": "pending_amount", - "fieldtype": "Currency", - "width": 130, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Received Qty Amount"), - "fieldname": "received_qty_amount", - "fieldtype": "Currency", - "width": 130, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100 - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - "width": 100 - } - ]) + columns.extend( + [ + { + "label": _("Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Received Qty"), + "fieldname": "received_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Pending Qty"), + "fieldname": "pending_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Billed Qty"), + "fieldname": "billed_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Qty to Bill"), + "fieldname": "qty_to_bill", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Billed Amount"), + "fieldname": "billed_amount", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Pending Amount"), + "fieldname": "pending_amount", + "fieldtype": "Currency", + "width": 130, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Received Qty Amount"), + "fieldname": "received_qty_amount", + "fieldtype": "Currency", + "width": 130, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, + ] + ) return columns diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py index 21643a896b7..11a74491a4c 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Purchase Order") data = get_data(filters, conditions) @@ -16,6 +17,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, conditions, filters): if not (data and conditions): return [] @@ -28,32 +30,27 @@ def get_chart_data(data, conditions, filters): # fetch only periodic columns as labels columns = conditions.get("columns")[start:-2][1::2] - labels = [column.split(':')[0] for column in columns] + labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start-1]: + if not row[start - 1]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[1::2] for i in range(len(row)): datapoints[i] += row[i] return { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : _("{0}").format(filters.get("period")) + _(" Purchase Value"), - "values" : datapoints - } - ] + "data": { + "labels": labels, + "datasets": [ + {"name": _("{0}").format(filters.get("period")) + _(" Purchase Value"), "values": datapoints} + ], }, - "type" : "line", - "lineOptions": { - "regionFill": 1 - } + "type": "line", + "lineOptions": {"regionFill": 1}, } diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py index f98e5f12c2d..21241e08603 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py @@ -6,26 +6,25 @@ import copy import frappe from frappe import _ +from frappe.query_builder.functions import Coalesce, Sum from frappe.utils import date_diff, flt, getdate def execute(filters=None): if not filters: - return [],[] + return [], [] validate_filters(filters) columns = get_columns(filters) - conditions = get_conditions(filters) + data = get_data(filters) - #get queried data - data = get_data(filters, conditions) - - #prepare data for report and chart views + # prepare data for report and chart views data, chart_data = prepare_data(data, filters) return columns, data, None, chart_data + def validate_filters(filters): from_date, to_date = filters.get("from_date"), filters.get("to_date") @@ -34,59 +33,74 @@ def validate_filters(filters): elif date_diff(to_date, from_date) < 0: frappe.throw(_("To Date cannot be before From Date.")) -def get_conditions(filters): - conditions = '' +def get_data(filters): + mr = frappe.qb.DocType("Material Request") + mr_item = frappe.qb.DocType("Material Request Item") + + query = ( + frappe.qb.from_(mr) + .join(mr_item) + .on(mr_item.parent == mr.name) + .select( + mr.name.as_("material_request"), + mr.transaction_date.as_("date"), + mr_item.schedule_date.as_("required_date"), + mr_item.item_code.as_("item_code"), + Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"), + Coalesce(mr_item.stock_uom, "").as_("uom"), + Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"), + Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), + (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_( + "qty_to_receive" + ), + Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), + (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))).as_( + "qty_to_order" + ), + mr_item.item_name, + mr_item.description, + mr.company, + ) + .where( + (mr.material_request_type == "Purchase") + & (mr.docstatus == 1) + & (mr.status != "Stopped") + & (mr.per_received < 100) + ) + ) + + query = get_conditions(filters, query, mr, mr_item) # add conditional conditions + + query = query.groupby(mr.name, mr_item.item_code).orderby(mr.transaction_date, mr.schedule_date) + data = query.run(as_dict=True) + return data + + +def get_conditions(filters, query, mr, mr_item): if filters.get("from_date") and filters.get("to_date"): - conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date")) - + query = query.where( + (mr.transaction_date >= filters.get("from_date")) + & (mr.transaction_date <= filters.get("to_date")) + ) if filters.get("company"): - conditions += " and mr.company = '{0}'".format(filters.get("company")) + query = query.where(mr.company == filters.get("company")) if filters.get("material_request"): - conditions += " and mr.name = '{0}'".format(filters.get("material_request")) + query = query.where(mr.name == filters.get("material_request")) if filters.get("item_code"): - conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code")) + query = query.where(mr_item.item_code == filters.get("item_code")) - return conditions + return query -def get_data(filters, conditions): - data = frappe.db.sql(""" - select - mr.name as material_request, - mr.transaction_date as date, - mr_item.schedule_date as required_date, - mr_item.item_code as item_code, - sum(ifnull(mr_item.stock_qty, 0)) as qty, - ifnull(mr_item.stock_uom, '') as uom, - sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty, - sum(ifnull(mr_item.received_qty, 0)) as received_qty, - (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive, - (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order, - mr_item.item_name as item_name, - mr_item.description as "description", - mr.company as company - from - `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where - mr_item.parent = mr.name - and mr.material_request_type = "Purchase" - and mr.docstatus = 1 - and mr.status != "Stopped" - {conditions} - group by mr.name, mr_item.item_code - having - sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0)) - order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1) - - return data def update_qty_columns(row_to_update, data_row): fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] for field in fields: row_to_update[field] += flt(data_row[field]) + def prepare_data(data, filters): """Prepare consolidated Report data and Chart data""" material_request_map, item_qty_map = {}, {} @@ -95,11 +109,11 @@ def prepare_data(data, filters): # item wise map for charts if not row["item_code"] in item_qty_map: item_qty_map[row["item_code"]] = { - "qty" : row["qty"], - "ordered_qty" : row["ordered_qty"], + "qty": row["qty"], + "ordered_qty": row["ordered_qty"], "received_qty": row["received_qty"], "qty_to_receive": row["qty_to_receive"], - "qty_to_order" : row["qty_to_order"], + "qty_to_order": row["qty_to_order"], } else: item_entry = item_qty_map[row["item_code"]] @@ -115,19 +129,20 @@ def prepare_data(data, filters): mr_row = material_request_map[row["material_request"]] mr_row["required_date"] = min(getdate(mr_row["required_date"]), getdate(row["required_date"])) - #sum numeric columns + # sum numeric columns update_qty_columns(mr_row, row) chart_data = prepare_chart_data(item_qty_map) if filters.get("group_by_mr"): - data =[] + data = [] for mr in material_request_map: data.append(material_request_map[mr]) return data, chart_data return data, chart_data + def prepare_chart_data(item_data): labels, qty_to_order, ordered_qty, received_qty, qty_to_receive = [], [], [], [], [] @@ -143,35 +158,22 @@ def prepare_chart_data(item_data): qty_to_receive.append(mr_row["qty_to_receive"]) chart_data = { - "data" : { + "data": { "labels": labels, "datasets": [ - { - 'name': _('Qty to Order'), - 'values': qty_to_order - }, - { - 'name': _('Ordered Qty'), - 'values': ordered_qty - }, - { - 'name': _('Received Qty'), - 'values': received_qty - }, - { - 'name': _('Qty to Receive'), - 'values': qty_to_receive - } - ] + {"name": _("Qty to Order"), "values": qty_to_order}, + {"name": _("Ordered Qty"), "values": ordered_qty}, + {"name": _("Received Qty"), "values": received_qty}, + {"name": _("Qty to Receive"), "values": qty_to_receive}, + ], }, "type": "bar", - "barOptions": { - "stacked": 1 - }, + "barOptions": {"stacked": 1}, } return chart_data + def get_columns(filters): columns = [ { @@ -179,92 +181,78 @@ def get_columns(filters): "fieldname": "material_request", "fieldtype": "Link", "options": "Material Request", - "width": 150 + "width": 150, }, - { - "label":_("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 90 - }, - { - "label":_("Required By"), - "fieldname": "required_date", - "fieldtype": "Date", - "width": 100 - } + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 90}, + {"label": _("Required By"), "fieldname": "required_date", "fieldtype": "Date", "width": 100}, ] if not filters.get("group_by_mr"): - columns.extend([{ - "label":_("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100 - }, - { - "label":_("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Stock UOM"), - "fieldname": "uom", - "fieldtype": "Data", - "width": 100, - }]) + columns.extend( + [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200}, + { + "label": _("Stock UOM"), + "fieldname": "uom", + "fieldtype": "Data", + "width": 100, + }, + ] + ) - columns.extend([ - { - "label": _("Stock Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Ordered Qty"), - "fieldname": "ordered_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Received Qty"), - "fieldname": "received_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Qty to Receive"), - "fieldname": "qty_to_receive", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Qty to Order"), - "fieldname": "qty_to_order", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - "width": 100 - } - ]) + columns.extend( + [ + { + "label": _("Stock Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Ordered Qty"), + "fieldname": "ordered_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Received Qty"), + "fieldname": "received_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Qty to Receive"), + "fieldname": "qty_to_receive", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Qty to Order"), + "fieldname": "qty_to_order", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, + ] + ) return columns diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py new file mode 100644 index 00000000000..5b84113a9cf --- /dev/null +++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py @@ -0,0 +1,69 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, today + +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt +from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import ( + get_data, +) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.material_request.material_request import make_purchase_order + + +class TestRequestedItemsToOrderAndReceive(FrappeTestCase): + def setUp(self) -> None: + create_item("Test MR Report Item") + self.setup_material_request() # to order and receive + self.setup_material_request(order=True, days=1) # to receive (ordered) + self.setup_material_request(order=True, receive=True, days=2) # complete (ordered & received) + + self.filters = frappe._dict( + company="_Test Company", + from_date=today(), + to_date=add_days(today(), 30), + item_code="Test MR Report Item", + ) + + def tearDown(self) -> None: + frappe.db.rollback() + + def test_date_range(self): + data = get_data(self.filters) + self.assertEqual(len(data), 2) # MRs today should be fetched + + data = get_data(self.filters.update({"from_date": add_days(today(), 10)})) + self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is in future + + def test_ordered_received_material_requests(self): + data = get_data(self.filters) + + # from the 3 MRs made, only 2 (to receive) should be fetched + self.assertEqual(len(data), 2) + self.assertEqual(data[0].ordered_qty, 0.0) + self.assertEqual(data[1].ordered_qty, 57.0) + + def setup_material_request(self, order=False, receive=False, days=0): + po = None + test_records = frappe.get_test_records("Material Request") + + mr = frappe.copy_doc(test_records[0]) + mr.transaction_date = add_days(today(), days) + mr.schedule_date = add_days(mr.transaction_date, 1) + for row in mr.items: + row.item_code = "Test MR Report Item" + row.item_name = "Test MR Report Item" + row.description = "Test MR Report Item" + row.uom = "Nos" + row.schedule_date = mr.schedule_date + mr.submit() + + if order or receive: + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.submit() + if receive: + pr = make_purchase_receipt(po.name) + pr.submit() diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 8e5c2f9a30d..1b2705a7be3 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -13,6 +13,7 @@ def execute(filters=None): return columns, data + def get_data(report_filters): data = [] orders = get_subcontracted_orders(report_filters) @@ -24,57 +25,85 @@ def get_data(report_filters): return data + def get_subcontracted_orders(report_filters): - fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`', - '`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`', - '`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`'] + fields = [ + "`tabPurchase Order Item`.`parent` as po_id", + "`tabPurchase Order Item`.`item_code`", + "`tabPurchase Order Item`.`item_name`", + "`tabPurchase Order Item`.`qty`", + "`tabPurchase Order Item`.`name`", + "`tabPurchase Order Item`.`received_qty`", + "`tabPurchase Order`.`status`", + ] filters = get_filters(report_filters) - return frappe.get_all('Purchase Order', fields = fields, filters=filters) or [] + return frappe.get_all("Purchase Order", fields=fields, filters=filters) or [] + def get_filters(report_filters): - filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'], - ['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]] + filters = [ + ["Purchase Order", "docstatus", "=", 1], + ["Purchase Order", "is_subcontracted", "=", "Yes"], + [ + "Purchase Order", + "transaction_date", + "between", + (report_filters.from_date, report_filters.to_date), + ], + ] - for field in ['name', 'company']: + for field in ["name", "company"]: if report_filters.get(field): - filters.append(['Purchase Order', field, '=', report_filters.get(field)]) + filters.append(["Purchase Order", field, "=", report_filters.get(field)]) return filters + def get_supplied_items(orders, report_filters): if not orders: return [] - fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty', - 'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name'] + fields = [ + "parent", + "main_item_code", + "rm_item_code", + "required_qty", + "supplied_qty", + "returned_qty", + "total_supplied_qty", + "consumed_qty", + "reference_name", + ] - filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1} + filters = {"parent": ("in", [d.po_id for d in orders]), "docstatus": 1} supplied_items = {} - for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters): + for row in frappe.get_all("Purchase Order Item Supplied", fields=fields, filters=filters): new_key = (row.parent, row.reference_name, row.main_item_code) supplied_items.setdefault(new_key, []).append(row) return supplied_items + def prepare_subcontracted_data(orders, supplied_items): po_details = {} for row in orders: key = (row.po_id, row.name, row.item_code) if key not in po_details: - po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []})) + po_details.setdefault(key, frappe._dict({"po_item": row, "supplied_items": []})) details = po_details[key] if supplied_items.get(key): for supplied_item in supplied_items[key]: - details['supplied_items'].append(supplied_item) + details["supplied_items"].append(supplied_item) return po_details + def get_subcontracted_data(po_details, data): for key, details in po_details.items(): res = details.po_item @@ -85,6 +114,7 @@ def get_subcontracted_data(po_details, data): res.update(row) data.append(res) + def get_columns(): return [ { @@ -92,62 +122,27 @@ def get_columns(): "fieldname": "po_id", "fieldtype": "Link", "options": "Purchase Order", - "width": 100 - }, - { - "label": _("Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 80 + "width": 100, }, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80}, { "label": _("Subcontracted Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 160 - }, - { - "label": _("Order Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 90 - }, - { - "label": _("Received Qty"), - "fieldname": "received_qty", - "fieldtype": "Float", - "width": 110 + "width": 160, }, + {"label": _("Order Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 90}, + {"label": _("Received Qty"), "fieldname": "received_qty", "fieldtype": "Float", "width": 110}, { "label": _("Supplied Item"), "fieldname": "rm_item_code", "fieldtype": "Link", "options": "Item", - "width": 160 + "width": 160, }, - { - "label": _("Required Qty"), - "fieldname": "required_qty", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Supplied Qty"), - "fieldname": "supplied_qty", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Consumed Qty"), - "fieldname": "consumed_qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Returned Qty"), - "fieldname": "returned_qty", - "fieldtype": "Float", - "width": 110 - } + {"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 110}, + {"label": _("Supplied Qty"), "fieldname": "supplied_qty", "fieldtype": "Float", "width": 110}, + {"label": _("Consumed Qty"), "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120}, + {"label": _("Returned Qty"), "fieldname": "returned_qty", "fieldtype": "Float", "width": 110}, ] diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py index 67e275f9851..004657b6e86 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py @@ -12,9 +12,10 @@ def execute(filters=None): data = [] columns = get_columns() - get_data(data , filters) + get_data(data, filters) return columns, data + def get_columns(): return [ { @@ -22,54 +23,39 @@ def get_columns(): "fieldtype": "Link", "fieldname": "purchase_order", "options": "Purchase Order", - "width": 150 - }, - { - "label": _("Date"), - "fieldtype": "Date", - "fieldname": "date", - "hidden": 1, - "width": 150 + "width": 150, }, + {"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "hidden": 1, "width": 150}, { "label": _("Supplier"), "fieldtype": "Link", "fieldname": "supplier", "options": "Supplier", - "width": 150 + "width": 150, }, { "label": _("Finished Good Item Code"), "fieldtype": "Data", "fieldname": "fg_item_code", - "width": 100 - }, - { - "label": _("Item name"), - "fieldtype": "Data", - "fieldname": "item_name", - "width": 100 + "width": 100, }, + {"label": _("Item name"), "fieldtype": "Data", "fieldname": "item_name", "width": 100}, { "label": _("Required Quantity"), "fieldtype": "Float", "fieldname": "required_qty", - "width": 100 + "width": 100, }, { "label": _("Received Quantity"), "fieldtype": "Float", "fieldname": "received_qty", - "width": 100 + "width": 100, }, - { - "label": _("Pending Quantity"), - "fieldtype": "Float", - "fieldname": "pending_qty", - "width": 100 - } + {"label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "pending_qty", "width": 100}, ] + def get_data(data, filters): po = get_po(filters) po_name = [v.name for v in po] @@ -77,29 +63,35 @@ def get_data(data, filters): for item in sub_items: for order in po: if order.name == item.parent and item.received_qty < item.qty: - row ={ - 'purchase_order': item.parent, - 'date': order.transaction_date, - 'supplier': order.supplier, - 'fg_item_code': item.item_code, - 'item_name': item.item_name, - 'required_qty': item.qty, - 'received_qty':item.received_qty, - 'pending_qty':item.qty - item.received_qty + row = { + "purchase_order": item.parent, + "date": order.transaction_date, + "supplier": order.supplier, + "fg_item_code": item.item_code, + "item_name": item.item_name, + "required_qty": item.qty, + "received_qty": item.received_qty, + "pending_qty": item.qty - item.received_qty, } data.append(row) + def get_po(filters): record_filters = [ - ["is_subcontracted", "=", "Yes"], - ["supplier", "=", filters.supplier], - ["transaction_date", "<=", filters.to_date], - ["transaction_date", ">=", filters.from_date], - ["docstatus", "=", 1] - ] - return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]) + ["is_subcontracted", "=", "Yes"], + ["supplier", "=", filters.supplier], + ["transaction_date", "<=", filters.to_date], + ["transaction_date", ">=", filters.from_date], + ["docstatus", "=", 1], + ] + return frappe.get_all( + "Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"] + ) + def get_purchase_order_item_supplied(po): - return frappe.get_all("Purchase Order Item", filters=[ - ('parent', 'IN', po) - ], fields=["parent", "item_code", "item_name", "qty", "received_qty"]) + return frappe.get_all( + "Purchase Order Item", + filters=[("parent", "IN", po)], + fields=["parent", "item_code", "item_name", "qty", "received_qty"], + ) diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index c2b38d38e18..26e4243eeee 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -16,26 +16,40 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry class TestSubcontractedItemToBeReceived(FrappeTestCase): - def test_pending_and_received_qty(self): - po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes') + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") transfer_param = [] - 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 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_purchase_receipt_against_po(po.name) po.reload() - 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)), - 'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))})) - self.assertEqual(data[0]['pending_qty'], 5) - self.assertEqual(data[0]['received_qty'], 5) - self.assertEqual(data[0]['purchase_order'], po.name) - self.assertEqual(data[0]['supplier'], po.supplier) + 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) + ), + "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)), + } + ) + ) + self.assertEqual(data[0]["pending_qty"], 5) + self.assertEqual(data[0]["received_qty"], 5) + self.assertEqual(data[0]["purchase_order"], po.name) + self.assertEqual(data[0]["supplier"], po.supplier) def make_purchase_receipt_against_po(po, quantity=5): pr = make_purchase_receipt(po) pr.items[0].qty = quantity - pr.supplier_warehouse = '_Test Warehouse 1 - _TC' + pr.supplier_warehouse = "_Test Warehouse 1 - _TC" pr.insert() pr.submit() diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index 6b605add4c7..98b18da4acb 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -15,6 +15,7 @@ def execute(filters=None): return columns, data or [] + def get_columns(): return [ { @@ -22,47 +23,28 @@ def get_columns(): "fieldtype": "Link", "fieldname": "purchase_order", "options": "Purchase Order", - "width": 200 - }, - { - "label": _("Date"), - "fieldtype": "Date", - "fieldname": "date", - "width": 150 + "width": 200, }, + {"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "width": 150}, { "label": _("Supplier"), "fieldtype": "Link", "fieldname": "supplier", "options": "Supplier", - "width": 150 - }, - { - "label": _("Item Code"), - "fieldtype": "Data", - "fieldname": "rm_item_code", - "width": 150 - }, - { - "label": _("Required Quantity"), - "fieldtype": "Float", - "fieldname": "reqd_qty", - "width": 150 + "width": 150, }, + {"label": _("Item Code"), "fieldtype": "Data", "fieldname": "rm_item_code", "width": 150}, + {"label": _("Required Quantity"), "fieldtype": "Float", "fieldname": "reqd_qty", "width": 150}, { "label": _("Transferred Quantity"), "fieldtype": "Float", "fieldname": "transferred_qty", - "width": 200 + "width": 200, }, - { - "label": _("Pending Quantity"), - "fieldtype": "Float", - "fieldname": "p_qty", - "width": 150 - } + {"label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", "width": 150}, ] + def get_data(filters): po_rm_item_details = get_po_items_to_supply(filters) @@ -76,6 +58,7 @@ def get_data(filters): return data + def get_po_items_to_supply(filters): return frappe.db.get_all( "Purchase Order", @@ -85,14 +68,14 @@ def get_po_items_to_supply(filters): "supplier as supplier", "`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code", "`tabPurchase Order Item Supplied`.required_qty as reqd_qty", - "`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty" + "`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty", ], - filters = [ + filters=[ ["Purchase Order", "per_received", "<", "100"], ["Purchase Order", "is_subcontracted", "=", "Yes"], ["Purchase Order", "supplier", "=", filters.supplier], ["Purchase Order", "transaction_date", "<=", filters.to_date], ["Purchase Order", "transaction_date", ">=", filters.from_date], - ["Purchase Order", "docstatus", "=", 1] - ] + ["Purchase Order", "docstatus", "=", 1], + ], ) 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 fc9acabc81d..401176d5cef 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 @@ -17,82 +17,87 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry class TestSubcontractedItemToBeTransferred(FrappeTestCase): - def test_pending_and_transferred_qty(self): - po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + item_code="_Test FG Item", is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) # Material Receipt of RMs - 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) + 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 + ) se = transfer_subcontracted_raw_materials(po) - 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)), - 'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)) - } - )) + 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) + ), + "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)), + } + ) + ) po.reload() - po_data = [row for row in data if row.get('purchase_order') == po.name] + po_data = [row for row in data if row.get("purchase_order") == po.name] # Alphabetically sort to be certain of order - po_data = sorted(po_data, key = lambda i: i['rm_item_code']) + po_data = sorted(po_data, key=lambda i: i["rm_item_code"]) self.assertEqual(len(po_data), 2) - self.assertEqual(po_data[0]['purchase_order'], po.name) + self.assertEqual(po_data[0]["purchase_order"], po.name) - self.assertEqual(po_data[0]['rm_item_code'], '_Test Item') - self.assertEqual(po_data[0]['p_qty'], 8) - self.assertEqual(po_data[0]['transferred_qty'], 2) + self.assertEqual(po_data[0]["rm_item_code"], "_Test Item") + self.assertEqual(po_data[0]["p_qty"], 8) + self.assertEqual(po_data[0]["transferred_qty"], 2) - self.assertEqual(po_data[1]['rm_item_code'], '_Test Item Home Desktop 100') - self.assertEqual(po_data[1]['p_qty'], 19) - self.assertEqual(po_data[1]['transferred_qty'], 1) + self.assertEqual(po_data[1]["rm_item_code"], "_Test Item Home Desktop 100") + self.assertEqual(po_data[1]["p_qty"], 19) + self.assertEqual(po_data[1]["transferred_qty"], 1) se.cancel() po.cancel() + def transfer_subcontracted_raw_materials(po): # Order of supplied items fetched in PO is flaky - transfer_qty_map = { - '_Test Item': 2, - '_Test Item Home Desktop 100': 1 - } + transfer_qty_map = {"_Test Item": 2, "_Test Item Home Desktop 100": 1} item_1 = po.supplied_items[0].rm_item_code item_2 = po.supplied_items[1].rm_item_code rm_item = [ { - 'name': po.supplied_items[0].name, - 'item_code': item_1, - 'rm_item_code': item_1, - 'item_name': item_1, - 'qty': transfer_qty_map[item_1], - 'warehouse': '_Test Warehouse - _TC', - 'rate': 100, - 'amount': 100 * transfer_qty_map[item_1], - 'stock_uom': 'Nos' + "name": po.supplied_items[0].name, + "item_code": item_1, + "rm_item_code": item_1, + "item_name": item_1, + "qty": transfer_qty_map[item_1], + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 100 * transfer_qty_map[item_1], + "stock_uom": "Nos", }, { - 'name': po.supplied_items[1].name, - 'item_code': item_2, - 'rm_item_code': item_2, - 'item_name': item_2, - 'qty': transfer_qty_map[item_2], - 'warehouse': '_Test Warehouse - _TC', - 'rate': 100, - 'amount': 100 * transfer_qty_map[item_2], - 'stock_uom': 'Nos' - } + "name": po.supplied_items[1].name, + "item_code": item_2, + "rm_item_code": item_2, + "item_name": item_2, + "qty": transfer_qty_map[item_2], + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 100 * transfer_qty_map[item_2], + "stock_uom": "Nos", + }, ] rm_item_string = json.dumps(rm_item) se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) - se.from_warehouse = '_Test Warehouse - _TC' - se.to_warehouse = '_Test Warehouse - _TC' - se.stock_entry_type = 'Send to Subcontractor' + se.from_warehouse = "_Test Warehouse - _TC" + se.to_warehouse = "_Test Warehouse - _TC" + se.stock_entry_type = "Send to Subcontractor" se.save() se.submit() return se diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 65f9ce3c57e..3013b6d1607 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -24,6 +24,7 @@ def execute(filters=None): return columns, data, message, chart_data + def get_conditions(filters): conditions = "" if filters.get("item_code"): @@ -43,8 +44,10 @@ def get_conditions(filters): return conditions + def get_data(filters, conditions): - supplier_quotation_data = frappe.db.sql(""" + supplier_quotation_data = frappe.db.sql( + """ SELECT sqi.parent, sqi.item_code, sqi.qty, sqi.stock_qty, sqi.amount, @@ -60,23 +63,33 @@ def get_data(filters, conditions): AND sq.company = %(company)s AND sq.transaction_date between %(from_date)s and %(to_date)s {0} - order by sq.transaction_date, sqi.item_code""".format(conditions), filters, as_dict=1) + order by sq.transaction_date, sqi.item_code""".format( + conditions + ), + filters, + as_dict=1, + ) return supplier_quotation_data + def prepare_data(supplier_quotation_data, filters): out, groups, qty_list, suppliers, chart_data = [], [], [], [], [] group_wise_map = defaultdict(list) supplier_qty_price_map = {} - group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" + group_by_field = ( + "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" + ) company_currency = frappe.db.get_default("currency") float_precision = cint(frappe.db.get_default("float_precision")) or 2 for data in supplier_quotation_data: - group = data.get(group_by_field) # get item or supplier value for this row + group = data.get(group_by_field) # get item or supplier value for this row - supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency") + supplier_currency = frappe.db.get_value( + "Supplier", data.get("supplier_name"), "default_currency" + ) if supplier_currency: exchange_rate = get_exchange_rate(supplier_currency, company_currency) @@ -84,16 +97,18 @@ def prepare_data(supplier_quotation_data, filters): exchange_rate = 1 row = { - "item_code": "" if group_by_field=="item_code" else data.get("item_code"), # leave blank if group by field - "supplier_name": "" if group_by_field=="supplier_name" else data.get("supplier_name"), + "item_code": "" + if group_by_field == "item_code" + else data.get("item_code"), # leave blank if group by field + "supplier_name": "" if group_by_field == "supplier_name" else data.get("supplier_name"), "quotation": data.get("parent"), "qty": data.get("qty"), "price": flt(data.get("amount") * exchange_rate, float_precision), "uom": data.get("uom"), - "stock_uom": data.get('stock_uom'), + "stock_uom": data.get("stock_uom"), "request_for_quotation": data.get("request_for_quotation"), - "valid_till": data.get('valid_till'), - "lead_time_days": data.get('lead_time_days') + "valid_till": data.get("valid_till"), + "lead_time_days": data.get("lead_time_days"), } row["price_per_unit"] = flt(row["price"]) / (flt(data.get("stock_qty")) or 1) @@ -119,8 +134,8 @@ def prepare_data(supplier_quotation_data, filters): # final data format for report view for group in groups: - group_entries = group_wise_map[group] # all entries pertaining to item/supplier - group_entries[0].update({group_by_field : group}) # Add item/supplier name in first group row + group_entries = group_wise_map[group] # all entries pertaining to item/supplier + group_entries[0].update({group_by_field: group}) # Add item/supplier name in first group row if highlight_min_price: prices = [group_entry["price_per_unit"] for group_entry in group_entries] @@ -137,6 +152,7 @@ def prepare_data(supplier_quotation_data, filters): return out, chart_data + def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): data_points_map = {} qty_list.sort() @@ -157,107 +173,89 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): for qty in qty_list: datapoints = { "name": currency_symbol + " (Qty " + str(qty) + " )", - "values": data_points_map[qty] + "values": data_points_map[qty], } dataset.append(datapoints) - chart_data = { - "data": { - "labels": suppliers, - "datasets": dataset - }, - "type": "bar" - } + chart_data = {"data": {"labels": suppliers, "datasets": dataset}, "type": "bar"} return chart_data + def get_columns(filters): group_by_columns = [ - { - "fieldname": "supplier_name", - "label": _("Supplier"), - "fieldtype": "Link", - "options": "Supplier", - "width": 150 - }, - { - "fieldname": "item_code", - "label": _("Item"), - "fieldtype": "Link", - "options": "Item", - "width": 150 - }] + { + "fieldname": "supplier_name", + "label": _("Supplier"), + "fieldtype": "Link", + "options": "Supplier", + "width": 150, + }, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", + "width": 150, + }, + ] columns = [ - { - "fieldname": "uom", - "label": _("UOM"), - "fieldtype": "Link", - "options": "UOM", - "width": 90 - }, - { - "fieldname": "qty", - "label": _("Quantity"), - "fieldtype": "Float", - "width": 80 - }, - { - "fieldname": "price", - "label": _("Price"), - "fieldtype": "Currency", - "options": "Company:company:default_currency", - "width": 110 - }, - { - "fieldname": "stock_uom", - "label": _("Stock UOM"), - "fieldtype": "Link", - "options": "UOM", - "width": 90 - }, - { - "fieldname": "price_per_unit", - "label": _("Price per Unit (Stock UOM)"), - "fieldtype": "Currency", - "options": "Company:company:default_currency", - "width": 120 - }, - { - "fieldname": "quotation", - "label": _("Supplier Quotation"), - "fieldtype": "Link", - "options": "Supplier Quotation", - "width": 200 - }, - { - "fieldname": "valid_till", - "label": _("Valid Till"), - "fieldtype": "Date", - "width": 100 - }, - { - "fieldname": "lead_time_days", - "label": _("Lead Time (Days)"), - "fieldtype": "Int", - "width": 100 - }, - { - "fieldname": "request_for_quotation", - "label": _("Request for Quotation"), - "fieldtype": "Link", - "options": "Request for Quotation", - "width": 150 - }] + {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90}, + {"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80}, + { + "fieldname": "price", + "label": _("Price"), + "fieldtype": "Currency", + "options": "Company:company:default_currency", + "width": 110, + }, + { + "fieldname": "stock_uom", + "label": _("Stock UOM"), + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "fieldname": "price_per_unit", + "label": _("Price per Unit (Stock UOM)"), + "fieldtype": "Currency", + "options": "Company:company:default_currency", + "width": 120, + }, + { + "fieldname": "quotation", + "label": _("Supplier Quotation"), + "fieldtype": "Link", + "options": "Supplier Quotation", + "width": 200, + }, + {"fieldname": "valid_till", "label": _("Valid Till"), "fieldtype": "Date", "width": 100}, + { + "fieldname": "lead_time_days", + "label": _("Lead Time (Days)"), + "fieldtype": "Int", + "width": 100, + }, + { + "fieldname": "request_for_quotation", + "label": _("Request for Quotation"), + "fieldtype": "Link", + "options": "Request for Quotation", + "width": 150, + }, + ] if filters.get("group_by") == "Group by Item": group_by_columns.reverse() - columns[0:0] = group_by_columns # add positioned group by columns to the report + columns[0:0] = group_by_columns # add positioned group by columns to the report return columns + def get_message(): - return """ + return """ Valid till :    diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 66c60d56379..e904af0dce3 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -3,18 +3,19 @@ import json +from typing import Dict import frappe from frappe import _ -from frappe.utils import cint, cstr, flt +from frappe.utils import cint, cstr, flt, getdate from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_end_of_life -def update_last_purchase_rate(doc, is_submit): +def update_last_purchase_rate(doc, is_submit) -> None: """updates last_purchase_rate in item table for each item""" - import frappe.utils - this_purchase_date = frappe.utils.getdate(doc.get('posting_date') or doc.get('transaction_date')) + + this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date")) for d in doc.get("items"): # get last purchase details @@ -22,9 +23,10 @@ def update_last_purchase_rate(doc, is_submit): # compare last purchase date and this transaction's date last_purchase_rate = None - if last_purchase_details and \ - (doc.get('docstatus') == 2 or last_purchase_details.purchase_date > this_purchase_date): - last_purchase_rate = last_purchase_details['base_net_rate'] + if last_purchase_details and ( + doc.get("docstatus") == 2 or last_purchase_details.purchase_date > this_purchase_date + ): + last_purchase_rate = last_purchase_details["base_net_rate"] elif is_submit == 1: # even if this transaction is the latest one, it should be submitted # for it to be considered for latest purchase rate @@ -36,12 +38,10 @@ def update_last_purchase_rate(doc, is_submit): frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx)) # update last purchsae rate - frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate)) + frappe.db.set_value("Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate)) - - -def validate_for_items(doc): +def validate_for_items(doc) -> None: items = [] for d in doc.get("items"): if not d.qty: @@ -49,45 +49,87 @@ def validate_for_items(doc): continue frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code)) - # update with latest quantities - bin = frappe.db.sql("""select projected_qty from `tabBin` where - item_code = %s and warehouse = %s""", (d.item_code, d.warehouse), as_dict=1) - - f_lst ={'projected_qty': bin and flt(bin[0]['projected_qty']) or 0, 'ordered_qty': 0, 'received_qty' : 0} - if d.doctype in ('Purchase Receipt Item', 'Purchase Invoice Item'): - f_lst.pop('received_qty') - for x in f_lst : - if d.meta.get_field(x): - d.set(x, f_lst[x]) - - item = frappe.db.sql("""select is_stock_item, - is_sub_contracted_item, end_of_life, disabled from `tabItem` where name=%s""", - d.item_code, as_dict=1)[0] - + set_stock_levels(row=d) # update with latest quantities + item = validate_item_and_get_basic_data(row=d) + validate_stock_item_warehouse(row=d, item=item) validate_end_of_life(d.item_code, item.end_of_life, item.disabled) - # validate stock item - if item.is_stock_item==1 and d.qty and not d.warehouse and not d.get("delivered_by_supplier"): - frappe.throw(_("Warehouse is mandatory for stock Item {0} in row {1}").format(d.item_code, d.idx)) - items.append(cstr(d.item_code)) - if items and len(items) != len(set(items)) and \ - not cint(frappe.db.get_single_value("Buying Settings", "allow_multiple_items") or 0): + if ( + items + and len(items) != len(set(items)) + and not cint(frappe.db.get_single_value("Buying Settings", "allow_multiple_items") or 0) + ): frappe.throw(_("Same item cannot be entered multiple times.")) -def check_on_hold_or_closed_status(doctype, docname): + +def set_stock_levels(row) -> None: + projected_qty = frappe.db.get_value( + "Bin", + { + "item_code": row.item_code, + "warehouse": row.warehouse, + }, + "projected_qty", + ) + + qty_data = { + "projected_qty": flt(projected_qty), + "ordered_qty": 0, + "received_qty": 0, + } + if row.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"): + qty_data.pop("received_qty") + + for field in qty_data: + if row.meta.get_field(field): + row.set(field, qty_data[field]) + + +def validate_item_and_get_basic_data(row) -> Dict: + item = frappe.db.get_values( + "Item", + filters={"name": row.item_code}, + fieldname=["is_stock_item", "is_sub_contracted_item", "end_of_life", "disabled"], + as_dict=1, + ) + if not item: + frappe.throw(_("Row #{0}: Item {1} does not exist").format(row.idx, frappe.bold(row.item_code))) + + return item[0] + + +def validate_stock_item_warehouse(row, item) -> None: + if ( + item.is_stock_item == 1 + and row.qty + and not row.warehouse + and not row.get("delivered_by_supplier") + ): + frappe.throw( + _("Row #{1}: Warehouse is mandatory for stock Item {0}").format( + frappe.bold(row.item_code), row.idx + ) + ) + + +def check_on_hold_or_closed_status(doctype, docname) -> None: status = frappe.db.get_value(doctype, docname, "status") if status in ("Closed", "On Hold"): - frappe.throw(_("{0} {1} status is {2}").format(doctype, docname, status), frappe.InvalidStatusError) + frappe.throw( + _("{0} {1} status is {2}").format(doctype, docname, status), frappe.InvalidStatusError + ) + @frappe.whitelist() def get_linked_material_requests(items): items = json.loads(items) mr_list = [] for item in items: - material_request = frappe.db.sql("""SELECT distinct mr.name AS mr_name, + material_request = frappe.db.sql( + """SELECT distinct mr.name AS mr_name, (mr_item.qty - mr_item.ordered_qty) AS qty, mr_item.item_code AS item_code, mr_item.name AS mr_item @@ -98,7 +140,10 @@ def get_linked_material_requests(items): AND mr.per_ordered < 99.99 AND mr.docstatus = 1 AND mr.status != 'Stopped' - ORDER BY mr_item.item_code ASC""",{"item": item}, as_dict=1) + ORDER BY mr_item.item_code ASC""", + {"item": item}, + as_dict=1, + ) if material_request: mr_list.append(material_request) diff --git a/erpnext/change_log/v13/v13.0.2.md b/erpnext/change_log/v13/v13_0_2.md similarity index 100% rename from erpnext/change_log/v13/v13.0.2.md rename to erpnext/change_log/v13/v13_0_2.md diff --git a/erpnext/change_log/v13/v13_10_0.md b/erpnext/change_log/v13/v13_10_0.md new file mode 100644 index 00000000000..ee844e5526a --- /dev/null +++ b/erpnext/change_log/v13/v13_10_0.md @@ -0,0 +1,58 @@ +# Version 13.10.0 Release Notes + +### Features & Enhancements +- POS invoice coupon code feature ([#27004](https://github.com/frappe/erpnext/pull/27004)) +- Add Primary Address and Contact section in Supplier ([#27197](https://github.com/frappe/erpnext/pull/27197)) +- Capacity for Service Unit, concurrent appointments based on Capacity, Patient enhancements ([#24860](https://github.com/frappe/erpnext/pull/24860)) +- Increase number of supported currency exchanges ([#26763](https://github.com/frappe/erpnext/pull/26763)) +- South Africa VAT Audit Report ([#27017](https://github.com/frappe/erpnext/pull/27017)) +- Training Event Status Update and Validations ([#26698](https://github.com/frappe/erpnext/pull/26698)) +- Allow draft POS Invoices even if no stock available ([#27106](https://github.com/frappe/erpnext/pull/27106)) +- Column for total amount due in Accounts Receivable/Payable Summary ([#27069](https://github.com/frappe/erpnext/pull/27069)) +- Provision to create customer from opportunity ([#27141](https://github.com/frappe/erpnext/pull/27141)) +- Employee reminders ([#25735](https://github.com/frappe/erpnext/pull/25735)) +- Fetching details from supplier/customer groups ([#26131](https://github.com/frappe/erpnext/pull/26131)) +- Unreconcile on cancellation of bank transaction ([#27109](https://github.com/frappe/erpnext/pull/27109)) + +### Fixes +- Healthcare Redesign Changes ([#27100](https://github.com/frappe/erpnext/pull/27100)) +- Eway bill version changed to 1.0.0421 ([#27044](https://github.com/frappe/erpnext/pull/27044)) +- Org Chart fixes ([#26952](https://github.com/frappe/erpnext/pull/26952)) +- TDS calculation on net total ([#27058](https://github.com/frappe/erpnext/pull/27058)) +- Dimension filter query fix to avoid including disabled dimensions ([#26988](https://github.com/frappe/erpnext/pull/26988)) +- Various minor perf fixes for ledger postings ([#26775](https://github.com/frappe/erpnext/pull/26775)) +- Healthcare Service Unit fixes ([#27273](https://github.com/frappe/erpnext/pull/27273)) +- Selected batch no changed on changing of qty ([#27126](https://github.com/frappe/erpnext/pull/27126)) +- Changed label to "Inpatient Visit Charge" in appointment type ([#26906](https://github.com/frappe/erpnext/pull/26906)) +- Stock Analytics Report must consider warehouse during calculation ([#26908](https://github.com/frappe/erpnext/pull/26908)) +- Reduce Sales Invoice row size ([#27136](https://github.com/frappe/erpnext/pull/27136)) +- Allow backdated discharge for inpatient ([#25124](https://github.com/frappe/erpnext/pull/25124)) +- Sequence of sub-operations in job card ([#27138](https://github.com/frappe/erpnext/pull/27138)) +- Social media post fixes ([#24664](https://github.com/frappe/erpnext/pull/24664)) +- Consolidated balance sheet showing incorrect values ([#26975](https://github.com/frappe/erpnext/pull/26975)) +- Correct company address not getting copied from Purchase Order to Invoice ([#27217](https://github.com/frappe/erpnext/pull/27217)) +- Add child item groups into the filters ([#26997](https://github.com/frappe/erpnext/pull/26997)) +- Pass planned start date to in work order from production plan ([#27031](https://github.com/frappe/erpnext/pull/27031)) +- Filtering of items in Sales and Purchase Orders ([#26936](https://github.com/frappe/erpnext/pull/26936)) +- Sales order qty update fails in "Update Items" button ([#26992](https://github.com/frappe/erpnext/pull/26992)) +- Refactor stock module onboarding ([#25745](https://github.com/frappe/erpnext/pull/25745)) +- Calculation of gross profit percentage in Gross Profit Report ([#27045](https://github.com/frappe/erpnext/pull/27045)) +- Correct price list rate field value in return Sales Invoice ([#27105](https://github.com/frappe/erpnext/pull/27105)) +- Return Qty in PR/DN for legacy data ([#27003](https://github.com/frappe/erpnext/pull/27003)) +- Sales pipeline graph issue ([#26626](https://github.com/frappe/erpnext/pull/26626)) +- Additional salary processing ([#27005](https://github.com/frappe/erpnext/pull/27005)) +- Dimension filter query fix to avoid including disabled dimensions ([#27006](https://github.com/frappe/erpnext/pull/27006)) +- Incorrect Gl Entry on period closing involving finance books ([#27104](https://github.com/frappe/erpnext/pull/26921)) +- Set production plan to completed even on over production ([#27032](https://github.com/frappe/erpnext/pull/27032)) +- Budget variance missing values ([#26963](https://github.com/frappe/erpnext/pull/26963)) +- No able to create asset depreciation entry when cost_center is mandatory ([#26912](https://github.com/frappe/erpnext/pull/26912)) +- Keep stock entry title & purpose in sync ([#27043](https://github.com/frappe/erpnext/pull/27043)) +- Add mandatory depends on condition for export type field ([#26957](https://github.com/frappe/erpnext/pull/26957)) +- Fixed patched which were breaking while migrating ([#27205](https://github.com/frappe/erpnext/pull/27205)) +- ZeroDivisionError on creating e-invoice for credit note ([#26919](https://github.com/frappe/erpnext/pull/26919)) +- Stock analytics report date range issues and add company filter ([#27014](https://github.com/frappe/erpnext/pull/27014)) +- Stock Ledger report not working if include uom selected in filter ([#27127](https://github.com/frappe/erpnext/pull/27127)) +- Show proper currency symbol in Taxes and Charges table ([#26935](https://github.com/frappe/erpnext/pull/26935)) +- Operation time auto set to zero ([#27190](https://github.com/frappe/erpnext/pull/27190)) +- Set account for change amount even if pos profile not found ([#26986](https://github.com/frappe/erpnext/pull/26986)) +- Discard empty rows from update items ([#27021](https://github.com/frappe/erpnext/pull/27021)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_11_0.md b/erpnext/change_log/v13/v13_11_0.md new file mode 100644 index 00000000000..d78c932d90e --- /dev/null +++ b/erpnext/change_log/v13/v13_11_0.md @@ -0,0 +1,45 @@ +# Version 13.11.0 Release Notes + +### Features & Enhancements + +- E-commerce Refactor ([#24603](https://github.com/frappe/erpnext/pull/24603)) +- Common party accounting ([#27039](https://github.com/frappe/erpnext/pull/27039)) +- Add provision for process loss in manufacturing. ([#26151](https://github.com/frappe/erpnext/pull/26151)) +- Taxjar Integration update ([#27143](https://github.com/frappe/erpnext/pull/27143)) +- Add Primary Address and Contact section in Supplier ([#27197](https://github.com/frappe/erpnext/pull/27197)) +- Color and Leave Type in leave application calendar ([#27246](https://github.com/frappe/erpnext/pull/27246)) +- Handle Asset on Issuing Credit Note ([#26159](https://github.com/frappe/erpnext/pull/26159)) +- Depreciate Asset after sale ([#26543](https://github.com/frappe/erpnext/pull/26543)) +- Treatment Plan Template ([#26557](https://github.com/frappe/erpnext/pull/26557)) +- Improve Product Bundle handling ([#27319](https://github.com/frappe/erpnext/pull/27124)) + +### Fixes + +- POS payment mode selection issue ([#27409](https://github.com/frappe/erpnext/pull/27409)) +- Customers 'primary_address' not updated automatically ([#26799](https://github.com/frappe/erpnext/pull/26799)) +- Production Plan UX and validation message ([#27278](https://github.com/frappe/erpnext/pull/27278)) +- Job Card overlap unknown column `jc.employee` ([#27403](https://github.com/frappe/erpnext/pull/27403)) +- Stock Ageing report issues for serialized items ([#27228](https://github.com/frappe/erpnext/pull/27228)) +- Shopping Cart and Variant Selection ([#27508](https://github.com/frappe/erpnext/pull/27508)) +- Dont fetch Stopped/Cancelled MRs in Stock Entry Get Items dialog ([#27326](https://github.com/frappe/erpnext/pull/27326)) +- Incorrect component amount calculation if dependent on another payment days based component ([#27349](https://github.com/frappe/erpnext/pull/27349)) +- Stripe's Price API for plan-price information ([#26107](https://github.com/frappe/erpnext/pull/26107)) +- Correct company address not getting copied from Purchase Order to Invoice ([#27217](https://github.com/frappe/erpnext/pull/27217)) +- Don't allow BOM's item code at any level of child items ([#27176](https://github.com/frappe/erpnext/pull/27176)) +- Handle Excess/Multiple Item Transfer against Job Card ([#27486](https://github.com/frappe/erpnext/pull/27486)) +- Fixed issue with accessing last salary slip for new employee ([#27247](https://github.com/frappe/erpnext/pull/27247)) +- Org Chart fixes ([#27290](https://github.com/frappe/erpnext/pull/27290)) +- Calculate operating cost based on BOM Quantity ([#27464](https://github.com/frappe/erpnext/pull/27464)) +- Healthcare Service Unit fixes ([#27273](https://github.com/frappe/erpnext/pull/27273)) +- Presentation currency conversion in reports ([#27316](https://github.com/frappe/erpnext/pull/27316)) +- Added delivery date filters to get sales orders in production plan ([#27367](https://github.com/frappe/erpnext/pull/27367)) +- Manually added weight per unit reset to zero after save ([#27330](https://github.com/frappe/erpnext/pull/27330)) +- Allow to change incoming rate manually in case of stand-alone credit note ([#27036](https://github.com/frappe/erpnext/pull/27036)) +- Cannot reconcile bank transactions against internal transfer payment entries ([#26932](https://github.com/frappe/erpnext/pull/26932)) +- Added item price to default price list ([#27353](https://github.com/frappe/erpnext/pull/27353)) +- Expense Claim reimbursed amount update issue ([#27204](https://github.com/frappe/erpnext/pull/27204)) +- Braintree payment processed twice ([#27300](https://github.com/frappe/erpnext/pull/27300)) +- Fetch from more than one sales order in Maintenance Visit ([#26924](https://github.com/frappe/erpnext/pull/26924)) +- Values with same account name and different account number in consolidated balance sheet report ([#27493](https://github.com/frappe/erpnext/pull/27493)) +- Don't create inward SLE against SI unless is internal customer enabled ([#27086](https://github.com/frappe/erpnext/pull/27086)) +- Paging and Discount filter ([#27332](https://github.com/frappe/erpnext/pull/27332)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_12_0.md b/erpnext/change_log/v13/v13_12_0.md new file mode 100644 index 00000000000..d60b262496b --- /dev/null +++ b/erpnext/change_log/v13/v13_12_0.md @@ -0,0 +1,43 @@ +# Version 13.12.0 Release Notes + +### Features & Enhancements +- Merge POS invoices based on customer group ([#27471](https://github.com/frappe/erpnext/pull/27471)) + +- Get items from material request in purchase order ([#24725](https://github.com/frappe/erpnext/pull/24725)) + - Earlier system was fetching all the items from material request to purchase order + - Now user can fetch the specific items from material request to purchase order + +- Validity dates in Tax Withholding Rates ([#27258](https://github.com/frappe/erpnext/pull/27258)) + - Replaced fiscal year with From Date and To Date, to start the TDS effect from the mid of the fiscal year. + +- Toggle for reduced depreciation rate as per IT Act ([#27600](https://github.com/frappe/erpnext/pull/27600)) + - Added a toggle in the Finance Book to enable/disable the automatic reduction in the depreciation rate. + +- Deduct the TDS using Journal Entry ([#27451](https://github.com/frappe/erpnext/pull/27451)) + - Refactored TDS payable monthly report to show the TDS data which was created using Journal Entry. + +- Party specific item ([#27281](https://github.com/frappe/erpnext/pull/27281)) + - User can set the specific items to the supplier + - While making purchase transactions, user can see the items which are linked to the respective supplier. + +- Provision to add scrap items in job card ([#27483](https://github.com/frappe/erpnext/pull/27483)) + +### Fixes + +- Maintain same rate in Stock Ledger until stock become positive ([#27227](https://github.com/frappe/erpnext/pull/27227)) +- Duplicate Contact error on add Patient ([#27427](https://github.com/frappe/erpnext/pull/27427)) +- Distribution of additional costs in Manufacture Stock Entry ([#27629](https://github.com/frappe/erpnext/pull/27629)) +- Website Items with same Item name unhandled, thumbnails missing ([#27720](https://github.com/frappe/erpnext/pull/27720)) +- Delivery Note for transfer w/o internal customer ([#27798](https://github.com/frappe/erpnext/pull/27798)) +- Setting of gain/loss if party account is in company currency ([#27659](https://github.com/frappe/erpnext/pull/27659)) +- Check if doctype has company_address field before setting the value ([#27441](https://github.com/frappe/erpnext/pull/27441)) +- Removed b2c limit check from CDNR Invoices ([#27516](https://github.com/frappe/erpnext/pull/27516)) +- Cannot delete a project if linked with sales order ([#27536](https://github.com/frappe/erpnext/pull/27536)) +- Employee advance return through multiple additional salaries ([#27438](https://github.com/frappe/erpnext/pull/27438)) +- Improvements in COA Importer ([#27584](https://github.com/frappe/erpnext/pull/27584)) +- Validate if item exists on uploading items in stock reco ([#27543](https://github.com/frappe/erpnext/pull/27543)) +- Handle Excess/Multiple Item Transfer against Job Card ([#27486](https://github.com/frappe/erpnext/pull/27486)) +- Values with same account name and different account number in consolidated balance sheet report ([#27493](https://github.com/frappe/erpnext/pull/27493)) +- Added project name in the purchase order analysis ([#27701](https://github.com/frappe/erpnext/pull/27701)) +- Shopping Cart and Variant Selection ([#27508](https://github.com/frappe/erpnext/pull/27508)) + diff --git a/erpnext/change_log/v13/v13_13_0.md b/erpnext/change_log/v13/v13_13_0.md new file mode 100644 index 00000000000..3da2d721744 --- /dev/null +++ b/erpnext/change_log/v13/v13_13_0.md @@ -0,0 +1,29 @@ +# Version 13.13.0 Release Notes + +### Features & Enhancements + +- HR Module onboarding ([#25741](https://github.com/frappe/erpnext/pull/25741)) +- Tracking multiple rounds for the interview ([#25482](https://github.com/frappe/erpnext/pull/25482)) +- HSN based tax breakup table check in GST Settings (India Localization) ([#27907](https://github.com/frappe/erpnext/pull/27907)) + +### Fixes + +- To improve stock transactions added indexes in stock queries and speed up bin updation ([#27758](https://github.com/frappe/erpnext/pull/27758)) +- Interstate internal transfer invoices not visible in GSTR-1 ([#27970](https://github.com/frappe/erpnext/pull/27970)) +- Account number and name incorrectly imported using COA importer ([#27967](https://github.com/frappe/erpnext/pull/27967)) +- Multiple fixes to timesheets ([#27775](https://github.com/frappe/erpnext/pull/27742)) +- Totals row incorrect value in GL Entry ([#27867](https://github.com/frappe/erpnext/pull/27867)) +- Sales Order delivery Date not getting set via data import ([#27862](https://github.com/frappe/erpnext/pull/27862)) +- Add cost center in gl entry for advance payment entry ([#27840](https://github.com/frappe/erpnext/pull/27840)) +- Item Variant selection empty popup on website ([#27924](https://github.com/frappe/erpnext/pull/27924)) +- Improve performance of fetching account balance in chart of accounts ([#27661](https://github.com/frappe/erpnext/pull/27661)) +- Chart Of Accounts import button not visible ([#27748](https://github.com/frappe/erpnext/pull/27748)) +- Website Items with same Item name unhandled, thumbnails missing ([#27720](https://github.com/frappe/erpnext/pull/27720)) +- Delete linked Transaction Deletion Record docs on deleting company ([#27785](https://github.com/frappe/erpnext/pull/27785)) +- Display appropriate message for Payment Term discrepancies in Payment Entry ([#27749](https://github.com/frappe/erpnext/pull/27749)) +- Updated buying onboarding tours. ([#27800](https://github.com/frappe/erpnext/pull/27800)) +- Fixed variant qty in BOM while making work order ([#27686](https://github.com/frappe/erpnext/pull/27686)) +- Availability slots display, disabled Practitioner Schedule ([#27812](https://github.com/frappe/erpnext/pull/27812)) +- Consolidated report not consider company currency ([#27863](https://github.com/frappe/erpnext/pull/27863)) +- Batch Number not copied from Purchase Receipt to Stock Entry ([#27794](https://github.com/frappe/erpnext/pull/27794)) +- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation ([#27728](https://github.com/frappe/erpnext/pull/27728)) diff --git a/erpnext/change_log/v13/v13_14_0.md b/erpnext/change_log/v13/v13_14_0.md new file mode 100644 index 00000000000..3769c7984a7 --- /dev/null +++ b/erpnext/change_log/v13/v13_14_0.md @@ -0,0 +1,42 @@ +# Version 13.14.0 Release Notes + +### Features & Enhancements + +- KSA E-Invoicing and VAT Report ([#27369](https://github.com/frappe/erpnext/pull/27369)) + - Added KSA VAT settings to setup KSA VAT accounts + - New report KSA VAT to check the vat amounts + - Print format for KSA VAT Invoice ([#28166](https://github.com/frappe/erpnext/pull/28166)) + +- Provision to setup tax for recurring additional salary in Salary Slip ([#27459](https://github.com/frappe/erpnext/pull/27459)) +- Add dispatch address in E-invoicing for India localization ([#28084](https://github.com/frappe/erpnext/pull/28084)) +- Employee initial work history updated when transfer is performed ([#27768](https://github.com/frappe/erpnext/pull/27768)) +- Provision to setup quality inspection teamplte in the operation which will be use in the Job Card([#28219](https://github.com/frappe/erpnext/pull/28219)) +- Improved sales invoice submission performance ([#27916](https://github.com/frappe/erpnext/pull/27916)) + + +### Fixes + +- Splitting outstanding rows as per payment terms ([#27946](https://github.com/frappe/erpnext/pull/27946)) + +- Make status filter in Fixed Asset Register optional ([#28126](https://github.com/frappe/erpnext/pull/28126)) +- Skip empty rows while updating unsaved BOM cost ([#28136](https://github.com/frappe/erpnext/pull/28136)) +- TDS round off not working from second transaction ([#27934](https://github.com/frappe/erpnext/pull/27934)) +- Update receivable/payable account on company change in the Sales / Purchase Invoice ([#28057](https://github.com/frappe/erpnext/pull/28057)) +- Changes in Maintenance Schedule gets overwritten on save ([#27990](https://github.com/frappe/erpnext/pull/27990)) +- Fetch thumbnail from Item master instead of regenerating ([#28005](https://github.com/frappe/erpnext/pull/28005)) +- Serial Nos not set in the row after scanning in popup ([#28202](https://github.com/frappe/erpnext/pull/28202)) +- Taxjar customer_address fix, currency fix ([#28262](https://github.com/frappe/erpnext/pull/28262)) +- TaxJar update - added nexus list, making api call only for nexus ([#27497](https://github.com/frappe/erpnext/pull/27497)) +- Don't reset rates in Timesheet Detail when Activity Type is cleared ([#28056](https://github.com/frappe/erpnext/pull/28056)) +- Show full item name in search widget ([#28283](https://github.com/frappe/erpnext/pull/28283)) +- Avoid automatic customer creation on website user login ([#27914](https://github.com/frappe/erpnext/pull/27914)) +- POS Closing Entry without linked invoices ([#28042](https://github.com/frappe/erpnext/pull/28042)) +- Added patch to fix production plan status ([#27567](https://github.com/frappe/erpnext/pull/27567)) +- Interstate internal transfer invoices was not displying in the GSTR-1 report ([#27970](https://github.com/frappe/erpnext/pull/27970)) +- Shows opening balance from filtered from date in the stock balance and stock ledger report ([#26877](https://github.com/frappe/erpnext/pull/26877)) +- Employee filter in YTD and MTD in salary slip ([#27997](https://github.com/frappe/erpnext/pull/27997)) +- Removed warehouse filter on Batch field for Material Receipt ([#28195](https://github.com/frappe/erpnext/pull/28195)) +- Account number and name incorrectly imported using COA importer ([#27967](https://github.com/frappe/erpnext/pull/27967)) +- Autoemail report not showing dynamic report filters ([#28114](https://github.com/frappe/erpnext/pull/28114)) +- Incorrect VAT Amount in UAE VAT 201 report ([#27994](https://github.com/frappe/erpnext/pull/27994)) +- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation([#27728](https://github.com/frappe/erpnext/pull/27728)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_15_0.md b/erpnext/change_log/v13/v13_15_0.md new file mode 100644 index 00000000000..25f18c5e28a --- /dev/null +++ b/erpnext/change_log/v13/v13_15_0.md @@ -0,0 +1,36 @@ +# Version 13.15.0 Release Notes + +### Features & Enhancements + +- Add count for Healthcare Practitioner on Healthcare dashboard +([#28286](https://github.com/frappe/erpnext/pull/28286)) +- Improved financial statement report loading time ([#28238](https://github.com/frappe/erpnext/pull/28238)) +- Improved general ledger report loading time ([#27987](https://github.com/frappe/erpnext/pull/27987)) +- Replaced "=" with "in" for multiple statuses in query ([#28193](https://github.com/frappe/erpnext/pull/28193)) +- Update rate in the item price if the Update Existing Price List Rate is enabled in the stock settings ([#28255](https://github.com/frappe/erpnext/pull/28255)) + +### Fixes + +- Serial Nos not set in the row after scanning in popup ([#28202](https://github.com/frappe/erpnext/pull/28202)) +- Help section background in dark mode ([#28406](https://github.com/frappe/erpnext/pull/28406)) +- Don't make naming series mandatory for items ([#28394](https://github.com/frappe/erpnext/pull/28394)) +- Work order creation from sales order ([#28388](https://github.com/frappe/erpnext/pull/28388)) +- Workspace links to ecommerce settings ([#28360](https://github.com/frappe/erpnext/pull/28360)) +- Currency wise pricing rule was not working ([#28417](https://github.com/frappe/erpnext/pull/28417)) +- Bug with qrcode generation for the Urdu language ([#28471](https://github.com/frappe/erpnext/pull/28471)) +- Removed item - item group name validation ([#28392](https://github.com/frappe/erpnext/pull/28392)) +- Silter only submitted fees in student fee collection report ([#28280](https://github.com/frappe/erpnext/pull/28280)) +- Update tax template name for 18% GST ([#28156](https://github.com/frappe/erpnext/pull/28156)) +- Get credit amount for bank account of type liability ([#28132](https://github.com/frappe/erpnext/pull/28132)) +- Default party account getting overriden in invoices ([#28363](https://github.com/frappe/erpnext/pull/28363)) +- Remove warehouse filter on Batch field for Material Receipt ([#28195](https://github.com/frappe/erpnext/pull/28195)) +- POS idx issue in taxes table while merging ([#28389](https://github.com/frappe/erpnext/pull/28389)) +- Address not set in the Dispatch Address field ([#28333](https://github.com/frappe/erpnext/pull/28333)) +- Not able to edit the supplier scorecard criteria name once created ([#28348](https://github.com/frappe/erpnext/pull/28348)) +- GST category not getting auto updated ([#28459](https://github.com/frappe/erpnext/pull/28459)) +- Sales Invoice with duplicate items not showing correct taxable value ([#28334](https://github.com/frappe/erpnext/pull/28334)) +- KSA Invoice print format for multicurrency invoices ([#28489](https://github.com/frappe/erpnext/pull/28489)) +- Performance issue while submitting the Journal Entry ([#28425](https://github.com/frappe/erpnext/pull/28425)) +- Pricing Rule not created against the Promotional Scheme ([#28398](https://github.com/frappe/erpnext/pull/28398)) +- Pull only Items that are in Job Card in a Stock Entry against Job Card ([#28228](https://github.com/frappe/erpnext/pull/28228)) +- Fixed sum of components in salary register ([#28237](https://github.com/frappe/erpnext/pull/28237)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_16_0.md b/erpnext/change_log/v13/v13_16_0.md new file mode 100644 index 00000000000..e66e3df66e3 --- /dev/null +++ b/erpnext/change_log/v13/v13_16_0.md @@ -0,0 +1,37 @@ +# Version 13.16.0 Release Notes + +### Features & Enhancements + +- Accounts, Selling & Assets Onboarding cleanup ([#27112](https://github.com/frappe/erpnext/pull/27112)) +- Create party link from customer/supplier ([#28387](https://github.com/frappe/erpnext/pull/28387)) + +### Fixes +- Customer, Supplier heatmap data not rendering ([#28553](https://github.com/frappe/erpnext/pull/28553)) +- Item-Warehouse based reposting ([#28124](https://github.com/frappe/erpnext/pull/28124)) +- Filter out cancelled and non-depreciable Assets in Asset Value Adjustment ([#28443](https://github.com/frappe/erpnext/pull/28443)) +- Allow creating Shift Assignment for same day ([#28613](https://github.com/frappe/erpnext/pull/28613)) +- Taxes and Charges template not getting copied from Purchase Order/Receipt to Invoice ([#28654](https://github.com/frappe/erpnext/pull/28654)) +- Invoice amount in KSA E Invoice QR Code ([#28708](https://github.com/frappe/erpnext/pull/28708)) +- Replaced `get_list` with `get_all` for child doctypes ([#28538](https://github.com/frappe/erpnext/pull/28538)) +- Don't requeue repost immediately and clear progress ([#28684](https://github.com/frappe/erpnext/pull/28684)) +- Employee Advance paid amount not updated on PE cancellation ([#28572](https://github.com/frappe/erpnext/pull/28572)) +- COA balance rendering bug ([#28468](https://github.com/frappe/erpnext/pull/28468)) +- The combine items checkbox to trigger get_items and sub_assembly button ([#28558](https://github.com/frappe/erpnext/pull/28558)) +- Added missing job card item link in material request ([#28222](https://github.com/frappe/erpnext/pull/28222)) +- Incorrect discount amount set when item is replaced ([#28556](https://github.com/frappe/erpnext/pull/28556)) +- Display 'Total' before the totals row in the Gross Profit report ([#28513](https://github.com/frappe/erpnext/pull/28513)) +- Cost Center wise ledger posting for Period Closing Voucher ([#28477](https://github.com/frappe/erpnext/pull/28477)) +- Allocated Amount in Advances not updated on updating expense amount in Expense Claim ([#28497](https://github.com/frappe/erpnext/pull/28497)) +- Employee link formatter showing incorrect value for Employee Name ([#28504](https://github.com/frappe/erpnext/pull/28504)) +- Remove RM Cost column as cost is not retrievable from Job card ([#28123](https://github.com/frappe/erpnext/pull/28123)) +- Fixed total stock summary UI glitch ([#28564](https://github.com/frappe/erpnext/pull/28564)) +- Shipping Rule picking up old net_rate ([#28302](https://github.com/frappe/erpnext/pull/28302)) +- Changed fields position in the work order form ([#28217](https://github.com/frappe/erpnext/pull/28217)) +- Warehouse Capacity Dashboard UI ([#28431](https://github.com/frappe/erpnext/pull/28431)) +- Fixed broken bom tree view and removed duplicate button ([#28512](https://github.com/frappe/erpnext/pull/28512)) +- Incorrect balance in "Warehouse Wise Item Balance and Age" report ([#28583](https://github.com/frappe/erpnext/pull/28583)) +- Tax Withholding for Advances using Payment Entry against suppliers ([#27348](https://github.com/frappe/erpnext/pull/27348)) +- Removed abbreviation renaming ([#27766](https://github.com/frappe/erpnext/pull/27766)) +- Accepted/Rejected/Received Qty UX ([#28269](https://github.com/frappe/erpnext/pull/28269)) +- QR Code as per ZATKA specification ([#28605](https://github.com/frappe/erpnext/pull/28605)) +- POS Item cart only taxes with amount displayed ([#28501](https://github.com/frappe/erpnext/pull/28501)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_17_0.md b/erpnext/change_log/v13/v13_17_0.md new file mode 100644 index 00000000000..c418463a673 --- /dev/null +++ b/erpnext/change_log/v13/v13_17_0.md @@ -0,0 +1,33 @@ +# Version 13.17.0 Release Notes + +### Features & Enhancements + +- Provision to consumed the serialized raw materials during Asset Repairs ([#28349](https://github.com/frappe/erpnext/pull/28349)) +- Grant commission on certain items ([#27467](https://github.com/frappe/erpnext/pull/27467)) +- KSA E-Invoing optimizations and POS support ([#28799](https://github.com/frappe/erpnext/pull/28799)) +- Added QI link in Job Card Dashboard ([#28643](https://github.com/frappe/erpnext/pull/28643)) +- Show Zero Values filter in consolidated financial statement ([#28636](https://github.com/frappe/erpnext/pull/28636)) +- Validate pending reposts before freezing stock/account ([#28815](https://github.com/frappe/erpnext/pull/28815)) + + +### Fixes + +- Mapping to maintenance visit gets erased ([#28917](https://github.com/frappe/erpnext/pull/28917)) +- Ignore mandatory fields while creating WO from SO ([#28772](https://github.com/frappe/erpnext/pull/28772)) +- Map serial no from schedule if only one ([#28745](https://github.com/frappe/erpnext/pull/28745)) +- TDS Monthly payable report ([#28764](https://github.com/frappe/erpnext/pull/28764)) +- Maintenance Visit purposes tables is not visible on submission ([#28792](https://github.com/frappe/erpnext/pull/28792)) +- fetch memberships for 80G certificate by from date only ([#28700](https://github.com/frappe/erpnext/pull/28700)) +- Do not add GST fields if company is not from India ([#28592](https://github.com/frappe/erpnext/pull/28592)) +- Wrong german translation of abbreviation: PAN ([#28802](https://github.com/frappe/erpnext/pull/28802)) +- Removed attachment limit from item doctype ([#28632](https://github.com/frappe/erpnext/pull/28632)) +- Better Error logging for deferred revenue/expense booking ([#28731](https://github.com/frappe/erpnext/pull/28731)) +- Create Depreciation Schedules for existing Assets accurately ([#28675](https://github.com/frappe/erpnext/pull/28675)) +- Incorrect hsn-wise summary if the invoice has repeated item code ([#28783](https://github.com/frappe/erpnext/pull/28783)) +- Paid invoices showing in Accounts Receivable /Accounts Payable report ([#28627](https://github.com/frappe/erpnext/pull/28627)) +- Shipping Rule picking up old net_rate ([#28302](https://github.com/frappe/erpnext/pull/28302)) +- Actual tax conversion in case of multicurrency invoices ([#28539](https://github.com/frappe/erpnext/pull/28539)) +- QRCode for invoices with special characters ([#28715](https://github.com/frappe/erpnext/pull/28715)) +- Incorrect outgoing rates when "Allow Continuous Material Consumption" enabled in manufacturing settings ([#28710](https://github.com/frappe/erpnext/pull/28710)) +- Misleading "Set Default Warehouse" fields after saving ([#28798](https://github.com/frappe/erpnext/pull/28798)) +- Taxjar Nexus list visible only if child table is visible ([#28656](https://github.com/frappe/erpnext/pull/28656)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_18_0.md b/erpnext/change_log/v13/v13_18_0.md new file mode 100644 index 00000000000..d0d83623384 --- /dev/null +++ b/erpnext/change_log/v13/v13_18_0.md @@ -0,0 +1,43 @@ +# Version 13.18.0 Release Notes + +### Features & Enhancements + +- Deferred Revenue and Expense report with actual and upcoming postings ([#28822](https://github.com/frappe/erpnext/pull/28822)) +- 'Invoice Number' field in Opening Invoice Creation Tool ([#29147](https://github.com/frappe/erpnext/pull/29147)) +- Added required_date field to set date in child table ([#28432](https://github.com/frappe/erpnext/pull/28432)) + +### Fixes + +- Enable ksa POS Invoice print format ([#28911](https://github.com/frappe/erpnext/pull/28911)) +- Rename non existent doctype field to the right one ([#29055](https://github.com/frappe/erpnext/pull/29055)) +- Mapped accounting dimensions for Bank Entry against Payroll Entry ([#29142](https://github.com/frappe/erpnext/pull/29142)) +- Validate Finished Goods for independent Manufacture entries ([#28555](https://github.com/frappe/erpnext/pull/28555)) +- Incorrect posting time fetching incorrect stock quantity in stock reconciliation ([#29103](https://github.com/frappe/erpnext/pull/29103)) +- Stock Ageing Report - Negative Opening Stock ([#28966](https://github.com/frappe/erpnext/pull/28966)) +- Can't change valuation_method on item ([#28876](https://github.com/frappe/erpnext/pull/28876)) +- Optimize rate updation on changing price list ([#28953](https://github.com/frappe/erpnext/pull/28953)) +- Added filter for dispatch address ([#28937](https://github.com/frappe/erpnext/pull/28937)) +- Convert Item links to Website Item links in `Item Card Group` template data ([#28985](https://github.com/frappe/erpnext/pull/28985)) +- Earned Leave allocation from Leave Policy Assignment ([#29163](https://github.com/frappe/erpnext/pull/29163)) +- Items not mapped when trying to create a Maintenance Visit via Maintenance Schedule ([#28917](https://github.com/frappe/erpnext/pull/28917)) +- For performance improvement, removed forcing of posting sort index on stock balance report ([#28902](https://github.com/frappe/erpnext/pull/28902)) +- Future recurring period calculation ([#29083](https://github.com/frappe/erpnext/pull/29083)) +- Nonstock items are showing in the Itemwise Recommended Reorder Level report ([#28873](https://github.com/frappe/erpnext/pull/28873)) +- Incorrect amount based on payment days in timesheet salary slip ([#28845](https://github.com/frappe/erpnext/pull/28845)) +- Currency fix for `cost` field in subscription plan ([#28821](https://github.com/frappe/erpnext/pull/28821)) +- Fetch selling price with pricing rule ([#28951](https://github.com/frappe/erpnext/pull/28951)) +- Filter out Claimed employee advances in Expense Claim ([#29046](https://github.com/frappe/erpnext/pull/29046)) +- Tax and Charges template not getting fetched based on tax category assigned ([#29092](https://github.com/frappe/erpnext/pull/29092)) +- Ignore links while setting default notification templates in Settings ([#29042](https://github.com/frappe/erpnext/pull/29042)) +- Reset "Value After Depreciation" on reversing journal entry during Asset return ([#28975](https://github.com/frappe/erpnext/pull/28975)) +- Multicurrency invoices using subscription ([#28916](https://github.com/frappe/erpnext/pull/28916)) +- Fetch the appointment letter content in the same order as template ([#28968](https://github.com/frappe/erpnext/pull/28968)) +- Incorrect serial no valuation report showing cancelled entries ([#29172](https://github.com/frappe/erpnext/pull/29172)) +- Start date validation for deferred invoices ([#29009](https://github.com/frappe/erpnext/pull/29009)) +- HSN-Wise summary report is incorrect if an invoice has same item code multiple times ([#28783](https://github.com/frappe/erpnext/pull/28783)) +- Incorrect logic for the "Reserved Qty for Production" field in BIN ([#28880](https://github.com/frappe/erpnext/pull/28880)) +- Issues in Bank Reconciliation tool ([#28996](https://github.com/frappe/erpnext/pull/28996)) +- Hide Raw Material table in the Job Card if material transfer is against work order ([#28746](https://github.com/frappe/erpnext/pull/28746)) +- Added "Is Reverse Charge" checkbox in Tax Category for Indian Companies ([#28935](https://github.com/frappe/erpnext/pull/28935)) +- Updates in term loan processing ([#28034](https://github.com/frappe/erpnext/pull/28034)) +- Incorrect bin qty on backdated reconciliation ([#28588](https://github.com/frappe/erpnext/pull/28588)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_19_0.md b/erpnext/change_log/v13/v13_19_0.md new file mode 100644 index 00000000000..c4f7421dafd --- /dev/null +++ b/erpnext/change_log/v13/v13_19_0.md @@ -0,0 +1,52 @@ +## Version 13.19.0 Release Notes + +### Features & Enhancements + +- Allow user to change the parent company ([#28983](https://github.com/frappe/erpnext/pull/28983)) +- Option to exclude holidays while marking monthly attendance ([#29185](https://github.com/frappe/erpnext/pull/29185)) +- Early payment discount on sales & purchase orders ([#29101](https://github.com/frappe/erpnext/pull/29101)) + +### Fixes + +- Filter query in bank reconciliation tool ([#29098](https://github.com/frappe/erpnext/pull/29098)) +- Compute batch ledger in python ([#29324](https://github.com/frappe/erpnext/pull/29324)) +- GL Entries for loan repayment via Salary ([#29169](https://github.com/frappe/erpnext/pull/29169)) +- Group by Cost Center in General Ledger report only if include_dimensions is checked ([#28883](https://github.com/frappe/erpnext/pull/28883)) +- Filter for leave period in Bulk Leave Policy Assignment ([#29272](https://github.com/frappe/erpnext/pull/29272)) +- Update idx after updating items in so/po ([#29134](https://github.com/frappe/erpnext/pull/29134)) +- Avoid resetting default warehouse fields for Manufacture Entry ([#29257](https://github.com/frappe/erpnext/pull/29257)) +- Don't validate FG in repack entry ([#29271](https://github.com/frappe/erpnext/pull/29271)) +- Map Accounting Dimensions for Bank Entry against Payroll Entry ([#29142](https://github.com/frappe/erpnext/pull/29142)) +- Ignore cancelled SLEs ([#29303](https://github.com/frappe/erpnext/pull/29303)) +- Show work order progress bar even it is closed ([#29312](https://github.com/frappe/erpnext/pull/29312)) +- Incorrect serial no valuation report showing cancelled entries ([#29172](https://github.com/frappe/erpnext/pull/29172)) +- Not able to make a reverse journal entry ([#29125](https://github.com/frappe/erpnext/pull/29125)) +- Show ledger balance in Accounts Receivable and Payable summary ([#29135](https://github.com/frappe/erpnext/pull/29135)) +- Add stock queue in SLE for FIFO valuation method ([#29302](https://github.com/frappe/erpnext/pull/29302)) +- Threshold fields shows incorrect currency ([#29270](https://github.com/frappe/erpnext/pull/29270)) +- Added patch to trim whitespace from the serial numbers ([#29306](https://github.com/frappe/erpnext/pull/29306)) +- Task Depends on not removed from Gantt chart ([#28309](https://github.com/frappe/erpnext/pull/28309)) +- Earned Leave allocation from Leave Policy Assignment ([#29163](https://github.com/frappe/erpnext/pull/29163)) +- Exclude existing serial numbers while auto creating new serial numbers ([#29292](https://github.com/frappe/erpnext/pull/29292)) +- Deferred revenue booking for multi currency invoices via Journal Entry ([#29115](https://github.com/frappe/erpnext/pull/29115)) +- Fixed autoname generated for Job Applicant ([#29260](https://github.com/frappe/erpnext/pull/29260)) +- Incorrect scrap item quantity calculated in the Manufacture type stock entry ([#29179](https://github.com/frappe/erpnext/pull/29179)) +- Inconsistency in calculating outstanding amount ([#29176](https://github.com/frappe/erpnext/pull/29176)) +- Accounts are coming from different company in the dropdown ([#29280](https://github.com/frappe/erpnext/pull/29280)) +- Can't create debit note with zero quantity ([#28994](https://github.com/frappe/erpnext/pull/28994)) +- "Update Cost" should ignore overridden routing times ([#29154](https://github.com/frappe/erpnext/pull/29154)) +- Modifying Opening invoice creation tool timestamp ([#29127](https://github.com/frappe/erpnext/pull/29127)) +- Future recurring period calculation ([#29083](https://github.com/frappe/erpnext/pull/29083)) +- India localization: NIL Rated, Exempted and Non GST Invoices in GSTR-1 report ([#29208](https://github.com/frappe/erpnext/pull/29208)) +- Purchase to Stock UOM conversion on Production Plan ([#28570](https://github.com/frappe/erpnext/pull/28570)) +- Validation in POS for item batch no stock quantity ([#28907](https://github.com/frappe/erpnext/pull/28907)) +- Shopping cart total quantity ([#29076](https://github.com/frappe/erpnext/pull/29076)) +- POS items added to cart despite low quantity ([#29126](https://github.com/frappe/erpnext/pull/29126)) +- Exclude unpublished items while fetching items from other item groups ([#29211](https://github.com/frappe/erpnext/pull/29211)) +- Get project from PO into payment entry ([#29182](https://github.com/frappe/erpnext/pull/29182)) +- Cover case when all material needs to be bought ([#29326](https://github.com/frappe/erpnext/pull/29326)) +- Validate setup on clicking Mark Attendance button in Shift Type ([#29146](https://github.com/frappe/erpnext/pull/29146)) +- Can't ignore pricing rule for one particular POS invoice ([#29222](https://github.com/frappe/erpnext/pull/29222)) +- Cart & Popup Logic of Item variant without Website Item ([#29383](https://github.com/frappe/erpnext/pull/29383)) +- Not able to submit salary slips from amended payroll entry. ([#29228](https://github.com/frappe/erpnext/pull/29228)) +- Tax and Charges template not getting fetched based on tax category assigned ([#29092](https://github.com/frappe/erpnext/pull/29092)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_20_0.md b/erpnext/change_log/v13/v13_20_0.md new file mode 100644 index 00000000000..10c8fc70d20 --- /dev/null +++ b/erpnext/change_log/v13/v13_20_0.md @@ -0,0 +1,32 @@ +## Version 13.20.0 Release Notes + +### Features & Enhancements + +- Provisional accounting for expenses ([#29451](https://github.com/frappe/erpnext/pull/29451)) + +### Fixes + +- Incorrect number of items fetched while creating delivery note ([#29454](https://github.com/frappe/erpnext/pull/29454)) +- Incorrect raw materials quantity in manufacture stock entry ([#29419](https://github.com/frappe/erpnext/pull/29419)) +- Refactored the update_serial_no function for old Maintenance Visits ([#28843](https://github.com/frappe/erpnext/pull/28843)) +- Ignore empty customer/supplier in item query ([#29610](https://github.com/frappe/erpnext/pull/29610)) +- The "Bypass Credit Limit Check" from customer has not fetched in the Customer Credit Balance report ([#29367](https://github.com/frappe/erpnext/pull/29367)) +- Reset conversion facture after changing the Stock UOM ([#29062](https://github.com/frappe/erpnext/pull/29062)) +- Cart Items rendering issue ([#29398](https://github.com/frappe/erpnext/pull/29398)) +- Honour 'include holidays' setting while marking attendance for leave application ([#29425](https://github.com/frappe/erpnext/pull/29425)) +- Cost of poor quality report time filters not working ([#28958](https://github.com/frappe/erpnext/pull/28958)) +- Incorrect packing items getting fetched on Sales Return / Credit Note ([#28607](https://github.com/frappe/erpnext/pull/28607)) +- Regenerate packing items on newly mapped doc ([#29642](https://github.com/frappe/erpnext/pull/29642)) +- From Time and To Time not updated in drag and drop action for Course Schedule ([#29114](https://github.com/frappe/erpnext/pull/29114)) +- Employee: set user image and validate user id only if user data is found ([#29452](https://github.com/frappe/erpnext/pull/29452)) +- Clear Depreciation Schedule before modification ([#28507](https://github.com/frappe/erpnext/pull/28507)) +- Fixed shopping cart qty badge ([#29077](https://github.com/frappe/erpnext/pull/29077)) +- Fetch "transfer material against" from BOM ([#29435](https://github.com/frappe/erpnext/pull/29435)) +- Cart & Popup Logic of Item variant without Website Item ([#29383](https://github.com/frappe/erpnext/pull/29383)) +- Timesheets: calculate to time based on from time and hours ([#28589](https://github.com/frappe/erpnext/pull/28589)) +- Dynamically compute BOM Level ([#29522](https://github.com/frappe/erpnext/pull/29522)) +- Contact duplication on converting lead to customer ([#29337](https://github.com/frappe/erpnext/pull/29337)) +- Fixed populate practitioner selected in form to check availability popup ([#29405](https://github.com/frappe/erpnext/pull/29405)) +- Compute batch ledger in python ([#29324](https://github.com/frappe/erpnext/pull/29324)) +- Incorrect packing list for recurring items & code cleanup ([#29456](https://github.com/frappe/erpnext/pull/29456)) +- Opening invoice creation tool can fetch multiple accounting dimension ([#29407](https://github.com/frappe/erpnext/pull/29407)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_21_0.md b/erpnext/change_log/v13/v13_21_0.md new file mode 100644 index 00000000000..a1f36e83e80 --- /dev/null +++ b/erpnext/change_log/v13/v13_21_0.md @@ -0,0 +1,49 @@ +## Version 13.21.0 Release Notes + +### Features & Enhancements + +- Provisional accounting for expenses ([#29451](https://github.com/frappe/erpnext/pull/29451)) +- Allowing non stock items in POS ([#29556](https://github.com/frappe/erpnext/pull/29556)) +- Option to disable Item Tax Template and Tax Category ([#29349](https://github.com/frappe/erpnext/pull/29349)) + +### Fixes + +- Ignore linked invoices on Journal Entry cancel ([#29641](https://github.com/frappe/erpnext/pull/29641)) +- Do not hide Loan Repayment Entry field in salary slip ([#29535](https://github.com/frappe/erpnext/pull/29535)) +- Coupon code is applied even if ignore_pricing_rule is enabled ([#29859](https://github.com/frappe/erpnext/pull/29859)) +- Reserved for Production calculation considered closed work orders ([#29723](https://github.com/frappe/erpnext/pull/29723)) +- Disable rounded total in opening invoice creation tool ([#29789](https://github.com/frappe/erpnext/pull/29789)) +- Report GSTR-1 minor fixes ([#29700](https://github.com/frappe/erpnext/pull/29700)) +- Ignore rate validation for work order ([#29690](https://github.com/frappe/erpnext/pull/29690)) +- Incorrect provisional profit and loss in balance sheet ([#29601](https://github.com/frappe/erpnext/pull/29601)) +- Multiple WO for a single Production Plan Item ([#29603](https://github.com/frappe/erpnext/pull/29603)) +- Validation for invalid serial nos at POS invoice level ([#29447](https://github.com/frappe/erpnext/pull/29447)) +- Incorrect Grand Total in case of inclusive taxes on item ([#29701](https://github.com/frappe/erpnext/pull/29701)) +- Currency in bank reconciliation chart ([#29709](https://github.com/frappe/erpnext/pull/29709)) +- Set Pending Qty in Prod Plan after updating Work Order ([#29705](https://github.com/frappe/erpnext/pull/29705)) +- Enable Allow on Submit for 'Is Active' field in Salary Structure ([#29630](https://github.com/frappe/erpnext/pull/29630)) +- Bypass "Validate Selling Price for Item Against Purchase Rate or Valuation Rate" for free items ([#29359](https://github.com/frappe/erpnext/pull/29359)) +- Generate Warehouse wise FIFO Queue always and later aggregate if required ([#29788](https://github.com/frappe/erpnext/pull/29788)) +- Fixes in TDS payable monthly report ([#29791](https://github.com/frappe/erpnext/pull/29791)) +- Incorrect pricing rule filtering on selecting first item ([#29778](https://github.com/frappe/erpnext/pull/29778)) +- Stock Ageing Transfer Bucket logic for Repack Entry with split batch rows ([#29816](https://github.com/frappe/erpnext/pull/29816)) +- Incorrect packing list for recurring items & code cleanup ([#29456](https://github.com/frappe/erpnext/pull/29456)) +- Cost center validation of asset ([#29373](https://github.com/frappe/erpnext/pull/29373)) +- Coupon code item pricing dynamic updation issue in pos screen ([#29599](https://github.com/frappe/erpnext/pull/29599)) +- Billed amount in delivery note items ([#29290](https://github.com/frappe/erpnext/pull/29290)) +- Regenerate packed items on newly mapped doc ([#29642](https://github.com/frappe/erpnext/pull/29642)) +- Cannot jump to sales invoice in gross profit report ([#29748](https://github.com/frappe/erpnext/pull/29748)) +- Fetch image form item ([#29523](https://github.com/frappe/erpnext/pull/29523)) +- Add missing key in Loan ([#29660](https://github.com/frappe/erpnext/pull/29660)) +- Weed out disabled variants via sql query instead of pythonic looping separately ([#29639](https://github.com/frappe/erpnext/pull/29639 ()) +- Loan repayment via Salary Slip ([#29716](https://github.com/frappe/erpnext/pull/29716)) +- Earned leaves not allocated if assignment is created on month-end based on Leave Policy ([#29650](https://github.com/frappe/erpnext/pull/29650)) +- Time out error while making work orders from production plan ([#29736](https://github.com/frappe/erpnext/pull/29736)) +- Removal of coupon code ([#29896](https://github.com/frappe/erpnext/pull/29896)) +- Earned Leave allocation based on joining date fixes ([#29711](https://github.com/frappe/erpnext/pull/29711)) +- Total Credit amount in TDS Payable monthly report ([#29907](https://github.com/frappe/erpnext/pull/29907)) +- Pricing rule on transactions doesn't work ([#29597](https://github.com/frappe/erpnext/pull/29597)) +- Billing status for zero amount reference doc ([#29659](https://github.com/frappe/erpnext/pull/29659)) +- Zero rated exports in GSTR-3B report ([#29609](https://github.com/frappe/erpnext/pull/29609)) +- Update SO via Work Order made from MR ([#29803](https://github.com/frappe/erpnext/pull/29803)) +- Currency in bank reconciliation tool ([#29848](https://github.com/frappe/erpnext/pull/29848)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_22_0.md b/erpnext/change_log/v13/v13_22_0.md new file mode 100644 index 00000000000..24e3539d833 --- /dev/null +++ b/erpnext/change_log/v13/v13_22_0.md @@ -0,0 +1,42 @@ +## Version 13.22.0 Release Notes + +### Features & Enhancements + +- feat: Payment Terms Status report (backport #29137) ([#29137](https://github.com/frappe/erpnext/pull/29137)) + +### Fixes + +- fix(LMS): program enrollment does not give any feedback (backport #29922) ([#29922](https://github.com/frappe/erpnext/pull/29922)) +- fix: Update SO via Work Order made from MR (attached to SO) (backport #29803) ([#29803](https://github.com/frappe/erpnext/pull/29803)) +- fix: org chart connectors not rendered when Employee Naming is set to Full Name ([#29997](https://github.com/frappe/erpnext/pull/29997)) +- perf: Weed out disabled variants via sql query instead of pythonic looping separately (backport #29639) ([#29639](https://github.com/frappe/erpnext/pull/29639)) +- fix: task status loop ([#26006](https://github.com/frappe/erpnext/pull/26006)) +- fix: Commission not applied while making Sales Order from Quotation (backport #29978) ([#29978](https://github.com/frappe/erpnext/pull/29978)) +- fix: Validate party account with company (backport #29879) ([#29879](https://github.com/frappe/erpnext/pull/29879)) +- fix: add supported currencies for GoCardless (backport #29805) ([#29805](https://github.com/frappe/erpnext/pull/29805)) +- fix(asset): no. of depr booked cannot be equal to total no. of depr (backport #29900) ([#29900](https://github.com/frappe/erpnext/pull/29900)) +- fix: Fetch conversion factor even if it already existed in row, on item change ([#29917](https://github.com/frappe/erpnext/pull/29917)) +- fix(ux): make "allow zero valuation rate" readonly if "s_warehouse" is set ([#29681](https://github.com/frappe/erpnext/pull/29681)) +- fix: Block merging items if both have product bundles (backport #29913) ([#29913](https://github.com/frappe/erpnext/pull/29913)) +- fix: JobCard TimeLog to_date (backport #29872) ([#29872](https://github.com/frappe/erpnext/pull/29872)) +- fix: Stock Ageing Transfer Bucket logic for Repack Entry with split batch rows (backport #29816) ([#29816](https://github.com/frappe/erpnext/pull/29816)) +- fix(Salary Slip): TypeError while clearing any amount field in components (backport #29931) ([#29931](https://github.com/frappe/erpnext/pull/29931)) +- fix: allow renaming and merging ([#29830](https://github.com/frappe/erpnext/pull/29830)) +- fix(pos): minor fixes (backport #29991) ([#29991](https://github.com/frappe/erpnext/pull/29991)) +- fix(e-commerce): Unique Shopping Cart Per Logged In User ([#29994](https://github.com/frappe/erpnext/pull/29994)) +- fix: currency in bank reconciliation tool (backport #29848) ([#29848](https://github.com/frappe/erpnext/pull/29848)) +- fix: Account filter in PSOA (backport #29928) ([#29928](https://github.com/frappe/erpnext/pull/29928)) +- fix: Taxjar minor fixes (backport #29942) ([#29942](https://github.com/frappe/erpnext/pull/29942)) +- fix: Total taxes and charges in payment entry for multi-currency payments (backport #29977) ([#29977](https://github.com/frappe/erpnext/pull/29977)) +- fix(e-invoicing): remove batch no from e-invoices (backport #30084) ([#30084](https://github.com/frappe/erpnext/pull/30084)) +- fix: Total Credit amount in TDS Payable monthly report (backport #29907) ([#29907](https://github.com/frappe/erpnext/pull/29907)) +- fix: GSTIN filter for GSTR-1 report (backport #29869) ([#29869](https://github.com/frappe/erpnext/pull/29869)) +- fix: coupon code is applied even if ignore_pricing_rule is enabled (backport #29859) ([#29859](https://github.com/frappe/erpnext/pull/29859)) +- feat: update ordered qty for packed items (backport #29939) ([#29939](https://github.com/frappe/erpnext/pull/29939)) +- fix: validate Work Order qty against Production Plan ([#29721](https://github.com/frappe/erpnext/pull/29721)) +- fix: Fetch valuation rate for stock items consumed during asset repair ([#29714](https://github.com/frappe/erpnext/pull/29714)) +- fix: Email translations (backport #29956) ([#29956](https://github.com/frappe/erpnext/pull/29956)) +- fix(Timesheet): fetch exchange rate only if currency is set (backport #30057) ([#30057](https://github.com/frappe/erpnext/pull/30057)) +- refactor: removed validation to check zero qty (backport #30015) ([#30015](https://github.com/frappe/erpnext/pull/30015)) +- fix(pos): removal of coupon code (backport #29896) ([#29896](https://github.com/frappe/erpnext/pull/29896)) +- fix: Error in consolidated financial statements (backport #29771) ([#29771](https://github.com/frappe/erpnext/pull/29771)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_23_0.md b/erpnext/change_log/v13/v13_23_0.md new file mode 100644 index 00000000000..2c87bb9f673 --- /dev/null +++ b/erpnext/change_log/v13/v13_23_0.md @@ -0,0 +1,63 @@ +## Version 13.23.0 Release Notes + +### Features & Enhancements + +- feat: Bank Reconciliation for loan documents (backport #29865) ([#29865](https://github.com/frappe/erpnext/pull/29865)) +- feat: Include child item group products in Item Group Page & cleanup (backport #30091) ([#30091](https://github.com/frappe/erpnext/pull/30091)) +- refactor: Employee Leave Balance (backport #29439) ([#29439](https://github.com/frappe/erpnext/pull/29439)) + +### Fixes + +- perf(asset): fetch only distinct depreciable assets (backport #30114) ([#30114](https://github.com/frappe/erpnext/pull/30114)) +- fix(pos): do not reset mode of payments in case of consolidation (backport #30198) ([#30198](https://github.com/frappe/erpnext/pull/30198)) +- fix(pos): loyalty points in case of returned pos invoice (backport #30242) ([#30242](https://github.com/frappe/erpnext/pull/30242)) +- fix(psoa): add company filter to account (backport #30145) ([#30145](https://github.com/frappe/erpnext/pull/30145)) +- fix(psoa): no such element: dict object['account'] (backport #30153) ([#30153](https://github.com/frappe/erpnext/pull/30153)) +- fix(translation) - correction for assets translation (backport #30000) ([#30000](https://github.com/frappe/erpnext/pull/30000)) +- fix(ux): Improve label for better understanding (backport #30096) ([#30096](https://github.com/frappe/erpnext/pull/30096)) +- fix: 'save_quotations_as_draft' checkbox not honoured (backport #30160) ([#30160](https://github.com/frappe/erpnext/pull/30160)) +- fix: Add missing currency option in Supplier Quotation's `rounded_total` field (backport #30229) ([#30229](https://github.com/frappe/erpnext/pull/30229)) +- fix: Ambigous column in picklist query (backport #30102) ([#30102](https://github.com/frappe/erpnext/pull/30102)) +- fix: Cleanup and fixes in Dimension-wise Accounts Balance Report (backport #30284) ([#30284](https://github.com/frappe/erpnext/pull/30284)) +- fix: Do not consider cancelled entries (backport #30207) ([#30207](https://github.com/frappe/erpnext/pull/30207)) +- fix: Do not update ignore pricing rule check implicitly (backport #30269) ([#30269](https://github.com/frappe/erpnext/pull/30269)) +- fix: Error in bank reconciliation statement (backport #30261) ([#30261](https://github.com/frappe/erpnext/pull/30261)) +- fix: Get MRs that are yet to be received but fully ordered in Report `Requested Items to Order and Receive` (backport #29987) ([#29987](https://github.com/frappe/erpnext/pull/29987)) +- fix: Item discounts for quotation and other docs (backport #29940) ([#29940](https://github.com/frappe/erpnext/pull/29940)) +- fix: Item-wise sales history report (backport #30080) ([#30080](https://github.com/frappe/erpnext/pull/30080)) +- fix: Job Card sub operations status and list view ([#30243](https://github.com/frappe/erpnext/pull/30243)) +- fix: KSA E-Invoice QR Code showing wrong VAT amount (backport #30230) ([#30230](https://github.com/frappe/erpnext/pull/30230)) +- fix: Leave Policy Assignment creation patch ([#30215](https://github.com/frappe/erpnext/pull/30215)) +- fix: Multi-currency bank reconciliation fixes (backport #29979) ([#29979](https://github.com/frappe/erpnext/pull/29979)) +- fix: Multiple fixes in Gross Profit report (backport #29740) ([#29740](https://github.com/frappe/erpnext/pull/29740)) +- fix: Nil and Exempted values in GSTR-3B Report (backport #30039) ([#30039](https://github.com/frappe/erpnext/pull/30039)) +- fix: Non Profit fixes ([#30280](https://github.com/frappe/erpnext/pull/30280)) +- fix: Pos return payment mode issue ([#26872](https://github.com/frappe/erpnext/pull/26872)) +- fix: Remove tax invoice no field (backport #30136) ([#30136](https://github.com/frappe/erpnext/pull/30136)) +- fix: Sales and Purchase return optimization (backport #30152) ([#30152](https://github.com/frappe/erpnext/pull/30152)) +- fix: Search query of payroll entry reference in Journal Entry ([#30225](https://github.com/frappe/erpnext/pull/30225)) +- fix: Shipping rule application fixes (backport #30181) ([#30181](https://github.com/frappe/erpnext/pull/30181)) +- fix: Sub-Category Routing in Item Group Page Listing pills (backport #30258) ([#30258](https://github.com/frappe/erpnext/pull/30258)) +- fix: Validate income/expense account in sales and purchase invoice (backport #30266) ([#30266](https://github.com/frappe/erpnext/pull/30266)) +- fix: cannot create multicurrency sales order with product bundles (backport #30166) ([#30166](https://github.com/frappe/erpnext/pull/30166)) +- fix: cannot create purchase order from sales order (backport #30217) ([#30217](https://github.com/frappe/erpnext/pull/30217)) +- fix: customer credit limit validation on update (backport #30110) ([#30110](https://github.com/frappe/erpnext/pull/30110)) +- fix: disable "get items" buttons from submitted production plan (backport #30223) ([#30223](https://github.com/frappe/erpnext/pull/30223)) +- fix: do not reset asset_category (backport #29696) ([#29696](https://github.com/frappe/erpnext/pull/29696)) +- fix: dont fetch entire barcode table in get_item_details ([#30131](https://github.com/frappe/erpnext/pull/30131)) +- fix: dont reset UOM in MR on every get_item_detail call (backport #30164) ([#30164](https://github.com/frappe/erpnext/pull/30164)) +- fix: fetch new fields from routing to bom ([#30169](https://github.com/frappe/erpnext/pull/30169)) +- fix: filter default_discount_account field in item_group_defaults table is_group = 0 ([#30095](https://github.com/frappe/erpnext/pull/30095)) +- fix: ignore non-unique swift numbers while migrating (backport #30132) ([#30132](https://github.com/frappe/erpnext/pull/30132)) +- fix: incorrect debit credit amount in presentation currency (backport #30244) ([#30244](https://github.com/frappe/erpnext/pull/30244)) +- fix: leave allocation records query ([#30118](https://github.com/frappe/erpnext/pull/30118)) +- fix: max_qty validation condition in WO (backport #30216) ([#30216](https://github.com/frappe/erpnext/pull/30216)) +- fix: pos return payment mode issue (#26872) ([#30220](https://github.com/frappe/erpnext/pull/30220)) +- fix: program enrollment button labels (backport #30148) ([#30148](https://github.com/frappe/erpnext/pull/30148)) +- fix: respect db multi_tenancy while fetching precision (backport #30301) ([#30301](https://github.com/frappe/erpnext/pull/30301)) +- fix: salary slip amount rounding errors ([#30248](https://github.com/frappe/erpnext/pull/30248)) +- fix: packed items return when items are removed ([#30263](https://github.com/frappe/erpnext/pull/30263)) +- fix: wrong payment days in salary slip for employees joining/leaving during mid payroll dates (backport #29082) ([#29082](https://github.com/frappe/erpnext/pull/29082)) +- refactor: removed unrequired code and test for standalone delivery note serial return ([#30276](https://github.com/frappe/erpnext/pull/30276)) +- revert: BU Schlüssel (a21f76f) (backport #29654) ([#29654](https://github.com/frappe/erpnext/pull/29654)) +- chore: add German translations (backport #30265) ([#30265](https://github.com/frappe/erpnext/pull/30265)) diff --git a/erpnext/change_log/v13/v13_24_0.md b/erpnext/change_log/v13/v13_24_0.md new file mode 100644 index 00000000000..d6920d67a5d --- /dev/null +++ b/erpnext/change_log/v13/v13_24_0.md @@ -0,0 +1,40 @@ +## Version 13.24.0 Release Notes + +### Features & Enhancements + +- feat: Create single PL/DN from several SO. ([#30238](https://github.com/frappe/erpnext/pull/30238)) + +### Fixes + +- fix: Changing item prices on converting orders/receipts to invoices ([#30365](https://github.com/frappe/erpnext/pull/30365)) +- fix(India): Auto tax fetching based on GSTIN ([#30385](https://github.com/frappe/erpnext/pull/30385)) +- fix: broken production item links on production plan ([#30399](https://github.com/frappe/erpnext/pull/30399)) +- fix(ux): warning for disabled carry forwarding in Policy Assignment ([#30331](https://github.com/frappe/erpnext/pull/30331)) +- fix: failing broken patches ([#30409](https://github.com/frappe/erpnext/pull/30409)) +- fix: Payment Request Amount calculation in case of multi-currency ([#30254](https://github.com/frappe/erpnext/pull/30254)) +- fix: disable deferred naming on SLE/GLE if hash method is used. ([#30286](https://github.com/frappe/erpnext/pull/30286)) +- fix: Rate change issue on save and mapping from other doc ([#30406](https://github.com/frappe/erpnext/pull/30406)) +- fix: Error in bank reconciliation statement ([#30261](https://github.com/frappe/erpnext/pull/30261)) +- fix: clear "Retain Sample" and "Max Sample Quantity" in Item card if Has Batch No is uncheck ([#30307](https://github.com/frappe/erpnext/pull/30307)) +- fix: (ux) Add `is_group=0` filter on website warehouse field in Website Item ([#30396](https://github.com/frappe/erpnext/pull/30396)) +- fix: Allow draft PE, PA to link to Vital Signs ([#29934](https://github.com/frappe/erpnext/pull/29934)) +- fix: Clean and fixes in Dimension-wise Accounts Balance Report ([#30284](https://github.com/frappe/erpnext/pull/30284)) +- fix: Write off amount wrongly calculated in POS Invoice ([#30395](https://github.com/frappe/erpnext/pull/30395)) +- fix: custom cash flow mapper doesn't show any data ([#30287](https://github.com/frappe/erpnext/pull/30287)) +- fix: unsupported operand type(s) for +=: 'int' and 'NoneType' ([#30420](https://github.com/frappe/erpnext/pull/30420)) +- fix: Check for onload property ([#30429](https://github.com/frappe/erpnext/pull/30429)) +- fix: Reset GST State number ([#30334](https://github.com/frappe/erpnext/pull/30334)) +- fix: Future recurring period calculation for additional salary ([#29581](https://github.com/frappe/erpnext/pull/29581)) +- fix: GST account not showing up in tax templates ([#30361](https://github.com/frappe/erpnext/pull/30361)) +- fix: P&L account validation on cancellation ([#30317](https://github.com/frappe/erpnext/pull/30317)) +- refactor: remove redundant if-statement ([#30311](https://github.com/frappe/erpnext/pull/30311)) +- fix: respect db multi_tenancy while fetching precision ([#30301](https://github.com/frappe/erpnext/pull/30301)) +- fix: show subassembly table always ([#30422](https://github.com/frappe/erpnext/pull/30422)) +- fix: Validate income/expense account in sales and purchase invoice ([#30266](https://github.com/frappe/erpnext/pull/30266)) +- fix: Add permission for KSA VAT documents ([#30304](https://github.com/frappe/erpnext/pull/30304)) +- fix(UX): misc serial no selector + warehouse.py refactor ([#30309](https://github.com/frappe/erpnext/pull/30309)) +- fix: While creating Payment Request from other forms, open a new Payment Request form without saving ([#30228](https://github.com/frappe/erpnext/pull/30228)) +- fix: Product Filters Lookup ([#30336](https://github.com/frappe/erpnext/pull/30336)) +- fix: Allow on Submit for Material Request Item Required Date ([#30174](https://github.com/frappe/erpnext/pull/30174)) +- fix: Taxes not getting fetched from item tax template ([#30343](https://github.com/frappe/erpnext/pull/30343)) +- fix: Incorrect default amount to pay for POS invoices ([#30438](https://github.com/frappe/erpnext/pull/30438)) diff --git a/erpnext/change_log/v13/v13_25_0.md b/erpnext/change_log/v13/v13_25_0.md new file mode 100644 index 00000000000..6c21a839870 --- /dev/null +++ b/erpnext/change_log/v13/v13_25_0.md @@ -0,0 +1,47 @@ +## Version 13.25.0 Release Notes + +### Features & Enhancements + +- feat: minor, pick list item reference on delivery note item table ([#30527](https://github.com/frappe/erpnext/pull/30527)) +- feat: configurable Contract naming ([#30450](https://github.com/frappe/erpnext/pull/30450)) + +### Fixes + +- fix: Account currency validation ([#30486](https://github.com/frappe/erpnext/pull/30486)) +- fix(india): minor e-invoicing fixes ([#30553](https://github.com/frappe/erpnext/pull/30553)) +- feat: Redisearch with consent (bp) ([#30539](https://github.com/frappe/erpnext/pull/30539)) +- fix: maintain FIFO queue even if outgoing_rate is not found ([#30563](https://github.com/frappe/erpnext/pull/30563)) +- fix(lead): reload address and contact before updating their links ([#29968](https://github.com/frappe/erpnext/pull/29968)) +- fix(lead): reload contact before updating links ([#29966](https://github.com/frappe/erpnext/pull/29966)) +- fix: Add non-existent Item check and cleanup in `validate_for_items` ([#30509](https://github.com/frappe/erpnext/pull/30509)) +- fix: incorrect payable amount for loan closure ([#30191](https://github.com/frappe/erpnext/pull/30191)) +- fix: Do not apply shipping rule for POS transactions ([#30575](https://github.com/frappe/erpnext/pull/30575)) +- perf: index barcode for faster scans ([#30543](https://github.com/frappe/erpnext/pull/30543)) +- fix: don't check for failed repost while freezing ([#30472](https://github.com/frappe/erpnext/pull/30472)) +- fix: if accepted warehouse not selected during rejection then stock ledger not created ([#30564](https://github.com/frappe/erpnext/pull/30564)) +- fix: hide pending qty only if original item is assigned ([#30599](https://github.com/frappe/erpnext/pull/30599)) +- fix(ux): refresh update to zero val checkbox ([#30567](https://github.com/frappe/erpnext/pull/30567)) +- refactor: Add exception handling in background job within BOM Update Tool ([#30146](https://github.com/frappe/erpnext/pull/30146)) +- fix: bom valuation - handle lack of LPP ([#30454](https://github.com/frappe/erpnext/pull/30454)) +- fix: total leaves allocated not validated and recalculated on updates post submission ([#30569](https://github.com/frappe/erpnext/pull/30569)) +- fix: convert dates to datetime before comparing in leave days calculation and fix half day edge case ([#30538](https://github.com/frappe/erpnext/pull/30538)) +- fix: Ignore user perm for party account company ([#30555](https://github.com/frappe/erpnext/pull/30555)) +- fix(asset): do not validate warehouse on asset purchase ([#30461](https://github.com/frappe/erpnext/pull/30461)) +- fix: credit limit validation in delivery note ([#30470](https://github.com/frappe/erpnext/pull/30470)) +- fix: enable row deletion in reference table ([#30453](https://github.com/frappe/erpnext/pull/30453)) +- fix: use `name` for links not `item_code` ([#30462](https://github.com/frappe/erpnext/pull/30462)) +- fix: multiple pos issues (copy #30324) ([#30515](https://github.com/frappe/erpnext/pull/30515)) +- fix: fetch from fields not working in eway bill dialog ([#30579](https://github.com/frappe/erpnext/pull/30579)) +- fix(pos): do not reset search input on item selection ([#30537](https://github.com/frappe/erpnext/pull/30537)) +- fix: explicitly check if additional salary is recurring while fetching components for payroll ([#30489](https://github.com/frappe/erpnext/pull/30489)) +- fix(India): Tax fetching based on tax category ([#30500](https://github.com/frappe/erpnext/pull/30500)) +- fix: submit Work Order when “Make Serial No / Batch from Work Order” is enabled ([#30468](https://github.com/frappe/erpnext/pull/30468)) +- fix: Dont set `idx` while adding WO items to Stock Entry ([#30377](https://github.com/frappe/erpnext/pull/30377)) +- fix(India): Auto tax fetching based on GSTIN ([#30385](https://github.com/frappe/erpnext/pull/30385)) +- fix: validate 0 transfer qty in stock entry (copy #30476) ([#30479](https://github.com/frappe/erpnext/pull/30479)) +- fix: Issues on loan repayment ([#30557](https://github.com/frappe/erpnext/pull/30557)) +- fix: Added validation for single_threshold in Tax With Holding Category ([#30382](https://github.com/frappe/erpnext/pull/30382)) +- fix: Remove trailing slashes "/" from route ([#30531](https://github.com/frappe/erpnext/pull/30531)) +- fix: validate 0 transfer qty in stock entry ([#30476](https://github.com/frappe/erpnext/pull/30476)) +- fix: Taxes getting overriden from mapped to target doc ([#30510](https://github.com/frappe/erpnext/pull/30510)) +- fix: move item tax to item tax template patch ([#30419](https://github.com/frappe/erpnext/pull/30419)) diff --git a/erpnext/change_log/v13/v13_26_0.md b/erpnext/change_log/v13/v13_26_0.md new file mode 100644 index 00000000000..b29d018039e --- /dev/null +++ b/erpnext/change_log/v13/v13_26_0.md @@ -0,0 +1,31 @@ +## Version 13.26.0 Release Notes + +### Features & Enhancements + +- feat: Receivable/Payable Account column and filter in AR/AP report ([#30620](https://github.com/frappe/erpnext/pull/30620)) +- feat(india): e-invoicing for intra-state union territory transactions ([#30626](https://github.com/frappe/erpnext/pull/30626)) +- feat: Ignore permlevel for specific fields ([#30686](https://github.com/frappe/erpnext/pull/30686)) +- feat: 'customer' column and more filter to Payment terms status report ([#30499](https://github.com/frappe/erpnext/pull/30499)) + +### Fixes + +- fix: fallback to item_name if description is not found ([#30619](https://github.com/frappe/erpnext/pull/30619)) +- fix: Deferred Revenue/Expense Account validation ([#30602](https://github.com/frappe/erpnext/pull/30602)) +- fix(pos): reload doc before set value ([#30610](https://github.com/frappe/erpnext/pull/30610)) +- fix: Exchange gain and loss button in Payment Entry ([#30606](https://github.com/frappe/erpnext/pull/30606)) +- fix(pos): cannot change paid amount in pos payments ([#30657](https://github.com/frappe/erpnext/pull/30657)) +- fix: hide pending qty only if original item is assigned ([#30599](https://github.com/frappe/erpnext/pull/30599)) +- fix(pos): reload doc before set value ([#30611](https://github.com/frappe/erpnext/pull/30611)) +- fix: Handle multiple item transfer in separate SEs against WO ([#30674](https://github.com/frappe/erpnext/pull/30674)) +- fix(patch): check null values in is_cancelled patch ([#30594](https://github.com/frappe/erpnext/pull/30594)) +- fix: Download JSON for GSTR-1 report ([#30651](https://github.com/frappe/erpnext/pull/30651)) +- fix: remove bad defaults from BOM operation ([#30644](https://github.com/frappe/erpnext/pull/30644)) +- fix: update translation ([#30474](https://github.com/frappe/erpnext/pull/30474)) +- fix: dont reassign mutable (list) to a different field ([#30634](https://github.com/frappe/erpnext/pull/30634)) +- fix: update translation ([#30654](https://github.com/frappe/erpnext/pull/30654)) +- fix: ignore item-less maintenance visit for sr no ([#30684](https://github.com/frappe/erpnext/pull/30684)) +- fix: removed unused courses template ([#30596](https://github.com/frappe/erpnext/pull/30596)) +- fix: Implicit ignore pricing rule check on returns ([#30662](https://github.com/frappe/erpnext/pull/30662)) +- fix: warehouse naming when suffix is present ([#30621](https://github.com/frappe/erpnext/pull/30621)) +- fix: Ignore disabled tax categories ([#30542](https://github.com/frappe/erpnext/pull/30542)) + diff --git a/erpnext/change_log/v13/v13_3_0.md b/erpnext/change_log/v13/v13_3_0.md new file mode 100644 index 00000000000..016dbb01f4d --- /dev/null +++ b/erpnext/change_log/v13/v13_3_0.md @@ -0,0 +1,73 @@ +# Version 13.3.0 Release Notes + +### Features & Enhancements + +- Purchase receipt creation from purchase invoice ([#25126](https://github.com/frappe/erpnext/pull/25126)) +- New Document Transaction Deletion ([#25354](https://github.com/frappe/erpnext/pull/25354)) +- Employee Referral ([#24997](https://github.com/frappe/erpnext/pull/24997)) +- Add Create Expense Claim button in Delivery Trip ([#25526](https://github.com/frappe/erpnext/pull/25526)) +- Reduced rate of asset depreciation as per IT Act ([#25648](https://github.com/frappe/erpnext/pull/25648)) +- Improve DATEV export ([#25238](https://github.com/frappe/erpnext/pull/25238)) +- Add pick batch button ([#25413](https://github.com/frappe/erpnext/pull/25413)) +- Enable custom field search on POS ([#25421](https://github.com/frappe/erpnext/pull/25421)) +- New check field in subscriptions for (not) submitting invoices ([#25394](https://github.com/frappe/erpnext/pull/25394)) +- Show POS reserved stock in stock projected qty report ([#25593](https://github.com/frappe/erpnext/pull/25593)) +- e-way bill validity field ([#25555](https://github.com/frappe/erpnext/pull/25555)) +- Significant reduction in time taken to save sales documents ([#25475](https://github.com/frappe/erpnext/pull/25475)) + +### Fixes + +- Bank statement import via google sheet ([#25677](https://github.com/frappe/erpnext/pull/25677)) +- Invoices not getting fetched during payment reconciliation ([#25598](https://github.com/frappe/erpnext/pull/25598)) +- Error on applying TDS without party ([#25632](https://github.com/frappe/erpnext/pull/25632)) +- Allow to cancel loan with cancelled repayment entry ([#25507](https://github.com/frappe/erpnext/pull/25507)) +- Can't open general ledger from consolidated financial report ([#25542](https://github.com/frappe/erpnext/pull/25542)) +- Add 'Partially Received' to Status drop-down list in Material Request ([#24857](https://github.com/frappe/erpnext/pull/24857)) +- Updated item filters for material request ([#25531](https://github.com/frappe/erpnext/pull/25531)) +- Added validation in stock entry to check duplicate serial nos ([#25611](https://github.com/frappe/erpnext/pull/25611)) +- Update shopify api version ([#25600](https://github.com/frappe/erpnext/pull/25600)) +- Dialog variable assignment after definition in POS ([#25680](https://github.com/frappe/erpnext/pull/25680)) +- Added tax_types list ([#25587](https://github.com/frappe/erpnext/pull/25587)) +- Include search fields in Project Link field query ([#25505](https://github.com/frappe/erpnext/pull/25505)) +- Item stock levels displaying inconsistently ([#25506](https://github.com/frappe/erpnext/pull/25506)) +- Change today to now to get data for reposting ([#25703](https://github.com/frappe/erpnext/pull/25703)) +- Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet ([#25700](https://github.com/frappe/erpnext/pull/25700)) +- Minor fixes in loan ([#25546](https://github.com/frappe/erpnext/pull/25546)) +- Fieldname when updating docfield property ([#25516](https://github.com/frappe/erpnext/pull/25516)) +- Use get_serial_nos for splitting ([#25590](https://github.com/frappe/erpnext/pull/25590)) +- Show item's full name on hover over item in POS ([#25554](https://github.com/frappe/erpnext/pull/25554)) +- Stock ledger entry created against draft stock entry ([#25540](https://github.com/frappe/erpnext/pull/25540)) +- Incorrect expense account set in pos invoice ([#25543](https://github.com/frappe/erpnext/pull/25543)) +- Stock balance and batch-wise balance history report showing different closing stock ([#25575](https://github.com/frappe/erpnext/pull/25575)) +- Make strings translatable ([#25521](https://github.com/frappe/erpnext/pull/25521)) +- Serial no changed after saving stock reconciliation ([#25541](https://github.com/frappe/erpnext/pull/25541)) +- Ignore fraction difference while making round off gl entry ([#25438](https://github.com/frappe/erpnext/pull/25438)) +- Sync shopify customer addresses ([#25481](https://github.com/frappe/erpnext/pull/25481)) +- Total stock summary report not working ([#25551](https://github.com/frappe/erpnext/pull/25551)) +- Rename field has not updated value of deposit and withdrawal fields ([#25545](https://github.com/frappe/erpnext/pull/25545)) +- Unexpected keyword argument 'merge_logs' ([#25489](https://github.com/frappe/erpnext/pull/25489)) +- Validation message of quality inspection in purchase receipt ([#25667](https://github.com/frappe/erpnext/pull/25667)) +- Added is_stock_item filter ([#25530](https://github.com/frappe/erpnext/pull/25530)) +- Fetch total stock at company in PO ([#25532](https://github.com/frappe/erpnext/pull/25532)) +- Updated filters for process statement of accounts ([#25384](https://github.com/frappe/erpnext/pull/25384)) +- Incorrect expense account set in pos invoice ([#25571](https://github.com/frappe/erpnext/pull/25571)) +- Client script breaking while settings tax labels ([#25653](https://github.com/frappe/erpnext/pull/25653)) +- Empty payment term column in accounts receivable report ([#25556](https://github.com/frappe/erpnext/pull/25556)) +- Designation insufficient permission on lead doctype. ([#25331](https://github.com/frappe/erpnext/pull/25331)) +- Force https for shopify webhook registration ([#25630](https://github.com/frappe/erpnext/pull/25630)) +- Patch regional fields for old companies ([#25673](https://github.com/frappe/erpnext/pull/25673)) +- Woocommerce order sync issue ([#25692](https://github.com/frappe/erpnext/pull/25692)) +- Allow to receive same serial numbers multiple times ([#25471](https://github.com/frappe/erpnext/pull/25471)) +- Update Allocated amount after Paid Amount is changed in PE ([#25515](https://github.com/frappe/erpnext/pull/25515)) +- Updating Standard Notification's channel field ([#25564](https://github.com/frappe/erpnext/pull/25564)) +- Report summary showing inflated values when values are accumulated in Group Company ([#25577](https://github.com/frappe/erpnext/pull/25577)) +- UI fixes related to overflowing payment section ([#25652](https://github.com/frappe/erpnext/pull/25652)) +- List invoices in Payment Reconciliation Payment ([#25524](https://github.com/frappe/erpnext/pull/25524)) +- Ageing errors in PSOA ([#25490](https://github.com/frappe/erpnext/pull/25490)) +- Prevent spurious defaults for items when making prec from dnote ([#25559](https://github.com/frappe/erpnext/pull/25559)) +- Stock reconciliation getting time out error during submission ([#25557](https://github.com/frappe/erpnext/pull/25557)) +- Timesheet filter date exclusive issue ([#25626](https://github.com/frappe/erpnext/pull/25626)) +- Update cost center in the item table fetched from POS Profile ([#25609](https://github.com/frappe/erpnext/pull/25609)) +- Updated modified time in purchase invoice to pull new fields ([#25678](https://github.com/frappe/erpnext/pull/25678)) +- Stock and Accounts Settings form refactor ([#25534](https://github.com/frappe/erpnext/pull/25534)) +- Payment amount showing in foreign currency ([#25292](https://github.com/frappe/erpnext/pull/25292)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_4_0.md b/erpnext/change_log/v13/v13_4_0.md new file mode 100644 index 00000000000..eaf4f762d49 --- /dev/null +++ b/erpnext/change_log/v13/v13_4_0.md @@ -0,0 +1,54 @@ +# Version 13.4.0 Release Notes + +### Features & Enhancements + +- Multiple GST enhancement and fixes ([#25249](https://github.com/frappe/erpnext/pull/25249)) +- Linking supplier with an item group for filtering items ([#25683](https://github.com/frappe/erpnext/pull/25683)) +- Leave Policy Assignment Refactor ([#24327](https://github.com/frappe/erpnext/pull/24327)) +- Dimension-wise Accounts Balance Report ([#25260](https://github.com/frappe/erpnext/pull/25260)) +- Show net values in Party Accounts ([#25714](https://github.com/frappe/erpnext/pull/25714)) +- Add pending qty section to batch/serial selector dialog ([#25519](https://github.com/frappe/erpnext/pull/25519)) +- enhancements in Training Event ([#25782](https://github.com/frappe/erpnext/pull/25782)) +- Refactored timesheet ([#25701](https://github.com/frappe/erpnext/pull/25701)) + +### Fixes + +- Process Statement of Accounts formatting ([#25777](https://github.com/frappe/erpnext/pull/25777)) +- Removed serial no validation for sales invoice ([#25817](https://github.com/frappe/erpnext/pull/25817)) +- Fetch email id from dialog box in pos past order summary ([#25808](https://github.com/frappe/erpnext/pull/25808)) +- Don't map set warehouse from delivery note to purchase receipt ([#25672](https://github.com/frappe/erpnext/pull/25672)) +- Apply permission while selecting projects ([#25765](https://github.com/frappe/erpnext/pull/25765)) +- Error on adding bank account to plaid ([#25658](https://github.com/frappe/erpnext/pull/25658)) +- Set disable rounded total if it is globally enabled ([#25789](https://github.com/frappe/erpnext/pull/25789)) +- Wrong amount on CR side in general ledger report for customer when different account currencies are involved ([#25654](https://github.com/frappe/erpnext/pull/25654)) +- Stock move dialog duplicate submit actions (V13) ([#25486](https://github.com/frappe/erpnext/pull/25486)) +- Cashflow mapper not showing data ([#25815](https://github.com/frappe/erpnext/pull/25815)) +- Ignore rounding diff while importing JV using data import ([#25816](https://github.com/frappe/erpnext/pull/25816)) +- Woocommerce order sync issue ([#25688](https://github.com/frappe/erpnext/pull/25688)) +- Expected amount in pos closing payments table ([#25737](https://github.com/frappe/erpnext/pull/25737)) +- Show only company addresses for ITC reversal entry ([#25867](https://github.com/frappe/erpnext/pull/25867)) +- Timeout error while loading warehouse tree ([#25694](https://github.com/frappe/erpnext/pull/25694)) +- Plaid Withdrawals and Deposits are recorded incorrectly ([#25784](https://github.com/frappe/erpnext/pull/25784)) +- Return case for item with available qty equal to one ([#25760](https://github.com/frappe/erpnext/pull/25760)) +- The status of repost item valuation showing In Progress since long time ([#25754](https://github.com/frappe/erpnext/pull/25754)) +- Updated applicable charges form in landed cost voucher ([#25732](https://github.com/frappe/erpnext/pull/25732)) +- Rearrange buttons for Company DocType ([#25617](https://github.com/frappe/erpnext/pull/25617)) +- Show uom for item in selector dialog ([#25697](https://github.com/frappe/erpnext/pull/25697)) +- Warehouse not found in stock entry ([#25776](https://github.com/frappe/erpnext/pull/25776)) +- Use dictionary filter instead of list (bp #25874 pre-release) ([#25875](https://github.com/frappe/erpnext/pull/25875)) +- Send emails on rfq submit ([#25695](https://github.com/frappe/erpnext/pull/25695)) +- Cannot bypass e-invoicing for non gst item invoices ([#25759](https://github.com/frappe/erpnext/pull/25759)) +- Validation message of quality inspection in purchase receipt ([#25666](https://github.com/frappe/erpnext/pull/25666)) +- Dialog variable assignment after definition in POS ([#25681](https://github.com/frappe/erpnext/pull/25681)) +- Wrong quantity after transaction for parallel stock transactions ([#25779](https://github.com/frappe/erpnext/pull/25779)) +- Item Variant Details Report ([#25797](https://github.com/frappe/erpnext/pull/25797)) +- Duplicate stock entry on multiple click ([#25742](https://github.com/frappe/erpnext/pull/25742)) +- Bank statement import via google sheet ([#25676](https://github.com/frappe/erpnext/pull/25676)) +- Change today to now to get data for reposting ([#25702](https://github.com/frappe/erpnext/pull/25702)) +- Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet ([#25698](https://github.com/frappe/erpnext/pull/25698)) +- Ageing error in PSOA ([#25857](https://github.com/frappe/erpnext/pull/25857)) +- Breaking cost center validation ([#25660](https://github.com/frappe/erpnext/pull/25660)) +- Project filter for Kanban Board ([#25744](https://github.com/frappe/erpnext/pull/25744)) +- Show allow zero valuation only when auto checked ([#25778](https://github.com/frappe/erpnext/pull/25778)) +- Missing cost center message on creating gl entries ([#25755](https://github.com/frappe/erpnext/pull/25755)) +- Address template with upper filter throws jinja error ([#25756](https://github.com/frappe/erpnext/pull/25756)) diff --git a/erpnext/change_log/v13/v13_5_0.md b/erpnext/change_log/v13/v13_5_0.md new file mode 100644 index 00000000000..64c323a23e5 --- /dev/null +++ b/erpnext/change_log/v13/v13_5_0.md @@ -0,0 +1,54 @@ +# Version 13.5.0 Release Notes + +### Features & Enhancements + +- Tax deduction against advance payments ([#25831](https://github.com/frappe/erpnext/pull/25831)) +- Cost-center wise period closing entry ([#25766](https://github.com/frappe/erpnext/pull/25766)) +- Create Quality Inspections from account and stock documents ([#25221](https://github.com/frappe/erpnext/pull/25221)) +- Item Taxes based on net rate ([#25961](https://github.com/frappe/erpnext/pull/25961)) +- Enable/disable gl entry posting for change given in pos ([#25822](https://github.com/frappe/erpnext/pull/25822)) +- Add Inactive status to Employee ([#26029](https://github.com/frappe/erpnext/pull/26029)) +- Added check box to combine items with same BOM ([#25478](https://github.com/frappe/erpnext/pull/25478)) +- Item Tax Templates for Germany ([#25858](https://github.com/frappe/erpnext/pull/25858)) +- Refactored leave balance report ([#25771](https://github.com/frappe/erpnext/pull/25771)) +- Refactored Vehicle Expenses Report ([#25727](https://github.com/frappe/erpnext/pull/25727)) +- Refactored maintenance schedule and visit document ([#25358](https://github.com/frappe/erpnext/pull/25358)) + +### Fixes + +- Cannot add same item with different rates ([#25849](https://github.com/frappe/erpnext/pull/25849)) +- Show only company addresses for ITC reversal entry ([#25866](https://github.com/frappe/erpnext/pull/25866)) +- Hiding Rounding Adjustment field ([#25380](https://github.com/frappe/erpnext/pull/25380)) +- Auto tax calculations in Payment Entry ([#26055](https://github.com/frappe/erpnext/pull/26055)) +- Not able to select the item code in work order ([#25915](https://github.com/frappe/erpnext/pull/25915)) +- Cannot reset plaid link for a bank account ([#25869](https://github.com/frappe/erpnext/pull/25869)) +- Student invalid password reset link ([#25826](https://github.com/frappe/erpnext/pull/25826)) +- Multiple pos issues ([#25928](https://github.com/frappe/erpnext/pull/25928)) +- Add Product Bundles to POS ([#25860](https://github.com/frappe/erpnext/pull/25860)) +- Enable Parallel tests ([#25862](https://github.com/frappe/erpnext/pull/25862)) +- Service item check on e-Invoicing ([#25986](https://github.com/frappe/erpnext/pull/25986)) +- Choose correct Salary Structure Assignment when getting data for formula eval ([#25981](https://github.com/frappe/erpnext/pull/25981)) +- Ignore internal transfer invoices from GST Reports ([#25969](https://github.com/frappe/erpnext/pull/25969)) +- Taxable value for invoices with additional discount ([#26056](https://github.com/frappe/erpnext/pull/26056)) +- Validate negative allocated amount in Payment Entry ([#25799](https://github.com/frappe/erpnext/pull/25799)) +- Allow all System Managers to delete company transactions ([#25834](https://github.com/frappe/erpnext/pull/25834)) +- Wrong round off gl entry posted in case of purchase invoice ([#25775](https://github.com/frappe/erpnext/pull/25775)) +- Use dictionary filter instead of list ([#25874](https://github.com/frappe/erpnext/pull/25874)) +- Ageing error in PSOA ([#25855](https://github.com/frappe/erpnext/pull/25855)) +- On click of duplicate button system has not copied the difference account ([#25988](https://github.com/frappe/erpnext/pull/25988)) +- Assign Product Bundle's conversion_factor to Pack… ([#25840](https://github.com/frappe/erpnext/pull/25840)) +- Rename Loan Management workspace to Loans ([#25856](https://github.com/frappe/erpnext/pull/25856)) +- Fix stock quantity calculation when negative_stock_allowe… ([#25859](https://github.com/frappe/erpnext/pull/25859)) +- Update cost center from pos profile ([#25971](https://github.com/frappe/erpnext/pull/25971)) +- Ensure website theme is applied correctly ([#25863](https://github.com/frappe/erpnext/pull/25863)) +- Only display GST card in Accounting Workspace if it's in India ([#26000](https://github.com/frappe/erpnext/pull/26000)) +- Incorrect gstin fetched incase of branch company address ([#25841](https://github.com/frappe/erpnext/pull/25841)) +- Sort account balances by account name ([#26009](https://github.com/frappe/erpnext/pull/26009)) +- Custom conversion factor field not mapped from job card to stock entry ([#25956](https://github.com/frappe/erpnext/pull/25956)) +- Chart of accounts importer always error ([#25882](https://github.com/frappe/erpnext/pull/25882)) +- Create POS Invoice for Product Bundles ([#25847](https://github.com/frappe/erpnext/pull/25847)) +- Wrap dates in getdate for leave application ([#25899](https://github.com/frappe/erpnext/pull/25899)) +- Closing entry shows incorrect expected amount ([#25868](https://github.com/frappe/erpnext/pull/25868)) +- Add Hold status column in the Issue Summary Report ([#25828](https://github.com/frappe/erpnext/pull/25828)) +- Rendering of broken image on pos ([#25872](https://github.com/frappe/erpnext/pull/25872)) +- Timeout error in the repost item valuation ([#25854](https://github.com/frappe/erpnext/pull/25854)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_6_0.md b/erpnext/change_log/v13/v13_6_0.md new file mode 100644 index 00000000000..d881b279e3f --- /dev/null +++ b/erpnext/change_log/v13/v13_6_0.md @@ -0,0 +1,72 @@ +# Version 13.6.0 Release Notes + +### Features & Enhancements + +- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523)) +- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044)) +- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184)) +- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878)) +- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705)) +- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030)) +- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696)) +- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891)) + +### Fixes + +- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046)) +- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176)) +- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092)) +- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978)) +- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073)) +- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245)) +- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230)) +- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125)) +- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134)) +- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196)) +- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083)) +- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941)) +- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945)) +- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011)) +- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070)) +- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071)) +- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122)) +- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220)) +- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003)) +- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229)) +- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269)) +- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045)) +- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170)) +- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032)) +- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095)) +- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023)) +- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191)) +- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188)) +- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217)) +- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152)) +- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108)) +- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202)) +- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906)) +- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894)) +- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997)) +- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051)) +- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043)) +- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143)) +- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211)) +- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126)) +- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192)) +- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081)) +- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187)) +- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195)) +- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947)) +- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951)) +- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968)) +- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037)) +- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198)) +- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100)) +- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098)) +- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062)) +- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031)) +- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203)) +- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185)) +- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934)) +- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201)) diff --git a/erpnext/change_log/v13/v13_7_0.md b/erpnext/change_log/v13/v13_7_0.md new file mode 100644 index 00000000000..589f610b939 --- /dev/null +++ b/erpnext/change_log/v13/v13_7_0.md @@ -0,0 +1,69 @@ +# Version 13.7.0 Release Notes + +### Features & Enhancements +- Optionally allow rejected quality inspection on submission ([#26133](https://github.com/frappe/erpnext/pull/26133)) +- Bootstrapped GST Setup for India ([#25415](https://github.com/frappe/erpnext/pull/25415)) +- Fetching details from supplier/customer groups ([#26454](https://github.com/frappe/erpnext/pull/26454)) +- Provision to make subcontracted purchase order from the production plan ([#26240](https://github.com/frappe/erpnext/pull/26240)) +- Optimized code for reposting item valuation ([#26432](https://github.com/frappe/erpnext/pull/26432)) + +### Fixes +- Auto process deferred accounting for multi-company setup ([#26277](https://github.com/frappe/erpnext/pull/26277)) +- Error while fetching item taxes ([#26218](https://github.com/frappe/erpnext/pull/26218)) +- Validation check for batch for stock reconciliation type in stock entry(bp #26370 ) ([#26488](https://github.com/frappe/erpnext/pull/26488)) +- Error popup for COA errors ([#26358](https://github.com/frappe/erpnext/pull/26358)) +- Precision for expected values in payment entry test ([#26394](https://github.com/frappe/erpnext/pull/26394)) +- Bank statement import ([#26287](https://github.com/frappe/erpnext/pull/26287)) +- LMS progress issue ([#26253](https://github.com/frappe/erpnext/pull/26253)) +- Paging buttons not working on item group portal page ([#26497](https://github.com/frappe/erpnext/pull/26497)) +- Omit item discount amount for e-invoicing ([#26353](https://github.com/frappe/erpnext/pull/26353)) +- Validate LCV for Invoices without Update Stock ([#26333](https://github.com/frappe/erpnext/pull/26333)) +- Remove cancelled entries in consolidated financial statements ([#26331](https://github.com/frappe/erpnext/pull/26331)) +- Fetching employee in payroll entry ([#26271](https://github.com/frappe/erpnext/pull/26271)) +- To fetch the correct field in Tax Rule ([#25927](https://github.com/frappe/erpnext/pull/25927)) +- Order and time of operations in multilevel BOM work order ([#25886](https://github.com/frappe/erpnext/pull/25886)) +- Fixed Budget Variance Graph color from all black to default ([#26368](https://github.com/frappe/erpnext/pull/26368)) +- TDS computation summary shows cancelled invoices (#26456) ([#26486](https://github.com/frappe/erpnext/pull/26486)) +- Do not consider cancelled entries in party dashboard ([#26231](https://github.com/frappe/erpnext/pull/26231)) +- Add validation for 'for_qty' else throws errors ([#25829](https://github.com/frappe/erpnext/pull/25829)) +- Move the rename abbreviation job to long queue (#26434) ([#26462](https://github.com/frappe/erpnext/pull/26462)) +- Query for Training Event ([#26388](https://github.com/frappe/erpnext/pull/26388)) +- Item group portal issues (backport) ([#26493](https://github.com/frappe/erpnext/pull/26493)) +- When lead is created with mobile_no, mobile_no value gets lost ([#26298](https://github.com/frappe/erpnext/pull/26298)) +- WIP needs to be set before submit on skip_transfer (bp #26499) ([#26507](https://github.com/frappe/erpnext/pull/26507)) +- Incorrect valuation rate in stock reconciliation ([#26259](https://github.com/frappe/erpnext/pull/26259)) +- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046)) +- Changed profitability analysis report width ([#26165](https://github.com/frappe/erpnext/pull/26165)) +- Unable to download GSTR-1 json ([#26468](https://github.com/frappe/erpnext/pull/26468)) +- Unallocated amount in Payment Entry after taxes ([#26472](https://github.com/frappe/erpnext/pull/26472)) +- Include Stock Reco logic in `update_qty_in_future_sle` ([#26158](https://github.com/frappe/erpnext/pull/26158)) +- Update cost not working in the draft BOM ([#26279](https://github.com/frappe/erpnext/pull/26279)) +- Cancellation of Loan Security Pledges ([#26252](https://github.com/frappe/erpnext/pull/26252)) +- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26405](https://github.com/frappe/erpnext/pull/26405)) +- Delete accounts (an empty file) ([#25323](https://github.com/frappe/erpnext/pull/25323)) +- Errors on parallel requests creation of company for India ([#26470](https://github.com/frappe/erpnext/pull/26470)) +- Incorrect bom no added for non-variant items on variant boms ([#26320](https://github.com/frappe/erpnext/pull/26320)) +- Incorrect discount amount on amended document ([#26466](https://github.com/frappe/erpnext/pull/26466)) +- Added a message to enable appointment booking if disabled ([#26334](https://github.com/frappe/erpnext/pull/26334)) +- fix(pos): taxes amount in pos item cart ([#26411](https://github.com/frappe/erpnext/pull/26411)) +- Track changes on batch ([#26382](https://github.com/frappe/erpnext/pull/26382)) +- Stock entry with putaway rule not working ([#26350](https://github.com/frappe/erpnext/pull/26350)) +- Only "Tax" type accounts should be shown for selection in GST Settings ([#26300](https://github.com/frappe/erpnext/pull/26300)) +- Added permission for employee to book appointment ([#26255](https://github.com/frappe/erpnext/pull/26255)) +- Allow to make job card without employee ([#26312](https://github.com/frappe/erpnext/pull/26312)) +- Project Portal Enhancements ([#26290](https://github.com/frappe/erpnext/pull/26290)) +- BOM stock report not working ([#26332](https://github.com/frappe/erpnext/pull/26332)) +- Order Items by weightage in the web items query ([#26284](https://github.com/frappe/erpnext/pull/26284)) +- Removed values out of sync validation from stock transactions ([#26226](https://github.com/frappe/erpnext/pull/26226)) +- Payroll-entry minor fix ([#26349](https://github.com/frappe/erpnext/pull/26349)) +- Allow user to change the To Date in the blanket order even after submit of order ([#26241](https://github.com/frappe/erpnext/pull/26241)) +- Value fetching for custom field in POS ([#26367](https://github.com/frappe/erpnext/pull/26367)) +- Iteration through accounts only when accounts exist ([#26391](https://github.com/frappe/erpnext/pull/26391)) +- Employee Inactive status implications ([#26244](https://github.com/frappe/erpnext/pull/26244)) +- Multi-currency issue ([#26458](https://github.com/frappe/erpnext/pull/26458)) +- FG item not fetched in manufacture entry ([#26509](https://github.com/frappe/erpnext/pull/26509)) +- Set query for training events ([#26303](https://github.com/frappe/erpnext/pull/26303)) +- Fetch batch items in stock reconciliation ([#26213](https://github.com/frappe/erpnext/pull/26213)) +- Employee selection not working in payroll entry ([#26278](https://github.com/frappe/erpnext/pull/26278)) +- POS item cart dom updates (#26459) ([#26461](https://github.com/frappe/erpnext/pull/26461)) +- dunning calculation of grand total when rate of interest is 0% ([#26285](https://github.com/frappe/erpnext/pull/26285)) \ No newline at end of file diff --git a/erpnext/change_log/v13/v13_8_0.md b/erpnext/change_log/v13/v13_8_0.md new file mode 100644 index 00000000000..98ed95ae04a --- /dev/null +++ b/erpnext/change_log/v13/v13_8_0.md @@ -0,0 +1,39 @@ +# Version 13.8.0 Release Notes + +### Features & Enhancements +- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222)) +- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677)) +- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564)) + +### Fixes +- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600)) +- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651)) +- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460)) +- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438)) +- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206)) +- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487)) +- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502)) +- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644)) +- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431)) +- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508)) +- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420)) +- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558)) +- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712)) +- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721)) +- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691)) +- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518)) +- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646)) +- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727)) +- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294)) +- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292)) +- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436)) +- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412)) +- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613)) +- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614)) +- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734)) +- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485)) +- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560)) +- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576)) +- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643)) +- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418)) +- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498)) diff --git a/erpnext/change_log/v13/v13_9_0.md b/erpnext/change_log/v13/v13_9_0.md new file mode 100644 index 00000000000..e52766673ce --- /dev/null +++ b/erpnext/change_log/v13/v13_9_0.md @@ -0,0 +1,46 @@ +# Version 13.9.0 Release Notes + +### Features & Enhancements +- Organizational Chart ([#26261](https://github.com/frappe/erpnext/pull/26261)) +- Enable discount accounting ([#26579](https://github.com/frappe/erpnext/pull/26579)) +- Added multi-select fields in promotional scheme to create multiple pricing rules ([#25622](https://github.com/frappe/erpnext/pull/25622)) +- Over transfer allowance for material transfers ([#26814](https://github.com/frappe/erpnext/pull/26814)) +- Enhancements in Tax Withholding Category ([#26661](https://github.com/frappe/erpnext/pull/26661)) + +### Fixes +- Sales Return cancellation if linked with Payment Entry ([#26883](https://github.com/frappe/erpnext/pull/26883)) +- Production plan not fetching sales order of a variant ([#25845](https://github.com/frappe/erpnext/pull/25845)) +- Stock Analytics Report must consider warehouse during calculation ([#26908](https://github.com/frappe/erpnext/pull/26908)) +- Incorrect date difference calculation ([#26805](https://github.com/frappe/erpnext/pull/26805)) +- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206)) +- Cannot cancel payment entry if linked with invoices ([#26703](https://github.com/frappe/erpnext/pull/26703)) +- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576)) +- Fetch Payment Terms from linked Sales/Purchase Order ([#26723](https://github.com/frappe/erpnext/pull/26723)) +- Let all System Managers be able to delete Company transactions ([#26819](https://github.com/frappe/erpnext/pull/26819)) +- Bank remittance report issue ([#26398](https://github.com/frappe/erpnext/pull/26398)) +- Faulty Gl Entry for Asset LCVs ([#26803](https://github.com/frappe/erpnext/pull/26803)) +- Clean Serial No input on Server Side ([#26878](https://github.com/frappe/erpnext/pull/26878)) +- Supplier invoice importer fix v13 ([#26633](https://github.com/frappe/erpnext/pull/26633)) +- POS payment modes displayed wrong total ([#26808](https://github.com/frappe/erpnext/pull/26808)) +- Fetching of item tax from hsn code ([#26736](https://github.com/frappe/erpnext/pull/26736)) +- Cannot cancel invoice if IRN cancelled on portal ([#26879](https://github.com/frappe/erpnext/pull/26879)) +- Validate python expressions ([#26856](https://github.com/frappe/erpnext/pull/26856)) +- POS Item Cart non-stop scroll issue ([#26693](https://github.com/frappe/erpnext/pull/26693)) +- Add mandatory depends on condition for export type field ([#26958](https://github.com/frappe/erpnext/pull/26958)) +- Cannot generate IRNs for standalone credit notes ([#26824](https://github.com/frappe/erpnext/pull/26824)) +- Added progress bar in Repost Item Valuation to check the status of reposting ([#26630](https://github.com/frappe/erpnext/pull/26630)) +- TDS calculation for first threshold breach for TDS category 194Q ([#26710](https://github.com/frappe/erpnext/pull/26710)) +- Student category mapping from the program enrollment tool ([#26739](https://github.com/frappe/erpnext/pull/26739)) +- Cost center & account validation in Sales/Purchase Taxes and Charges ([#26881](https://github.com/frappe/erpnext/pull/26881)) +- Reset weight_per_unit on replacing Item ([#26791](https://github.com/frappe/erpnext/pull/26791)) +- Do not fetch fully return issued purchase receipts ([#26825](https://github.com/frappe/erpnext/pull/26825)) +- Incorrect amount in work order required items table. ([#26585](https://github.com/frappe/erpnext/pull/26585)) +- Additional discount calculations in Invoices ([#26553](https://github.com/frappe/erpnext/pull/26553)) +- Refactored Asset Repair ([#26415](https://github.com/frappe/erpnext/pull/25798)) +- Exchange rate revaluation posting date and precision fixes ([#26650](https://github.com/frappe/erpnext/pull/26650)) +- POS Invoice consolidated Sales Invoice field set to no copy ([#26768](https://github.com/frappe/erpnext/pull/26768)) +- Consider grand total for threshold check ([#26683](https://github.com/frappe/erpnext/pull/26683)) +- Budget variance missing values ([#26966](https://github.com/frappe/erpnext/pull/26966)) +- GL Entries for exchange gain loss ([#26728](https://github.com/frappe/erpnext/pull/26728)) +- Add missing cess amount in GSTR-3B report ([#26544](https://github.com/frappe/erpnext/pull/26544)) +- GST Reports timeout issue ([#26575](https://github.com/frappe/erpnext/pull/26575)) \ No newline at end of file diff --git a/erpnext/commands/__init__.py b/erpnext/commands/__init__.py index 59311192148..13e5c5bd15b 100644 --- a/erpnext/commands/__init__.py +++ b/erpnext/commands/__init__.py @@ -9,18 +9,17 @@ from frappe.commands import get_site, pass_context def call_command(cmd, context): return click.Context(cmd, obj=context).forward(cmd) -@click.command('make-demo') -@click.option('--site', help='site name') -@click.option('--domain', default='Manufacturing') -@click.option('--days', default=100, - help='Run the demo for so many days. Default 100') -@click.option('--resume', default=False, is_flag=True, - help='Continue running the demo for given days') -@click.option('--reinstall', default=False, is_flag=True, - help='Reinstall site before demo') + +@click.command("make-demo") +@click.option("--site", help="site name") +@click.option("--domain", default="Manufacturing") +@click.option("--days", default=100, help="Run the demo for so many days. Default 100") +@click.option( + "--resume", default=False, is_flag=True, help="Continue running the demo for given days" +) +@click.option("--reinstall", default=False, is_flag=True, help="Reinstall site before demo") @pass_context -def make_demo(context, site, domain='Manufacturing', days=100, - resume=False, reinstall=False): +def make_demo(context, site, domain="Manufacturing", days=100, resume=False, reinstall=False): "Reinstall site and setup demo" from frappe.commands.site import _reinstall from frappe.installer import install_app @@ -31,19 +30,20 @@ def make_demo(context, site, domain='Manufacturing', days=100, with frappe.init_site(site): frappe.connect() from erpnext.demo import demo + demo.simulate(days=days) else: if reinstall: _reinstall(site, yes=True) with frappe.init_site(site=site): frappe.connect() - if not 'erpnext' in frappe.get_installed_apps(): - install_app('erpnext') + if not "erpnext" in frappe.get_installed_apps(): + install_app("erpnext") # import needs site from erpnext.demo import demo + demo.make(domain, days) -commands = [ - make_demo -] + +commands = [make_demo] diff --git a/erpnext/config/education.py b/erpnext/config/education.py index 3ead3ef9572..c37cf2b196e 100644 --- a/erpnext/config/education.py +++ b/erpnext/config/education.py @@ -1,4 +1,3 @@ - from frappe import _ @@ -12,113 +11,61 @@ def get_data(): "name": "Student", "onboard": 1, }, - { - "type": "doctype", - "name": "Guardian" - }, - { - "type": "doctype", - "name": "Student Log" - }, - { - "type": "doctype", - "name": "Student Group" - } - ] + {"type": "doctype", "name": "Guardian"}, + {"type": "doctype", "name": "Student Log"}, + {"type": "doctype", "name": "Student Group"}, + ], }, { "label": _("Admission"), "items": [ - - { - "type": "doctype", - "name": "Student Applicant" - }, - { - "type": "doctype", - "name": "Web Academy Applicant" - }, - { - "type": "doctype", - "name": "Student Admission" - }, - { - "type": "doctype", - "name": "Program Enrollment" - } - ] + {"type": "doctype", "name": "Student Applicant"}, + {"type": "doctype", "name": "Web Academy Applicant"}, + {"type": "doctype", "name": "Student Admission"}, + {"type": "doctype", "name": "Program Enrollment"}, + ], }, { "label": _("Attendance"), "items": [ - { - "type": "doctype", - "name": "Student Attendance" - }, - { - "type": "doctype", - "name": "Student Leave Application" - }, + {"type": "doctype", "name": "Student Attendance"}, + {"type": "doctype", "name": "Student Leave Application"}, { "type": "report", "is_query_report": True, "name": "Absent Student Report", - "doctype": "Student Attendance" + "doctype": "Student Attendance", }, { "type": "report", "is_query_report": True, "name": "Student Batch-Wise Attendance", - "doctype": "Student Attendance" + "doctype": "Student Attendance", }, - ] + ], }, { "label": _("Tools"), "items": [ - { - "type": "doctype", - "name": "Student Attendance Tool" - }, - { - "type": "doctype", - "name": "Assessment Result Tool" - }, - { - "type": "doctype", - "name": "Student Group Creation Tool" - }, - { - "type": "doctype", - "name": "Program Enrollment Tool" - }, - { - "type": "doctype", - "name": "Course Scheduling Tool" - } - ] + {"type": "doctype", "name": "Student Attendance Tool"}, + {"type": "doctype", "name": "Assessment Result Tool"}, + {"type": "doctype", "name": "Student Group Creation Tool"}, + {"type": "doctype", "name": "Program Enrollment Tool"}, + {"type": "doctype", "name": "Course Scheduling Tool"}, + ], }, { "label": _("Assessment"), "items": [ - { - "type": "doctype", - "name": "Assessment Plan" - }, + {"type": "doctype", "name": "Assessment Plan"}, { "type": "doctype", "name": "Assessment Group", "link": "Tree/Assessment Group", }, - { - "type": "doctype", - "name": "Assessment Result" - }, - { - "type": "doctype", - "name": "Assessment Criteria" - } - ] + {"type": "doctype", "name": "Assessment Result"}, + {"type": "doctype", "name": "Assessment Criteria"}, + ], }, { "label": _("Assessment Reports"), @@ -127,60 +74,38 @@ def get_data(): "type": "report", "is_query_report": True, "name": "Course wise Assessment Report", - "doctype": "Assessment Result" + "doctype": "Assessment Result", }, { "type": "report", "is_query_report": True, "name": "Final Assessment Grades", - "doctype": "Assessment Result" + "doctype": "Assessment Result", }, { "type": "report", "is_query_report": True, "name": "Assessment Plan Status", - "doctype": "Assessment Plan" + "doctype": "Assessment Plan", }, - { - "type": "doctype", - "name": "Student Report Generation Tool" - } - ] + {"type": "doctype", "name": "Student Report Generation Tool"}, + ], }, { "label": _("Fees"), "items": [ - { - "type": "doctype", - "name": "Fees" - }, - { - "type": "doctype", - "name": "Fee Schedule" - }, - { - "type": "doctype", - "name": "Fee Structure" - }, - { - "type": "doctype", - "name": "Fee Category" - } - ] + {"type": "doctype", "name": "Fees"}, + {"type": "doctype", "name": "Fee Schedule"}, + {"type": "doctype", "name": "Fee Structure"}, + {"type": "doctype", "name": "Fee Category"}, + ], }, { "label": _("Schedule"), "items": [ - { - "type": "doctype", - "name": "Course Schedule", - "route": "/app/List/Course Schedule/Calendar" - }, - { - "type": "doctype", - "name": "Course Scheduling Tool" - } - ] + {"type": "doctype", "name": "Course Schedule", "route": "/app/List/Course Schedule/Calendar"}, + {"type": "doctype", "name": "Course Scheduling Tool"}, + ], }, { "label": _("Masters"), @@ -207,72 +132,39 @@ def get_data(): "type": "doctype", "name": "Room", "onboard": 1, - } - ] + }, + ], }, { "label": _("Content Masters"), "items": [ - { - "type": "doctype", - "name": "Article" - }, - { - "type": "doctype", - "name": "Video" - }, - { - "type": "doctype", - "name": "Quiz" - } - ] + {"type": "doctype", "name": "Article"}, + {"type": "doctype", "name": "Video"}, + {"type": "doctype", "name": "Quiz"}, + ], }, { "label": _("LMS Activity"), "items": [ - { - "type": "doctype", - "name": "Course Enrollment" - }, - { - "type": "doctype", - "name": "Course Activity" - }, - { - "type": "doctype", - "name": "Quiz Activity" - } - ] + {"type": "doctype", "name": "Course Enrollment"}, + {"type": "doctype", "name": "Course Activity"}, + {"type": "doctype", "name": "Quiz Activity"}, + ], }, { "label": _("Settings"), "items": [ - { - "type": "doctype", - "name": "Student Category" - }, - { - "type": "doctype", - "name": "Student Batch Name" - }, + {"type": "doctype", "name": "Student Category"}, + {"type": "doctype", "name": "Student Batch Name"}, { "type": "doctype", "name": "Grading Scale", "onboard": 1, }, - { - "type": "doctype", - "name": "Academic Term" - }, - { - "type": "doctype", - "name": "Academic Year" - }, - { - "type": "doctype", - "name": "Education Settings" - } - ] + {"type": "doctype", "name": "Academic Term"}, + {"type": "doctype", "name": "Academic Year"}, + {"type": "doctype", "name": "Education Settings"}, + ], }, { "label": _("Other Reports"), @@ -281,20 +173,20 @@ def get_data(): "type": "report", "is_query_report": True, "name": "Student and Guardian Contact Details", - "doctype": "Program Enrollment" + "doctype": "Program Enrollment", }, { "type": "report", "is_query_report": True, "name": "Student Monthly Attendance Sheet", - "doctype": "Student Attendance" + "doctype": "Student Attendance", }, { "type": "report", "name": "Student Fee Collection", "doctype": "Fees", - "is_query_report": True - } - ] - } + "is_query_report": True, + }, + ], + }, ] diff --git a/erpnext/config/projects.py b/erpnext/config/projects.py index 168dead9bf4..6186a4e8204 100644 --- a/erpnext/config/projects.py +++ b/erpnext/config/projects.py @@ -1,4 +1,3 @@ - from frappe import _ @@ -45,7 +44,7 @@ def get_data(): "description": _("Project Update."), "dependencies": ["Project"], }, - ] + ], }, { "label": _("Time Tracking"), @@ -68,7 +67,7 @@ def get_data(): "description": _("Cost of various activities"), "dependencies": ["Activity Type"], }, - ] + ], }, { "label": _("Reports"), @@ -96,7 +95,6 @@ def get_data(): "doctype": "Project", "dependencies": ["Project"], }, - ] + ], }, - ] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f146071c0bd..49196dfe22e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -57,10 +57,22 @@ from erpnext.stock.get_item_details import ( from erpnext.utilities.transaction_base import TransactionBase -class AccountMissingError(frappe.ValidationError): pass +class AccountMissingError(frappe.ValidationError): + pass + + +force_item_fields = ( + "item_group", + "brand", + "stock_uom", + "is_fixed_asset", + "item_tax_rate", + "pricing_rules", + "weight_per_unit", + "weight_uom", + "total_weight", +) -force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", - "pricing_rules", "weight_per_unit", "weight_uom", "total_weight") class AccountsController(TransactionBase): def __init__(self, *args, **kwargs): @@ -68,14 +80,14 @@ class AccountsController(TransactionBase): def get_print_settings(self): print_setting_fields = [] - items_field = self.meta.get_field('items') + items_field = self.meta.get_field("items") - if items_field and items_field.fieldtype == 'Table': - print_setting_fields += ['compact_item_print', 'print_uom_after_quantity'] + if items_field and items_field.fieldtype == "Table": + print_setting_fields += ["compact_item_print", "print_uom_after_quantity"] - taxes_field = self.meta.get_field('taxes') - if taxes_field and taxes_field.fieldtype == 'Table': - print_setting_fields += ['print_taxes_with_zero_amount'] + taxes_field = self.meta.get_field("taxes") + if taxes_field and taxes_field.fieldtype == "Table": + print_setting_fields += ["print_taxes_with_zero_amount"] return print_setting_fields @@ -87,34 +99,44 @@ class AccountsController(TransactionBase): return self.__company_currency def onload(self): - self.set_onload("make_payment_via_journal_entry", - frappe.db.get_single_value('Accounts Settings', 'make_payment_via_journal_entry')) + self.set_onload( + "make_payment_via_journal_entry", + frappe.db.get_single_value("Accounts Settings", "make_payment_via_journal_entry"), + ) if self.is_new(): - relevant_docs = ("Quotation", "Purchase Order", "Sales Order", - "Purchase Invoice", "Sales Invoice") + relevant_docs = ( + "Quotation", + "Purchase Order", + "Sales Order", + "Purchase Invoice", + "Sales Invoice", + ) if self.doctype in relevant_docs: self.set_payment_schedule() def ensure_supplier_is_not_blocked(self): - is_supplier_payment = self.doctype == 'Payment Entry' and self.party_type == 'Supplier' - is_buying_invoice = self.doctype in ['Purchase Invoice', 'Purchase Order'] + is_supplier_payment = self.doctype == "Payment Entry" and self.party_type == "Supplier" + is_buying_invoice = self.doctype in ["Purchase Invoice", "Purchase Order"] supplier = None supplier_name = None if is_buying_invoice or is_supplier_payment: supplier_name = self.supplier if is_buying_invoice else self.party - supplier = frappe.get_doc('Supplier', supplier_name) + supplier = frappe.get_doc("Supplier", supplier_name) if supplier and supplier_name and supplier.on_hold: - if (is_buying_invoice and supplier.hold_type in ['All', 'Invoices']) or \ - (is_supplier_payment and supplier.hold_type in ['All', 'Payments']): + if (is_buying_invoice and supplier.hold_type in ["All", "Invoices"]) or ( + is_supplier_payment and supplier.hold_type in ["All", "Payments"] + ): if not supplier.release_date or getdate(nowdate()) <= supplier.release_date: frappe.msgprint( - _('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1) + _("{0} is blocked so this transaction cannot proceed").format(supplier_name), + raise_exception=1, + ) def validate(self): - if not self.get('is_return') and not self.get('is_debit_note'): + if not self.get("is_return") and not self.get("is_debit_note"): self.validate_qty_is_not_zero() if self.get("_action") and self._action != "update_after_submit": @@ -147,8 +169,8 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() - if self.doctype in ['Purchase Invoice', 'Sales Invoice']: - pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid" + if self.doctype in ["Purchase Invoice", "Sales Invoice"]: + pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() @@ -159,9 +181,10 @@ class AccountsController(TransactionBase): else: self.validate_deferred_start_and_end_date() + self.validate_deferred_income_expense_account() self.set_inter_company_account() - if self.doctype == 'Purchase Invoice': + if self.doctype == "Purchase Invoice": self.calculate_paid_amount() # apply tax withholding only if checked and applicable self.set_tax_withholding() @@ -170,7 +193,7 @@ class AccountsController(TransactionBase): validate_einvoice_fields(self) - if self.doctype != 'Material Request': + if self.doctype != "Material Request": apply_pricing_rule_on_transaction(self) def before_cancel(self): @@ -178,26 +201,58 @@ class AccountsController(TransactionBase): def on_trash(self): # delete sl and gl entries on deletion of transaction - if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'): - frappe.db.sql("delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)) - frappe.db.sql("delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)) + if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): + frappe.db.sql( + "delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name) + ) + frappe.db.sql( + "delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", + (self.doctype, self.name), + ) + + def validate_deferred_income_expense_account(self): + field_map = { + "Sales Invoice": "deferred_revenue_account", + "Purchase Invoice": "deferred_expense_account", + } + + for item in self.get("items"): + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): + if not item.get(field_map.get(self.doctype)): + default_deferred_account = frappe.db.get_value( + "Company", self.company, "default_" + field_map.get(self.doctype) + ) + if not default_deferred_account: + frappe.throw( + _( + "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" + ).format(item.idx) + ) + else: + item.set(field_map.get(self.doctype), default_deferred_account) def validate_deferred_start_and_end_date(self): for d in self.items: if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): if not (d.service_start_date and d.service_end_date): - frappe.throw(_("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx)) + frappe.throw( + _("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx) + ) elif getdate(d.service_start_date) > getdate(d.service_end_date): - frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) + frappe.throw( + _("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx) + ) elif getdate(self.posting_date) > getdate(d.service_end_date): - frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) + frappe.throw( + _("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx) + ) def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() self.set_due_date() self.set_payment_schedule() self.validate_payment_schedule_amount() - if not self.get('ignore_default_payment_terms_template'): + if not self.get("ignore_default_payment_terms_template"): self.validate_due_date() self.validate_advance_entries() @@ -213,8 +268,16 @@ class AccountsController(TransactionBase): self.validate_non_invoice_documents_schedule() def before_print(self, settings=None): - if self.doctype in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice', - 'Supplier Quotation', 'Purchase Receipt', 'Delivery Note', 'Quotation']: + if self.doctype in [ + "Purchase Order", + "Sales Order", + "Sales Invoice", + "Purchase Invoice", + "Supplier Quotation", + "Purchase Receipt", + "Delivery Note", + "Quotation", + ]: if self.get("group_same_items"): self.group_similar_items() @@ -235,7 +298,9 @@ class AccountsController(TransactionBase): if is_paid: if not self.cash_bank_account: # show message that the amount is not paid - frappe.throw(_("Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified")) + frappe.throw( + _("Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified") + ) if cint(self.is_return) and self.grand_total > self.paid_amount: self.paid_amount = flt(flt(self.grand_total), self.precision("paid_amount")) @@ -243,8 +308,9 @@ class AccountsController(TransactionBase): elif not flt(self.paid_amount) and flt(self.outstanding_amount) > 0: self.paid_amount = flt(flt(self.outstanding_amount), self.precision("paid_amount")) - self.base_paid_amount = flt(self.paid_amount * self.conversion_rate, - self.precision("base_paid_amount")) + self.base_paid_amount = flt( + self.paid_amount * self.conversion_rate, self.precision("base_paid_amount") + ) def set_missing_values(self, for_validate=False): if frappe.flags.in_test: @@ -255,13 +321,14 @@ class AccountsController(TransactionBase): def calculate_taxes_and_totals(self): from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals + calculate_taxes_and_totals(self) if self.doctype in ( - 'Sales Order', - 'Delivery Note', - 'Sales Invoice', - 'POS Invoice', + "Sales Order", + "Delivery Note", + "Sales Invoice", + "POS Invoice", ): self.calculate_commission() self.calculate_contribution() @@ -275,50 +342,75 @@ class AccountsController(TransactionBase): date_field = "transaction_date" if date_field and self.get(date_field): - validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company, - self.meta.get_label(date_field), self) + validate_fiscal_year( + self.get(date_field), self.fiscal_year, self.company, self.meta.get_label(date_field), self + ) def validate_party_accounts(self): - if self.doctype not in ('Sales Invoice', 'Purchase Invoice'): + if self.doctype not in ("Sales Invoice", "Purchase Invoice"): return - if self.doctype == 'Sales Invoice': - party_account_field = 'debit_to' - item_field = 'income_account' + if self.doctype == "Sales Invoice": + party_account_field = "debit_to" + item_field = "income_account" else: - party_account_field = 'credit_to' - item_field = 'expense_account' + party_account_field = "credit_to" + item_field = "expense_account" - for item in self.get('items'): + for item in self.get("items"): if item.get(item_field) == self.get(party_account_field): - frappe.throw(_("Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}").format(item.idx, - frappe.bold(frappe.unscrub(item_field)), item.get(item_field), - frappe.bold(frappe.unscrub(party_account_field)), self.get(party_account_field))) + frappe.throw( + _("Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}").format( + item.idx, + frappe.bold(frappe.unscrub(item_field)), + item.get(item_field), + frappe.bold(frappe.unscrub(party_account_field)), + self.get(party_account_field), + ) + ) def validate_inter_company_reference(self): - if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): + if self.doctype not in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"): return if self.is_internal_transfer(): - if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference') - or self.get('inter_company_order_reference')): + if not ( + self.get("inter_company_reference") + or self.get("inter_company_invoice_reference") + or self.get("inter_company_order_reference") + ): msg = _("Internal Sale or Delivery Reference missing.") msg += _("Please create purchase from internal sale or delivery document itself") frappe.throw(msg, title=_("Internal Sales Reference Missing")) def validate_due_date(self): - if self.get('is_pos'): return + if self.get("is_pos"): + return from erpnext.accounts.party import validate_due_date + if self.doctype == "Sales Invoice": if not self.due_date: frappe.throw(_("Due Date is mandatory")) - validate_due_date(self.posting_date, self.due_date, - "Customer", self.customer, self.company, self.payment_terms_template) + validate_due_date( + self.posting_date, + self.due_date, + "Customer", + self.customer, + self.company, + self.payment_terms_template, + ) elif self.doctype == "Purchase Invoice": - validate_due_date(self.bill_date or self.posting_date, self.due_date, - "Supplier", self.supplier, self.company, self.bill_date, self.payment_terms_template) + validate_due_date( + self.bill_date or self.posting_date, + self.due_date, + "Supplier", + self.supplier, + self.company, + self.bill_date, + self.payment_terms_template, + ) def set_price_list_currency(self, buying_or_selling): if self.meta.get_field("posting_date"): @@ -336,15 +428,15 @@ class AccountsController(TransactionBase): args = "for_buying" if self.meta.get_field(fieldname) and self.get(fieldname): - self.price_list_currency = frappe.db.get_value("Price List", - self.get(fieldname), "currency") + self.price_list_currency = frappe.db.get_value("Price List", self.get(fieldname), "currency") if self.price_list_currency == self.company_currency: self.plc_conversion_rate = 1.0 elif not self.plc_conversion_rate: - self.plc_conversion_rate = get_exchange_rate(self.price_list_currency, - self.company_currency, transaction_date, args) + self.plc_conversion_rate = get_exchange_rate( + self.price_list_currency, self.company_currency, transaction_date, args + ) # currency if not self.currency: @@ -353,8 +445,9 @@ class AccountsController(TransactionBase): elif self.currency == self.company_currency: self.conversion_rate = 1.0 elif not self.conversion_rate: - self.conversion_rate = get_exchange_rate(self.currency, - self.company_currency, transaction_date, args) + self.conversion_rate = get_exchange_rate( + self.currency, self.company_currency, transaction_date, args + ) def set_missing_item_details(self, for_validate=False): """set missing item values""" @@ -370,7 +463,11 @@ class AccountsController(TransactionBase): parent_dict.update({"document_type": document_type}) # party_name field used for customer in quotation - if self.doctype == "Quotation" and self.quotation_to == "Customer" and parent_dict.get("party_name"): + if ( + self.doctype == "Quotation" + and self.quotation_to == "Customer" + and parent_dict.get("party_name") + ): parent_dict.update({"customer": parent_dict.get("party_name")}) self.pricing_rules = [] @@ -382,7 +479,9 @@ class AccountsController(TransactionBase): args["doctype"] = self.doctype args["name"] = self.name args["child_docname"] = item.name - args["ignore_pricing_rule"] = self.ignore_pricing_rule if hasattr(self, 'ignore_pricing_rule') else 0 + args["ignore_pricing_rule"] = ( + self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0 + ) if not args.get("transaction_date"): args["transaction_date"] = args.get("posting_date") @@ -394,10 +493,10 @@ class AccountsController(TransactionBase): for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: - if (item.get(fieldname) is None or fieldname in force_item_fields): + if item.get(fieldname) is None or fieldname in force_item_fields: item.set(fieldname, value) - elif fieldname in ['cost_center', 'conversion_factor'] and not item.get(fieldname): + elif fieldname in ["cost_center", "conversion_factor"] and not item.get(fieldname): item.set(fieldname, value) elif fieldname == "serial_no": @@ -405,7 +504,7 @@ class AccountsController(TransactionBase): item_conversion_factor = item.get("conversion_factor") or 1.0 item_qty = abs(item.get("qty")) * item_conversion_factor - if item_qty != len(get_serial_nos(item.get('serial_no'))): + if item_qty != len(get_serial_nos(item.get("serial_no"))): item.set(fieldname, value) elif ( @@ -424,13 +523,17 @@ class AccountsController(TransactionBase): # reset pricing rule fields if pricing_rule_removed item.set(fieldname, value) - if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): - item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) + if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field( + "is_fixed_asset" + ): + item.set("is_fixed_asset", ret.get("is_fixed_asset", 0)) # Double check for cost center # Items add via promotional scheme may not have cost center set - if hasattr(item, 'cost_center') and not item.get('cost_center'): - item.set('cost_center', self.get('cost_center') or erpnext.get_default_cost_center(self.company)) + if hasattr(item, "cost_center") and not item.get("cost_center"): + item.set( + "cost_center", self.get("cost_center") or erpnext.get_default_cost_center(self.company) + ) if ret.get("pricing_rules"): self.apply_pricing_rule_on_items(item, ret) @@ -442,7 +545,7 @@ class AccountsController(TransactionBase): def apply_pricing_rule_on_items(self, item, pricing_rule_args): if not pricing_rule_args.get("validate_applied_rule", 0): # if user changed the discount percentage then set user's discount percentage ? - if pricing_rule_args.get("price_or_product_discount") == 'Price': + if pricing_rule_args.get("price_or_product_discount") == "Price": item.set("pricing_rules", pricing_rule_args.get("pricing_rules")) item.set("discount_percentage", pricing_rule_args.get("discount_percentage")) item.set("discount_amount", pricing_rule_args.get("discount_amount")) @@ -450,39 +553,48 @@ class AccountsController(TransactionBase): item.set("price_list_rate", pricing_rule_args.get("price_list_rate")) if item.get("price_list_rate"): - item.rate = flt(item.price_list_rate * - (1.0 - (flt(item.discount_percentage) / 100.0)), item.precision("rate")) + item.rate = flt( + item.price_list_rate * (1.0 - (flt(item.discount_percentage) / 100.0)), + item.precision("rate"), + ) - if item.get('discount_amount'): + if item.get("discount_amount"): item.rate = item.price_list_rate - item.discount_amount if item.get("apply_discount_on_discounted_rate") and pricing_rule_args.get("rate"): item.rate = pricing_rule_args.get("rate") - elif pricing_rule_args.get('free_item_data'): - apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data')) + elif pricing_rule_args.get("free_item_data"): + apply_pricing_rule_for_free_items(self, pricing_rule_args.get("free_item_data")) elif pricing_rule_args.get("validate_applied_rule"): - for pricing_rule in get_applied_pricing_rules(item.get('pricing_rules')): + for pricing_rule in get_applied_pricing_rules(item.get("pricing_rules")): pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule) - for field in ['discount_percentage', 'discount_amount', 'rate']: + for field in ["discount_percentage", "discount_amount", "rate"]: if item.get(field) < pricing_rule_doc.get(field): title = get_link_to_form("Pricing Rule", pricing_rule) - frappe.msgprint(_("Row {0}: user has not applied the rule {1} on the item {2}") - .format(item.idx, frappe.bold(title), frappe.bold(item.item_code))) + frappe.msgprint( + _("Row {0}: user has not applied the rule {1} on the item {2}").format( + item.idx, frappe.bold(title), frappe.bold(item.item_code) + ) + ) def set_pricing_rule_details(self, item_row, args): pricing_rules = get_applied_pricing_rules(args.get("pricing_rules")) - if not pricing_rules: return + if not pricing_rules: + return for pricing_rule in pricing_rules: - self.append("pricing_rules", { - "pricing_rule": pricing_rule, - "item_code": item_row.item_code, - "child_docname": item_row.name, - "rule_applied": True - }) + self.append( + "pricing_rules", + { + "pricing_rule": pricing_rule, + "item_code": item_row.item_code, + "child_docname": item_row.name, + "rule_applied": True, + }, + ) def set_taxes(self): if not self.meta.get_field("taxes"): @@ -493,14 +605,18 @@ class AccountsController(TransactionBase): if (self.is_new() or self.is_pos_profile_changed()) and not self.get("taxes"): if self.company and not self.get("taxes_and_charges"): # get the default tax master - self.taxes_and_charges = frappe.db.get_value(tax_master_doctype, - {"is_default": 1, 'company': self.company}) + self.taxes_and_charges = frappe.db.get_value( + tax_master_doctype, {"is_default": 1, "company": self.company} + ) self.append_taxes_from_master(tax_master_doctype) def is_pos_profile_changed(self): - if (self.doctype == 'Sales Invoice' and self.is_pos and - self.pos_profile != frappe.db.get_value('Sales Invoice', self.name, 'pos_profile')): + if ( + self.doctype == "Sales Invoice" + and self.is_pos + and self.pos_profile != frappe.db.get_value("Sales Invoice", self.name, "pos_profile") + ): return True def append_taxes_from_master(self, tax_master_doctype=None): @@ -517,44 +633,54 @@ class AccountsController(TransactionBase): def validate_enabled_taxes_and_charges(self): taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges") if frappe.db.get_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"): - frappe.throw(_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges)) + frappe.throw( + _("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges) + ) def validate_tax_account_company(self): for d in self.get("taxes"): if d.account_head: tax_account_company = frappe.db.get_value("Account", d.account_head, "company") if tax_account_company != self.company: - frappe.throw(_("Row #{0}: Account {1} does not belong to company {2}") - .format(d.idx, d.account_head, self.company)) + frappe.throw( + _("Row #{0}: Account {1} does not belong to company {2}").format( + d.idx, d.account_head, self.company + ) + ) def get_gl_dict(self, args, account_currency=None, item=None): """this method populates the common properties of a gl entry record""" - posting_date = args.get('posting_date') or self.get('posting_date') + posting_date = args.get("posting_date") or self.get("posting_date") fiscal_years = get_fiscal_years(posting_date, company=self.company) if len(fiscal_years) > 1: - frappe.throw(_("Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year").format( - formatdate(posting_date))) + frappe.throw( + _("Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year").format( + formatdate(posting_date) + ) + ) else: fiscal_year = fiscal_years[0][0] - gl_dict = frappe._dict({ - 'company': self.company, - 'posting_date': posting_date, - 'fiscal_year': fiscal_year, - 'voucher_type': self.doctype, - 'voucher_no': self.name, - 'remarks': self.get("remarks") or self.get("remark"), - 'debit': 0, - 'credit': 0, - 'debit_in_account_currency': 0, - 'credit_in_account_currency': 0, - 'is_opening': self.get("is_opening") or "No", - 'party_type': None, - 'party': None, - 'project': self.get("project"), - 'post_net_value': args.get('post_net_value') - }) + gl_dict = frappe._dict( + { + "company": self.company, + "posting_date": posting_date, + "fiscal_year": fiscal_year, + "voucher_type": self.doctype, + "voucher_no": self.name, + "remarks": self.get("remarks") or self.get("remark"), + "debit": 0, + "credit": 0, + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + "is_opening": self.get("is_opening") or "No", + "party_type": None, + "party": None, + "project": self.get("project"), + "post_net_value": args.get("post_net_value"), + } + ) accounting_dimensions = get_accounting_dimensions() dimension_dict = frappe._dict() @@ -570,13 +696,24 @@ class AccountsController(TransactionBase): if not account_currency: account_currency = get_account_currency(gl_dict.account) - if gl_dict.account and self.doctype not in ["Journal Entry", - "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice", "Stock Entry"]: + if gl_dict.account and self.doctype not in [ + "Journal Entry", + "Period Closing Voucher", + "Payment Entry", + "Purchase Receipt", + "Purchase Invoice", + "Stock Entry", + ]: self.validate_account_currency(gl_dict.account, account_currency) - if gl_dict.account and self.doctype not in ["Journal Entry", "Period Closing Voucher", "Payment Entry"]: - set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"), - self.company_currency) + if gl_dict.account and self.doctype not in [ + "Journal Entry", + "Period Closing Voucher", + "Payment Entry", + ]: + set_balance_in_account_currency( + gl_dict, account_currency, self.get("conversion_rate"), self.company_currency + ) return gl_dict @@ -592,14 +729,21 @@ class AccountsController(TransactionBase): valid_currency.append(self.currency) if account_currency not in valid_currency: - frappe.throw(_("Account {0} is invalid. Account Currency must be {1}") - .format(account, (' ' + _("or") + ' ').join(valid_currency))) + frappe.throw( + _("Account {0} is invalid. Account Currency must be {1}").format( + account, (" " + _("or") + " ").join(valid_currency) + ) + ) def clear_unallocated_advances(self, childtype, parentfield): self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]})) - frappe.db.sql("""delete from `tab%s` where parentfield=%s and parent = %s - and allocated_amount = 0""" % (childtype, '%s', '%s'), (parentfield, self.name)) + frappe.db.sql( + """delete from `tab%s` where parentfield=%s and parent = %s + and allocated_amount = 0""" + % (childtype, "%s", "%s"), + (parentfield, self.name), + ) @frappe.whitelist() def apply_shipping_rule(self): @@ -609,16 +753,16 @@ class AccountsController(TransactionBase): self.calculate_taxes_and_totals() def get_shipping_address(self): - '''Returns Address object from shipping address fields if present''' + """Returns Address object from shipping address fields if present""" # shipping address fields can be `shipping_address_name` or `shipping_address` # try getting value from both - for fieldname in ('shipping_address_name', 'shipping_address'): + for fieldname in ("shipping_address_name", "shipping_address"): shipping_field = self.meta.get_field(fieldname) - if shipping_field and shipping_field.fieldtype == 'Link': + if shipping_field and shipping_field.fieldtype == "Link": if self.get(fieldname): - return frappe.get_doc('Address', self.get(fieldname)) + return frappe.get_doc("Address", self.get(fieldname)) return {} @@ -634,10 +778,10 @@ class AccountsController(TransactionBase): if d.against_order: allocated_amount = flt(d.amount) else: - if self.get('party_account_currency') == self.company_currency: - amount = self.get('base_rounded_total') or self.base_grand_total + if self.get("party_account_currency") == self.company_currency: + amount = self.get("base_rounded_total") or self.base_grand_total else: - amount = self.get('rounded_total') or self.grand_total + amount = self.get("rounded_total") or self.grand_total allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) @@ -650,7 +794,7 @@ class AccountsController(TransactionBase): "remarks": d.remarks, "advance_amount": flt(d.amount), "allocated_amount": allocated_amount, - "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry + "ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry } self.append("advances", advance_row) @@ -671,21 +815,24 @@ class AccountsController(TransactionBase): order_field = "purchase_order" order_doctype = "Purchase Order" - order_list = list(set(d.get(order_field) - for d in self.get("items") if d.get(order_field))) + order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field))) - journal_entries = get_advance_journal_entries(party_type, party, party_account, - amount_field, order_doctype, order_list, include_unallocated) + journal_entries = get_advance_journal_entries( + party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated + ) - payment_entries = get_advance_payment_entries(party_type, party, party_account, - order_doctype, order_list, include_unallocated) + payment_entries = get_advance_payment_entries( + party_type, party, party_account, order_doctype, order_list, include_unallocated + ) res = journal_entries + payment_entries return res def is_inclusive_tax(self): - is_inclusive = cint(frappe.db.get_single_value("Accounts Settings", "show_inclusive_tax_in_print")) + is_inclusive = cint( + frappe.db.get_single_value("Accounts Settings", "show_inclusive_tax_in_print") + ) if is_inclusive: is_inclusive = 0 @@ -696,10 +843,10 @@ class AccountsController(TransactionBase): def validate_advance_entries(self): order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order" - order_list = list(set(d.get(order_field) - for d in self.get("items") if d.get(order_field))) + order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field))) - if not order_list: return + if not order_list: + return advance_entries = self.get_advance_entries(include_unallocated=False) @@ -707,22 +854,24 @@ class AccountsController(TransactionBase): advance_entries_against_si = [d.reference_name for d in self.get("advances")] for d in advance_entries: if not advance_entries_against_si or d.reference_name not in advance_entries_against_si: - frappe.msgprint(_( - "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.") - .format(d.reference_name, d.against_order)) + frappe.msgprint( + _( + "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice." + ).format(d.reference_name, d.against_order) + ) def set_advance_gain_or_loss(self): - if self.get('conversion_rate') == 1 or not self.get("advances"): + if self.get("conversion_rate") == 1 or not self.get("advances"): return - is_purchase_invoice = self.doctype == 'Purchase Invoice' + is_purchase_invoice = self.doctype == "Purchase Invoice" party_account = self.credit_to if is_purchase_invoice else self.debit_to if get_account_currency(party_account) != self.currency: return for d in self.get("advances"): advance_exchange_rate = d.ref_exchange_rate - if (d.allocated_amount and self.conversion_rate != advance_exchange_rate): + if d.allocated_amount and self.conversion_rate != advance_exchange_rate: base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount @@ -731,61 +880,71 @@ class AccountsController(TransactionBase): d.exchange_gain_loss = difference def make_exchange_gain_loss_gl_entries(self, gl_entries): - if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: + if self.get("doctype") in ["Purchase Invoice", "Sales Invoice"]: for d in self.get("advances"): if d.exchange_gain_loss: - is_purchase_invoice = self.get('doctype') == 'Purchase Invoice' + is_purchase_invoice = self.get("doctype") == "Purchase Invoice" party = self.supplier if is_purchase_invoice else self.customer party_account = self.credit_to if is_purchase_invoice else self.debit_to party_type = "Supplier" if is_purchase_invoice else "Customer" - gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') + gain_loss_account = frappe.db.get_value("Company", self.company, "exchange_gain_loss_account") if not gain_loss_account: - frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}") - .format(self.get('company'))) + frappe.throw( + _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) + ) account_currency = get_account_currency(gain_loss_account) if account_currency != self.company_currency: - frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency)) + frappe.throw( + _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) + ) # for purchase - dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit' + dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" if not is_purchase_invoice: # just reverse for sales? - dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" gl_entries.append( - self.get_gl_dict({ - "account": gain_loss_account, - "account_currency": account_currency, - "against": party, - dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), - dr_or_cr: abs(d.exchange_gain_loss), - "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company), - "project": self.project - }, item=d) + self.get_gl_dict( + { + "account": gain_loss_account, + "account_currency": account_currency, + "against": party, + dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + dr_or_cr: abs(d.exchange_gain_loss), + "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company), + "project": self.project, + }, + item=d, + ) ) - dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" gl_entries.append( - self.get_gl_dict({ - "account": party_account, - "party_type": party_type, - "party": party, - "against": gain_loss_account, - dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate), - dr_or_cr: abs(d.exchange_gain_loss), - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": party_account, + "party_type": party_type, + "party": party, + "against": gain_loss_account, + dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate), + dr_or_cr: abs(d.exchange_gain_loss), + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) def update_against_document_in_jv(self): """ - Links invoice and advance voucher: - 1. cancel advance voucher - 2. split into multiple rows if partially adjusted, assign against voucher - 3. submit advance voucher + Links invoice and advance voucher: + 1. cancel advance voucher + 2. split into multiple rows if partially adjusted, assign against voucher + 3. submit advance voucher """ if self.doctype == "Sales Invoice": @@ -800,45 +959,56 @@ class AccountsController(TransactionBase): dr_or_cr = "debit_in_account_currency" lst = [] - for d in self.get('advances'): + for d in self.get("advances"): if flt(d.allocated_amount) > 0: - args = frappe._dict({ - 'voucher_type': d.reference_type, - 'voucher_no': d.reference_name, - 'voucher_detail_no': d.reference_row, - 'against_voucher_type': self.doctype, - 'against_voucher': self.name, - 'account': party_account, - 'party_type': party_type, - 'party': party, - 'is_advance': 'Yes', - 'dr_or_cr': dr_or_cr, - 'unadjusted_amount': flt(d.advance_amount), - 'allocated_amount': flt(d.allocated_amount), - 'precision': d.precision('advance_amount'), - 'exchange_rate': (self.conversion_rate - if self.party_account_currency != self.company_currency else 1), - 'grand_total': (self.base_grand_total - if self.party_account_currency == self.company_currency else self.grand_total), - 'outstanding_amount': self.outstanding_amount, - 'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'), - 'exchange_gain_loss': flt(d.get('exchange_gain_loss')) - }) + args = frappe._dict( + { + "voucher_type": d.reference_type, + "voucher_no": d.reference_name, + "voucher_detail_no": d.reference_row, + "against_voucher_type": self.doctype, + "against_voucher": self.name, + "account": party_account, + "party_type": party_type, + "party": party, + "is_advance": "Yes", + "dr_or_cr": dr_or_cr, + "unadjusted_amount": flt(d.advance_amount), + "allocated_amount": flt(d.allocated_amount), + "precision": d.precision("advance_amount"), + "exchange_rate": ( + self.conversion_rate if self.party_account_currency != self.company_currency else 1 + ), + "grand_total": ( + self.base_grand_total + if self.party_account_currency == self.company_currency + else self.grand_total + ), + "outstanding_amount": self.outstanding_amount, + "difference_account": frappe.db.get_value( + "Company", self.company, "exchange_gain_loss_account" + ), + "exchange_gain_loss": flt(d.get("exchange_gain_loss")), + } + ) lst.append(args) if lst: from erpnext.accounts.utils import reconcile_against_document + reconcile_against_document(lst) def on_cancel(self): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries if self.doctype in ["Sales Invoice", "Purchase Invoice"]: - if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): + if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"): unlink_ref_doc_from_payment_entries(self) elif self.doctype in ["Sales Order", "Purchase Order"]: - if frappe.db.get_single_value('Accounts Settings', 'unlink_advance_payment_on_cancelation_of_order'): + if frappe.db.get_single_value( + "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order" + ): unlink_ref_doc_from_payment_entries(self) if self.doctype == "Sales Order": @@ -849,33 +1019,32 @@ class AccountsController(TransactionBase): for item in self.items: so_items.append(item.name) - linked_po = list(set(frappe.get_all( - 'Purchase Order Item', - filters = { - 'sales_order': self.name, - 'sales_order_item': ['in', so_items], - 'docstatus': ['<', 2] - }, - pluck='parent' - ))) + linked_po = list( + set( + frappe.get_all( + "Purchase Order Item", + filters={ + "sales_order": self.name, + "sales_order_item": ["in", so_items], + "docstatus": ["<", 2], + }, + pluck="parent", + ) + ) + ) if linked_po: frappe.db.set_value( - 'Purchase Order Item', { - 'sales_order': self.name, - 'sales_order_item': ['in', so_items], - 'docstatus': ['<', 2] - },{ - 'sales_order': None, - 'sales_order_item': None - } + "Purchase Order Item", + {"sales_order": self.name, "sales_order_item": ["in", so_items], "docstatus": ["<", 2]}, + {"sales_order": None, "sales_order_item": None}, ) frappe.msgprint(_("Purchase Orders {0} are un-linked").format("\n".join(linked_po))) def get_tax_map(self): tax_map = {} - for tax in self.get('taxes'): + for tax in self.get("taxes"): tax_map.setdefault(tax.account_head, 0.0) tax_map[tax.account_head] += tax.tax_amount @@ -885,7 +1054,11 @@ class AccountsController(TransactionBase): amount = item.net_amount base_amount = item.base_net_amount - if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'): + if ( + enable_discount_accounting + and self.get("discount_amount") + and self.get("additional_discount_account") + ): amount = item.amount base_amount = item.base_amount @@ -895,15 +1068,21 @@ class AccountsController(TransactionBase): amount = tax.tax_amount_after_discount_amount base_amount = tax.base_tax_amount_after_discount_amount - if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account') \ - and self.get('apply_discount_on') == 'Grand Total': + if ( + enable_discount_accounting + and self.get("discount_amount") + and self.get("additional_discount_account") + and self.get("apply_discount_on") == "Grand Total" + ): amount = tax.tax_amount base_amount = tax.base_tax_amount return amount, base_amount def make_discount_gl_entries(self, gl_entries): - enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + enable_discount_accounting = cint( + frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + ) if enable_discount_accounting: if self.doctype == "Purchase Invoice": @@ -917,61 +1096,81 @@ class AccountsController(TransactionBase): supplier_or_customer = self.customer for item in self.get("items"): - if item.get('discount_amount') and item.get('discount_account'): + if item.get("discount_amount") and item.get("discount_account"): discount_amount = item.discount_amount * item.qty if self.doctype == "Purchase Invoice": - income_or_expense_account = (item.expense_account + income_or_expense_account = ( + item.expense_account if (not item.enable_deferred_expense or self.is_return) - else item.deferred_expense_account) + else item.deferred_expense_account + ) else: - income_or_expense_account = (item.income_account + income_or_expense_account = ( + item.income_account if (not item.enable_deferred_revenue or self.is_return) - else item.deferred_revenue_account) + else item.deferred_revenue_account + ) account_currency = get_account_currency(item.discount_account) gl_entries.append( - self.get_gl_dict({ - "account": item.discount_account, - "against": supplier_or_customer, - dr_or_cr: flt(discount_amount, item.precision('discount_amount')), - dr_or_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'), - item.precision('discount_amount')), - "cost_center": item.cost_center, - "project": item.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": item.discount_account, + "against": supplier_or_customer, + dr_or_cr: flt(discount_amount, item.precision("discount_amount")), + dr_or_cr + + "_in_account_currency": flt( + discount_amount * self.get("conversion_rate"), item.precision("discount_amount") + ), + "cost_center": item.cost_center, + "project": item.project, + }, + account_currency, + item=item, + ) ) account_currency = get_account_currency(income_or_expense_account) gl_entries.append( - self.get_gl_dict({ - "account": income_or_expense_account, - "against": supplier_or_customer, - rev_dr_cr: flt(discount_amount, item.precision('discount_amount')), - rev_dr_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'), - item.precision('discount_amount')), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": income_or_expense_account, + "against": supplier_or_customer, + rev_dr_cr: flt(discount_amount, item.precision("discount_amount")), + rev_dr_cr + + "_in_account_currency": flt( + discount_amount * self.get("conversion_rate"), item.precision("discount_amount") + ), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) ) - if self.get('discount_amount') and self.get('additional_discount_account'): + if self.get("discount_amount") and self.get("additional_discount_account"): gl_entries.append( - self.get_gl_dict({ - "account": self.additional_discount_account, - "against": supplier_or_customer, - dr_or_cr: self.discount_amount, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.additional_discount_account, + "against": supplier_or_customer, + dr_or_cr: self.discount_amount, + "cost_center": self.cost_center, + }, + item=self, + ) ) - def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): from erpnext.controllers.status_updater import get_allowance_for item_allowance = {} global_qty_allowance, global_amount_allowance = None, None - role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') + role_allowed_to_over_bill = frappe.db.get_single_value( + "Accounts Settings", "role_allowed_to_over_bill" + ) user_roles = frappe.get_roles() total_overbilled_amt = 0.0 @@ -980,21 +1179,29 @@ class AccountsController(TransactionBase): if not item.get(item_ref_dn): continue - ref_amt = flt(frappe.db.get_value(ref_dt + " Item", - item.get(item_ref_dn), based_on), self.precision(based_on, item)) + ref_amt = flt( + frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on), + self.precision(based_on, item), + ) if not ref_amt: frappe.msgprint( - _("System will not check overbilling since amount for Item {0} in {1} is zero") - .format(item.item_code, ref_dt), title=_("Warning"), indicator="orange") + _("System will not check overbilling since amount for Item {0} in {1} is zero").format( + item.item_code, ref_dt + ), + title=_("Warning"), + indicator="orange", + ) continue already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on) - total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)), - self.precision(based_on, item)) + total_billed_amt = flt( + flt(already_billed) + flt(item.get(based_on)), self.precision(based_on, item) + ) - allowance, item_allowance, global_qty_allowance, global_amount_allowance = \ - get_allowance_for(item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount") + allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for( + item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount" + ) max_allowed_amt = flt(ref_amt * (100 + allowance) / 100) @@ -1009,20 +1216,29 @@ class AccountsController(TransactionBase): if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles: if self.doctype != "Purchase Invoice": self.throw_overbill_exception(item, max_allowed_amt) - elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")): + elif not cint( + frappe.db.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ) + ): self.throw_overbill_exception(item, max_allowed_amt) if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1: - frappe.msgprint(_("Overbilling of {} ignored because you have {} role.") - .format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True) + frappe.msgprint( + _("Overbilling of {} ignored because you have {} role.").format( + total_overbilled_amt, role_allowed_to_over_bill + ), + indicator="orange", + alert=True, + ) def get_billed_amount_for_item(self, item, item_ref_dn, based_on): - ''' - Returns Sum of Amount of - Sales/Purchase Invoice Items - that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`) - that are submitted OR not submitted but are under current invoice - ''' + """ + Returns Sum of Amount of + Sales/Purchase Invoice Items + that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`) + that are submitted OR not submitted but are under current invoice + """ from frappe.query_builder import Criterion from frappe.query_builder.functions import Sum @@ -1034,41 +1250,51 @@ class AccountsController(TransactionBase): result = ( frappe.qb.from_(item_doctype) .select(Sum(based_on_field)) + .where(join_field == item.get(item_ref_dn)) .where( - join_field == item.get(item_ref_dn) - ).where( - Criterion.any([ # select all items from other invoices OR current invoices - Criterion.all([ # for selecting items from other invoices - item_doctype.docstatus == 1, - item_doctype.parent != self.name - ]), - Criterion.all([ # for selecting items from current invoice, that are linked to same reference - item_doctype.docstatus == 0, - item_doctype.parent == self.name, - item_doctype.name != item.name - ]) - ]) + Criterion.any( + [ # select all items from other invoices OR current invoices + Criterion.all( + [ # for selecting items from other invoices + item_doctype.docstatus == 1, + item_doctype.parent != self.name, + ] + ), + Criterion.all( + [ # for selecting items from current invoice, that are linked to same reference + item_doctype.docstatus == 0, + item_doctype.parent == self.name, + item_doctype.name != item.name, + ] + ), + ] + ) ) ).run() return result[0][0] if result else 0 def throw_overbill_exception(self, item, max_allowed_amt): - frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") - .format(item.item_code, item.idx, max_allowed_amt)) + frappe.throw( + _( + "Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings" + ).format(item.item_code, item.idx, max_allowed_amt) + ) def get_company_default(self, fieldname, ignore_validation=False): from erpnext.accounts.utils import get_company_default + return get_company_default(self.company, fieldname, ignore_validation=ignore_validation) def get_stock_items(self): stock_items = [] item_codes = list(set(item.item_code for item in self.get("items"))) if item_codes: - stock_items = [r[0] for r in frappe.db.sql(""" - select name from `tabItem` - where name in (%s) and is_stock_item=1 - """ % (", ".join((["%s"] * len(item_codes))),), item_codes)] + stock_items = frappe.db.get_values( + "Item", {"name": ["in", item_codes], "is_stock_item": 1}, as_dict=True, cache=True + ) + if stock_items: + stock_items = [d.get("name") for d in stock_items] return stock_items @@ -1082,7 +1308,8 @@ class AccountsController(TransactionBase): rev_dr_or_cr = "credit_in_account_currency" party = self.supplier - advance = frappe.db.sql(""" + advance = frappe.db.sql( + """ select account_currency, sum({dr_or_cr}) - sum({rev_dr_cr}) as amount from @@ -1090,17 +1317,22 @@ class AccountsController(TransactionBase): where against_voucher_type = %s and against_voucher = %s and party=%s and docstatus = 1 - """.format(dr_or_cr=dr_or_cr, rev_dr_cr=rev_dr_or_cr), (self.doctype, self.name, party), as_dict=1) #nosec + """.format( + dr_or_cr=dr_or_cr, rev_dr_cr=rev_dr_or_cr + ), + (self.doctype, self.name, party), + as_dict=1, + ) # nosec if advance: advance = advance[0] advance_paid = flt(advance.amount, self.precision("advance_paid")) - formatted_advance_paid = fmt_money(advance_paid, precision=self.precision("advance_paid"), - currency=advance.account_currency) + formatted_advance_paid = fmt_money( + advance_paid, precision=self.precision("advance_paid"), currency=advance.account_currency + ) - frappe.db.set_value(self.doctype, self.name, "party_account_currency", - advance.account_currency) + frappe.db.set_value(self.doctype, self.name, "party_account_currency", advance.account_currency) if advance.account_currency == self.currency: order_total = self.get("rounded_total") or self.grand_total @@ -1109,34 +1341,50 @@ class AccountsController(TransactionBase): order_total = self.get("base_rounded_total") or self.base_grand_total precision = "base_rounded_total" if self.get("base_rounded_total") else "base_grand_total" - formatted_order_total = fmt_money(order_total, precision=self.precision(precision), - currency=advance.account_currency) + formatted_order_total = fmt_money( + order_total, precision=self.precision(precision), currency=advance.account_currency + ) if self.currency == self.company_currency and advance_paid > order_total: - frappe.throw(_("Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2})") - .format(formatted_advance_paid, self.name, formatted_order_total)) + frappe.throw( + _( + "Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2})" + ).format(formatted_advance_paid, self.name, formatted_order_total) + ) frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid) @property def company_abbr(self): if not hasattr(self, "_abbr"): - self._abbr = frappe.db.get_value('Company', self.company, "abbr") + self._abbr = frappe.db.get_value("Company", self.company, "abbr") return self._abbr def raise_missing_debit_credit_account_error(self, party_type, party): """Raise an error if debit to/credit to account does not exist.""" - db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To") + db_or_cr = ( + frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To") + ) rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable" link_to_party = frappe.utils.get_link_to_form(party_type, party) link_to_company = frappe.utils.get_link_to_form("Company", self.company) - message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or '') + message = _("{0} Account not found against Customer {1}.").format( + db_or_cr, frappe.bold(party) or "" + ) message += "
" + _("Please set one of the following:") + "
" - message += "
  • " + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "
  • " - message += "
  • " + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "
" + message += ( + "
  • " + + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + + "
  • " + ) + message += ( + "
  • " + + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + + "
" + ) frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError) @@ -1147,10 +1395,15 @@ class AccountsController(TransactionBase): def get_party(self): party_type = None if self.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"): - party_type = 'Customer' + party_type = "Customer" - elif self.doctype in ("Supplier Quotation", "Purchase Order", "Purchase Receipt", "Purchase Invoice"): - party_type = 'Supplier' + elif self.doctype in ( + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ): + party_type = "Supplier" elif self.meta.get_field("customer"): party_type = "Customer" @@ -1168,11 +1421,17 @@ class AccountsController(TransactionBase): if party_type and party: party_account_currency = get_party_account_currency(party_type, party, self.company) - if (party_account_currency - and party_account_currency != self.company_currency - and self.currency != party_account_currency): - frappe.throw(_("Accounting Entry for {0}: {1} can only be made in currency: {2}") - .format(party_type, party, party_account_currency), InvalidCurrency) + if ( + party_account_currency + and party_account_currency != self.company_currency + and self.currency != party_account_currency + ): + frappe.throw( + _("Accounting Entry for {0}: {1} can only be made in currency: {2}").format( + party_type, party, party_account_currency + ), + InvalidCurrency, + ) # Note: not validating with gle account because we don't have the account # at quotation / sales order level and we shouldn't stop someone @@ -1183,15 +1442,21 @@ class AccountsController(TransactionBase): for adv in self.advances: consider_for_total_advance = True if adv.reference_name == linked_doc_name: - frappe.db.sql("""delete from `tab{0} Advance` - where name = %s""".format(self.doctype), adv.name) + frappe.db.sql( + """delete from `tab{0} Advance` + where name = %s""".format( + self.doctype + ), + adv.name, + ) consider_for_total_advance = False if consider_for_total_advance: total_allocated_amount += flt(adv.allocated_amount, adv.precision("allocated_amount")) - frappe.db.set_value(self.doctype, self.name, "total_advance", - total_allocated_amount, update_modified=False) + frappe.db.set_value( + self.doctype, self.name, "total_advance", total_allocated_amount, update_modified=False + ) def group_similar_items(self): group_item_qty = {} @@ -1223,11 +1488,11 @@ class AccountsController(TransactionBase): self.remove(item) def set_payment_schedule(self): - if self.doctype == 'Sales Invoice' and self.is_pos: - self.payment_terms_template = '' + if self.doctype == "Sales Invoice" and self.is_pos: + self.payment_terms_template = "" return - party_account_currency = self.get('party_account_currency') + party_account_currency = self.get("party_account_currency") if not party_account_currency: party_type, party = self.get_party() @@ -1245,47 +1510,68 @@ class AccountsController(TransactionBase): base_grand_total = base_grand_total - flt(self.base_write_off_amount) grand_total = grand_total - flt(self.write_off_amount) po_or_so, doctype, fieldname = self.get_order_details() - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + automatically_fetch_payment_terms = cint( + frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) if self.get("total_advance"): if party_account_currency == self.company_currency: base_grand_total -= self.get("total_advance") - grand_total = flt(base_grand_total / self.get("conversion_rate"), self.precision("grand_total")) + grand_total = flt( + base_grand_total / self.get("conversion_rate"), self.precision("grand_total") + ) else: grand_total -= self.get("total_advance") - base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) + base_grand_total = flt( + grand_total * self.get("conversion_rate"), self.precision("base_grand_total") + ) if not self.get("payment_schedule"): - if self.doctype in ["Sales Invoice", "Purchase Invoice"] and automatically_fetch_payment_terms \ - and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype): + if ( + self.doctype in ["Sales Invoice", "Purchase Invoice"] + and automatically_fetch_payment_terms + and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype) + ): self.fetch_payment_terms_from_order(po_or_so, doctype) - if self.get('payment_terms_template'): + if self.get("payment_terms_template"): self.ignore_default_payment_terms_template = 1 elif self.get("payment_terms_template"): - data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total) + data = get_payment_terms( + self.payment_terms_template, posting_date, grand_total, base_grand_total + ) for item in data: self.append("payment_schedule", item) elif self.doctype not in ["Purchase Receipt"]: - data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total) + data = dict( + due_date=due_date, + invoice_portion=100, + payment_amount=grand_total, + base_payment_amount=base_grand_total, + ) self.append("payment_schedule", data) for d in self.get("payment_schedule"): if d.invoice_portion: - d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) - d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount')) + d.payment_amount = flt( + grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount") + ) + d.base_payment_amount = flt( + base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount") + ) d.outstanding = d.payment_amount elif not d.invoice_portion: - d.base_payment_amount = flt(d.payment_amount * self.get("conversion_rate"), d.precision('base_payment_amount')) - + d.base_payment_amount = flt( + d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount") + ) def get_order_details(self): if self.doctype == "Sales Invoice": - po_or_so = self.get('items')[0].get('sales_order') + po_or_so = self.get("items")[0].get("sales_order") po_or_so_doctype = "Sales Order" po_or_so_doctype_name = "sales_order" else: - po_or_so = self.get('items')[0].get('purchase_order') + po_or_so = self.get("items")[0].get("purchase_order") po_or_so_doctype = "Purchase Order" po_or_so_doctype_name = "purchase_order" @@ -1301,21 +1587,21 @@ class AccountsController(TransactionBase): return False def all_items_have_same_po_or_so(self, po_or_so, fieldname): - for item in self.get('items'): + for item in self.get("items"): if item.get(fieldname) != po_or_so: return False return True def linked_order_has_payment_terms_template(self, po_or_so, doctype): - return frappe.get_value(doctype, po_or_so, 'payment_terms_template') + return frappe.get_value(doctype, po_or_so, "payment_terms_template") def linked_order_has_payment_schedule(self, po_or_so): - return frappe.get_all('Payment Schedule', filters={'parent': po_or_so}) + return frappe.get_all("Payment Schedule", filters={"parent": po_or_so}) def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype): """ - Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice. + Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice. """ po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so) @@ -1324,19 +1610,19 @@ class AccountsController(TransactionBase): for schedule in po_or_so.payment_schedule: payment_schedule = { - 'payment_term': schedule.payment_term, - 'due_date': schedule.due_date, - 'invoice_portion': schedule.invoice_portion, - 'mode_of_payment': schedule.mode_of_payment, - 'description': schedule.description + "payment_term": schedule.payment_term, + "due_date": schedule.due_date, + "invoice_portion": schedule.invoice_portion, + "mode_of_payment": schedule.mode_of_payment, + "description": schedule.description, } - if schedule.discount_type == 'Percentage': - payment_schedule['discount_type'] = schedule.discount_type - payment_schedule['discount'] = schedule.discount + if schedule.discount_type == "Percentage": + payment_schedule["discount_type"] = schedule.discount_type + payment_schedule["discount"] = schedule.discount if not schedule.invoice_portion: - payment_schedule['payment_amount'] = schedule.payment_amount + payment_schedule["payment_amount"] = schedule.payment_amount self.append("payment_schedule", payment_schedule) @@ -1349,23 +1635,29 @@ class AccountsController(TransactionBase): dates = [] li = [] - if self.doctype == 'Sales Invoice' and self.is_pos: return + if self.doctype == "Sales Invoice" and self.is_pos: + return for d in self.get("payment_schedule"): if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date): - frappe.throw(_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(d.idx)) + frappe.throw( + _("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(d.idx) + ) elif d.due_date in dates: li.append(_("{0} in row {1}").format(d.due_date, d.idx)) dates.append(d.due_date) if li: - duplicates = '
' + '
'.join(li) - frappe.throw(_("Rows with duplicate due dates in other rows were found: {0}").format(duplicates)) + duplicates = "
" + "
".join(li) + frappe.throw( + _("Rows with duplicate due dates in other rows were found: {0}").format(duplicates) + ) def validate_payment_schedule_amount(self): - if self.doctype == 'Sales Invoice' and self.is_pos: return + if self.doctype == "Sales Invoice" and self.is_pos: + return - party_account_currency = self.get('party_account_currency') + party_account_currency = self.get("party_account_currency") if not party_account_currency: party_type, party = self.get_party() @@ -1389,14 +1681,25 @@ class AccountsController(TransactionBase): if self.get("total_advance"): if party_account_currency == self.company_currency: base_grand_total -= self.get("total_advance") - grand_total = flt(base_grand_total / self.get("conversion_rate"), self.precision("grand_total")) + grand_total = flt( + base_grand_total / self.get("conversion_rate"), self.precision("grand_total") + ) else: grand_total -= self.get("total_advance") - base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total")) + base_grand_total = flt( + grand_total * self.get("conversion_rate"), self.precision("base_grand_total") + ) - if flt(total, self.precision("grand_total")) - flt(grand_total, self.precision("grand_total")) > 0.1 or \ - flt(base_total, self.precision("base_grand_total")) - flt(base_grand_total, self.precision("base_grand_total")) > 0.1: - frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")) + if ( + flt(total, self.precision("grand_total")) - flt(grand_total, self.precision("grand_total")) + > 0.1 + or flt(base_total, self.precision("base_grand_total")) + - flt(base_grand_total, self.precision("base_grand_total")) + > 0.1 + ): + frappe.throw( + _("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total") + ) def is_rounded_total_disabled(self): if self.meta.get_field("disable_rounded_total"): @@ -1406,30 +1709,33 @@ class AccountsController(TransactionBase): def set_inter_company_account(self): """ - Set intercompany account for inter warehouse transactions - This account will be used in case billing company and internal customer's - representation company is same + Set intercompany account for inter warehouse transactions + This account will be used in case billing company and internal customer's + representation company is same """ if self.is_internal_transfer() and not self.unrealized_profit_loss_account: - unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account') + unrealized_profit_loss_account = frappe.db.get_value( + "Company", self.company, "unrealized_profit_loss_account" + ) if not unrealized_profit_loss_account: - msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format( - frappe.bold(self.company)) + msg = _( + "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}" + ).format(frappe.bold(self.company)) frappe.throw(msg) self.unrealized_profit_loss_account = unrealized_profit_loss_account def is_internal_transfer(self): """ - It will an internal transfer if its an internal customer and representation - company is same as billing company + It will an internal transfer if its an internal customer and representation + company is same as billing company """ - if self.doctype in ('Sales Invoice', 'Delivery Note', 'Sales Order'): - internal_party_field = 'is_internal_customer' - elif self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): - internal_party_field = 'is_internal_supplier' + if self.doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + internal_party_field = "is_internal_customer" + elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"): + internal_party_field = "is_internal_supplier" if self.get(internal_party_field) and (self.represents_company == self.company): return True @@ -1437,11 +1743,11 @@ class AccountsController(TransactionBase): return False def process_common_party_accounting(self): - is_invoice = self.doctype in ['Sales Invoice', 'Purchase Invoice'] + is_invoice = self.doctype in ["Sales Invoice", "Purchase Invoice"] if not is_invoice: return - if frappe.db.get_single_value('Accounts Settings', 'enable_common_party_accounting'): + if frappe.db.get_single_value("Accounts Settings", "enable_common_party_accounting"): party_link = self.get_common_party_link() if party_link and self.outstanding_amount: self.create_advance_and_reconcile(party_link) @@ -1449,10 +1755,10 @@ class AccountsController(TransactionBase): def get_common_party_link(self): party_type, party = self.get_party() return frappe.db.get_value( - doctype='Party Link', - filters={'secondary_role': party_type, 'secondary_party': party}, - fieldname=['primary_role', 'primary_party'], - as_dict=True + doctype="Party Link", + filters={"secondary_role": party_type, "secondary_party": party}, + fieldname=["primary_role", "primary_party"], + as_dict=True, ) def create_advance_and_reconcile(self, party_link): @@ -1462,11 +1768,11 @@ class AccountsController(TransactionBase): primary_account = get_party_account(primary_party_type, primary_party, self.company) secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) - jv = frappe.new_doc('Journal Entry') - jv.voucher_type = 'Journal Entry' + jv = frappe.new_doc("Journal Entry") + jv.voucher_type = "Journal Entry" jv.posting_date = self.posting_date jv.company = self.company - jv.remark = 'Adjustment for {} {}'.format(self.doctype, self.name) + jv.remark = "Adjustment for {} {}".format(self.doctype, self.name) reconcilation_entry = frappe._dict() advance_entry = frappe._dict() @@ -1482,21 +1788,22 @@ class AccountsController(TransactionBase): advance_entry.party_type = primary_party_type advance_entry.party = primary_party advance_entry.cost_center = self.cost_center - advance_entry.is_advance = 'Yes' + advance_entry.is_advance = "Yes" - if self.doctype == 'Sales Invoice': + if self.doctype == "Sales Invoice": reconcilation_entry.credit_in_account_currency = self.outstanding_amount advance_entry.debit_in_account_currency = self.outstanding_amount else: advance_entry.credit_in_account_currency = self.outstanding_amount reconcilation_entry.debit_in_account_currency = self.outstanding_amount - jv.append('accounts', reconcilation_entry) - jv.append('accounts', advance_entry) + jv.append("accounts", reconcilation_entry) + jv.append("accounts", advance_entry) jv.save() jv.submit() + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) @@ -1504,7 +1811,8 @@ def get_tax_rate(account_head): @frappe.whitelist() def get_default_taxes_and_charges(master_doctype, tax_template=None, company=None): - if not company: return {} + if not company: + return {} if tax_template and company: tax_template_company = frappe.db.get_value(master_doctype, tax_template, "company") @@ -1514,8 +1822,8 @@ def get_default_taxes_and_charges(master_doctype, tax_template=None, company=Non default_tax = frappe.db.get_value(master_doctype, {"is_default": 1, "company": company}) return { - 'taxes_and_charges': default_tax, - 'taxes': get_taxes_and_charges(master_doctype, default_tax) + "taxes_and_charges": default_tax, + "taxes": get_taxes_and_charges(master_doctype, default_tax), } @@ -1524,6 +1832,7 @@ def get_taxes_and_charges(master_doctype, master_name): if not master_name: return from frappe.model import default_fields + tax_master = frappe.get_doc(master_doctype, master_name) taxes_and_charges = [] @@ -1542,109 +1851,162 @@ def get_taxes_and_charges(master_doctype, master_name): def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, company): """common validation for currency and price list currency""" - company_currency = frappe.get_cached_value('Company', company, "default_currency") + company_currency = frappe.get_cached_value("Company", company, "default_currency") if not conversion_rate: throw( - _("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.") - .format(conversion_rate_label, currency, company_currency) + _("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.").format( + conversion_rate_label, currency, company_currency + ) ) def validate_taxes_and_charges(tax): - if tax.charge_type in ['Actual', 'On Net Total', 'On Paid Amount'] and tax.row_id: - frappe.throw(_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'")) - elif tax.charge_type in ['On Previous Row Amount', 'On Previous Row Total']: + if tax.charge_type in ["Actual", "On Net Total", "On Paid Amount"] and tax.row_id: + frappe.throw( + _("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'") + ) + elif tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]: if cint(tax.idx) == 1: frappe.throw( - _("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row")) + _( + "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" + ) + ) elif not tax.row_id: - frappe.throw(_("Please specify a valid Row ID for row {0} in table {1}").format(tax.idx, _(tax.doctype))) + frappe.throw( + _("Please specify a valid Row ID for row {0} in table {1}").format(tax.idx, _(tax.doctype)) + ) elif tax.row_id and cint(tax.row_id) >= cint(tax.idx): - frappe.throw(_("Cannot refer row number greater than or equal to current row number for this Charge type")) + frappe.throw( + _("Cannot refer row number greater than or equal to current row number for this Charge type") + ) if tax.charge_type == "Actual": tax.rate = None -def validate_account_head(idx, account, company): - account_company = frappe.get_cached_value('Account', account, 'company') +def validate_account_head(idx, account, company, context=""): + account_company = frappe.get_cached_value("Account", account, "company") if account_company != company: - frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') - .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account')) + frappe.throw( + _("Row {0}: {3} Account {1} does not belong to Company {2}").format( + idx, frappe.bold(account), frappe.bold(company), context + ), + title=_("Invalid Account"), + ) def validate_cost_center(tax, doc): if not tax.cost_center: return - company = frappe.get_cached_value('Cost Center', - tax.cost_center, 'company') + company = frappe.get_cached_value("Cost Center", tax.cost_center, "company") if company != doc.company: - frappe.throw(_('Row {0}: Cost Center {1} does not belong to Company {2}') - .format(tax.idx, frappe.bold(tax.cost_center), frappe.bold(doc.company)), title=_('Invalid Cost Center')) + frappe.throw( + _("Row {0}: Cost Center {1} does not belong to Company {2}").format( + tax.idx, frappe.bold(tax.cost_center), frappe.bold(doc.company) + ), + title=_("Invalid Cost Center"), + ) def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): - throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) + throw( + _("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format( + tax.idx, row_range + ) + ) if cint(getattr(tax, "included_in_print_rate", None)): if tax.charge_type == "Actual": # inclusive tax cannot be of type Actual - throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) - elif tax.charge_type == "On Previous Row Amount" and \ - not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate): + throw( + _("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format( + tax.idx + ) + ) + elif tax.charge_type == "On Previous Row Amount" and not cint( + doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate + ): # referred row should also be inclusive _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]): + elif tax.charge_type == "On Previous Row Total" and not all( + [cint(t.included_in_print_rate) for t in doc.get("taxes")[: cint(tax.row_id) - 1]] + ): # all rows about the referred tax should be inclusive _on_previous_row_error("1 - %d" % (tax.row_id,)) elif tax.get("category") == "Valuation": frappe.throw(_("Valuation type charges can not be marked as Inclusive")) -def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): +def set_balance_in_account_currency( + gl_dict, account_currency=None, conversion_rate=None, company_currency=None +): if (not conversion_rate) and (account_currency != company_currency): - frappe.throw(_("Account: {0} with currency: {1} can not be selected") - .format(gl_dict.account, account_currency)) + frappe.throw( + _("Account: {0} with currency: {1} can not be selected").format( + gl_dict.account, account_currency + ) + ) - gl_dict["account_currency"] = company_currency if account_currency == company_currency \ - else account_currency + gl_dict["account_currency"] = ( + company_currency if account_currency == company_currency else account_currency + ) # set debit/credit in account currency if not provided if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency): - gl_dict.debit_in_account_currency = gl_dict.debit if account_currency == company_currency \ + gl_dict.debit_in_account_currency = ( + gl_dict.debit + if account_currency == company_currency else flt(gl_dict.debit / conversion_rate, 2) + ) if flt(gl_dict.credit) and not flt(gl_dict.credit_in_account_currency): - gl_dict.credit_in_account_currency = gl_dict.credit if account_currency == company_currency \ + gl_dict.credit_in_account_currency = ( + gl_dict.credit + if account_currency == company_currency else flt(gl_dict.credit / conversion_rate, 2) + ) -def get_advance_journal_entries(party_type, party, party_account, amount_field, - order_doctype, order_list, include_unallocated=True): - dr_or_cr = "credit_in_account_currency" if party_type == "Customer" else "debit_in_account_currency" +def get_advance_journal_entries( + party_type, + party, + party_account, + amount_field, + order_doctype, + order_list, + include_unallocated=True, +): + dr_or_cr = ( + "credit_in_account_currency" if party_type == "Customer" else "debit_in_account_currency" + ) conditions = [] if include_unallocated: conditions.append("ifnull(t2.reference_name, '')=''") if order_list: - order_condition = ', '.join(['%s'] * len(order_list)) - conditions.append(" (t2.reference_type = '{0}' and ifnull(t2.reference_name, '') in ({1}))" \ - .format(order_doctype, order_condition)) + order_condition = ", ".join(["%s"] * len(order_list)) + conditions.append( + " (t2.reference_type = '{0}' and ifnull(t2.reference_name, '') in ({1}))".format( + order_doctype, order_condition + ) + ) reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else "" - journal_entries = frappe.db.sql(""" + # nosemgrep + journal_entries = frappe.db.sql( + """ select "Journal Entry" as reference_type, t1.name as reference_name, t1.remark as remarks, t2.{0} as amount, t2.name as reference_row, - t2.reference_name as against_order + t2.reference_name as against_order, t2.exchange_rate from `tabJournal Entry` t1, `tabJournal Entry Account` t2 where @@ -1652,31 +2014,50 @@ def get_advance_journal_entries(party_type, party, party_account, amount_field, and t2.party_type = %s and t2.party = %s and t2.is_advance = 'Yes' and t1.docstatus = 1 and {1} > 0 {2} - order by t1.posting_date""".format(amount_field, dr_or_cr, reference_condition), - [party_account, party_type, party] + order_list, as_dict=1) + order by t1.posting_date""".format( + amount_field, dr_or_cr, reference_condition + ), + [party_account, party_type, party] + order_list, + as_dict=1, + ) return list(journal_entries) -def get_advance_payment_entries(party_type, party, party_account, order_doctype, - order_list=None, include_unallocated=True, against_all_orders=False, limit=None, condition=None): +def get_advance_payment_entries( + party_type, + party, + party_account, + order_doctype, + order_list=None, + include_unallocated=True, + against_all_orders=False, + limit=None, + condition=None, +): party_account_field = "paid_from" if party_type == "Customer" else "paid_to" - currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" + currency_field = ( + "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" + ) payment_type = "Receive" if party_type == "Customer" else "Pay" - exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" + exchange_rate_field = ( + "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" + ) payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" if order_list or against_all_orders: if order_list: - reference_condition = " and t2.reference_name in ({0})" \ - .format(', '.join(['%s'] * len(order_list))) + reference_condition = " and t2.reference_name in ({0})".format( + ", ".join(["%s"] * len(order_list)) + ) else: reference_condition = "" order_list = [] - payment_entries_against_order = frappe.db.sql(""" + payment_entries_against_order = frappe.db.sql( + """ select "Payment Entry" as reference_type, t1.name as reference_name, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, @@ -1688,12 +2069,16 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 and t2.reference_doctype = %s {2} order by t1.posting_date {3} - """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field), - [party_account, payment_type, party_type, party, - order_doctype] + order_list, as_dict=1) + """.format( + currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field + ), + [party_account, payment_type, party_type, party, order_doctype] + order_list, + as_dict=1, + ) if include_unallocated: - unallocated_payment_entries = frappe.db.sql(""" + unallocated_payment_entries = frappe.db.sql( + """ select "Payment Entry" as reference_type, name as reference_name, posting_date, remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency from `tabPayment Entry` @@ -1701,11 +2086,16 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, {0} = %s and party_type = %s and party = %s and payment_type = %s and docstatus = 1 and unallocated_amount > 0 {condition} order by posting_date {1} - """.format(party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or ""), - (party_account, party_type, party, payment_type), as_dict=1) + """.format( + party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or "" + ), + (party_account, party_type, party, payment_type), + as_dict=1, + ) return list(payment_entries_against_order) + list(unallocated_payment_entries) + def update_invoice_status(): """Updates status as Overdue for applicable invoices. Runs daily.""" today = getdate() @@ -1723,10 +2113,7 @@ def update_invoice_status(): payable_amount = ( frappe.qb.from_(payment_schedule) .select(Sum(payment_amount)) - .where( - (payment_schedule.parent == invoice.name) - & (payment_schedule.due_date < today) - ) + .where((payment_schedule.parent == invoice.name) & (payment_schedule.due_date < today)) ) total = ( @@ -1741,21 +2128,14 @@ def update_invoice_status(): .else_(invoice.base_rounded_total) ) - total_amount = ( - frappe.qb.terms.Case() - .when(consider_base_amount, base_total) - .else_(total) - ) + total_amount = frappe.qb.terms.Case().when(consider_base_amount, base_total).else_(total) is_overdue = total_amount - invoice.outstanding_amount < payable_amount conditions = ( (invoice.docstatus == 1) & (invoice.outstanding_amount > 0) - & ( - invoice.status.like("Unpaid%") - | invoice.status.like("Partly Paid%") - ) + & (invoice.status.like("Unpaid%") | invoice.status.like("Partly Paid%")) & ( ((invoice.is_pos & invoice.due_date < today) | is_overdue) if doctype == "Sales Invoice" @@ -1773,7 +2153,9 @@ def update_invoice_status(): @frappe.whitelist() -def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None): +def get_payment_terms( + terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None +): if not terms_template: return @@ -1781,14 +2163,18 @@ def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_ schedule = [] for d in terms_doc.get("terms"): - term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date) + term_details = get_payment_term_details( + d, posting_date, grand_total, base_grand_total, bill_date + ) schedule.append(term_details) return schedule @frappe.whitelist() -def get_payment_term_details(term, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None): +def get_payment_term_details( + term, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None +): term_details = frappe._dict() if isinstance(term, text_type): term = frappe.get_doc("Payment Term", term) @@ -1815,6 +2201,7 @@ def get_payment_term_details(term, posting_date=None, grand_total=None, base_gra return term_details + def get_due_date(term, posting_date=None, bill_date=None): due_date = None date = bill_date or posting_date @@ -1826,6 +2213,7 @@ def get_due_date(term, posting_date=None, bill_date=None): due_date = add_months(get_last_day(date), term.credit_months) return due_date + def get_discount_date(term, posting_date=None, bill_date=None): discount_validity = None date = bill_date or posting_date @@ -1837,64 +2225,73 @@ def get_discount_date(term, posting_date=None, bill_date=None): discount_validity = add_months(get_last_day(date), term.discount_validity) return discount_validity + def get_supplier_block_status(party_name): """ Returns a dict containing the values of `on_hold`, `release_date` and `hold_type` of a `Supplier` """ - supplier = frappe.get_doc('Supplier', party_name) + supplier = frappe.get_doc("Supplier", party_name) info = { - 'on_hold': supplier.on_hold, - 'release_date': supplier.release_date, - 'hold_type': supplier.hold_type + "on_hold": supplier.on_hold, + "release_date": supplier.release_date, + "hold_type": supplier.hold_type, } return info + def set_child_tax_template_and_map(item, child_item, parent_doc): args = { - 'item_code': item.item_code, - 'posting_date': parent_doc.transaction_date, - 'tax_category': parent_doc.get('tax_category'), - 'company': parent_doc.get('company') - } + "item_code": item.item_code, + "posting_date": parent_doc.transaction_date, + "tax_category": parent_doc.get("tax_category"), + "company": parent_doc.get("company"), + } child_item.item_tax_template = _get_item_tax_template(args, item.taxes) if child_item.get("item_tax_template"): - child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True) + child_item.item_tax_rate = get_item_tax_map( + parent_doc.get("company"), child_item.item_tax_template, as_json=True + ) + def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True): - add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template") + add_taxes_from_item_tax_template = frappe.db.get_single_value( + "Accounts Settings", "add_taxes_from_item_tax_template" + ) if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template: tax_map = json.loads(child_item.get("item_tax_rate")) for tax_type in tax_map: tax_rate = flt(tax_map[tax_type]) - taxes = parent_doc.get('taxes') or [] + taxes = parent_doc.get("taxes") or [] # add new row for tax head only if missing found = any(tax.account_head == tax_type for tax in taxes) if not found: tax_row = parent_doc.append("taxes", {}) - tax_row.update({ - "description" : str(tax_type).split(' - ')[0], - "charge_type" : "On Net Total", - "account_head" : tax_type, - "rate" : tax_rate - }) + tax_row.update( + { + "description": str(tax_type).split(" - ")[0], + "charge_type": "On Net Total", + "account_head": tax_type, + "rate": tax_rate, + } + ) if parent_doc.doctype == "Purchase Order": - tax_row.update({ - "category" : "Total", - "add_deduct_tax" : "Add" - }) + tax_row.update({"category": "Total", "add_deduct_tax": "Add"}) if db_insert: tax_row.db_insert() -def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item): + +def set_order_defaults( + parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item +): """ Returns a Sales/Purchase Order Item child item containing the default values """ p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) child_item = frappe.new_doc(child_doctype, p_doc, child_docname) - item = frappe.get_doc("Item", trans_item.get('item_code')) + item = frappe.get_doc("Item", trans_item.get("item_code")) for field in ("item_code", "item_name", "description", "item_group"): child_item.update({field: item.get(field)}) @@ -1904,8 +2301,10 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child child_item.stock_uom = item.stock_uom child_item.uom = trans_item.get("uom") or item.stock_uom child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) - conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) - child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor + conversion_factor = flt( + get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor") + ) + child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor if child_doctype == "Purchase Order Item": # Initialized value will update in parent validation @@ -1914,28 +2313,53 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child if child_doctype == "Sales Order Item": child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: - frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") - .format(frappe.bold("default warehouse"), frappe.bold(item.item_code))) + frappe.throw( + _("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.").format( + frappe.bold("default warehouse"), frappe.bold(item.item_code) + ) + ) set_child_tax_template_and_map(item, child_item, p_doc) add_taxes_from_tax_template(child_item, p_doc) return child_item + def validate_child_on_delete(row, parent): """Check if partially transacted item (row) is being deleted.""" if parent.doctype == "Sales Order": if flt(row.delivered_qty): - frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(row.idx, row.item_code)) + frappe.throw( + _("Row #{0}: Cannot delete item {1} which has already been delivered").format( + row.idx, row.item_code + ) + ) if flt(row.work_order_qty): - frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(row.idx, row.item_code)) + frappe.throw( + _("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format( + row.idx, row.item_code + ) + ) if flt(row.ordered_qty): - frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(row.idx, row.item_code)) + frappe.throw( + _("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format( + row.idx, row.item_code + ) + ) if parent.doctype == "Purchase Order" and flt(row.received_qty): - frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(row.idx, row.item_code)) + frappe.throw( + _("Row #{0}: Cannot delete item {1} which has already been received").format( + row.idx, row.item_code + ) + ) if flt(row.billed_amt): - frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(row.idx, row.item_code)) + frappe.throw( + _("Row #{0}: Cannot delete item {1} which has already been billed.").format( + row.idx, row.item_code + ) + ) + def update_bin_on_delete(row, doctype): """Update bin for deleted item (row).""" @@ -1945,6 +2369,7 @@ def update_bin_on_delete(row, doctype): get_reserved_qty, update_bin_qty, ) + qty_dict = {} if doctype == "Sales Order": @@ -1958,6 +2383,7 @@ def update_bin_on_delete(row, doctype): if row.warehouse: update_bin_qty(row.item_code, row.warehouse, qty_dict) + def validate_and_delete_children(parent, data): deleted_children = [] updated_item_names = [d.get("docname") for d in data] @@ -1980,14 +2406,18 @@ def validate_and_delete_children(parent, data): @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'): + def check_doc_permissions(doc, perm_type="create"): try: doc.check_permission(perm_type) except frappe.PermissionError: - actions = { 'create': 'add', 'write': 'update'} + actions = {"create": "add", "write": "update"} - frappe.throw(_("You do not have permissions to {} items in a {}.") - .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) + frappe.throw( + _("You do not have permissions to {} items in a {}.").format( + actions[perm_type], parent_doctype + ), + title=_("Insufficient Permissions"), + ) def validate_workflow_conditions(doc): workflow = get_workflow_name(doc.doctype) @@ -2007,13 +2437,17 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if not transitions: frappe.throw( - _("You are not allowed to update as per the conditions set in {} Workflow.").format(get_link_to_form("Workflow", workflow)), - title=_("Insufficient Permissions") + _("You are not allowed to update as per the conditions set in {} Workflow.").format( + get_link_to_form("Workflow", workflow) + ), + title=_("Insufficient Permissions"), ) def get_new_child_item(item_row): child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" - return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row) + return set_order_defaults( + parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row + ) def validate_quantity(child_item, d): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): @@ -2024,10 +2458,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil data = json.loads(trans_items) - sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] + sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"] parent = frappe.get_doc(parent_doctype, parent_doctype_name) - check_doc_permissions(parent, 'write') + check_doc_permissions(parent, "write") validate_and_delete_children(parent, data) for d in data: @@ -2039,28 +2473,38 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if not d.get("docname"): new_child_flag = True - check_doc_permissions(parent, 'create') + check_doc_permissions(parent, "create") child_item = get_new_child_item(d) else: - check_doc_permissions(parent, 'write') - child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) + check_doc_permissions(parent, "write") + child_item = frappe.get_doc(parent_doctype + " Item", d.get("docname")) prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty")) - prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(d.get("conversion_factor")) + prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt( + d.get("conversion_factor") + ) prev_uom, new_uom = child_item.get("uom"), d.get("uom") - if parent_doctype == 'Sales Order': + if parent_doctype == "Sales Order": prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date") - elif parent_doctype == 'Purchase Order': + elif parent_doctype == "Purchase Order": prev_date, new_date = child_item.get("schedule_date"), d.get("schedule_date") rate_unchanged = prev_rate == new_rate qty_unchanged = prev_qty == new_qty uom_unchanged = prev_uom == new_uom conversion_factor_unchanged = prev_con_fac == new_con_fac - date_unchanged = prev_date == getdate(new_date) if prev_date and new_date else False # in case of delivery note etc - if rate_unchanged and qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged: + date_unchanged = ( + prev_date == getdate(new_date) if prev_date and new_date else False + ) # in case of delivery note etc + if ( + rate_unchanged + and qty_unchanged + and conversion_factor_unchanged + and uom_unchanged + and date_unchanged + ): continue validate_quantity(child_item, d) @@ -2070,9 +2514,14 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil conv_fac_precision = child_item.precision("conversion_factor") or 2 qty_precision = child_item.precision("qty") or 2 - if flt(child_item.billed_amt, rate_precision) > flt(flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision): - frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.") - .format(child_item.idx, child_item.item_code)) + if flt(child_item.billed_amt, rate_precision) > flt( + flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision + ): + frappe.throw( + _("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format( + child_item.idx, child_item.item_code + ) + ) else: child_item.rate = flt(d.get("rate"), rate_precision) @@ -2080,18 +2529,22 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if child_item.stock_uom == child_item.uom: child_item.conversion_factor = 1 else: - child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision) + child_item.conversion_factor = flt(d.get("conversion_factor"), conv_fac_precision) if d.get("uom"): child_item.uom = d.get("uom") - conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")) - child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision) or conversion_factor + conversion_factor = flt( + get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor") + ) + child_item.conversion_factor = ( + flt(d.get("conversion_factor"), conv_fac_precision) or conversion_factor + ) - if d.get("delivery_date") and parent_doctype == 'Sales Order': - child_item.delivery_date = d.get('delivery_date') + if d.get("delivery_date") and parent_doctype == "Sales Order": + child_item.delivery_date = d.get("delivery_date") - if d.get("schedule_date") and parent_doctype == 'Purchase Order': - child_item.schedule_date = d.get('schedule_date') + if d.get("schedule_date") and parent_doctype == "Purchase Order": + child_item.schedule_date = d.get("schedule_date") if flt(child_item.price_list_rate): if flt(child_item.rate) > flt(child_item.price_list_rate): @@ -2101,14 +2554,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if parent_doctype in sales_doctypes: child_item.margin_type = "Amount" - child_item.margin_rate_or_amount = flt(child_item.rate - child_item.price_list_rate, - child_item.precision("margin_rate_or_amount")) + child_item.margin_rate_or_amount = flt( + child_item.rate - child_item.price_list_rate, child_item.precision("margin_rate_or_amount") + ) child_item.rate_with_margin = child_item.rate else: - child_item.discount_percentage = flt((1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0, - child_item.precision("discount_percentage")) - child_item.discount_amount = flt( - child_item.price_list_rate) - flt(child_item.rate) + child_item.discount_percentage = flt( + (1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0, + child_item.precision("discount_percentage"), + ) + child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate) if parent_doctype in sales_doctypes: child_item.margin_type = "" @@ -2131,11 +2586,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if parent_doctype == "Sales Order": make_packing_list(parent) parent.set_gross_profit() - frappe.get_doc('Authorization Control').validate_approving_authority(parent.doctype, - parent.company, parent.base_grand_total) + frappe.get_doc("Authorization Control").validate_approving_authority( + parent.doctype, parent.company, parent.base_grand_total + ) parent.set_payment_schedule() - if parent_doctype == 'Purchase Order': + if parent_doctype == "Purchase Order": parent.validate_minimum_order_qty() parent.validate_budget() if parent.is_against_so(): @@ -2149,8 +2605,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.save() - if parent_doctype == 'Purchase Order': - update_last_purchase_rate(parent, is_submit = 1) + if parent_doctype == "Purchase Order": + update_last_purchase_rate(parent, is_submit=1) parent.update_prevdoc_status() parent.update_requested_qty() parent.update_ordered_qty() @@ -2163,7 +2619,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil else: parent.update_reserved_qty() parent.update_project() - parent.update_prevdoc_status('submit') + parent.update_prevdoc_status("submit") parent.update_delivery_status() parent.reload() @@ -2173,10 +2629,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_billing_percentage() parent.set_status() + @erpnext.allow_regional def validate_regional(doc): pass + @erpnext.allow_regional def validate_einvoice_fields(doc): pass diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index e5f35e1b72e..8e644814a54 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -20,12 +20,14 @@ from erpnext.stock.utils import get_incoming_rate class QtyMismatchError(ValidationError): pass + class BuyingController(StockController, Subcontracting): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] def get_feed(self): if self.get("supplier_name"): - return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, - self.grand_total) + return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total) def validate(self): super(BuyingController, self).validate() @@ -40,16 +42,18 @@ class BuyingController(StockController, Subcontracting): self.set_supplier_address() self.validate_asset_return() - if self.doctype=="Purchase Invoice": + if self.doctype == "Purchase Invoice": self.validate_purchase_receipt_if_update_stock() - if self.doctype=="Purchase Receipt" or (self.doctype=="Purchase Invoice" and self.update_stock): + if self.doctype == "Purchase Receipt" or ( + self.doctype == "Purchase Invoice" and self.update_stock + ): # self.validate_purchase_return() self.validate_rejected_warehouse() self.validate_accepted_rejected_qty() validate_for_items(self) - #sub-contracting + # sub-contracting self.validate_for_subcontracting() self.create_raw_materials_supplied("supplied_items") self.set_landed_cost_voucher_amount() @@ -59,8 +63,12 @@ class BuyingController(StockController, Subcontracting): def onload(self): super(BuyingController, self).onload() - self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings', - 'backflush_raw_materials_of_subcontract_based_on')) + self.set_onload( + "backflush_based_on", + frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ), + ) def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) @@ -77,9 +85,9 @@ class BuyingController(StockController, Subcontracting): doctype=self.doctype, company=self.company, party_address=self.get("supplier_address"), - shipping_address=self.get('shipping_address'), - fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'), - ignore_permissions=self.flags.ignore_permissions + shipping_address=self.get("shipping_address"), + fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"), + ignore_permissions=self.flags.ignore_permissions, ) ) @@ -88,14 +96,16 @@ class BuyingController(StockController, Subcontracting): def set_supplier_from_item_default(self): if self.meta.get_field("supplier") and not self.supplier: for d in self.get("items"): - supplier = frappe.db.get_value("Item Default", - {"parent": d.item_code, "company": self.company}, "default_supplier") + supplier = frappe.db.get_value( + "Item Default", {"parent": d.item_code, "company": self.company}, "default_supplier" + ) if supplier: self.supplier = supplier else: item_group = frappe.db.get_value("Item", d.item_code, "item_group") - supplier = frappe.db.get_value("Item Default", - {"parent": item_group, "company": self.company}, "default_supplier") + supplier = frappe.db.get_value( + "Item Default", {"parent": item_group, "company": self.company}, "default_supplier" + ) if supplier: self.supplier = supplier break @@ -106,55 +116,71 @@ class BuyingController(StockController, Subcontracting): self.update_tax_category(msg) def update_tax_category(self, msg): - tax_for_valuation = [d for d in self.get("taxes") - if d.category in ["Valuation", "Valuation and Total"]] + tax_for_valuation = [ + d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"] + ] if tax_for_valuation: for d in tax_for_valuation: - d.category = 'Total' + d.category = "Total" msgprint(msg) def validate_asset_return(self): - if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return: + if self.doctype not in ["Purchase Receipt", "Purchase Invoice"] or not self.is_return: return - purchase_doc_field = 'purchase_receipt' if self.doctype == 'Purchase Receipt' else 'purchase_invoice' - not_cancelled_asset = [d.name for d in frappe.db.get_all("Asset", { - purchase_doc_field: self.return_against, - "docstatus": 1 - })] + purchase_doc_field = ( + "purchase_receipt" if self.doctype == "Purchase Receipt" else "purchase_invoice" + ) + not_cancelled_asset = [ + d.name + for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1}) + ] if self.is_return and len(not_cancelled_asset): - frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.") - .format(self.return_against), title=_("Not Allowed")) + frappe.throw( + _( + "{} has submitted assets linked to it. You need to cancel the assets to create purchase return." + ).format(self.return_against), + title=_("Not Allowed"), + ) def get_asset_items(self): - if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']: + if self.doctype not in ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]: return [] return [d.item_code for d in self.items if d.is_fixed_asset] def set_landed_cost_voucher_amount(self): for d in self.get("items"): - lc_voucher_data = frappe.db.sql("""select sum(applicable_charges), cost_center + lc_voucher_data = frappe.db.sql( + """select sum(applicable_charges), cost_center from `tabLanded Cost Item` - where docstatus = 1 and purchase_receipt_item = %s""", d.name) + where docstatus = 1 and purchase_receipt_item = %s""", + d.name, + ) d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0 if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: - d.db_set('cost_center', lc_voucher_data[0][1]) + d.db_set("cost_center", lc_voucher_data[0][1]) def validate_from_warehouse(self): - for item in self.get('items'): - if item.get('from_warehouse') and (item.get('from_warehouse') == item.get('warehouse')): - frappe.throw(_("Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same").format(item.idx)) + for item in self.get("items"): + if item.get("from_warehouse") and (item.get("from_warehouse") == item.get("warehouse")): + frappe.throw( + _("Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same").format(item.idx) + ) - if item.get('from_warehouse') and self.get('is_subcontracted') == 'Yes': - frappe.throw(_("Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor").format(item.idx)) + if item.get("from_warehouse") and self.get("is_subcontracted") == "Yes": + frappe.throw( + _( + "Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor" + ).format(item.idx) + ) def set_supplier_address(self): address_dict = { - 'supplier_address': 'address_display', - 'shipping_address': 'shipping_address_display' + "supplier_address": "address_display", + "shipping_address": "shipping_address_display", } for address_field, address_display_field in address_dict.items(): @@ -163,6 +189,7 @@ class BuyingController(StockController, Subcontracting): def set_total_in_words(self): from frappe.utils import money_in_words + if self.meta.get_field("base_in_words"): if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled(): amount = self.base_rounded_total @@ -181,10 +208,10 @@ class BuyingController(StockController, Subcontracting): # update valuation rate def update_valuation_rate(self, reset_outgoing_rate=True): """ - item_tax_amount is the total tax amount applied on that item - stored for valuation + item_tax_amount is the total tax amount applied on that item + stored for valuation - TODO: rename item_tax_amount to valuation_tax_amount + TODO: rename item_tax_amount to valuation_tax_amount """ stock_and_asset_items = [] stock_and_asset_items = self.get_stock_items() + self.get_asset_items() @@ -192,36 +219,50 @@ class BuyingController(StockController, Subcontracting): stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 last_item_idx = 1 for d in self.get("items"): - if (d.item_code and d.item_code in stock_and_asset_items): + if d.item_code and d.item_code in stock_and_asset_items: stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_amount += flt(d.base_net_amount) last_item_idx = d.idx - total_valuation_amount = sum(flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes") - if d.category in ["Valuation", "Valuation and Total"]) + total_valuation_amount = sum( + flt(d.base_tax_amount_after_discount_amount) + for d in self.get("taxes") + if d.category in ["Valuation", "Valuation and Total"] + ) valuation_amount_adjustment = total_valuation_amount for i, item in enumerate(self.get("items")): if item.item_code and item.qty and item.item_code in stock_and_asset_items: - item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \ + item_proportion = ( + flt(item.base_net_amount) / stock_and_asset_items_amount + if stock_and_asset_items_amount else flt(item.qty) / stock_and_asset_items_qty + ) if i == (last_item_idx - 1): - item.item_tax_amount = flt(valuation_amount_adjustment, - self.precision("item_tax_amount", item)) + item.item_tax_amount = flt( + valuation_amount_adjustment, self.precision("item_tax_amount", item) + ) else: - item.item_tax_amount = flt(item_proportion * total_valuation_amount, - self.precision("item_tax_amount", item)) + item.item_tax_amount = flt( + item_proportion * total_valuation_amount, self.precision("item_tax_amount", item) + ) valuation_amount_adjustment -= item.item_tax_amount self.round_floats_in(item) - if flt(item.conversion_factor)==0.0: - item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 + if flt(item.conversion_factor) == 0.0: + item.conversion_factor = ( + get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 + ) qty_in_stock_uom = flt(item.qty * item.conversion_factor) item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) - item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost - + flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom) + item.valuation_rate = ( + item.base_net_amount + + item.item_tax_amount + + item.rm_supp_cost + + flt(item.landed_cost_voucher_amount) + ) / qty_in_stock_uom else: item.valuation_rate = 0.0 @@ -242,44 +283,53 @@ class BuyingController(StockController, Subcontracting): # Get outgoing rate based on original item cost based on valuation method if not d.get(frappe.scrub(ref_doctype)): - outgoing_rate = get_incoming_rate({ - "item_code": d.item_code, - "warehouse": d.get('from_warehouse'), - "posting_date": self.get('posting_date') or self.get('transation_date'), - "posting_time": self.get('posting_time'), - "qty": -1 * flt(d.get('stock_qty')), - "serial_no": d.get('serial_no'), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - "allow_zero_valuation": d.get("allow_zero_valuation") - }, raise_error_if_no_rate=False) + outgoing_rate = get_incoming_rate( + { + "item_code": d.item_code, + "warehouse": d.get("from_warehouse"), + "posting_date": self.get("posting_date") or self.get("transation_date"), + "posting_time": self.get("posting_time"), + "qty": -1 * flt(d.get("stock_qty")), + "serial_no": d.get("serial_no"), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation"), + }, + raise_error_if_no_rate=False, + ) - rate = flt(outgoing_rate * d.conversion_factor, d.precision('rate')) + rate = flt(outgoing_rate * d.conversion_factor, d.precision("rate")) else: - rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), 'rate') + rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), "rate") if self.is_internal_transfer(): if rate != d.rate: d.rate = rate d.discount_percentage = 0 d.discount_amount = 0 - frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") - .format(d.idx), alert=1) + frappe.msgprint( + _( + "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" + ).format(d.idx), + alert=1, + ) def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): supplied_items_cost = 0.0 for d in self.get("supplied_items"): if d.reference_name == item_row_id: - if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'): - rate = get_incoming_rate({ - "item_code": d.rm_item_code, - "warehouse": self.supplier_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1 * d.consumed_qty, - "serial_no": d.serial_no - }) + if reset_outgoing_rate and frappe.get_cached_value("Item", d.rm_item_code, "is_stock_item"): + rate = get_incoming_rate( + { + "item_code": d.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * d.consumed_qty, + "serial_no": d.serial_no, + } + ) if rate > 0: d.rate = rate @@ -314,7 +364,7 @@ class BuyingController(StockController, Subcontracting): item.bom = None def create_raw_materials_supplied(self, raw_material_table): - if self.is_subcontracted=="Yes": + if self.is_subcontracted == "Yes": self.set_materials_for_subcontracted_items(raw_material_table) elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: @@ -322,19 +372,17 @@ class BuyingController(StockController, Subcontracting): item.rm_supp_cost = 0.0 if self.is_subcontracted == "No" and self.get("supplied_items"): - self.set('supplied_items', []) + self.set("supplied_items", []) @property def sub_contracted_items(self): if not hasattr(self, "_sub_contracted_items"): self._sub_contracted_items = [] - item_codes = list(set(item.item_code for item in - self.get("items"))) + item_codes = list(set(item.item_code for item in self.get("items"))) if item_codes: - items = frappe.get_all('Item', filters={ - 'name': ['in', item_codes], - 'is_sub_contracted_item': 1 - }) + items = frappe.get_all( + "Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1} + ) self._sub_contracted_items = [item.name for item in items] return self._sub_contracted_items @@ -348,9 +396,11 @@ class BuyingController(StockController, Subcontracting): frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx)) d.stock_qty = flt(d.qty) * flt(d.conversion_factor) - if self.doctype=="Purchase Receipt" and d.meta.get_field("received_stock_qty"): + if self.doctype == "Purchase Receipt" and d.meta.get_field("received_stock_qty"): # Set Received Qty in Stock UOM - d.received_stock_qty = flt(d.received_qty) * flt(d.conversion_factor, d.precision("conversion_factor")) + d.received_stock_qty = flt(d.received_qty) * flt( + d.conversion_factor, d.precision("conversion_factor") + ) def validate_purchase_return(self): for d in self.get("items"): @@ -366,20 +416,26 @@ class BuyingController(StockController, Subcontracting): d.rejected_warehouse = self.rejected_warehouse if not d.rejected_warehouse: - frappe.throw(_("Row #{0}: Rejected Warehouse is mandatory against rejected Item {1}").format(d.idx, d.item_code)) + frappe.throw( + _("Row #{0}: Rejected Warehouse is mandatory against rejected Item {1}").format( + d.idx, d.item_code + ) + ) # validate accepted and rejected qty def validate_accepted_rejected_qty(self): for d in self.get("items"): - self.validate_negative_quantity(d, ["received_qty","qty", "rejected_qty"]) + self.validate_negative_quantity(d, ["received_qty", "qty", "rejected_qty"]) if not flt(d.received_qty) and (flt(d.qty) or flt(d.rejected_qty)): d.received_qty = flt(d.qty) + flt(d.rejected_qty) # Check Received Qty = Accepted Qty + Rejected Qty val = flt(d.qty) + flt(d.rejected_qty) - if (flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty"))): - message = _("Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1}").format(d.idx, d.item_code) + if flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty")): + message = _( + "Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1}" + ).format(d.idx, d.item_code) frappe.throw(msg=message, title=_("Mismatch"), exc=QtyMismatchError) def validate_negative_quantity(self, item_row, field_list): @@ -389,15 +445,20 @@ class BuyingController(StockController, Subcontracting): item_row = item_row.as_dict() for fieldname in field_list: if flt(item_row[fieldname]) < 0: - frappe.throw(_("Row #{0}: {1} can not be negative for item {2}").format(item_row['idx'], - frappe.get_meta(item_row.doctype).get_label(fieldname), item_row['item_code'])) + frappe.throw( + _("Row #{0}: {1} can not be negative for item {2}").format( + item_row["idx"], + frappe.get_meta(item_row.doctype).get_label(fieldname), + item_row["item_code"], + ) + ) def check_for_on_hold_or_closed_status(self, ref_doctype, ref_fieldname): for d in self.get("items"): if d.get(ref_fieldname): status = frappe.db.get_value(ref_doctype, d.get(ref_fieldname), "status") if status in ("Closed", "On Hold"): - frappe.throw(_("{0} {1} is {2}").format(ref_doctype,d.get(ref_fieldname), status)) + frappe.throw(_("{0} {1} is {2}").format(ref_doctype, d.get(ref_fieldname), status)) def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False): self.update_ordered_and_reserved_qty() @@ -405,76 +466,92 @@ class BuyingController(StockController, Subcontracting): sl_entries = [] stock_items = self.get_stock_items() - for d in self.get('items'): - if d.item_code in stock_items and d.warehouse: + for d in self.get("items"): + if d.item_code not in stock_items: + continue + + if d.warehouse: pr_qty = flt(d.qty) * flt(d.conversion_factor) if pr_qty: - if d.from_warehouse and ((not cint(self.is_return) and self.docstatus==1) - or (cint(self.is_return) and self.docstatus==2)): - from_warehouse_sle = self.get_sl_entries(d, { - "actual_qty": -1 * pr_qty, - "warehouse": d.from_warehouse, - "outgoing_rate": d.rate, - "recalculate_rate": 1, - "dependant_sle_voucher_detail_no": d.name - }) + if d.from_warehouse and ( + (not cint(self.is_return) and self.docstatus == 1) + or (cint(self.is_return) and self.docstatus == 2) + ): + from_warehouse_sle = self.get_sl_entries( + d, + { + "actual_qty": -1 * pr_qty, + "warehouse": d.from_warehouse, + "outgoing_rate": d.rate, + "recalculate_rate": 1, + "dependant_sle_voucher_detail_no": d.name, + }, + ) sl_entries.append(from_warehouse_sle) - sle = self.get_sl_entries(d, { - "actual_qty": flt(pr_qty), - "serial_no": cstr(d.serial_no).strip() - }) - if self.is_return: - outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d) + sle = self.get_sl_entries( + d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()} + ) - sle.update({ - "outgoing_rate": outgoing_rate, - "recalculate_rate": 1 - }) + if self.is_return: + outgoing_rate = get_rate_for_return( + self.doctype, self.name, d.item_code, self.return_against, item_row=d + ) + + sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1}) if d.from_warehouse: sle.dependant_sle_voucher_detail_no = d.name else: val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9 incoming_rate = flt(d.valuation_rate, val_rate_db_precision) - sle.update({ - "incoming_rate": incoming_rate, - "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0 - }) + sle.update( + { + "incoming_rate": incoming_rate, + "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0, + } + ) sl_entries.append(sle) - if d.from_warehouse and ((not cint(self.is_return) and self.docstatus==2) - or (cint(self.is_return) and self.docstatus==1)): - from_warehouse_sle = self.get_sl_entries(d, { - "actual_qty": -1 * pr_qty, - "warehouse": d.from_warehouse, - "recalculate_rate": 1 - }) + if d.from_warehouse and ( + (not cint(self.is_return) and self.docstatus == 2) + or (cint(self.is_return) and self.docstatus == 1) + ): + from_warehouse_sle = self.get_sl_entries( + d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1} + ) sl_entries.append(from_warehouse_sle) - if flt(d.rejected_qty) != 0: - sl_entries.append(self.get_sl_entries(d, { - "warehouse": d.rejected_warehouse, - "actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor), - "serial_no": cstr(d.rejected_serial_no).strip(), - "incoming_rate": 0.0 - })) + if flt(d.rejected_qty) != 0: + sl_entries.append( + self.get_sl_entries( + d, + { + "warehouse": d.rejected_warehouse, + "actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor), + "serial_no": cstr(d.rejected_serial_no).strip(), + "incoming_rate": 0.0, + }, + ) + ) self.make_sl_entries_for_supplier_warehouse(sl_entries) - self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock, - via_landed_cost_voucher=via_landed_cost_voucher) + self.make_sl_entries( + sl_entries, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) def update_ordered_and_reserved_qty(self): po_map = {} for d in self.get("items"): - if self.doctype=="Purchase Receipt" \ - and d.purchase_order: - po_map.setdefault(d.purchase_order, []).append(d.purchase_order_item) + if self.doctype == "Purchase Receipt" and d.purchase_order: + po_map.setdefault(d.purchase_order, []).append(d.purchase_order_item) - elif self.doctype=="Purchase Invoice" and d.purchase_order and d.po_detail: + elif self.doctype == "Purchase Invoice" and d.purchase_order and d.po_detail: po_map.setdefault(d.purchase_order, []).append(d.po_detail) for po, po_item_rows in po_map.items(): @@ -482,68 +559,78 @@ class BuyingController(StockController, Subcontracting): po_obj = frappe.get_doc("Purchase Order", po) if po_obj.status in ["Closed", "Cancelled"]: - frappe.throw(_("{0} {1} is cancelled or closed").format(_("Purchase Order"), po), - frappe.InvalidStatusError) + frappe.throw( + _("{0} {1} is cancelled or closed").format(_("Purchase Order"), po), + frappe.InvalidStatusError, + ) po_obj.update_ordered_qty(po_item_rows) if self.is_subcontracted: po_obj.update_reserved_qty_for_subcontract() def make_sl_entries_for_supplier_warehouse(self, sl_entries): - if hasattr(self, 'supplied_items'): - for d in self.get('supplied_items'): + if hasattr(self, "supplied_items"): + for d in self.get("supplied_items"): # negative quantity is passed, as raw material qty has to be decreased # when PR is submitted and it has to be increased when PR is cancelled - sl_entries.append(self.get_sl_entries(d, { - "item_code": d.rm_item_code, - "warehouse": self.supplier_warehouse, - "actual_qty": -1*flt(d.consumed_qty), - "dependant_sle_voucher_detail_no": d.reference_name - })) + sl_entries.append( + self.get_sl_entries( + d, + { + "item_code": d.rm_item_code, + "warehouse": self.supplier_warehouse, + "actual_qty": -1 * flt(d.consumed_qty), + "dependant_sle_voucher_detail_no": d.reference_name, + }, + ) + ) def on_submit(self): - if self.get('is_return'): + if self.get("is_return"): return - if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: - field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' + if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: + field = "purchase_invoice" if self.doctype == "Purchase Invoice" else "purchase_receipt" self.process_fixed_asset() self.update_fixed_asset(field) - if self.doctype in ['Purchase Order', 'Purchase Receipt']: - update_last_purchase_rate(self, is_submit = 1) + if self.doctype in ["Purchase Order", "Purchase Receipt"]: + update_last_purchase_rate(self, is_submit=1) def on_cancel(self): super(BuyingController, self).on_cancel() - if self.get('is_return'): + if self.get("is_return"): return - if self.doctype in ['Purchase Order', 'Purchase Receipt']: - update_last_purchase_rate(self, is_submit = 0) + if self.doctype in ["Purchase Order", "Purchase Receipt"]: + update_last_purchase_rate(self, is_submit=0) - if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: - field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' + if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: + field = "purchase_invoice" if self.doctype == "Purchase Invoice" else "purchase_receipt" self.delete_linked_asset() self.update_fixed_asset(field, delete_asset=True) def validate_budget(self): if self.docstatus == 1: - for data in self.get('items'): + for data in self.get("items"): args = data.as_dict() - args.update({ - 'doctype': self.doctype, - 'company': self.company, - 'posting_date': (self.schedule_date - if self.doctype == 'Material Request' else self.transaction_date) - }) + args.update( + { + "doctype": self.doctype, + "company": self.company, + "posting_date": ( + self.schedule_date if self.doctype == "Material Request" else self.transaction_date + ), + } + ) validate_expense_against_budget(args) def process_fixed_asset(self): - if self.doctype == 'Purchase Invoice' and not self.update_stock: + if self.doctype == "Purchase Invoice" and not self.update_stock: return asset_items = self.get_asset_items() @@ -558,10 +645,10 @@ class BuyingController(StockController, Subcontracting): if d.is_fixed_asset: item_data = items_data.get(d.item_code) - if item_data.get('auto_create_assets'): + if item_data.get("auto_create_assets"): # If asset has to be auto created # Check for asset naming series - if item_data.get('asset_naming_series'): + if item_data.get("asset_naming_series"): created_assets = [] for qty in range(cint(d.qty)): @@ -570,21 +657,31 @@ class BuyingController(StockController, Subcontracting): if len(created_assets) > 5: # dont show asset form links if more than 5 assets are created - messages.append(_('{} Assets created for {}').format(len(created_assets), frappe.bold(d.item_code))) - else: - assets_link = list(map(lambda d: frappe.utils.get_link_to_form('Asset', d), created_assets)) - assets_link = frappe.bold(','.join(assets_link)) - - is_plural = 's' if len(created_assets) != 1 else '' messages.append( - _('Asset{} {assets_link} created for {}').format(is_plural, frappe.bold(d.item_code), assets_link=assets_link) + _("{} Assets created for {}").format(len(created_assets), frappe.bold(d.item_code)) + ) + else: + assets_link = list(map(lambda d: frappe.utils.get_link_to_form("Asset", d), created_assets)) + assets_link = frappe.bold(",".join(assets_link)) + + is_plural = "s" if len(created_assets) != 1 else "" + messages.append( + _("Asset{} {assets_link} created for {}").format( + is_plural, frappe.bold(d.item_code), assets_link=assets_link + ) ) else: - frappe.throw(_("Row {}: Asset Naming Series is mandatory for the auto creation for item {}") - .format(d.idx, frappe.bold(d.item_code))) + frappe.throw( + _("Row {}: Asset Naming Series is mandatory for the auto creation for item {}").format( + d.idx, frappe.bold(d.item_code) + ) + ) else: - messages.append(_("Assets not created for {0}. You will have to create asset manually.") - .format(frappe.bold(d.item_code))) + messages.append( + _("Assets not created for {0}. You will have to create asset manually.").format( + frappe.bold(d.item_code) + ) + ) for message in messages: frappe.msgprint(message, title="Success", indicator="green") @@ -593,26 +690,29 @@ class BuyingController(StockController, Subcontracting): if not row.asset_location: frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code)) - item_data = frappe.db.get_value('Item', - row.item_code, ['asset_naming_series', 'asset_category'], as_dict=1) + item_data = frappe.db.get_value( + "Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1 + ) purchase_amount = flt(row.base_rate + row.item_tax_amount) - asset = frappe.get_doc({ - 'doctype': 'Asset', - 'item_code': row.item_code, - 'asset_name': row.item_name, - 'naming_series': item_data.get('asset_naming_series') or 'AST', - 'asset_category': item_data.get('asset_category'), - 'location': row.asset_location, - 'company': self.company, - 'supplier': self.supplier, - 'purchase_date': self.posting_date, - 'calculate_depreciation': 1, - 'purchase_receipt_amount': purchase_amount, - 'gross_purchase_amount': purchase_amount, - 'purchase_receipt': self.name if self.doctype == 'Purchase Receipt' else None, - 'purchase_invoice': self.name if self.doctype == 'Purchase Invoice' else None - }) + asset = frappe.get_doc( + { + "doctype": "Asset", + "item_code": row.item_code, + "asset_name": row.item_name, + "naming_series": item_data.get("asset_naming_series") or "AST", + "asset_category": item_data.get("asset_category"), + "location": row.asset_location, + "company": self.company, + "supplier": self.supplier, + "purchase_date": self.posting_date, + "calculate_depreciation": 1, + "purchase_receipt_amount": purchase_amount, + "gross_purchase_amount": purchase_amount, + "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, + "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, + } + ) asset.flags.ignore_validate = True asset.flags.ignore_mandatory = True @@ -621,22 +721,25 @@ class BuyingController(StockController, Subcontracting): return asset.name - def update_fixed_asset(self, field, delete_asset = False): + def update_fixed_asset(self, field, delete_asset=False): for d in self.get("items"): if d.is_fixed_asset: - is_auto_create_enabled = frappe.db.get_value('Item', d.item_code, 'auto_create_assets') - assets = frappe.db.get_all('Asset', filters={ field : self.name, 'item_code' : d.item_code }) + is_auto_create_enabled = frappe.db.get_value("Item", d.item_code, "auto_create_assets") + assets = frappe.db.get_all("Asset", filters={field: self.name, "item_code": d.item_code}) for asset in assets: - asset = frappe.get_doc('Asset', asset.name) + asset = frappe.get_doc("Asset", asset.name) if delete_asset and is_auto_create_enabled: # need to delete movements to delete assets otherwise throws link exists error movements = frappe.db.sql( """SELECT asm.name FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item - WHERE asm_item.parent=asm.name and asm_item.asset=%s""", asset.name, as_dict=1) + WHERE asm_item.parent=asm.name and asm_item.asset=%s""", + asset.name, + as_dict=1, + ) for movement in movements: - frappe.delete_doc('Asset Movement', movement.name, force=1) + frappe.delete_doc("Asset Movement", movement.name, force=1) frappe.delete_doc("Asset", asset.name, force=1) continue @@ -649,8 +752,11 @@ class BuyingController(StockController, Subcontracting): asset.set(field, None) asset.supplier = None if asset.docstatus == 1 and delete_asset: - frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue.') - .format(frappe.utils.get_link_to_form('Asset', asset.name))) + frappe.throw( + _( + "Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue." + ).format(frappe.utils.get_link_to_form("Asset", asset.name)) + ) asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_mandatory = True @@ -660,7 +766,7 @@ class BuyingController(StockController, Subcontracting): asset.save() def delete_linked_asset(self): - if self.doctype == 'Purchase Invoice' and not self.get('update_stock'): + if self.doctype == "Purchase Invoice" and not self.get("update_stock"): return frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name) @@ -671,37 +777,47 @@ class BuyingController(StockController, Subcontracting): if any(d.schedule_date for d in self.get("items")): # Select earliest schedule_date. - self.schedule_date = min(d.schedule_date for d in self.get("items") - if d.schedule_date is not None) + self.schedule_date = min( + d.schedule_date for d in self.get("items") if d.schedule_date is not None + ) if self.schedule_date: - for d in self.get('items'): + for d in self.get("items"): if not d.schedule_date: d.schedule_date = self.schedule_date - if (d.schedule_date and self.transaction_date and - getdate(d.schedule_date) < getdate(self.transaction_date)): + if ( + d.schedule_date + and self.transaction_date + and getdate(d.schedule_date) < getdate(self.transaction_date) + ): frappe.throw(_("Row #{0}: Reqd by Date cannot be before Transaction Date").format(d.idx)) else: frappe.throw(_("Please enter Reqd by Date")) def validate_items(self): # validate items to see if they have is_purchase_item or is_subcontracted_item enabled - if self.doctype=="Material Request": return + if self.doctype == "Material Request": + return - if hasattr(self, "is_subcontracted") and self.is_subcontracted == 'Yes': + if hasattr(self, "is_subcontracted") and self.is_subcontracted == "Yes": validate_item_type(self, "is_sub_contracted_item", "subcontracted") else: validate_item_type(self, "is_purchase_item", "purchase") + def get_asset_item_details(asset_items): asset_items_data = {} - for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], - filters = {'name': ('in', asset_items)}): + for d in frappe.get_all( + "Item", + fields=["name", "auto_create_assets", "asset_naming_series"], + filters={"name": ("in", asset_items)}, + ): asset_items_data.setdefault(d.name, d) return asset_items_data + def validate_item_type(doc, fieldname, message): # iterate through items and check if they are valid sales or purchase items items = [d.item_code for d in doc.items if d.item_code] @@ -712,16 +828,28 @@ def validate_item_type(doc, fieldname, message): item_list = ", ".join(["%s" % frappe.db.escape(d) for d in items]) - invalid_items = [d[0] for d in frappe.db.sql(""" + invalid_items = [ + d[0] + for d in frappe.db.sql( + """ select item_code from tabItem where name in ({0}) and {1}=0 - """.format(item_list, fieldname), as_list=True)] + """.format( + item_list, fieldname + ), + as_list=True, + ) + ] if invalid_items: items = ", ".join([d for d in invalid_items]) if len(invalid_items) > 1: - error_message = _("Following items {0} are not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message) + error_message = _( + "Following items {0} are not marked as {1} item. You can enable them as {1} item from its Item master" + ).format(items, message) else: - error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message) + error_message = _( + "Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master" + ).format(items, message) frappe.throw(error_message) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index fb405ff81aa..c26f815b30b 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -11,24 +11,30 @@ from frappe.utils import cstr, flt from six import string_types -class ItemVariantExistsError(frappe.ValidationError): pass -class InvalidItemAttributeValueError(frappe.ValidationError): pass -class ItemTemplateCannotHaveStock(frappe.ValidationError): pass +class ItemVariantExistsError(frappe.ValidationError): + pass + + +class InvalidItemAttributeValueError(frappe.ValidationError): + pass + + +class ItemTemplateCannotHaveStock(frappe.ValidationError): + pass + @frappe.whitelist() -def get_variant(template, args=None, variant=None, manufacturer=None, - manufacturer_part_no=None): +def get_variant(template, args=None, variant=None, manufacturer=None, manufacturer_part_no=None): """Validates Attributes and their Values, then looks for an exactly - matching Item Variant + matching Item Variant - :param item: Template Item - :param args: A dictionary with "Attribute" as key and "Attribute Value" as value + :param item: Template Item + :param args: A dictionary with "Attribute" as key and "Attribute Value" as value """ - item_template = frappe.get_doc('Item', template) + item_template = frappe.get_doc("Item", template) - if item_template.variant_based_on=='Manufacturer' and manufacturer: - return make_variant_based_on_manufacturer(item_template, manufacturer, - manufacturer_part_no) + if item_template.variant_based_on == "Manufacturer" and manufacturer: + return make_variant_based_on_manufacturer(item_template, manufacturer, manufacturer_part_no) else: if isinstance(args, string_types): args = json.loads(args) @@ -37,28 +43,30 @@ def get_variant(template, args=None, variant=None, manufacturer=None, frappe.throw(_("Please specify at least one attribute in the Attributes table")) return find_variant(template, args, variant) + def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part_no): - '''Make and return a new variant based on manufacturer and - manufacturer part no''' + """Make and return a new variant based on manufacturer and + manufacturer part no""" from frappe.model.naming import append_number_if_name_exists - variant = frappe.new_doc('Item') + variant = frappe.new_doc("Item") copy_attributes_to_variant(template, variant) variant.manufacturer = manufacturer variant.manufacturer_part_no = manufacturer_part_no - variant.item_code = append_number_if_name_exists('Item', template.name) + variant.item_code = append_number_if_name_exists("Item", template.name) return variant + def validate_item_variant_attributes(item, args=None): if isinstance(item, string_types): - item = frappe.get_doc('Item', item) + item = frappe.get_doc("Item", item) if not args: - args = {d.attribute.lower():d.attribute_value for d in item.attributes} + args = {d.attribute.lower(): d.attribute_value for d in item.attributes} attribute_values, numeric_values = get_attribute_values(item) @@ -74,6 +82,7 @@ def validate_item_variant_attributes(item, args=None): attributes_list = attribute_values.get(attribute.lower(), []) validate_item_attribute_value(attributes_list, attribute, value, item.name, from_variant=True) + def validate_is_incremental(numeric_attribute, attribute, value, item): from_range = numeric_attribute.from_range to_range = numeric_attribute.to_range @@ -85,30 +94,48 @@ def validate_is_incremental(numeric_attribute, attribute, value, item): is_in_range = from_range <= flt(value) <= to_range precision = max(len(cstr(v).split(".")[-1].rstrip("0")) for v in (value, increment)) - #avoid precision error by rounding the remainder + # avoid precision error by rounding the remainder remainder = flt((flt(value) - from_range) % increment, precision) - is_incremental = remainder==0 or remainder==increment + is_incremental = remainder == 0 or remainder == increment if not (is_in_range and is_incremental): - frappe.throw(_("Value for Attribute {0} must be within the range of {1} to {2} in the increments of {3} for Item {4}")\ - .format(attribute, from_range, to_range, increment, item), - InvalidItemAttributeValueError, title=_('Invalid Attribute')) + frappe.throw( + _( + "Value for Attribute {0} must be within the range of {1} to {2} in the increments of {3} for Item {4}" + ).format(attribute, from_range, to_range, increment, item), + InvalidItemAttributeValueError, + title=_("Invalid Attribute"), + ) -def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True): - allow_rename_attribute_value = frappe.db.get_single_value('Item Variant Settings', 'allow_rename_attribute_value') + +def validate_item_attribute_value( + attributes_list, attribute, attribute_value, item, from_variant=True +): + allow_rename_attribute_value = frappe.db.get_single_value( + "Item Variant Settings", "allow_rename_attribute_value" + ) if allow_rename_attribute_value: pass elif attribute_value not in attributes_list: if from_variant: - frappe.throw(_("{0} is not a valid Value for Attribute {1} of Item {2}.").format( - frappe.bold(attribute_value), frappe.bold(attribute), frappe.bold(item)), InvalidItemAttributeValueError, title=_("Invalid Value")) + frappe.throw( + _("{0} is not a valid Value for Attribute {1} of Item {2}.").format( + frappe.bold(attribute_value), frappe.bold(attribute), frappe.bold(item) + ), + InvalidItemAttributeValueError, + title=_("Invalid Value"), + ) else: msg = _("The value {0} is already assigned to an existing Item {1}.").format( - frappe.bold(attribute_value), frappe.bold(item)) - msg += "
" + _("To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings.").format(frappe.bold("Allow Rename Attribute Value")) + frappe.bold(attribute_value), frappe.bold(item) + ) + msg += "
" + _( + "To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings." + ).format(frappe.bold("Allow Rename Attribute Value")) + + frappe.throw(msg, InvalidItemAttributeValueError, title=_("Edit Not Allowed")) - frappe.throw(msg, InvalidItemAttributeValueError, title=_('Edit Not Allowed')) def get_attribute_values(item): if not frappe.flags.attribute_values: @@ -117,9 +144,11 @@ def get_attribute_values(item): for t in frappe.get_all("Item Attribute Value", fields=["parent", "attribute_value"]): attribute_values.setdefault(t.parent.lower(), []).append(t.attribute_value) - for t in frappe.get_all('Item Variant Attribute', + for t in frappe.get_all( + "Item Variant Attribute", fields=["attribute", "from_range", "to_range", "increment"], - filters={'numeric_values': 1, 'parent': item.variant_of}): + filters={"numeric_values": 1, "parent": item.variant_of}, + ): numeric_values[t.attribute.lower()] = t frappe.flags.attribute_values = attribute_values @@ -127,14 +156,22 @@ def get_attribute_values(item): return frappe.flags.attribute_values, frappe.flags.numeric_values + def find_variant(template, args, variant_item_code=None): - conditions = ["""(iv_attribute.attribute={0} and iv_attribute.attribute_value={1})"""\ - .format(frappe.db.escape(key), frappe.db.escape(cstr(value))) for key, value in args.items()] + conditions = [ + """(iv_attribute.attribute={0} and iv_attribute.attribute_value={1})""".format( + frappe.db.escape(key), frappe.db.escape(cstr(value)) + ) + for key, value in args.items() + ] conditions = " or ".join(conditions) from erpnext.e_commerce.variant_selector.utils import get_item_codes_by_attributes - possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code] + + possible_variants = [ + i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code + ] for variant in possible_variants: variant = frappe.get_doc("Item", variant) @@ -146,7 +183,7 @@ def find_variant(template, args, variant_item_code=None): for attribute, value in args.items(): for row in variant.attributes: - if row.attribute==attribute and row.attribute_value== cstr(value): + if row.attribute == attribute and row.attribute_value == cstr(value): # this row matches match_count += 1 break @@ -154,6 +191,7 @@ def find_variant(template, args, variant_item_code=None): if match_count == len(args.keys()): return variant.name + @frappe.whitelist() def create_variant(item, args): if isinstance(args, string_types): @@ -161,14 +199,11 @@ def create_variant(item, args): template = frappe.get_doc("Item", item) variant = frappe.new_doc("Item") - variant.variant_based_on = 'Item Attribute' + variant.variant_based_on = "Item Attribute" variant_attributes = [] for d in template.attributes: - variant_attributes.append({ - "attribute": d.attribute, - "attribute_value": args.get(d.attribute) - }) + variant_attributes.append({"attribute": d.attribute, "attribute_value": args.get(d.attribute)}) variant.set("attributes", variant_attributes) copy_attributes_to_variant(template, variant) @@ -176,6 +211,7 @@ def create_variant(item, args): return variant + @frappe.whitelist() def enqueue_multiple_variant_creation(item, args): # There can be innumerable attribute combinations, enqueue @@ -190,9 +226,14 @@ def enqueue_multiple_variant_creation(item, args): if total_variants < 10: return create_multiple_variants(item, args) else: - frappe.enqueue("erpnext.controllers.item_variant.create_multiple_variants", - item=item, args=args, now=frappe.flags.in_test); - return 'queued' + frappe.enqueue( + "erpnext.controllers.item_variant.create_multiple_variants", + item=item, + args=args, + now=frappe.flags.in_test, + ) + return "queued" + def create_multiple_variants(item, args): count = 0 @@ -205,26 +246,27 @@ def create_multiple_variants(item, args): if not get_variant(item, args=attribute_values): variant = create_variant(item, attribute_values) variant.save() - count +=1 + count += 1 return count + def generate_keyed_value_combinations(args): """ From this: - args = {"attr1": ["a", "b", "c"], "attr2": ["1", "2"], "attr3": ["A"]} + args = {"attr1": ["a", "b", "c"], "attr2": ["1", "2"], "attr3": ["A"]} To this: - [ - {u'attr1': u'a', u'attr2': u'1', u'attr3': u'A'}, - {u'attr1': u'b', u'attr2': u'1', u'attr3': u'A'}, - {u'attr1': u'c', u'attr2': u'1', u'attr3': u'A'}, - {u'attr1': u'a', u'attr2': u'2', u'attr3': u'A'}, - {u'attr1': u'b', u'attr2': u'2', u'attr3': u'A'}, - {u'attr1': u'c', u'attr2': u'2', u'attr3': u'A'} - ] + [ + {u'attr1': u'a', u'attr2': u'1', u'attr3': u'A'}, + {u'attr1': u'b', u'attr2': u'1', u'attr3': u'A'}, + {u'attr1': u'c', u'attr2': u'1', u'attr3': u'A'}, + {u'attr1': u'a', u'attr2': u'2', u'attr3': u'A'}, + {u'attr1': u'b', u'attr2': u'2', u'attr3': u'A'}, + {u'attr1': u'c', u'attr2': u'2', u'attr3': u'A'} + ] """ # Return empty list if empty @@ -260,17 +302,27 @@ def generate_keyed_value_combinations(args): return results + def copy_attributes_to_variant(item, variant): # copy non no-copy fields - exclude_fields = ["naming_series", "item_code", "item_name", "published_in_website", - "opening_stock", "variant_of", "valuation_rate", "has_variants", "attributes"] + exclude_fields = [ + "naming_series", + "item_code", + "item_name", + "published_in_website", + "opening_stock", + "variant_of", + "valuation_rate", + "has_variants", + "attributes", + ] - if item.variant_based_on=='Manufacturer': + if item.variant_based_on == "Manufacturer": # don't copy manufacturer values if based on part no - exclude_fields += ['manufacturer', 'manufacturer_part_no'] + exclude_fields += ["manufacturer", "manufacturer_part_no"] - allow_fields = [d.field_name for d in frappe.get_all("Variant Field", fields = ['field_name'])] + allow_fields = [d.field_name for d in frappe.get_all("Variant Field", fields=["field_name"])] if "variant_based_on" not in allow_fields: allow_fields.append("variant_based_on") for field in item.meta.fields: @@ -289,11 +341,11 @@ def copy_attributes_to_variant(item, variant): variant.variant_of = item.name - if 'description' not in allow_fields: + if "description" not in allow_fields: if not variant.description: - variant.description = "" + variant.description = "" else: - if item.variant_based_on=='Item Attribute': + if item.variant_based_on == "Item Attribute": if variant.attributes: attributes_description = item.description + " " for d in variant.attributes: @@ -302,6 +354,7 @@ def copy_attributes_to_variant(item, variant): if attributes_description not in variant.description: variant.description = attributes_description + def make_variant_item_code(template_item_code, template_item_name, variant): """Uses template's item code and abbreviations to make variant's item code""" if variant.item_code: @@ -309,13 +362,14 @@ def make_variant_item_code(template_item_code, template_item_name, variant): abbreviations = [] for attr in variant.attributes: - item_attribute = frappe.db.sql("""select i.numeric_values, v.abbr + item_attribute = frappe.db.sql( + """select i.numeric_values, v.abbr from `tabItem Attribute` i left join `tabItem Attribute Value` v on (i.name=v.parent) - where i.name=%(attribute)s and (v.attribute_value=%(attribute_value)s or i.numeric_values = 1)""", { - "attribute": attr.attribute, - "attribute_value": attr.attribute_value - }, as_dict=True) + where i.name=%(attribute)s and (v.attribute_value=%(attribute_value)s or i.numeric_values = 1)""", + {"attribute": attr.attribute, "attribute_value": attr.attribute_value}, + as_dict=True, + ) if not item_attribute: continue @@ -323,13 +377,16 @@ def make_variant_item_code(template_item_code, template_item_name, variant): # frappe.bold(attr.attribute_value)), title=_('Invalid Attribute'), # exc=InvalidItemAttributeValueError) - abbr_or_value = cstr(attr.attribute_value) if item_attribute[0].numeric_values else item_attribute[0].abbr + abbr_or_value = ( + cstr(attr.attribute_value) if item_attribute[0].numeric_values else item_attribute[0].abbr + ) abbreviations.append(abbr_or_value) if abbreviations: variant.item_code = "{0}-{1}".format(template_item_code, "-".join(abbreviations)) variant.item_name = "{0}-{1}".format(template_item_name, "-".join(abbreviations)) + @frappe.whitelist() def create_variant_doc_for_quick_entry(template, args): variant_based_on = frappe.db.get_value("Item", template, "variant_based_on") diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index cf9de52d4cd..d2c80961a36 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - def set_print_templates_for_item_table(doc, settings): doc.print_templates = { "items": "templates/print_formats/includes/items.html", @@ -20,16 +19,21 @@ def set_print_templates_for_item_table(doc, settings): doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"] if settings.compact_item_print: - doc.child_print_templates["items"]["description"] =\ - "templates/print_formats/includes/item_table_description.html" + doc.child_print_templates["items"][ + "description" + ] = "templates/print_formats/includes/item_table_description.html" doc.flags.format_columns = format_columns + def set_print_templates_for_taxes(doc, settings): doc.flags.show_inclusive_tax_in_print = doc.is_inclusive_tax() - doc.print_templates.update({ - "total": "templates/print_formats/includes/total.html", - "taxes": "templates/print_formats/includes/taxes.html" - }) + doc.print_templates.update( + { + "total": "templates/print_formats/includes/total.html", + "taxes": "templates/print_formats/includes/taxes.html", + } + ) + def format_columns(display_columns, compact_fields): compact_fields = compact_fields + ["image", "item_code", "item_name"] diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index f5c566023c6..d8e9bcafa21 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -21,7 +21,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] fields = get_fields("Employee", ["name", "employee_name"]) - return frappe.db.sql("""select {fields} from `tabEmployee` + return frappe.db.sql( + """select {fields} from `tabEmployee` where status in ('Active', 'Suspended') and docstatus < 2 and ({key} like %(txt)s @@ -32,17 +33,16 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), idx desc, name, employee_name - limit %(start)s, %(page_len)s""".format(**{ - 'fields': ", ".join(fields), - 'key': searchfield, - 'fcond': get_filters_cond(doctype, filters, conditions), - 'mcond': get_match_cond(doctype) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) + limit %(start)s, %(page_len)s""".format( + **{ + "fields": ", ".join(fields), + "key": searchfield, + "fcond": get_filters_cond(doctype, filters, conditions), + "mcond": get_match_cond(doctype), + } + ), + {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + ) # searches for leads which are not converted @@ -51,7 +51,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): def lead_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Lead", ["name", "lead_name", "company_name"]) - return frappe.db.sql("""select {fields} from `tabLead` + return frappe.db.sql( + """select {fields} from `tabLead` where docstatus < 2 and ifnull(status, '') != 'Converted' and ({key} like %(txt)s @@ -64,19 +65,15 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999), idx desc, name, lead_name - limit %(start)s, %(page_len)s""".format(**{ - 'fields': ", ".join(fields), - 'key': searchfield, - 'mcond':get_match_cond(doctype) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) + limit %(start)s, %(page_len)s""".format( + **{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} + ), + {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + ) + + # searches for customer - # searches for customer @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def customer_query(doctype, txt, searchfield, start, page_len, filters): @@ -93,7 +90,8 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): searchfields = frappe.get_meta("Customer").get_search_fields() searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) - return frappe.db.sql("""select {fields} from `tabCustomer` + return frappe.db.sql( + """select {fields} from `tabCustomer` where docstatus < 2 and ({scond}) and disabled=0 {fcond} {mcond} @@ -102,17 +100,16 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999), idx desc, name, customer_name - limit %(start)s, %(page_len)s""".format(**{ - "fields": ", ".join(fields), - "scond": searchfields, - "mcond": get_match_cond(doctype), - "fcond": get_filters_cond(doctype, filters, conditions).replace('%', '%%'), - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) + limit %(start)s, %(page_len)s""".format( + **{ + "fields": ", ".join(fields), + "scond": searchfields, + "mcond": get_match_cond(doctype), + "fcond": get_filters_cond(doctype, filters, conditions).replace("%", "%%"), + } + ), + {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + ) # searches for supplier @@ -128,7 +125,8 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Supplier", fields) - return frappe.db.sql("""select {field} from `tabSupplier` + return frappe.db.sql( + """select {field} from `tabSupplier` where docstatus < 2 and ({key} like %(txt)s or supplier_name like %(txt)s) and disabled=0 @@ -139,36 +137,32 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999), idx desc, name, supplier_name - limit %(start)s, %(page_len)s """.format(**{ - 'field': ', '.join(fields), - 'key': searchfield, - 'mcond':get_match_cond(doctype) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) + limit %(start)s, %(page_len)s """.format( + **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} + ), + {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + ) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def tax_account_query(doctype, txt, searchfield, start, page_len, filters): - company_currency = erpnext.get_company_currency(filters.get('company')) + company_currency = erpnext.get_company_currency(filters.get("company")) def get_accounts(with_account_type_filter): - account_type_condition = '' + account_type_condition = "" if with_account_type_filter: account_type_condition = "AND account_type in %(account_types)s" - accounts = frappe.db.sql(""" + accounts = frappe.db.sql( + """ SELECT name, parent_account FROM `tabAccount` WHERE `tabAccount`.docstatus!=2 {account_type_condition} AND is_group = 0 AND company = %(company)s - AND account_currency = %(currency)s + AND (account_currency = %(currency)s or ifnull(account_currency, '') = '') AND `{searchfield}` LIKE %(txt)s {mcond} ORDER BY idx DESC, name @@ -176,7 +170,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): """.format( account_type_condition=account_type_condition, searchfield=searchfield, - mcond=get_match_cond(doctype) + mcond=get_match_cond(doctype), ), dict( account_types=filters.get("account_type"), @@ -184,8 +178,8 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): currency=company_currency, txt="%{}%".format(txt), offset=start, - limit=page_len - ) + limit=page_len, + ), ) return accounts @@ -206,7 +200,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if isinstance(filters, str): filters = json.loads(filters) - #Get searchfields from meta and use in Item Link field query + # Get searchfields from meta and use in Item Link field query meta = frappe.get_meta("Item", cached=True) searchfields = meta.get_search_fields() @@ -216,49 +210,56 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if ignored_field in searchfields: searchfields.remove(ignored_field) - columns = '' - extra_searchfields = [field for field in searchfields - if not field in ["name", "item_group", "description", "item_name"]] + columns = "" + extra_searchfields = [ + field + for field in searchfields + if not field in ["name", "item_group", "description", "item_name"] + ] if extra_searchfields: columns = ", " + ", ".join(extra_searchfields) - searchfields = searchfields + [field for field in[searchfield or "name", "item_code", "item_group", "item_name"] - if not field in searchfields] + searchfields = searchfields + [ + field + for field in [searchfield or "name", "item_code", "item_group", "item_name"] + if not field in searchfields + ] searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) if filters and isinstance(filters, dict): - if filters.get('customer') or filters.get('supplier'): - party = filters.get('customer') or filters.get('supplier') - item_rules_list = frappe.get_all('Party Specific Item', - filters = {'party': party}, fields = ['restrict_based_on', 'based_on_value']) + if filters.get("customer") or filters.get("supplier"): + party = filters.get("customer") or filters.get("supplier") + item_rules_list = frappe.get_all( + "Party Specific Item", filters={"party": party}, fields=["restrict_based_on", "based_on_value"] + ) filters_dict = {} for rule in item_rules_list: - if rule['restrict_based_on'] == 'Item': - rule['restrict_based_on'] = 'name' + if rule["restrict_based_on"] == "Item": + rule["restrict_based_on"] = "name" filters_dict[rule.restrict_based_on] = [] for rule in item_rules_list: filters_dict[rule.restrict_based_on].append(rule.based_on_value) for filter in filters_dict: - filters[scrub(filter)] = ['in', filters_dict[filter]] + filters[scrub(filter)] = ["in", filters_dict[filter]] - if filters.get('customer'): - del filters['customer'] + if filters.get("customer"): + del filters["customer"] else: - del filters['supplier'] + del filters["supplier"] else: - filters.pop('customer', None) - filters.pop('supplier', None) + filters.pop("customer", None) + filters.pop("supplier", None) - - description_cond = '' - if frappe.db.count('Item', cache=True) < 50000: + description_cond = "" + if frappe.db.count("Item", cache=True) < 50000: # scan description only if items are less than 50000 - description_cond = 'or tabItem.description LIKE %(txt)s' - return frappe.db.sql("""select + description_cond = "or tabItem.description LIKE %(txt)s" + return frappe.db.sql( + """select tabItem.name, tabItem.item_name, tabItem.item_group, if(length(tabItem.description) > 40, \ concat(substr(tabItem.description, 1, 40), "..."), description) as description @@ -279,16 +280,19 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals limit %(start)s, %(page_len)s """.format( columns=columns, scond=searchfields, - fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), - mcond=get_match_cond(doctype).replace('%', '%%'), - description_cond = description_cond), - { - "today": nowdate(), - "txt": "%%%s%%" % txt, - "_txt": txt.replace("%", ""), - "start": start, - "page_len": page_len - }, as_dict=as_dict) + fcond=get_filters_cond(doctype, filters, conditions).replace("%", "%%"), + mcond=get_match_cond(doctype).replace("%", "%%"), + description_cond=description_cond, + ), + { + "today": nowdate(), + "txt": "%%%s%%" % txt, + "_txt": txt.replace("%", ""), + "start": start, + "page_len": page_len, + }, + as_dict=as_dict, + ) @frappe.whitelist() @@ -297,7 +301,8 @@ def bom(doctype, txt, searchfield, start, page_len, filters): conditions = [] fields = get_fields("BOM", ["name", "item"]) - return frappe.db.sql("""select {fields} + return frappe.db.sql( + """select {fields} from tabBOM where tabBOM.docstatus=1 and tabBOM.is_active=1 @@ -308,30 +313,35 @@ def bom(doctype, txt, searchfield, start, page_len, filters): idx desc, name limit %(start)s, %(page_len)s """.format( fields=", ".join(fields), - fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), - mcond=get_match_cond(doctype).replace('%', '%%'), - key=searchfield), + fcond=get_filters_cond(doctype, filters, conditions).replace("%", "%%"), + mcond=get_match_cond(doctype).replace("%", "%%"), + key=searchfield, + ), { - 'txt': '%' + txt + '%', - '_txt': txt.replace("%", ""), - 'start': start or 0, - 'page_len': page_len or 20 - }) + "txt": "%" + txt + "%", + "_txt": txt.replace("%", ""), + "start": start or 0, + "page_len": page_len or 20, + }, + ) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_project_name(doctype, txt, searchfield, start, page_len, filters): - cond = '' - if filters and filters.get('customer'): + cond = "" + if filters and filters.get("customer"): cond = """(`tabProject`.customer = %s or - ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer"))) + ifnull(`tabProject`.customer,"")="") and""" % ( + frappe.db.escape(filters.get("customer")) + ) fields = get_fields("Project", ["name", "project_name"]) searchfields = frappe.get_meta("Project").get_search_fields() searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) - return frappe.db.sql("""select {fields} from `tabProject` + return frappe.db.sql( + """select {fields} from `tabProject` where `tabProject`.status not in ("Completed", "Cancelled") and {cond} {scond} {match_cond} @@ -340,15 +350,15 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): idx desc, `tabProject`.name asc limit {start}, {page_len}""".format( - fields=", ".join(['`tabProject`.{0}'.format(f) for f in fields]), + fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]), cond=cond, scond=searchfields, match_cond=get_match_cond(doctype), start=start, - page_len=page_len), { - "txt": "%{0}%".format(txt), - "_txt": txt.replace('%', '') - }) + page_len=page_len, + ), + {"txt": "%{0}%".format(txt), "_txt": txt.replace("%", "")}, + ) @frappe.whitelist() @@ -356,7 +366,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): fields = get_fields("Delivery Note", ["name", "customer", "posting_date"]) - return frappe.db.sql(""" + return frappe.db.sql( + """ select %(fields)s from `tabDelivery Note` where `tabDelivery Note`.`%(key)s` like %(txt)s and @@ -371,15 +382,19 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, ) ) %(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s - """ % { - "fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]), - "key": searchfield, - "fcond": get_filters_cond(doctype, filters, []), - "mcond": get_match_cond(doctype), - "start": start, - "page_len": page_len, - "txt": "%(txt)s" - }, {"txt": ("%%%s%%" % txt)}, as_dict=as_dict) + """ + % { + "fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]), + "key": searchfield, + "fcond": get_filters_cond(doctype, filters, []), + "mcond": get_match_cond(doctype), + "start": start, + "page_len": page_len, + "txt": "%(txt)s", + }, + {"txt": ("%%%s%%" % txt)}, + as_dict=as_dict, + ) @frappe.whitelist() @@ -391,12 +406,12 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): batch_nos = None args = { - 'item_code': filters.get("item_code"), - 'warehouse': filters.get("warehouse"), - 'posting_date': filters.get('posting_date'), - 'txt': "%{0}%".format(txt), + "item_code": filters.get("item_code"), + "warehouse": filters.get("warehouse"), + "posting_date": filters.get("posting_date"), + "txt": "%{0}%".format(txt), "start": start, - "page_len": page_len + "page_len": page_len, } having_clause = "having sum(sle.actual_qty) > 0" @@ -406,20 +421,21 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): meta = frappe.get_meta("Batch", cached=True) searchfields = meta.get_search_fields() - search_columns = '' - search_cond = '' + search_columns = "" + search_cond = "" if searchfields: search_columns = ", " + ", ".join(searchfields) search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields]) - if args.get('warehouse'): - searchfields = ['batch.' + field for field in searchfields] + if args.get("warehouse"): + searchfields = ["batch." + field for field in searchfields] if searchfields: search_columns = ", " + ", ".join(searchfields) search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields]) - batch_nos = frappe.db.sql("""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom, + batch_nos = frappe.db.sql( + """select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom, concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date) {search_columns} from `tabStock Ledger Entry` sle @@ -439,16 +455,19 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): group by batch_no {having_clause} order by batch.expiry_date, sle.batch_no desc limit %(start)s, %(page_len)s""".format( - search_columns = search_columns, + search_columns=search_columns, cond=cond, match_conditions=get_match_cond(doctype), - having_clause = having_clause, - search_cond = search_cond - ), args) + having_clause=having_clause, + search_cond=search_cond, + ), + args, + ) return batch_nos else: - return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) + return frappe.db.sql( + """select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) {search_columns} from `tabBatch` batch where batch.disabled = 0 @@ -462,8 +481,14 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): {match_conditions} order by expiry_date, name desc - limit %(start)s, %(page_len)s""".format(cond, search_columns = search_columns, - search_cond = search_cond, match_conditions=get_match_cond(doctype)), args) + limit %(start)s, %(page_len)s""".format( + cond, + search_columns=search_columns, + search_cond=search_cond, + match_conditions=get_match_cond(doctype), + ), + args, + ) @frappe.whitelist() @@ -486,25 +511,33 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): if searchfield and txt: filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt]) - return frappe.desk.reportview.execute("Account", filters = filter_list, - fields = ["name", "parent_account"], - limit_start=start, limit_page_length=page_len, as_list=True) + return frappe.desk.reportview.execute( + "Account", + filters=filter_list, + fields=["name", "parent_account"], + limit_start=start, + limit_page_length=page_len, + as_list=True, + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date + return frappe.db.sql( + """select distinct bo.name, bo.blanket_order_type, bo.to_date from `tabBlanket Order` bo, `tabBlanket Order Item` boi where boi.parent = bo.name and boi.item_code = {item_code} and bo.blanket_order_type = '{blanket_order_type}' and bo.company = {company} - and bo.docstatus = 1""" - .format(item_code = frappe.db.escape(filters.get("item")), - blanket_order_type = filters.get("blanket_order_type"), - company = frappe.db.escape(filters.get("company")) - )) + and bo.docstatus = 1""".format( + item_code=frappe.db.escape(filters.get("item")), + blanket_order_type=filters.get("blanket_order_type"), + company=frappe.db.escape(filters.get("company")), + ) + ) @frappe.whitelist() @@ -515,23 +548,26 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): # income account can be any Credit account, # but can also be a Asset account with account_type='Income Account' in special circumstances. # Hence the first condition is an "OR" - if not filters: filters = {} + if not filters: + filters = {} condition = "" if filters.get("company"): condition += "and tabAccount.company = %(company)s" - return frappe.db.sql("""select tabAccount.name from `tabAccount` + return frappe.db.sql( + """select tabAccount.name from `tabAccount` where (tabAccount.report_type = "Profit and Loss" or tabAccount.account_type in ("Income Account", "Temporary")) and tabAccount.is_group=0 and tabAccount.`{key}` LIKE %(txt)s {condition} {match_condition} - order by idx desc, name""" - .format(condition=condition, match_condition=get_match_cond(doctype), key=searchfield), { - 'txt': '%' + txt + '%', - 'company': filters.get("company", "") - }) + order by idx desc, name""".format( + condition=condition, match_condition=get_match_cond(doctype), key=searchfield + ), + {"txt": "%" + txt + "%", "company": filters.get("company", "")}, + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -539,68 +575,73 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( get_dimension_filter_map, ) + dimension_filters = get_dimension_filter_map() - dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account'))) + dimension_filters = dimension_filters.get((filters.get("dimension"), filters.get("account"))) query_filters = [] or_filters = [] - fields = ['name'] + fields = ["name"] searchfields = frappe.get_meta(doctype).get_search_fields() meta = frappe.get_meta(doctype) if meta.is_tree: - query_filters.append(['is_group', '=', 0]) + query_filters.append(["is_group", "=", 0]) - if meta.has_field('disabled'): - query_filters.append(['disabled', '!=', 1]) + if meta.has_field("disabled"): + query_filters.append(["disabled", "!=", 1]) - if meta.has_field('company'): - query_filters.append(['company', '=', filters.get('company')]) + if meta.has_field("company"): + query_filters.append(["company", "=", filters.get("company")]) for field in searchfields: - or_filters.append([field, 'LIKE', "%%%s%%" % txt]) + or_filters.append([field, "LIKE", "%%%s%%" % txt]) fields.append(field) if dimension_filters: - if dimension_filters['allow_or_restrict'] == 'Allow': - query_selector = 'in' + if dimension_filters["allow_or_restrict"] == "Allow": + query_selector = "in" else: - query_selector = 'not in' + query_selector = "not in" - if len(dimension_filters['allowed_dimensions']) == 1: - dimensions = tuple(dimension_filters['allowed_dimensions'] * 2) + if len(dimension_filters["allowed_dimensions"]) == 1: + dimensions = tuple(dimension_filters["allowed_dimensions"] * 2) else: - dimensions = tuple(dimension_filters['allowed_dimensions']) + dimensions = tuple(dimension_filters["allowed_dimensions"]) - query_filters.append(['name', query_selector, dimensions]) + query_filters.append(["name", query_selector, dimensions]) - output = frappe.get_list(doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1) + output = frappe.get_list( + doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1 + ) return [tuple(d) for d in set(output)] + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_expense_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond - if not filters: filters = {} + if not filters: + filters = {} condition = "" if filters.get("company"): condition += "and tabAccount.company = %(company)s" - return frappe.db.sql("""select tabAccount.name from `tabAccount` + return frappe.db.sql( + """select tabAccount.name from `tabAccount` where (tabAccount.report_type = "Profit and Loss" or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress")) and tabAccount.is_group=0 and tabAccount.docstatus!=2 and tabAccount.{key} LIKE %(txt)s - {condition} {match_condition}""" - .format(condition=condition, key=searchfield, - match_condition=get_match_cond(doctype)), { - 'company': filters.get("company", ""), - 'txt': '%' + txt + '%' - }) + {condition} {match_condition}""".format( + condition=condition, key=searchfield, match_condition=get_match_cond(doctype) + ), + {"company": filters.get("company", ""), "txt": "%" + txt + "%"}, + ) @frappe.whitelist() @@ -621,14 +662,16 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): limit {start}, {page_len} """.format( - bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"),bin_conditions, ignore_permissions=True), - key=searchfield, - fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions), - mcond=get_match_cond(doctype), - start=start, - page_len=page_len, - txt=frappe.db.escape('%{0}%'.format(txt)) - ) + bin_conditions=get_filters_cond( + doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True + ), + key=searchfield, + fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions), + mcond=get_match_cond(doctype), + start=start, + page_len=page_len, + txt=frappe.db.escape("%{0}%".format(txt)), + ) return frappe.db.sql(query) @@ -647,10 +690,12 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): query = """select batch_id from `tabBatch` where disabled = 0 and (expiry_date >= CURDATE() or expiry_date IS NULL) - and name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt))) + and name like {txt}""".format( + txt=frappe.db.escape("%{0}%".format(txt)) + ) - if filters and filters.get('item'): - query += " and item = {item}".format(item = frappe.db.escape(filters.get('item'))) + if filters and filters.get("item"): + query += " and item = {item}".format(item=frappe.db.escape(filters.get("item"))) return frappe.db.sql(query, filters) @@ -659,8 +704,8 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): @frappe.validate_and_sanitize_search_inputs def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters): item_filters = [ - ['manufacturer', 'like', '%' + txt + '%'], - ['item_code', '=', filters.get("item_code")] + ["manufacturer", "like", "%" + txt + "%"], + ["item_code", "=", filters.get("item_code")], ] item_manufacturers = frappe.get_all( @@ -669,7 +714,7 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters) filters=item_filters, limit_start=start, limit_page_length=page_len, - as_list=1 + as_list=1, ) return item_manufacturers @@ -681,10 +726,14 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): select pr.name from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pritem where pr.docstatus = 1 and pritem.parent = pr.name - and pr.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt))) + and pr.name like {txt}""".format( + txt=frappe.db.escape("%{0}%".format(txt)) + ) - if filters and filters.get('item_code'): - query += " and pritem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code'))) + if filters and filters.get("item_code"): + query += " and pritem.item_code = {item_code}".format( + item_code=frappe.db.escape(filters.get("item_code")) + ) return frappe.db.sql(query, filters) @@ -696,10 +745,14 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): select pi.name from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` piitem where pi.docstatus = 1 and piitem.parent = pi.name - and pi.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt))) + and pi.name like {txt}""".format( + txt=frappe.db.escape("%{0}%".format(txt)) + ) - if filters and filters.get('item_code'): - query += " and piitem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code'))) + if filters and filters.get("item_code"): + query += " and piitem.item_code = {item_code}".format( + item_code=frappe.db.escape(filters.get("item_code")) + ) return frappe.db.sql(query, filters) @@ -714,18 +767,22 @@ def get_healthcare_service_units(doctype, txt, searchfield, start, page_len, fil is_group = 0 and company = {company} and name like {txt}""".format( - company = frappe.db.escape(filters.get('company')), txt = frappe.db.escape('%{0}%'.format(txt))) + company=frappe.db.escape(filters.get("company")), txt=frappe.db.escape("%{0}%".format(txt)) + ) - if filters and filters.get('inpatient_record'): + if filters and filters.get("inpatient_record"): from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import ( get_current_healthcare_service_unit, ) - service_unit = get_current_healthcare_service_unit(filters.get('inpatient_record')) + + service_unit = get_current_healthcare_service_unit(filters.get("inpatient_record")) # if the patient is admitted, then appointments should be allowed against the admission service unit, # inspite of it being an Inpatient Occupancy service unit if service_unit: - query += " and (allow_appointments = 1 or name = {service_unit})".format(service_unit = frappe.db.escape(service_unit)) + query += " and (allow_appointments = 1 or name = {service_unit})".format( + service_unit=frappe.db.escape(service_unit) + ) else: query += " and allow_appointments = 1" else: @@ -738,27 +795,29 @@ def get_healthcare_service_units(doctype, txt, searchfield, start, page_len, fil @frappe.validate_and_sanitize_search_inputs def get_tax_template(doctype, txt, searchfield, start, page_len, filters): - item_doc = frappe.get_cached_doc('Item', filters.get('item_code')) - item_group = filters.get('item_group') - company = filters.get('company') + item_doc = frappe.get_cached_doc("Item", filters.get("item_code")) + item_group = filters.get("item_group") + company = filters.get("company") taxes = item_doc.taxes or [] while item_group: - item_group_doc = frappe.get_cached_doc('Item Group', item_group) + item_group_doc = frappe.get_cached_doc("Item Group", item_group) taxes += item_group_doc.taxes or [] item_group = item_group_doc.parent_item_group if not taxes: - return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True) + return frappe.get_all( + "Item Tax Template", filters={"disabled": 0, "company": company}, as_list=True + ) else: - valid_from = filters.get('valid_from') + valid_from = filters.get("valid_from") valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from args = { - 'item_code': filters.get('item_code'), - 'posting_date': valid_from, - 'tax_category': filters.get('tax_category'), - 'company': company + "item_code": filters.get("item_code"), + "posting_date": valid_from, + "tax_category": filters.get("tax_category"), + "company": company, } taxes = _get_item_tax_template(args, taxes, for_validate=True) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index df3c5f10c1b..0ad39949b6d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -11,7 +11,9 @@ import erpnext from erpnext.stock.utils import get_incoming_rate -class StockOverReturnError(frappe.ValidationError): pass +class StockOverReturnError(frappe.ValidationError): + pass + def validate_return(doc): if not doc.meta.get_field("is_return") or not doc.is_return: @@ -21,32 +23,50 @@ def validate_return(doc): validate_return_against(doc) validate_returned_items(doc) + def validate_return_against(doc): if not frappe.db.exists(doc.doctype, doc.return_against): - frappe.throw(_("Invalid {0}: {1}") - .format(doc.meta.get_label("return_against"), doc.return_against)) + frappe.throw( + _("Invalid {0}: {1}").format(doc.meta.get_label("return_against"), doc.return_against) + ) else: ref_doc = frappe.get_doc(doc.doctype, doc.return_against) party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier" - if ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) and ref_doc.docstatus == 1: + if ( + ref_doc.company == doc.company + and ref_doc.get(party_type) == doc.get(party_type) + and ref_doc.docstatus == 1 + ): # validate posting date time return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") - ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00") + ref_posting_datetime = "%s %s" % ( + ref_doc.posting_date, + ref_doc.get("posting_time") or "00:00:00", + ) if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime): - frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime))) + frappe.throw( + _("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime)) + ) # validate same exchange rate if doc.conversion_rate != ref_doc.conversion_rate: - frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})") - .format(doc.doctype, doc.return_against, ref_doc.conversion_rate)) + frappe.throw( + _("Exchange Rate must be same as {0} {1} ({2})").format( + doc.doctype, doc.return_against, ref_doc.conversion_rate + ) + ) # validate update stock if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock: - frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}") - .format(doc.return_against)) + frappe.throw( + _("'Update Stock' can not be checked because items are not delivered via {0}").format( + doc.return_against + ) + ) + def validate_returned_items(doc): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -54,43 +74,61 @@ def validate_returned_items(doc): valid_items = frappe._dict() select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor" - if doc.doctype != 'Purchase Invoice': + if doc.doctype != "Purchase Invoice": select_fields += ",serial_no, batch_no" - if doc.doctype in ['Purchase Invoice', 'Purchase Receipt']: + if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: select_fields += ",rejected_qty, received_qty" - for d in frappe.db.sql("""select {0} from `tab{1} Item` where parent = %s""" - .format(select_fields, doc.doctype), doc.return_against, as_dict=1): - valid_items = get_ref_item_dict(valid_items, d) + for d in frappe.db.sql( + """select {0} from `tab{1} Item` where parent = %s""".format(select_fields, doc.doctype), + doc.return_against, + as_dict=1, + ): + valid_items = get_ref_item_dict(valid_items, d) if doc.doctype in ("Delivery Note", "Sales Invoice"): - for d in frappe.db.sql("""select item_code, qty, serial_no, batch_no from `tabPacked Item` - where parent = %s""", doc.return_against, as_dict=1): - valid_items = get_ref_item_dict(valid_items, d) + for d in frappe.db.sql( + """select item_code, qty, serial_no, batch_no from `tabPacked Item` + where parent = %s""", + doc.return_against, + as_dict=1, + ): + valid_items = get_ref_item_dict(valid_items, d) already_returned_items = get_already_returned_items(doc) # ( not mandatory when it is Purchase Invoice or a Sales Invoice without Update Stock ) - warehouse_mandatory = not ((doc.doctype=="Purchase Invoice" or doc.doctype=="Sales Invoice") and not doc.update_stock) + warehouse_mandatory = not ( + (doc.doctype == "Purchase Invoice" or doc.doctype == "Sales Invoice") and not doc.update_stock + ) items_returned = False for d in doc.get("items"): - if d.item_code and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0): + if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0): if d.item_code not in valid_items: - frappe.throw(_("Row # {0}: Returned Item {1} does not exist in {2} {3}") - .format(d.idx, d.item_code, doc.doctype, doc.return_against)) + frappe.throw( + _("Row # {0}: Returned Item {1} does not exist in {2} {3}").format( + d.idx, d.item_code, doc.doctype, doc.return_against + ) + ) else: ref = valid_items.get(d.item_code, frappe._dict()) validate_quantity(doc, d, ref, valid_items, already_returned_items) if ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") and flt(d.rate) > ref.rate: - frappe.throw(_("Row # {0}: Rate cannot be greater than the rate used in {1} {2}") - .format(d.idx, doc.doctype, doc.return_against)) + frappe.throw( + _("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format( + d.idx, doc.doctype, doc.return_against + ) + ) elif ref.batch_no and d.batch_no not in ref.batch_no: - frappe.throw(_("Row # {0}: Batch No must be same as {1} {2}") - .format(d.idx, doc.doctype, doc.return_against)) + frappe.throw( + _("Row # {0}: Batch No must be same as {1} {2}").format( + d.idx, doc.doctype, doc.return_against + ) + ) elif ref.serial_no: if not d.serial_no: @@ -99,11 +137,16 @@ def validate_returned_items(doc): serial_nos = get_serial_nos(d.serial_no) for s in serial_nos: if s not in ref.serial_no: - frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") - .format(d.idx, s, doc.doctype, doc.return_against)) + frappe.throw( + _("Row # {0}: Serial No {1} does not match with {2} {3}").format( + d.idx, s, doc.doctype, doc.return_against + ) + ) - if (warehouse_mandatory and not d.get("warehouse") and - frappe.db.get_value("Item", d.item_code, "is_stock_item") + if ( + warehouse_mandatory + and not d.get("warehouse") + and frappe.db.get_value("Item", d.item_code, "is_stock_item") ): frappe.throw(_("Warehouse is mandatory")) @@ -115,21 +158,23 @@ def validate_returned_items(doc): if not items_returned: frappe.throw(_("Atleast one item should be entered with negative quantity in return document")) + def validate_quantity(doc, args, ref, valid_items, already_returned_items): - fields = ['stock_qty'] - if doc.doctype in ['Purchase Receipt', 'Purchase Invoice']: - fields.extend(['received_qty', 'rejected_qty']) + fields = ["stock_qty"] + if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]: + fields.extend(["received_qty", "rejected_qty"]) already_returned_data = already_returned_items.get(args.item_code) or {} company_currency = erpnext.get_company_currency(doc.company) - stock_qty_precision = get_field_precision(frappe.get_meta(doc.doctype + " Item") - .get_field("stock_qty"), company_currency) + stock_qty_precision = get_field_precision( + frappe.get_meta(doc.doctype + " Item").get_field("stock_qty"), company_currency + ) for column in fields: returned_qty = flt(already_returned_data.get(column, 0)) if len(already_returned_data) > 0 else 0 - if column == 'stock_qty': + if column == "stock_qty": reference_qty = ref.get(column) current_stock_qty = args.get(column) else: @@ -137,38 +182,49 @@ def validate_quantity(doc, args, ref, valid_items, already_returned_items): current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0) max_returnable_qty = flt(reference_qty, stock_qty_precision) - returned_qty - label = column.replace('_', ' ').title() + label = column.replace("_", " ").title() if reference_qty: if flt(args.get(column)) > 0: frappe.throw(_("{0} must be negative in return document").format(label)) elif returned_qty >= reference_qty and args.get(column): - frappe.throw(_("Item {0} has already been returned") - .format(args.item_code), StockOverReturnError) + frappe.throw( + _("Item {0} has already been returned").format(args.item_code), StockOverReturnError + ) elif abs(flt(current_stock_qty, stock_qty_precision)) > max_returnable_qty: - frappe.throw(_("Row # {0}: Cannot return more than {1} for Item {2}") - .format(args.idx, max_returnable_qty, args.item_code), StockOverReturnError) + frappe.throw( + _("Row # {0}: Cannot return more than {1} for Item {2}").format( + args.idx, max_returnable_qty, args.item_code + ), + StockOverReturnError, + ) + def get_ref_item_dict(valid_items, ref_item_row): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - valid_items.setdefault(ref_item_row.item_code, frappe._dict({ - "qty": 0, - "rate": 0, - "stock_qty": 0, - "rejected_qty": 0, - "received_qty": 0, - "serial_no": [], - "conversion_factor": ref_item_row.get("conversion_factor", 1), - "batch_no": [] - })) + valid_items.setdefault( + ref_item_row.item_code, + frappe._dict( + { + "qty": 0, + "rate": 0, + "stock_qty": 0, + "rejected_qty": 0, + "received_qty": 0, + "serial_no": [], + "conversion_factor": ref_item_row.get("conversion_factor", 1), + "batch_no": [], + } + ), + ) item_dict = valid_items[ref_item_row.item_code] item_dict["qty"] += ref_item_row.qty - item_dict["stock_qty"] += ref_item_row.get('stock_qty', 0) + item_dict["stock_qty"] += ref_item_row.get("stock_qty", 0) if ref_item_row.get("rate", 0) > item_dict["rate"]: item_dict["rate"] = ref_item_row.get("rate", 0) - if ref_item_row.parenttype in ['Purchase Invoice', 'Purchase Receipt']: + if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt"]: item_dict["received_qty"] += ref_item_row.received_qty item_dict["rejected_qty"] += ref_item_row.rejected_qty @@ -180,13 +236,15 @@ def get_ref_item_dict(valid_items, ref_item_row): return valid_items + def get_already_returned_items(doc): - column = 'child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty' - if doc.doctype in ['Purchase Invoice', 'Purchase Receipt']: + column = "child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty" + if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty, sum(abs(child.received_qty) * child.conversion_factor) as received_qty""" - data = frappe.db.sql(""" + data = frappe.db.sql( + """ select {0} from `tab{1} Item` child, `tab{2}` par @@ -194,60 +252,84 @@ def get_already_returned_items(doc): child.parent = par.name and par.docstatus = 1 and par.is_return = 1 and par.return_against = %s group by item_code - """.format(column, doc.doctype, doc.doctype), doc.return_against, as_dict=1) + """.format( + column, doc.doctype, doc.doctype + ), + doc.return_against, + as_dict=1, + ) items = {} for d in data: - items.setdefault(d.item_code, frappe._dict({ - "qty": d.get("qty"), - "stock_qty": d.get("stock_qty"), - "received_qty": d.get("received_qty"), - "rejected_qty": d.get("rejected_qty") - })) + items.setdefault( + d.item_code, + frappe._dict( + { + "qty": d.get("qty"), + "stock_qty": d.get("stock_qty"), + "received_qty": d.get("received_qty"), + "rejected_qty": d.get("rejected_qty"), + } + ), + ) return items -def get_returned_qty_map_for_row(row_name, doctype): + +def get_returned_qty_map_for_row(return_against, party, row_name, doctype): child_doctype = doctype + " Item" reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) + if doctype in ("Purchase Receipt", "Purchase Invoice"): + party_type = "supplier" + else: + party_type = "customer" + fields = [ "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), - "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype) + "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype), ] if doctype in ("Purchase Receipt", "Purchase Invoice"): fields += [ "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), - "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype) + "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), ] if doctype == "Purchase Receipt": fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] - data = frappe.db.get_list(doctype, - fields = fields, - filters = [ + # Used retrun against and supplier and is_retrun because there is an index added for it + data = frappe.db.get_list( + doctype, + fields=fields, + filters=[ + [doctype, "return_against", "=", return_against], + [doctype, party_type, "=", party], [doctype, "docstatus", "=", 1], [doctype, "is_return", "=", 1], - [child_doctype, reference_field, "=", row_name] - ]) + [child_doctype, reference_field, "=", row_name], + ], + ) return data[0] + def make_return_doc(doctype, source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + company = frappe.db.get_value("Delivery Note", source_name, "company") - default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return") + default_warehouse_for_sales_return = frappe.db.get_value( + "Company", company, "default_warehouse_for_sales_return" + ) def set_missing_values(source, target): doc = frappe.get_doc(target) doc.is_return = 1 doc.return_against = source.name - doc.ignore_pricing_rule = 1 doc.set_warehouse = "" if doctype == "Sales Invoice" or doctype == "POS Invoice": doc.is_pos = source.is_pos @@ -265,29 +347,34 @@ def make_return_doc(doctype, source_name, target_doc=None): tax.tax_amount = -1 * tax.tax_amount if doc.get("is_return"): - if doc.doctype == 'Sales Invoice' or doc.doctype == 'POS Invoice': + if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice": doc.consolidated_invoice = "" - doc.set('payments', []) + doc.set("payments", []) for data in source.payments: paid_amount = 0.00 base_paid_amount = 0.00 - data.base_amount = flt(data.amount*source.conversion_rate, source.precision("base_paid_amount")) + data.base_amount = flt( + data.amount * source.conversion_rate, source.precision("base_paid_amount") + ) paid_amount += data.amount base_paid_amount += data.base_amount - doc.append('payments', { - 'mode_of_payment': data.mode_of_payment, - 'type': data.type, - 'amount': -1 * paid_amount, - 'base_amount': -1 * base_paid_amount, - 'account': data.account, - 'default': data.default - }) + doc.append( + "payments", + { + "mode_of_payment": data.mode_of_payment, + "type": data.type, + "amount": -1 * paid_amount, + "base_amount": -1 * base_paid_amount, + "account": data.account, + "default": data.default, + }, + ) if doc.is_pos: doc.paid_amount = -1 * source.paid_amount - elif doc.doctype == 'Purchase Invoice': + elif doc.doctype == "Purchase Invoice": doc.paid_amount = -1 * source.paid_amount doc.base_paid_amount = -1 * source.base_paid_amount - doc.payment_terms_template = '' + doc.payment_terms_template = "" doc.payment_schedule = [] if doc.get("is_return") and hasattr(doc, "packed_items"): @@ -304,16 +391,24 @@ def make_return_doc(doctype, source_name, target_doc=None): returned_serial_nos = get_returned_serial_nos(source_doc, source_parent) serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos)) if serial_nos: - target_doc.serial_no = '\n'.join(serial_nos) + target_doc.serial_no = "\n".join(serial_nos) if doctype == "Purchase Receipt": - returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) - target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) - target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0)) - target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) + returned_qty_map = get_returned_qty_map_for_row( + source_parent.name, source_parent.supplier, source_doc.name, doctype + ) + target_doc.received_qty = -1 * flt( + source_doc.received_qty - (returned_qty_map.get("received_qty") or 0) + ) + target_doc.rejected_qty = -1 * flt( + source_doc.rejected_qty - (returned_qty_map.get("rejected_qty") or 0) + ) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) - target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) - target_doc.received_stock_qty = -1 * flt(source_doc.received_stock_qty - (returned_qty_map.get('received_stock_qty') or 0)) + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)) + target_doc.received_stock_qty = -1 * flt( + source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0) + ) target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item @@ -321,12 +416,18 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_receipt_item = source_doc.name elif doctype == "Purchase Invoice": - returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) - target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) - target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0)) - target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) + returned_qty_map = get_returned_qty_map_for_row( + source_parent.name, source_parent.supplier, source_doc.name, doctype + ) + target_doc.received_qty = -1 * flt( + source_doc.received_qty - (returned_qty_map.get("received_qty") or 0) + ) + target_doc.rejected_qty = -1 * flt( + source_doc.rejected_qty - (returned_qty_map.get("rejected_qty") or 0) + ) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) - target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)) target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_receipt = source_doc.purchase_receipt target_doc.rejected_warehouse = source_doc.rejected_warehouse @@ -335,9 +436,11 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_invoice_item = source_doc.name elif doctype == "Delivery Note": - returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) - target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) - target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) + returned_qty_map = get_returned_qty_map_for_row( + source_parent.name, source_parent.customer, source_doc.name, doctype + ) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)) target_doc.against_sales_order = source_doc.against_sales_order target_doc.against_sales_invoice = source_doc.against_sales_invoice @@ -348,9 +451,11 @@ def make_return_doc(doctype, source_name, target_doc=None): if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice" or doctype == "POS Invoice": - returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) - target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) - target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) + returned_qty_map = get_returned_qty_map_for_row( + source_parent.name, source_parent.customer, source_doc.name, doctype + ) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)) target_doc.sales_order = source_doc.sales_order target_doc.delivery_note = source_doc.delivery_note @@ -369,39 +474,56 @@ def make_return_doc(doctype, source_name, target_doc=None): def update_terms(source_doc, target_doc, source_parent): target_doc.payment_amount = -source_doc.payment_amount - doclist = get_mapped_doc(doctype, source_name, { - doctype: { - "doctype": doctype, - - "validation": { - "docstatus": ["=", 1], - } - }, - doctype +" Item": { - "doctype": doctype + " Item", - "field_map": { - "serial_no": "serial_no", - "batch_no": "batch_no" + doclist = get_mapped_doc( + doctype, + source_name, + { + doctype: { + "doctype": doctype, + "validation": { + "docstatus": ["=", 1], + }, }, - "postprocess": update_item + doctype + + " Item": { + "doctype": doctype + " Item", + "field_map": {"serial_no": "serial_no", "batch_no": "batch_no"}, + "postprocess": update_item, + }, + "Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms}, }, - "Payment Schedule": { - "doctype": "Payment Schedule", - "postprocess": update_terms - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) + + doclist.set_onload("ignore_price_list", True) return doclist -def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, - item_row=None, voucher_detail_no=None, sle=None): + +def get_rate_for_return( + voucher_type, + voucher_no, + item_code, + return_against=None, + item_row=None, + voucher_detail_no=None, + sle=None, +): if not return_against: return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against") return_against_item_field = get_return_against_item_fields(voucher_type) - filters = get_filters(voucher_type, voucher_no, voucher_detail_no, - return_against, item_code, return_against_item_field, item_row) + filters = get_filters( + voucher_type, + voucher_no, + voucher_detail_no, + return_against, + item_code, + return_against_item_field, + item_row, + ) if voucher_type in ("Purchase Receipt", "Purchase Invoice"): select_field = "incoming_rate" @@ -409,52 +531,65 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None select_field = "abs(stock_value_difference / actual_qty)" rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) - if not (rate and return_against) and voucher_type in ['Sales Invoice', 'Delivery Note']: - rate = frappe.db.get_value(f'{voucher_type} Item', voucher_detail_no, 'incoming_rate') + if not (rate and return_against) and voucher_type in ["Sales Invoice", "Delivery Note"]: + rate = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "incoming_rate") if not rate and sle: - rate = get_incoming_rate({ - "item_code": sle.item_code, - "warehouse": sle.warehouse, - "posting_date": sle.get('posting_date'), - "posting_time": sle.get('posting_time'), - "qty": sle.actual_qty, - "serial_no": sle.get('serial_no'), - "company": sle.company, - "voucher_type": sle.voucher_type, - "voucher_no": sle.voucher_no - }, raise_error_if_no_rate=False) + rate = get_incoming_rate( + { + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.get("posting_date"), + "posting_time": sle.get("posting_time"), + "qty": sle.actual_qty, + "serial_no": sle.get("serial_no"), + "company": sle.company, + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no, + }, + raise_error_if_no_rate=False, + ) return rate + def get_return_against_item_fields(voucher_type): return_against_item_fields = { "Purchase Receipt": "purchase_receipt_item", "Purchase Invoice": "purchase_invoice_item", "Delivery Note": "dn_detail", - "Sales Invoice": "sales_invoice_item" + "Sales Invoice": "sales_invoice_item", } return return_against_item_fields[voucher_type] -def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, item_code, return_against_item_field, item_row): - filters = { - "voucher_type": voucher_type, - "voucher_no": return_against, - "item_code": item_code - } + +def get_filters( + voucher_type, + voucher_no, + voucher_detail_no, + return_against, + item_code, + return_against_item_field, + item_row, +): + filters = {"voucher_type": voucher_type, "voucher_no": return_against, "item_code": item_code} if item_row: reference_voucher_detail_no = item_row.get(return_against_item_field) else: - reference_voucher_detail_no = frappe.db.get_value(voucher_type + " Item", voucher_detail_no, return_against_item_field) + reference_voucher_detail_no = frappe.db.get_value( + voucher_type + " Item", voucher_detail_no, return_against_item_field + ) if reference_voucher_detail_no: filters["voucher_detail_no"] = reference_voucher_detail_no return filters + def get_returned_serial_nos(child_doc, parent_doc): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + return_ref_field = frappe.scrub(child_doc.doctype) if child_doc.doctype == "Delivery Note Item": return_ref_field = "dn_detail" @@ -463,10 +598,14 @@ def get_returned_serial_nos(child_doc, parent_doc): fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)] - filters = [[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "is_return", "=", 1], - [child_doc.doctype, return_ref_field, "=", child_doc.name], [parent_doc.doctype, "docstatus", "=", 1]] + filters = [ + [parent_doc.doctype, "return_against", "=", parent_doc.name], + [parent_doc.doctype, "is_return", "=", 1], + [child_doc.doctype, return_ref_field, "=", child_doc.name], + [parent_doc.doctype, "docstatus", "=", 1], + ] - for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): + for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): serial_nos.extend(get_serial_nos(row.serial_no)) return serial_nos diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 31b22093998..e1c59cbf672 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -16,9 +16,11 @@ from erpnext.stock.utils import get_incoming_rate class SellingController(StockController): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"] + def get_feed(self): - return _("To {0} | {1} {2}").format(self.customer_name, self.currency, - self.grand_total) + return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) def onload(self): super(SellingController, self).onload() @@ -64,32 +66,43 @@ class SellingController(StockController): if customer: from erpnext.accounts.party import _get_party_details + fetch_payment_terms_template = False - if (self.get("__islocal") or - self.company != frappe.db.get_value(self.doctype, self.name, 'company')): + if self.get("__islocal") or self.company != frappe.db.get_value( + self.doctype, self.name, "company" + ): fetch_payment_terms_template = True - party_details = _get_party_details(customer, + party_details = _get_party_details( + customer, ignore_permissions=self.flags.ignore_permissions, - doctype=self.doctype, company=self.company, - posting_date=self.get('posting_date'), + doctype=self.doctype, + company=self.company, + posting_date=self.get("posting_date"), fetch_payment_terms_template=fetch_payment_terms_template, - party_address=self.customer_address, shipping_address=self.shipping_address_name, - company_address=self.get('company_address')) + party_address=self.customer_address, + shipping_address=self.shipping_address_name, + company_address=self.get("company_address"), + ) if not self.meta.get_field("sales_team"): party_details.pop("sales_team") self.update_if_missing(party_details) elif lead: from erpnext.crm.doctype.lead.lead import get_lead_details - self.update_if_missing(get_lead_details(lead, - posting_date=self.get('transaction_date') or self.get('posting_date'), - company=self.company)) - if self.get('taxes_and_charges') and not self.get('taxes') and not for_validate: - taxes = get_taxes_and_charges('Sales Taxes and Charges Template', self.taxes_and_charges) + self.update_if_missing( + get_lead_details( + lead, + posting_date=self.get("transaction_date") or self.get("posting_date"), + company=self.company, + ) + ) + + if self.get("taxes_and_charges") and not self.get("taxes") and not for_validate: + taxes = get_taxes_and_charges("Sales Taxes and Charges Template", self.taxes_and_charges) for tax in taxes: - self.append('taxes', tax) + self.append("taxes", tax) def set_price_list_and_item_details(self, for_validate=False): self.set_price_list_currency("Selling") @@ -98,12 +111,15 @@ class SellingController(StockController): def remove_shipping_charge(self): if self.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule) - existing_shipping_charge = self.get("taxes", { - "doctype": "Sales Taxes and Charges", - "charge_type": "Actual", - "account_head": shipping_rule.account, - "cost_center": shipping_rule.cost_center - }) + existing_shipping_charge = self.get( + "taxes", + { + "doctype": "Sales Taxes and Charges", + "charge_type": "Actual", + "account_head": shipping_rule.account, + "cost_center": shipping_rule.cost_center, + }, + ) if existing_shipping_charge: self.get("taxes").remove(existing_shipping_charge[-1]) self.calculate_taxes_and_totals() @@ -112,8 +128,9 @@ class SellingController(StockController): from frappe.utils import money_in_words if self.meta.get_field("base_in_words"): - base_amount = abs(self.base_grand_total - if self.is_rounded_total_disabled() else self.base_rounded_total) + base_amount = abs( + self.base_grand_total if self.is_rounded_total_disabled() else self.base_rounded_total + ) self.base_in_words = money_in_words(base_amount, self.company_currency) if self.meta.get_field("in_words"): @@ -124,15 +141,15 @@ class SellingController(StockController): if not self.meta.get_field("commission_rate"): return - self.round_floats_in( - self, ("amount_eligible_for_commission", "commission_rate") - ) + self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate")) if not (0 <= self.commission_rate <= 100.0): - throw("{} {}".format( - _(self.meta.get_label("commission_rate")), - _("must be between 0 and 100"), - )) + throw( + "{} {}".format( + _(self.meta.get_label("commission_rate")), + _("must be between 0 and 100"), + ) + ) self.amount_eligible_for_commission = sum( item.base_net_amount for item in self.items if item.grant_commission @@ -140,7 +157,7 @@ class SellingController(StockController): self.total_commission = flt( self.amount_eligible_for_commission * self.commission_rate / 100.0, - self.precision("total_commission") + self.precision("total_commission"), ) def calculate_contribution(self): @@ -154,12 +171,14 @@ class SellingController(StockController): sales_person.allocated_amount = flt( self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, - self.precision("allocated_amount", sales_person)) + self.precision("allocated_amount", sales_person), + ) if sales_person.commission_rate: sales_person.incentives = flt( sales_person.allocated_amount * flt(sales_person.commission_rate) / 100.0, - self.precision("incentives", sales_person)) + self.precision("incentives", sales_person), + ) total += sales_person.allocated_percentage @@ -183,25 +202,29 @@ class SellingController(StockController): def validate_selling_price(self): def throw_message(idx, item_name, rate, ref_rate_field): - throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}. + throw( + _( + """Row #{0}: Selling rate for item {1} is lower than its {2}. Selling {3} should be atleast {4}.

Alternatively, you can disable selling price validation in {5} to bypass - this validation.""").format( - idx, - bold(item_name), - bold(ref_rate_field), - bold("net rate"), - bold(rate), - get_link_to_form("Selling Settings", "Selling Settings"), - ), title=_("Invalid Selling Price")) + this validation.""" + ).format( + idx, + bold(item_name), + bold(ref_rate_field), + bold("net rate"), + bold(rate), + get_link_to_form("Selling Settings", "Selling Settings"), + ), + title=_("Invalid Selling Price"), + ) - if ( - self.get("is_return") - or not frappe.db.get_single_value("Selling Settings", "validate_selling_price") + if self.get("is_return") or not frappe.db.get_single_value( + "Selling Settings", "validate_selling_price" ): return - is_internal_customer = self.get('is_internal_customer') + is_internal_customer = self.get("is_internal_customer") valuation_rate_map = {} for item in self.items: @@ -212,17 +235,10 @@ class SellingController(StockController): "Item", item.item_code, ("last_purchase_rate", "is_stock_item") ) - last_purchase_rate_in_sales_uom = ( - last_purchase_rate * (item.conversion_factor or 1) - ) + last_purchase_rate_in_sales_uom = last_purchase_rate * (item.conversion_factor or 1) if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom): - throw_message( - item.idx, - item.item_name, - last_purchase_rate_in_sales_uom, - "last purchase rate" - ) + throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate") if is_internal_customer or not is_stock_item: continue @@ -238,7 +254,8 @@ class SellingController(StockController): for valuation_rate in valuation_rate_map ) - valuation_rates = frappe.db.sql(f""" + valuation_rates = frappe.db.sql( + f""" select item_code, warehouse, valuation_rate from @@ -246,7 +263,9 @@ class SellingController(StockController): where ({" or ".join(or_conditions)}) and valuation_rate > 0 - """, as_dict=True) + """, + as_dict=True, + ) for rate in valuation_rates: valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate @@ -255,24 +274,15 @@ class SellingController(StockController): if not item.item_code or item.is_free_item: continue - last_valuation_rate = valuation_rate_map.get( - (item.item_code, item.warehouse) - ) + last_valuation_rate = valuation_rate_map.get((item.item_code, item.warehouse)) if not last_valuation_rate: continue - last_valuation_rate_in_sales_uom = ( - last_valuation_rate * (item.conversion_factor or 1) - ) + last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1) if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom): - throw_message( - item.idx, - item.item_name, - last_valuation_rate_in_sales_uom, - "valuation rate" - ) + throw_message(item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate") def get_item_list(self): il = [] @@ -284,68 +294,90 @@ class SellingController(StockController): for p in self.get("packed_items"): if p.parent_detail_docname == d.name and p.parent_item == d.item_code: # the packing details table's qty is already multiplied with parent's qty - il.append(frappe._dict({ - 'warehouse': p.warehouse or d.warehouse, - 'item_code': p.item_code, - 'qty': flt(p.qty), - 'uom': p.uom, - 'batch_no': cstr(p.batch_no).strip(), - 'serial_no': cstr(p.serial_no).strip(), - 'name': d.name, - 'target_warehouse': p.target_warehouse, - 'company': self.company, - 'voucher_type': self.doctype, - 'allow_zero_valuation': d.allow_zero_valuation_rate, - 'sales_invoice_item': d.get("sales_invoice_item"), - 'dn_detail': d.get("dn_detail"), - 'incoming_rate': p.get("incoming_rate") - })) + il.append( + frappe._dict( + { + "warehouse": p.warehouse or d.warehouse, + "item_code": p.item_code, + "qty": flt(p.qty), + "uom": p.uom, + "batch_no": cstr(p.batch_no).strip(), + "serial_no": cstr(p.serial_no).strip(), + "name": d.name, + "target_warehouse": p.target_warehouse, + "company": self.company, + "voucher_type": self.doctype, + "allow_zero_valuation": d.allow_zero_valuation_rate, + "sales_invoice_item": d.get("sales_invoice_item"), + "dn_detail": d.get("dn_detail"), + "incoming_rate": p.get("incoming_rate"), + } + ) + ) else: - il.append(frappe._dict({ - 'warehouse': d.warehouse, - 'item_code': d.item_code, - 'qty': d.stock_qty, - 'uom': d.uom, - 'stock_uom': d.stock_uom, - 'conversion_factor': d.conversion_factor, - 'batch_no': cstr(d.get("batch_no")).strip(), - 'serial_no': cstr(d.get("serial_no")).strip(), - 'name': d.name, - 'target_warehouse': d.target_warehouse, - 'company': self.company, - 'voucher_type': self.doctype, - 'allow_zero_valuation': d.allow_zero_valuation_rate, - 'sales_invoice_item': d.get("sales_invoice_item"), - 'dn_detail': d.get("dn_detail"), - 'incoming_rate': d.get("incoming_rate") - })) + il.append( + frappe._dict( + { + "warehouse": d.warehouse, + "item_code": d.item_code, + "qty": d.stock_qty, + "uom": d.uom, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "batch_no": cstr(d.get("batch_no")).strip(), + "serial_no": cstr(d.get("serial_no")).strip(), + "name": d.name, + "target_warehouse": d.target_warehouse, + "company": self.company, + "voucher_type": self.doctype, + "allow_zero_valuation": d.allow_zero_valuation_rate, + "sales_invoice_item": d.get("sales_invoice_item"), + "dn_detail": d.get("dn_detail"), + "incoming_rate": d.get("incoming_rate"), + } + ) + ) return il def has_product_bundle(self, item_code): - return frappe.db.sql("""select name from `tabProduct Bundle` - where new_item_code=%s and docstatus != 2""", item_code) + return frappe.db.sql( + """select name from `tabProduct Bundle` + where new_item_code=%s and docstatus != 2""", + item_code, + ) def get_already_delivered_qty(self, current_docname, so, so_detail): - delivered_via_dn = frappe.db.sql("""select sum(qty) from `tabDelivery Note Item` + delivered_via_dn = frappe.db.sql( + """select sum(qty) from `tabDelivery Note Item` where so_detail = %s and docstatus = 1 and against_sales_order = %s - and parent != %s""", (so_detail, so, current_docname)) + and parent != %s""", + (so_detail, so, current_docname), + ) - delivered_via_si = frappe.db.sql("""select sum(si_item.qty) + delivered_via_si = frappe.db.sql( + """select sum(si_item.qty) from `tabSales Invoice Item` si_item, `tabSales Invoice` si where si_item.parent = si.name and si.update_stock = 1 and si_item.so_detail = %s and si.docstatus = 1 and si_item.sales_order = %s - and si.name != %s""", (so_detail, so, current_docname)) + and si.name != %s""", + (so_detail, so, current_docname), + ) - total_delivered_qty = (flt(delivered_via_dn[0][0]) if delivered_via_dn else 0) \ - + (flt(delivered_via_si[0][0]) if delivered_via_si else 0) + total_delivered_qty = (flt(delivered_via_dn[0][0]) if delivered_via_dn else 0) + ( + flt(delivered_via_si[0][0]) if delivered_via_si else 0 + ) return total_delivered_qty def get_so_qty_and_warehouse(self, so_detail): - so_item = frappe.db.sql("""select qty, warehouse from `tabSales Order Item` - where name = %s and docstatus = 1""", so_detail, as_dict=1) + so_item = frappe.db.sql( + """select qty, warehouse from `tabSales Order Item` + where name = %s and docstatus = 1""", + so_detail, + as_dict=1, + ) so_qty = so_item and flt(so_item[0]["qty"]) or 0.0 so_warehouse = so_item and so_item[0]["warehouse"] or "" return so_qty, so_warehouse @@ -371,8 +403,9 @@ class SellingController(StockController): sales_order = frappe.get_doc("Sales Order", so) if sales_order.status in ["Closed", "Cancelled"]: - frappe.throw(_("{0} {1} is cancelled or closed").format(_("Sales Order"), so), - frappe.InvalidStatusError) + frappe.throw( + _("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError + ) sales_order.update_reserved_qty(so_item_rows) @@ -384,42 +417,51 @@ class SellingController(StockController): for d in items: if not self.get("return_against"): # Get incoming rate based on original item cost based on valuation method - qty = flt(d.get('stock_qty') or d.get('actual_qty')) + qty = flt(d.get("stock_qty") or d.get("actual_qty")) if not (self.get("is_return") and d.incoming_rate): - d.incoming_rate = get_incoming_rate({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": self.get('posting_date') or self.get('transaction_date'), - "posting_time": self.get('posting_time') or nowtime(), - "qty": qty if cint(self.get("is_return")) else (-1 * qty), - "serial_no": d.get('serial_no'), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - "allow_zero_valuation": d.get("allow_zero_valuation") - }, raise_error_if_no_rate=False) + d.incoming_rate = get_incoming_rate( + { + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": self.get("posting_date") or self.get("transaction_date"), + "posting_time": self.get("posting_time") or nowtime(), + "qty": qty if cint(self.get("is_return")) else (-1 * qty), + "serial_no": d.get("serial_no"), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation"), + }, + raise_error_if_no_rate=False, + ) # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): if d.doctype == "Packed Item": - incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate')) + incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision("incoming_rate")) if d.incoming_rate != incoming_rate: d.incoming_rate = incoming_rate else: - rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate")) if d.rate != rate: d.rate = rate d.discount_percentage = 0 d.discount_amount = 0 - frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") - .format(d.idx), alert=1) + frappe.msgprint( + _( + "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" + ).format(d.idx), + alert=1, + ) elif self.get("return_against"): # Get incoming rate of return entry from reference document # based on original item cost as per valuation method - d.incoming_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d) + d.incoming_rate = get_rate_for_return( + self.doctype, self.name, d.item_code, self.return_against, item_row=d + ) def update_stock_ledger(self): self.update_reserved_qty() @@ -428,63 +470,66 @@ class SellingController(StockController): # Loop over items and packed items table for d in self.get_item_list(): if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty): - if flt(d.conversion_factor)==0.0: - d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 + if flt(d.conversion_factor) == 0.0: + d.conversion_factor = ( + get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 + ) # On cancellation or return entry submission, make stock ledger entry for # target warehouse first, to update serial no values properly - if d.warehouse and ((not cint(self.is_return) and self.docstatus==1) - or (cint(self.is_return) and self.docstatus==2)): - sl_entries.append(self.get_sle_for_source_warehouse(d)) + if d.warehouse and ( + (not cint(self.is_return) and self.docstatus == 1) + or (cint(self.is_return) and self.docstatus == 2) + ): + sl_entries.append(self.get_sle_for_source_warehouse(d)) if d.target_warehouse: sl_entries.append(self.get_sle_for_target_warehouse(d)) - if d.warehouse and ((not cint(self.is_return) and self.docstatus==2) - or (cint(self.is_return) and self.docstatus==1)): - sl_entries.append(self.get_sle_for_source_warehouse(d)) + if d.warehouse and ( + (not cint(self.is_return) and self.docstatus == 2) + or (cint(self.is_return) and self.docstatus == 1) + ): + sl_entries.append(self.get_sle_for_source_warehouse(d)) self.make_sl_entries(sl_entries) def get_sle_for_source_warehouse(self, item_row): - sle = self.get_sl_entries(item_row, { - "actual_qty": -1*flt(item_row.qty), - "incoming_rate": item_row.incoming_rate, - "recalculate_rate": cint(self.is_return) - }) + sle = self.get_sl_entries( + item_row, + { + "actual_qty": -1 * flt(item_row.qty), + "incoming_rate": item_row.incoming_rate, + "recalculate_rate": cint(self.is_return), + }, + ) if item_row.target_warehouse and not cint(self.is_return): sle.dependant_sle_voucher_detail_no = item_row.name return sle def get_sle_for_target_warehouse(self, item_row): - sle = self.get_sl_entries(item_row, { - "actual_qty": flt(item_row.qty), - "warehouse": item_row.target_warehouse - }) + sle = self.get_sl_entries( + item_row, {"actual_qty": flt(item_row.qty), "warehouse": item_row.target_warehouse} + ) if self.docstatus == 1: if not cint(self.is_return): - sle.update({ - "incoming_rate": item_row.incoming_rate, - "recalculate_rate": 1 - }) + sle.update({"incoming_rate": item_row.incoming_rate, "recalculate_rate": 1}) else: - sle.update({ - "outgoing_rate": item_row.incoming_rate - }) + sle.update({"outgoing_rate": item_row.incoming_rate}) if item_row.warehouse: sle.dependant_sle_voucher_detail_no = item_row.name return sle def set_po_nos(self, for_validate=False): - if self.doctype == 'Sales Invoice' and hasattr(self, "items"): + if self.doctype == "Sales Invoice" and hasattr(self, "items"): if for_validate and self.po_no: return self.set_pos_for_sales_invoice() - if self.doctype == 'Delivery Note' and hasattr(self, "items"): + if self.doctype == "Delivery Note" and hasattr(self, "items"): if for_validate and self.po_no: return self.set_pos_for_delivery_note() @@ -493,34 +538,39 @@ class SellingController(StockController): po_nos = [] if self.po_no: po_nos.append(self.po_no) - self.get_po_nos('Sales Order', 'sales_order', po_nos) - self.get_po_nos('Delivery Note', 'delivery_note', po_nos) - self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) + self.get_po_nos("Sales Order", "sales_order", po_nos) + self.get_po_nos("Delivery Note", "delivery_note", po_nos) + self.po_no = ", ".join(list(set(x.strip() for x in ",".join(po_nos).split(",")))) def set_pos_for_delivery_note(self): po_nos = [] if self.po_no: po_nos.append(self.po_no) - self.get_po_nos('Sales Order', 'against_sales_order', po_nos) - self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) - self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) + self.get_po_nos("Sales Order", "against_sales_order", po_nos) + self.get_po_nos("Sales Invoice", "against_sales_invoice", po_nos) + self.po_no = ", ".join(list(set(x.strip() for x in ",".join(po_nos).split(",")))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): doc_list = list(set(d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname))) if doc_list: - po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')] + po_nos += [ + d.po_no + for d in frappe.get_all(ref_doctype, "po_no", filters={"name": ("in", doc_list)}) + if d.get("po_no") + ] def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: - item.gross_profit = flt(((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item)) - + item.gross_profit = flt( + ((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item) + ) def set_customer_address(self): address_dict = { - 'customer_address': 'address_display', - 'shipping_address_name': 'shipping_address', - 'company_address': 'company_address_display' + "customer_address": "address_display", + "shipping_address_name": "shipping_address", + "company_address": "company_address_display", } for address_field, address_display_field in address_dict.items(): @@ -536,15 +586,31 @@ class SellingController(StockController): if self.doctype == "POS Invoice": return - for d in self.get('items'): + for d in self.get("items"): if self.doctype == "Sales Invoice": - stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or ''] + stock_items = [ + d.item_code, + d.description, + d.warehouse, + d.sales_order or d.delivery_note, + d.batch_no or "", + ] non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note] elif self.doctype == "Delivery Note": - stock_items = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or ''] - non_stock_items = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice] + stock_items = [ + d.item_code, + d.description, + d.warehouse, + d.against_sales_order or d.against_sales_invoice, + d.batch_no or "", + ] + non_stock_items = [ + d.item_code, + d.description, + d.against_sales_order or d.against_sales_invoice, + ] elif self.doctype in ["Sales Order", "Quotation"]: - stock_items = [d.item_code, d.description, d.warehouse, ''] + stock_items = [d.item_code, d.description, d.warehouse, ""] non_stock_items = [d.item_code, d.description] if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1: @@ -552,7 +618,7 @@ class SellingController(StockController): duplicate_items_msg += "

" duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format( frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"), - get_link_to_form("Selling Settings", "Selling Settings") + get_link_to_form("Selling Settings", "Selling Settings"), ) if stock_items in check_list: frappe.throw(duplicate_items_msg) @@ -570,22 +636,26 @@ class SellingController(StockController): for d in items: if d.get("target_warehouse") and d.get("warehouse") == d.get("target_warehouse"): warehouse = frappe.bold(d.get("target_warehouse")) - frappe.throw(_("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same") - .format(d.idx, warehouse, warehouse)) + frappe.throw( + _("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same").format( + d.idx, warehouse, warehouse + ) + ) if not self.get("is_internal_customer") and any(d.get("target_warehouse") for d in items): msg = _("Target Warehouse is set for some items but the customer is not an internal customer.") msg += " " + _("This {} will be treated as material transfer.").format(_(self.doctype)) frappe.msgprint(msg, title="Internal Transfer", alert=True) - def validate_items(self): # validate items to see if they have is_sales_item enabled from erpnext.controllers.buying_controller import validate_item_type + validate_item_type(self, "is_sales_item", "sales") + def set_default_income_account_for_item(obj): for d in obj.get("items"): if d.item_code: if getattr(d, "income_account", None): - set_item_default(d.item_code, obj.company, 'income_account', d.income_account) + set_item_default(d.item_code, obj.company, "income_account", d.income_account) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index affde4aa8ab..3c0a10e0860 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -8,12 +8,15 @@ from frappe.model.document import Document from frappe.utils import comma_or, flt, getdate, now, nowdate -class OverAllowanceError(frappe.ValidationError): pass +class OverAllowanceError(frappe.ValidationError): + pass + def validate_status(status, options): if status not in options: frappe.throw(_("Status must be one of {0}").format(comma_or(options))) + status_map = { "Lead": [ ["Lost Quotation", "has_lost_quotation"], @@ -26,7 +29,7 @@ status_map = { ["Lost", "has_lost_quotation"], ["Quotation", "has_active_quotation"], ["Converted", "has_ordered_quotation"], - ["Closed", "eval:self.status=='Closed'"] + ["Closed", "eval:self.status=='Closed'"], ], "Quotation": [ ["Draft", None], @@ -37,20 +40,41 @@ status_map = { ], "Sales Order": [ ["Draft", None], - ["To Deliver and Bill", "eval:self.per_delivered < 100 and self.per_billed < 100 and self.docstatus == 1"], - ["To Bill", "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1"], - ["To Deliver", "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1 and not self.skip_delivery_note"], - ["Completed", "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1"], + [ + "To Deliver and Bill", + "eval:self.per_delivered < 100 and self.per_billed < 100 and self.docstatus == 1", + ], + [ + "To Bill", + "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1", + ], + [ + "To Deliver", + "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1 and not self.skip_delivery_note", + ], + [ + "Completed", + "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1", + ], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed'"], ["On Hold", "eval:self.status=='On Hold'"], ], "Purchase Order": [ ["Draft", None], - ["To Receive and Bill", "eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1"], + [ + "To Receive and Bill", + "eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1", + ], ["To Bill", "eval:self.per_received >= 100 and self.per_billed < 100 and self.docstatus == 1"], - ["To Receive", "eval:self.per_received < 100 and self.per_billed == 100 and self.docstatus == 1"], - ["Completed", "eval:self.per_received >= 100 and self.per_billed == 100 and self.docstatus == 1"], + [ + "To Receive", + "eval:self.per_received < 100 and self.per_billed == 100 and self.docstatus == 1", + ], + [ + "Completed", + "eval:self.per_received >= 100 and self.per_billed == 100 and self.docstatus == 1", + ], ["Delivered", "eval:self.status=='Delivered'"], ["Cancelled", "eval:self.docstatus==2"], ["On Hold", "eval:self.status=='On Hold'"], @@ -77,18 +101,39 @@ status_map = { ["Stopped", "eval:self.status == 'Stopped'"], ["Cancelled", "eval:self.docstatus == 2"], ["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"], - ["Ordered", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"], - ["Transferred", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'"], - ["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"], - ["Received", "eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"], - ["Partially Received", "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"], - ["Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1"], - ["Manufactured", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'"] + [ + "Ordered", + "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'", + ], + [ + "Transferred", + "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'", + ], + [ + "Issued", + "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'", + ], + [ + "Received", + "eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'", + ], + [ + "Partially Received", + "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'", + ], + [ + "Partially Ordered", + "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1", + ], + [ + "Manufactured", + "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'", + ], ], "Bank Transaction": [ ["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"], ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"], - ["Cancelled", "eval:self.docstatus == 2"] + ["Cancelled", "eval:self.docstatus == 2"], ], "POS Opening Entry": [ ["Draft", None], @@ -106,15 +151,16 @@ status_map = { "Transaction Deletion Record": [ ["Draft", None], ["Completed", "eval:self.docstatus == 1"], - ] + ], } + class StatusUpdater(Document): """ - Updates the status of the calling records - Delivery Note: Update Delivered Qty, Update Percent and Validate over delivery - Sales Invoice: Update Billed Amt, Update Percent and Validate over billing - Installation Note: Update Installed Qty, Update Percent Qty and Validate over installation + Updates the status of the calling records + Delivery Note: Update Delivered Qty, Update Percent and Validate over delivery + Sales Invoice: Update Billed Amt, Update Percent and Validate over billing + Installation Note: Update Installed Qty, Update Percent Qty and Validate over installation """ def update_prevdoc_status(self): @@ -123,8 +169,8 @@ class StatusUpdater(Document): def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): - if self.get('amended_from'): - self.status = 'Draft' + if self.get("amended_from"): + self.status = "Draft" return if self.doctype in status_map: @@ -139,20 +185,33 @@ class StatusUpdater(Document): self.status = s[0] break elif s[1].startswith("eval:"): - if frappe.safe_eval(s[1][5:], None, { "self": self.as_dict(), "getdate": getdate, - "nowdate": nowdate, "get_value": frappe.db.get_value }): + if frappe.safe_eval( + s[1][5:], + None, + { + "self": self.as_dict(), + "getdate": getdate, + "nowdate": nowdate, + "get_value": frappe.db.get_value, + }, + ): self.status = s[0] break elif getattr(self, s[1])(): self.status = s[0] break - if self.status != _status and self.status not in ("Cancelled", "Partially Ordered", - "Ordered", "Issued", "Transferred"): + if self.status != _status and self.status not in ( + "Cancelled", + "Partially Ordered", + "Ordered", + "Issued", + "Transferred", + ): self.add_comment("Label", _(self.status)) if update: - self.db_set('status', self.status, update_modified = update_modified) + self.db_set("status", self.status, update_modified=update_modified) def validate_qty(self): """Validates qty at row level""" @@ -167,57 +226,78 @@ class StatusUpdater(Document): # get unique transactions to update for d in self.get_all_children(): - if hasattr(d, 'qty') and d.qty < 0 and not self.get('is_return'): + if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code)) - if hasattr(d, 'qty') and d.qty > 0 and self.get('is_return'): + if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) - if d.doctype == args['source_dt'] and d.get(args["join_field"]): - args['name'] = d.get(args['join_field']) + if d.doctype == args["source_dt"] and d.get(args["join_field"]): + args["name"] = d.get(args["join_field"]) # get all qty where qty > target_field - item = frappe.db.sql("""select item_code, `{target_ref_field}`, + item = frappe.db.sql( + """select item_code, `{target_ref_field}`, `{target_field}`, parenttype, parent from `tab{target_dt}` where `{target_ref_field}` < `{target_field}` - and name=%s and docstatus=1""".format(**args), - args['name'], as_dict=1) + and name=%s and docstatus=1""".format( + **args + ), + args["name"], + as_dict=1, + ) if item: item = item[0] - item['idx'] = d.idx - item['target_ref_field'] = args['target_ref_field'].replace('_', ' ') + item["idx"] = d.idx + item["target_ref_field"] = args["target_ref_field"].replace("_", " ") # if not item[args['target_ref_field']]: # msgprint(_("Note: System will not check over-delivery and over-booking for Item {0} as quantity or amount is 0").format(item.item_code)) - if args.get('no_allowance'): - item['reduce_by'] = item[args['target_field']] - item[args['target_ref_field']] - if item['reduce_by'] > .01: + if args.get("no_allowance"): + item["reduce_by"] = item[args["target_field"]] - item[args["target_ref_field"]] + if item["reduce_by"] > 0.01: self.limits_crossed_error(args, item, "qty") - elif item[args['target_ref_field']]: + elif item[args["target_ref_field"]]: self.check_overflow_with_allowance(item, args) def check_overflow_with_allowance(self, item, args): """ - Checks if there is overflow condering a relaxation allowance + Checks if there is overflow condering a relaxation allowance """ - qty_or_amount = "qty" if "qty" in args['target_ref_field'] else "amount" + qty_or_amount = "qty" if "qty" in args["target_ref_field"] else "amount" # check if overflow is within allowance - allowance, self.item_allowance, self.global_qty_allowance, self.global_amount_allowance = \ - get_allowance_for(item['item_code'], self.item_allowance, - self.global_qty_allowance, self.global_amount_allowance, qty_or_amount) + ( + allowance, + self.item_allowance, + self.global_qty_allowance, + self.global_amount_allowance, + ) = get_allowance_for( + item["item_code"], + self.item_allowance, + self.global_qty_allowance, + self.global_amount_allowance, + qty_or_amount, + ) - role_allowed_to_over_deliver_receive = frappe.db.get_single_value('Stock Settings', 'role_allowed_to_over_deliver_receive') - role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') - role = role_allowed_to_over_deliver_receive if qty_or_amount == 'qty' else role_allowed_to_over_bill + role_allowed_to_over_deliver_receive = frappe.db.get_single_value( + "Stock Settings", "role_allowed_to_over_deliver_receive" + ) + role_allowed_to_over_bill = frappe.db.get_single_value( + "Accounts Settings", "role_allowed_to_over_bill" + ) + role = ( + role_allowed_to_over_deliver_receive if qty_or_amount == "qty" else role_allowed_to_over_bill + ) - overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) / - item[args['target_ref_field']]) * 100 + overflow_percent = ( + (item[args["target_field"]] - item[args["target_ref_field"]]) / item[args["target_ref_field"]] + ) * 100 if overflow_percent - allowance > 0.01: - item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100) - item['reduce_by'] = item[args['target_field']] - item['max_allowed'] + item["max_allowed"] = flt(item[args["target_ref_field"]] * (100 + allowance) / 100) + item["reduce_by"] = item[args["target_field"]] - item["max_allowed"] if role not in frappe.get_roles(): self.limits_crossed_error(args, item, qty_or_amount) @@ -225,45 +305,55 @@ class StatusUpdater(Document): self.warn_about_bypassing_with_role(item, qty_or_amount, role) def limits_crossed_error(self, args, item, qty_or_amount): - '''Raise exception for limits crossed''' + """Raise exception for limits crossed""" if qty_or_amount == "qty": - action_msg = _('To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.') + action_msg = _( + 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' + ) else: - action_msg = _('To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.') + action_msg = _( + 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.' + ) - frappe.throw(_('This document is over limit by {0} {1} for item {4}. Are you making another {3} against the same {2}?') - .format( + frappe.throw( + _( + "This document is over limit by {0} {1} for item {4}. Are you making another {3} against the same {2}?" + ).format( frappe.bold(_(item["target_ref_field"].title())), frappe.bold(item["reduce_by"]), - frappe.bold(_(args.get('target_dt'))), + frappe.bold(_(args.get("target_dt"))), frappe.bold(_(self.doctype)), - frappe.bold(item.get('item_code')) - ) + '

' + action_msg, OverAllowanceError, title = _('Limit Crossed')) + frappe.bold(item.get("item_code")), + ) + + "

" + + action_msg, + OverAllowanceError, + title=_("Limit Crossed"), + ) def warn_about_bypassing_with_role(self, item, qty_or_amount, role): action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling") - msg = (_("{} of {} {} ignored for item {} because you have {} role.") - .format( - action, - _(item["target_ref_field"].title()), - frappe.bold(item["reduce_by"]), - frappe.bold(item.get('item_code')), - role) - ) + msg = _("{} of {} {} ignored for item {} because you have {} role.").format( + action, + _(item["target_ref_field"].title()), + frappe.bold(item["reduce_by"]), + frappe.bold(item.get("item_code")), + role, + ) frappe.msgprint(msg, indicator="orange", alert=True) def update_qty(self, update_modified=True): """Updates qty or amount at row level - :param update_modified: If true, updates `modified` and `modified_by` for target parent doc + :param update_modified: If true, updates `modified` and `modified_by` for target parent doc """ for args in self.status_updater: # condition to include current record (if submit or no if cancel) if self.docstatus == 1: - args['cond'] = ' or parent="%s"' % self.name.replace('"', '\"') + args["cond"] = ' or parent="%s"' % self.name.replace('"', '"') else: - args['cond'] = ' and parent!="%s"' % self.name.replace('"', '\"') + args["cond"] = ' and parent!="%s"' % self.name.replace('"', '"') self._update_children(args, update_modified) @@ -273,56 +363,73 @@ class StatusUpdater(Document): def _update_children(self, args, update_modified): """Update quantities or amount in child table""" for d in self.get_all_children(): - if d.doctype != args['source_dt']: + if d.doctype != args["source_dt"]: continue self._update_modified(args, update_modified) # updates qty in the child table - args['detail_id'] = d.get(args['join_field']) + args["detail_id"] = d.get(args["join_field"]) - args['second_source_condition'] = "" - if args.get('second_source_dt') and args.get('second_source_field') \ - and args.get('second_join_field'): + args["second_source_condition"] = "" + if ( + args.get("second_source_dt") + and args.get("second_source_field") + and args.get("second_join_field") + ): if not args.get("second_source_extra_cond"): args["second_source_extra_cond"] = "" - args['second_source_condition'] = frappe.db.sql(""" select ifnull((select sum(%(second_source_field)s) + args["second_source_condition"] = frappe.db.sql( + """ select ifnull((select sum(%(second_source_field)s) from `tab%(second_source_dt)s` where `%(second_join_field)s`="%(detail_id)s" and (`tab%(second_source_dt)s`.docstatus=1) - %(second_source_extra_cond)s), 0) """ % args)[0][0] + %(second_source_extra_cond)s), 0) """ + % args + )[0][0] - if args['detail_id']: - if not args.get("extra_cond"): args["extra_cond"] = "" + if args["detail_id"]: + if not args.get("extra_cond"): + args["extra_cond"] = "" - args["source_dt_value"] = frappe.db.sql(""" + args["source_dt_value"] = ( + frappe.db.sql( + """ (select ifnull(sum(%(source_field)s), 0) from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s" and (docstatus=1 %(cond)s) %(extra_cond)s) - """ % args)[0][0] or 0.0 + """ + % args + )[0][0] + or 0.0 + ) - if args['second_source_condition']: - args["source_dt_value"] += flt(args['second_source_condition']) + if args["second_source_condition"]: + args["source_dt_value"] += flt(args["second_source_condition"]) - frappe.db.sql("""update `tab%(target_dt)s` + frappe.db.sql( + """update `tab%(target_dt)s` set %(target_field)s = %(source_dt_value)s %(update_modified)s - where name='%(detail_id)s'""" % args) + where name='%(detail_id)s'""" + % args + ) def _update_percent_field_in_targets(self, args, update_modified=True): """Update percent field in parent transaction""" - if args.get('percent_join_field_parent'): + if args.get("percent_join_field_parent"): # if reference to target doc where % is to be updated, is # in source doc's parent form, consider percent_join_field_parent - args['name'] = self.get(args['percent_join_field_parent']) + args["name"] = self.get(args["percent_join_field_parent"]) self._update_percent_field(args, update_modified) else: - distinct_transactions = set(d.get(args['percent_join_field']) - for d in self.get_all_children(args['source_dt'])) + distinct_transactions = set( + d.get(args["percent_join_field"]) for d in self.get_all_children(args["source_dt"]) + ) for name in distinct_transactions: if name: - args['name'] = name + args["name"] = name self._update_percent_field(args, update_modified) def _update_percent_field(self, args, update_modified=True): @@ -330,23 +437,29 @@ class StatusUpdater(Document): self._update_modified(args, update_modified) - if args.get('target_parent_field'): - frappe.db.sql("""update `tab%(target_parent_dt)s` + if args.get("target_parent_field"): + frappe.db.sql( + """update `tab%(target_parent_dt)s` set %(target_parent_field)s = round( ifnull((select ifnull(sum(if(abs(%(target_ref_field)s) > abs(%(target_field)s), abs(%(target_field)s), abs(%(target_ref_field)s))), 0) / sum(abs(%(target_ref_field)s)) * 100 from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6) %(update_modified)s - where name='%(name)s'""" % args) + where name='%(name)s'""" + % args + ) # update field - if args.get('status_field'): - frappe.db.sql("""update `tab%(target_parent_dt)s` + if args.get("status_field"): + frappe.db.sql( + """update `tab%(target_parent_dt)s` set %(status_field)s = if(%(target_parent_field)s<0.001, 'Not %(keyword)s', if(%(target_parent_field)s>=99.999999, 'Fully %(keyword)s', 'Partly %(keyword)s')) - where name='%(name)s'""" % args) + where name='%(name)s'""" + % args + ) if update_modified: target = frappe.get_doc(args["target_parent_dt"], args["name"]) @@ -355,22 +468,24 @@ class StatusUpdater(Document): def _update_modified(self, args, update_modified): if not update_modified: - args['update_modified'] = '' + args["update_modified"] = "" return - args['update_modified'] = ', modified = {0}, modified_by = {1}'.format( - frappe.db.escape(now()), - frappe.db.escape(frappe.session.user) + args["update_modified"] = ", modified = {0}, modified_by = {1}".format( + frappe.db.escape(now()), frappe.db.escape(frappe.session.user) ) def update_billing_status_for_zero_amount_refdoc(self, ref_dt): ref_fieldname = frappe.scrub(ref_dt) - ref_docs = [item.get(ref_fieldname) for item in (self.get('items') or []) if item.get(ref_fieldname)] + ref_docs = [ + item.get(ref_fieldname) for item in (self.get("items") or []) if item.get(ref_fieldname) + ] if not ref_docs: return - zero_amount_refdocs = frappe.db.sql_list(""" + zero_amount_refdocs = frappe.db.sql_list( + """ SELECT name from @@ -379,21 +494,34 @@ class StatusUpdater(Document): docstatus = 1 and base_net_total = 0 and name in %(ref_docs)s - """.format(ref_dt=ref_dt), { - 'ref_docs': ref_docs - }) + """.format( + ref_dt=ref_dt + ), + {"ref_docs": ref_docs}, + ) if zero_amount_refdocs: self.update_billing_status(zero_amount_refdocs, ref_dt, ref_fieldname) def update_billing_status(self, zero_amount_refdoc, ref_dt, ref_fieldname): for ref_dn in zero_amount_refdoc: - ref_doc_qty = flt(frappe.db.sql("""select ifnull(sum(qty), 0) from `tab%s Item` - where parent=%s""" % (ref_dt, '%s'), (ref_dn))[0][0]) + ref_doc_qty = flt( + frappe.db.sql( + """select ifnull(sum(qty), 0) from `tab%s Item` + where parent=%s""" + % (ref_dt, "%s"), + (ref_dn), + )[0][0] + ) - billed_qty = flt(frappe.db.sql("""select ifnull(sum(qty), 0) - from `tab%s Item` where %s=%s and docstatus=1""" % - (self.doctype, ref_fieldname, '%s'), (ref_dn))[0][0]) + billed_qty = flt( + frappe.db.sql( + """select ifnull(sum(qty), 0) + from `tab%s Item` where %s=%s and docstatus=1""" + % (self.doctype, ref_fieldname, "%s"), + (ref_dn), + )[0][0] + ) per_billed = (min(ref_doc_qty, billed_qty) / ref_doc_qty) * 100 @@ -402,7 +530,7 @@ class StatusUpdater(Document): ref_doc.db_set("per_billed", per_billed) # set billling status - if hasattr(ref_doc, 'billing_status'): + if hasattr(ref_doc, "billing_status"): if ref_doc.per_billed < 0.001: ref_doc.db_set("billing_status", "Not Billed") elif ref_doc.per_billed > 99.999999: @@ -412,29 +540,51 @@ class StatusUpdater(Document): ref_doc.set_status(update=True) -def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"): + +def get_allowance_for( + item_code, + item_allowance=None, + global_qty_allowance=None, + global_amount_allowance=None, + qty_or_amount="qty", +): """ - Returns the allowance for the item, if not set, returns global allowance + Returns the allowance for the item, if not set, returns global allowance """ if item_allowance is None: item_allowance = {} if qty_or_amount == "qty": if item_allowance.get(item_code, frappe._dict()).get("qty"): - return item_allowance[item_code].qty, item_allowance, global_qty_allowance, global_amount_allowance + return ( + item_allowance[item_code].qty, + item_allowance, + global_qty_allowance, + global_amount_allowance, + ) else: if item_allowance.get(item_code, frappe._dict()).get("amount"): - return item_allowance[item_code].amount, item_allowance, global_qty_allowance, global_amount_allowance + return ( + item_allowance[item_code].amount, + item_allowance, + global_qty_allowance, + global_amount_allowance, + ) - qty_allowance, over_billing_allowance = \ - frappe.db.get_value('Item', item_code, ['over_delivery_receipt_allowance', 'over_billing_allowance']) + qty_allowance, over_billing_allowance = frappe.db.get_value( + "Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"] + ) if qty_or_amount == "qty" and not qty_allowance: if global_qty_allowance == None: - global_qty_allowance = flt(frappe.db.get_single_value('Stock Settings', 'over_delivery_receipt_allowance')) + global_qty_allowance = flt( + frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") + ) qty_allowance = global_qty_allowance elif qty_or_amount == "amount" and not over_billing_allowance: if global_amount_allowance == None: - global_amount_allowance = flt(frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance')) + global_amount_allowance = flt( + frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") + ) over_billing_allowance = global_amount_allowance if qty_or_amount == "qty": diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8972c328796..feec42f43a3 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -21,14 +21,22 @@ from erpnext.stock import get_warehouse_account_map from erpnext.stock.stock_ledger import get_items_to_be_repost -class QualityInspectionRequiredError(frappe.ValidationError): pass -class QualityInspectionRejectedError(frappe.ValidationError): pass -class QualityInspectionNotSubmittedError(frappe.ValidationError): pass +class QualityInspectionRequiredError(frappe.ValidationError): + pass + + +class QualityInspectionRejectedError(frappe.ValidationError): + pass + + +class QualityInspectionNotSubmittedError(frappe.ValidationError): + pass + class StockController(AccountsController): def validate(self): super(StockController, self).validate() - if not self.get('is_return'): + if not self.get("is_return"): self.validate_inspection() self.validate_serialized_batch() self.clean_serial_nos() @@ -41,44 +49,56 @@ class StockController(AccountsController): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - provisional_accounting_for_non_stock_items = \ - cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) - if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items: + if ( + cint(erpnext.is_perpetual_inventory_enabled(self.company)) + or provisional_accounting_for_non_stock_items + ): warehouse_account = get_warehouse_account_map(self.company) - if self.docstatus==1: + if self.docstatus == 1: if not gl_entries: gl_entries = self.get_gl_entries(warehouse_account) make_gl_entries(gl_entries, from_repost=from_repost) - elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1: + elif self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self.docstatus == 1: gl_entries = [] gl_entries = self.get_asset_gl_entry(gl_entries) make_gl_entries(gl_entries, from_repost=from_repost) def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + for d in self.get("items"): - if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no: - serial_nos = frappe.get_all("Serial No", + if hasattr(d, "serial_no") and hasattr(d, "batch_no") and d.serial_no and d.batch_no: + serial_nos = frappe.get_all( + "Serial No", fields=["batch_no", "name", "warehouse"], - filters={ - "name": ("in", get_serial_nos(d.serial_no)) - } + filters={"name": ("in", get_serial_nos(d.serial_no))}, ) for row in serial_nos: if row.warehouse and row.batch_no != d.batch_no: - frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") - .format(d.idx, row.name, d.batch_no)) + frappe.throw( + _("Row #{0}: Serial No {1} does not belong to Batch {2}").format( + d.idx, row.name, d.batch_no + ) + ) if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") if expiry_date and getdate(expiry_date) < getdate(self.posting_date): - frappe.throw(_("Row #{0}: The batch {1} has already expired.") - .format(d.idx, get_link_to_form("Batch", d.get("batch_no")))) + frappe.throw( + _("Row #{0}: The batch {1} has already expired.").format( + d.idx, get_link_to_form("Batch", d.get("batch_no")) + ) + ) def clean_serial_nos(self): from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string @@ -88,13 +108,14 @@ class StockController(AccountsController): # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) - for row in self.get('packed_items') or []: + for row in self.get("packed_items") or []: if hasattr(row, "serial_no") and row.serial_no: # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) - def get_gl_entries(self, warehouse_account=None, default_expense_account=None, - default_cost_center=None): + def get_gl_entries( + self, warehouse_account=None, default_expense_account=None, default_cost_center=None + ): if not warehouse_account: warehouse_account = get_warehouse_account_map(self.company) @@ -116,44 +137,61 @@ class StockController(AccountsController): self.check_expense_account(item_row) # expense account/ target_warehouse / source_warehouse - if item_row.get('target_warehouse'): - warehouse = item_row.get('target_warehouse') + if item_row.get("target_warehouse"): + warehouse = item_row.get("target_warehouse") expense_account = warehouse_account[warehouse]["account"] else: expense_account = item_row.expense_account - gl_list.append(self.get_gl_dict({ - "account": warehouse_account[sle.warehouse]["account"], - "against": expense_account, - "cost_center": item_row.cost_center, - "project": item_row.project or self.get('project'), - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(sle.stock_value_difference, precision), - "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", - }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) + gl_list.append( + self.get_gl_dict( + { + "account": warehouse_account[sle.warehouse]["account"], + "against": expense_account, + "cost_center": item_row.cost_center, + "project": item_row.project or self.get("project"), + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(sle.stock_value_difference, precision), + "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", + }, + warehouse_account[sle.warehouse]["account_currency"], + item=item_row, + ) + ) - gl_list.append(self.get_gl_dict({ - "account": expense_account, - "against": warehouse_account[sle.warehouse]["account"], - "cost_center": item_row.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(sle.stock_value_difference, precision), - "project": item_row.get("project") or self.get("project"), - "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No" - }, item=item_row)) + gl_list.append( + self.get_gl_dict( + { + "account": expense_account, + "against": warehouse_account[sle.warehouse]["account"], + "cost_center": item_row.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(sle.stock_value_difference, precision), + "project": item_row.get("project") or self.get("project"), + "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", + }, + item=item_row, + ) + ) elif sle.warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(sle.warehouse) if warehouse_with_no_account: for wh in warehouse_with_no_account: if frappe.db.get_value("Warehouse", wh, "company"): - frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company)) + frappe.throw( + _( + "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." + ).format(wh, self.company) + ) return process_gl_map(gl_list, precision=precision) def get_debit_field_precision(self): if not frappe.flags.debit_field_precision: - frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency") + frappe.flags.debit_field_precision = frappe.get_precision( + "GL Entry", "debit_in_account_currency" + ) return frappe.flags.debit_field_precision @@ -163,12 +201,16 @@ class StockController(AccountsController): is_opening = "Yes" if reconciliation_purpose == "Opening Stock" else "No" details = [] for voucher_detail_no in sle_map: - details.append(frappe._dict({ - "name": voucher_detail_no, - "expense_account": default_expense_account, - "cost_center": default_cost_center, - "is_opening": is_opening - })) + details.append( + frappe._dict( + { + "name": voucher_detail_no, + "expense_account": default_expense_account, + "cost_center": default_cost_center, + "is_opening": is_opening, + } + ) + ) return details else: details = self.get("items") @@ -207,7 +249,8 @@ class StockController(AccountsController): def get_stock_ledger_details(self): stock_ledger = {} - stock_ledger_entries = frappe.db.sql(""" + stock_ledger_entries = frappe.db.sql( + """ select name, warehouse, stock_value_difference, valuation_rate, voucher_detail_no, item_code, posting_date, posting_time, @@ -216,110 +259,154 @@ class StockController(AccountsController): `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s and is_cancelled = 0 - """, (self.doctype, self.name), as_dict=True) + """, + (self.doctype, self.name), + as_dict=True, + ) for sle in stock_ledger_entries: stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) return stock_ledger def make_batches(self, warehouse_field): - '''Create batches if required. Called before submit''' + """Create batches if required. Called before submit""" for d in self.items: if d.get(warehouse_field) and not d.batch_no: - has_batch_no, create_new_batch = frappe.db.get_value('Item', d.item_code, ['has_batch_no', 'create_new_batch']) + has_batch_no, create_new_batch = frappe.db.get_value( + "Item", d.item_code, ["has_batch_no", "create_new_batch"] + ) if has_batch_no and create_new_batch: - d.batch_no = frappe.get_doc(dict( - doctype='Batch', - item=d.item_code, - supplier=getattr(self, 'supplier', None), - reference_doctype=self.doctype, - reference_name=self.name)).insert().name + d.batch_no = ( + frappe.get_doc( + dict( + doctype="Batch", + item=d.item_code, + supplier=getattr(self, "supplier", None), + reference_doctype=self.doctype, + reference_name=self.name, + ) + ) + .insert() + .name + ) def check_expense_account(self, item): if not item.get("expense_account"): msg = _("Please set an Expense Account in the Items table") - frappe.throw(_("Row #{0}: Expense Account not set for the Item {1}. {2}") - .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing")) + frappe.throw( + _("Row #{0}: Expense Account not set for the Item {1}. {2}").format( + item.idx, frappe.bold(item.item_code), msg + ), + title=_("Expense Account Missing"), + ) else: - is_expense_account = frappe.get_cached_value("Account", - item.get("expense_account"), "report_type")=="Profit and Loss" - if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account: - frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account") - .format(item.get("expense_account"))) + is_expense_account = ( + frappe.get_cached_value("Account", item.get("expense_account"), "report_type") + == "Profit and Loss" + ) + if ( + self.doctype + not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") + and not is_expense_account + ): + frappe.throw( + _("Expense / Difference account ({0}) must be a 'Profit or Loss' account").format( + item.get("expense_account") + ) + ) if is_expense_account and not item.get("cost_center"): - frappe.throw(_("{0} {1}: Cost Center is mandatory for Item {2}").format( - _(self.doctype), self.name, item.get("item_code"))) + frappe.throw( + _("{0} {1}: Cost Center is mandatory for Item {2}").format( + _(self.doctype), self.name, item.get("item_code") + ) + ) def delete_auto_created_batches(self): for d in self.items: - if not d.batch_no: continue + if not d.batch_no: + continue - frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None) + frappe.db.set_value( + "Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None + ) d.batch_no = None d.db_set("batch_no", None) - for data in frappe.get_all("Batch", - {'reference_name': self.name, 'reference_doctype': self.doctype}): + for data in frappe.get_all( + "Batch", {"reference_name": self.name, "reference_doctype": self.doctype} + ): frappe.delete_doc("Batch", data.name) def get_sl_entries(self, d, args): - sl_dict = frappe._dict({ - "item_code": d.get("item_code", None), - "warehouse": d.get("warehouse", None), - "posting_date": self.posting_date, - "posting_time": self.posting_time, - 'fiscal_year': get_fiscal_year(self.posting_date, company=self.company)[0], - "voucher_type": self.doctype, - "voucher_no": self.name, - "voucher_detail_no": d.name, - "actual_qty": (self.docstatus==1 and 1 or -1)*flt(d.get("stock_qty")), - "stock_uom": frappe.db.get_value("Item", args.get("item_code") or d.get("item_code"), "stock_uom"), - "incoming_rate": 0, - "company": self.company, - "batch_no": cstr(d.get("batch_no")).strip(), - "serial_no": d.get("serial_no"), - "project": d.get("project") or self.get('project'), - "is_cancelled": 1 if self.docstatus==2 else 0 - }) + sl_dict = frappe._dict( + { + "item_code": d.get("item_code", None), + "warehouse": d.get("warehouse", None), + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "fiscal_year": get_fiscal_year(self.posting_date, company=self.company)[0], + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": d.name, + "actual_qty": (self.docstatus == 1 and 1 or -1) * flt(d.get("stock_qty")), + "stock_uom": frappe.db.get_value( + "Item", args.get("item_code") or d.get("item_code"), "stock_uom" + ), + "incoming_rate": 0, + "company": self.company, + "batch_no": cstr(d.get("batch_no")).strip(), + "serial_no": d.get("serial_no"), + "project": d.get("project") or self.get("project"), + "is_cancelled": 1 if self.docstatus == 2 else 0, + } + ) sl_dict.update(args) return sl_dict - def make_sl_entries(self, sl_entries, allow_negative_stock=False, - via_landed_cost_voucher=False): + def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.stock_ledger import make_sl_entries + make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) def make_gl_entries_on_cancel(self): - if frappe.db.sql("""select name from `tabGL Entry` where voucher_type=%s - and voucher_no=%s""", (self.doctype, self.name)): - self.make_gl_entries() + if frappe.db.sql( + """select name from `tabGL Entry` where voucher_type=%s + and voucher_no=%s""", + (self.doctype, self.name), + ): + self.make_gl_entries() def get_serialized_items(self): serialized_items = [] item_codes = list(set(d.item_code for d in self.get("items"))) if item_codes: - serialized_items = frappe.db.sql_list("""select name from `tabItem` - where has_serial_no=1 and name in ({})""".format(", ".join(["%s"]*len(item_codes))), - tuple(item_codes)) + serialized_items = frappe.db.sql_list( + """select name from `tabItem` + where has_serial_no=1 and name in ({})""".format( + ", ".join(["%s"] * len(item_codes)) + ), + tuple(item_codes), + ) return serialized_items def validate_warehouse(self): 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))) + warehouses = list(set(d.warehouse for d in self.get("items") if getattr(d, "warehouse", None))) - target_warehouses = list(set([d.target_warehouse for d in - self.get("items") if getattr(d, "target_warehouse", None)])) + target_warehouses = list( + set([d.target_warehouse for d in self.get("items") if getattr(d, "target_warehouse", None)]) + ) warehouses.extend(target_warehouses) - from_warehouse = list(set([d.from_warehouse for d in - self.get("items") if getattr(d, "from_warehouse", None)])) + from_warehouse = list( + set([d.from_warehouse for d in self.get("items") if getattr(d, "from_warehouse", None)]) + ) warehouses.extend(from_warehouse) @@ -332,14 +419,17 @@ class StockController(AccountsController): if self.doctype == "Delivery Note": target_ref_field = "amount - (returned_qty * rate)" - self._update_percent_field({ - "target_dt": self.doctype + " Item", - "target_parent_dt": self.doctype, - "target_parent_field": "per_billed", - "target_ref_field": target_ref_field, - "target_field": "billed_amt", - "name": self.name, - }, update_modified) + self._update_percent_field( + { + "target_dt": self.doctype + " Item", + "target_parent_dt": self.doctype, + "target_parent_field": "per_billed", + "target_ref_field": target_ref_field, + "target_field": "billed_amt", + "name": self.name, + }, + update_modified, + ) def validate_inspection(self): """Checks if quality inspection is set/ is valid for Items that require inspection.""" @@ -347,24 +437,28 @@ class StockController(AccountsController): "Purchase Receipt": "inspection_required_before_purchase", "Purchase Invoice": "inspection_required_before_purchase", "Sales Invoice": "inspection_required_before_delivery", - "Delivery Note": "inspection_required_before_delivery" + "Delivery Note": "inspection_required_before_delivery", } inspection_required_fieldname = inspection_fieldname_map.get(self.doctype) # return if inspection is not required on document level - if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or - (self.doctype == "Stock Entry" and not self.inspection_required) or - (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): - return + if ( + (not inspection_required_fieldname and self.doctype != "Stock Entry") + or (self.doctype == "Stock Entry" and not self.inspection_required) + or (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock) + ): + return - for row in self.get('items'): + for row in self.get("items"): qi_required = False - if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)): + if inspection_required_fieldname and frappe.db.get_value( + "Item", row.item_code, inspection_required_fieldname + ): qi_required = True elif self.doctype == "Stock Entry" and row.t_warehouse: - qi_required = True # inward stock needs inspection + qi_required = True # inward stock needs inspection - if qi_required: # validate row only if inspection is required on item level + if qi_required: # validate row only if inspection is required on item level self.validate_qi_presence(row) if self.docstatus == 1: self.validate_qi_submission(row) @@ -381,12 +475,16 @@ class StockController(AccountsController): def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" - action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") + action = frappe.db.get_single_value( + "Stock Settings", "action_if_quality_inspection_is_not_submitted" + ) qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") if not qa_docstatus == 1: - link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) - msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" + link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection) + msg = ( + f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" + ) if action == "Stop": frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: @@ -398,7 +496,7 @@ class StockController(AccountsController): qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") if qa_status == "Rejected": - link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection) msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" if action == "Stop": frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) @@ -411,48 +509,71 @@ class StockController(AccountsController): frappe.get_doc("Blanket Order", blanket_order).update_ordered_qty() def validate_customer_provided_item(self): - for d in self.get('items'): + for d in self.get("items"): # Customer Provided parts will have zero valuation rate - if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): + if frappe.db.get_value("Item", d.item_code, "is_customer_provided_item"): d.allow_zero_valuation_rate = 1 def set_rate_of_stock_uom(self): - if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: + if self.doctype in [ + "Purchase Receipt", + "Purchase Invoice", + "Purchase Order", + "Sales Invoice", + "Sales Order", + "Delivery Note", + "Quotation", + ]: for d in self.get("items"): d.stock_uom_rate = d.rate / (d.conversion_factor or 1) def validate_internal_transfer(self): - if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ - and self.is_internal_transfer(): + if ( + self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt") + and self.is_internal_transfer() + ): self.validate_in_transit_warehouses() self.validate_multi_currency() self.validate_packed_items() def validate_in_transit_warehouses(self): - if (self.doctype == 'Sales Invoice' and self.get('update_stock')) or self.doctype == 'Delivery Note': - for item in self.get('items'): + if ( + self.doctype == "Sales Invoice" and self.get("update_stock") + ) or self.doctype == "Delivery Note": + for item in self.get("items"): if not item.target_warehouse: - frappe.throw(_("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx)) + frappe.throw( + _("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx) + ) - if (self.doctype == 'Purchase Invoice' and self.get('update_stock')) or self.doctype == 'Purchase Receipt': - for item in self.get('items'): + if ( + self.doctype == "Purchase Invoice" and self.get("update_stock") + ) or self.doctype == "Purchase Receipt": + for item in self.get("items"): if not item.from_warehouse: - frappe.throw(_("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx)) + frappe.throw( + _("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx) + ) def validate_multi_currency(self): if self.currency != self.company_currency: frappe.throw(_("Internal transfers can only be done in company's default currency")) def validate_packed_items(self): - if self.doctype in ('Sales Invoice', 'Delivery Note Item') and self.get('packed_items'): + if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"): frappe.throw(_("Packed Items cannot be transferred internally")) def validate_putaway_capacity(self): # if over receipt is attempted while 'apply putaway rule' is disabled # and if rule was applied on the transaction, validate it. from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity - valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice", - "Stock Reconciliation") + + valid_doctype = self.doctype in ( + "Purchase Receipt", + "Stock Entry", + "Purchase Invoice", + "Stock Reconciliation", + ) if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: valid_doctype = False @@ -461,14 +582,15 @@ class StockController(AccountsController): rule_map = defaultdict(dict) for item in self.get("items"): warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" - rule = frappe.db.get_value("Putaway Rule", - { - "item_code": item.get("item_code"), - "warehouse": item.get(warehouse_field) - }, - ["name", "disable"], as_dict=True) + rule = frappe.db.get_value( + "Putaway Rule", + {"item_code": item.get("item_code"), "warehouse": item.get(warehouse_field)}, + ["name", "disable"], + as_dict=True, + ) if rule: - if rule.get("disabled"): continue # dont validate for disabled rule + if rule.get("disabled"): + continue # dont validate for disabled rule if self.doctype == "Stock Reconciliation": stock_qty = flt(item.qty) @@ -489,46 +611,55 @@ class StockController(AccountsController): frappe.throw(msg=message, title=_("Over Receipt")) def prepare_over_receipt_message(self, rule, values): - message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ - .format( - frappe.bold(values["qty_put"]), frappe.bold(values["item"]), - frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) - ) + message = _( + "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}." + ).format( + frappe.bold(values["qty_put"]), + frappe.bold(values["item"]), + frappe.bold(values["warehouse"]), + frappe.bold(values["capacity"]), + ) message += "

" rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link) return message def repost_future_sle_and_gle(self): - args = frappe._dict({ - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "voucher_type": self.doctype, - "voucher_no": self.name, - "company": self.company - }) + args = frappe._dict( + { + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + } + ) if future_sle_exists(args) or repost_required_for_queue(self): - item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")) + item_based_reposting = cint( + frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting") + ) if item_based_reposting: create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name) else: create_repost_item_valuation_entry(args) + def repost_required_for_queue(doc: StockController) -> bool: """check if stock document contains repeated item-warehouse with queue based valuation. if queue exists for repeated items then SLEs need to reprocessed in background again. """ - consuming_sles = frappe.db.get_all("Stock Ledger Entry", + consuming_sles = frappe.db.get_all( + "Stock Ledger Entry", filters={ "voucher_type": doc.doctype, "voucher_no": doc.name, "actual_qty": ("<", 0), - "is_cancelled": 0 + "is_cancelled": 0, }, - fields=["item_code", "warehouse", "stock_queue"] + fields=["item_code", "warehouse", "stock_queue"], ) item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles] @@ -551,32 +682,41 @@ def make_quality_inspections(doctype, docname, 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") - )) + 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 = 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']]}) + return frappe.db.exists( + "Repost Item Valuation", {"docstatus": 1, "status": ["in", ["Queued", "In Progress"]]} + ) + def future_sle_exists(args, sl_entries=None): key = (args.voucher_type, args.voucher_no) @@ -593,7 +733,8 @@ def future_sle_exists(args, sl_entries=None): or_conditions = get_conditions_to_validate_future_sle(sl_entries) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ select item_code, warehouse, count(name) as total_row from `tabStock Ledger Entry` force index (item_warehouse) where @@ -604,43 +745,55 @@ def future_sle_exists(args, sl_entries=None): and is_cancelled = 0 GROUP BY item_code, warehouse - """.format(" or ".join(or_conditions)), args, as_dict=1) + """.format( + " or ".join(or_conditions) + ), + args, + as_dict=1, + ) for d in data: frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row return len(data) -def validate_future_sle_not_exists(args, key, sl_entries=None): - item_key = '' - if args.get('item_code'): - item_key = (args.get('item_code'), args.get('warehouse')) - if not sl_entries and hasattr(frappe.local, 'future_sle'): - if (not frappe.local.future_sle.get(key) or - (item_key and item_key not in frappe.local.future_sle.get(key))): +def validate_future_sle_not_exists(args, key, sl_entries=None): + item_key = "" + if args.get("item_code"): + item_key = (args.get("item_code"), args.get("warehouse")) + + if not sl_entries and hasattr(frappe.local, "future_sle"): + if not frappe.local.future_sle.get(key) or ( + item_key and item_key not in frappe.local.future_sle.get(key) + ): return True + def get_cached_data(args, key): - if not hasattr(frappe.local, 'future_sle'): + if not hasattr(frappe.local, "future_sle"): frappe.local.future_sle = {} if key not in frappe.local.future_sle: frappe.local.future_sle[key] = frappe._dict({}) - if args.get('item_code'): - item_key = (args.get('item_code'), args.get('warehouse')) + if args.get("item_code"): + item_key = (args.get("item_code"), args.get("warehouse")) count = frappe.local.future_sle[key].get(item_key) return True if (count or count == 0) else False else: return frappe.local.future_sle[key] + def get_sle_entries_against_voucher(args): - return frappe.get_all("Stock Ledger Entry", + return frappe.get_all( + "Stock Ledger Entry", filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, fields=["item_code", "warehouse"], - order_by="creation asc") + order_by="creation asc", + ) + def get_conditions_to_validate_future_sle(sl_entries): warehouse_items_map = {} @@ -654,16 +807,18 @@ def get_conditions_to_validate_future_sle(sl_entries): for warehouse, items in warehouse_items_map.items(): or_conditions.append( f"""warehouse = {frappe.db.escape(warehouse)} - and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") + and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""" + ) return or_conditions + def create_repost_item_valuation_entry(args): args = frappe._dict(args) repost_entry = frappe.new_doc("Repost Item Valuation") repost_entry.based_on = args.based_on if not args.based_on: - repost_entry.based_on = 'Transaction' if args.voucher_no else "Item and Warehouse" + repost_entry.based_on = "Transaction" if args.voucher_no else "Item and Warehouse" repost_entry.voucher_type = args.voucher_type repost_entry.voucher_no = args.voucher_no repost_entry.item_code = args.item_code diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index c52c688b73e..70830882efa 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -8,9 +8,9 @@ from frappe.utils import cint, flt, get_link_to_form from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos -class Subcontracting(): +class Subcontracting: def set_materials_for_subcontracted_items(self, raw_material_table): - if self.doctype == 'Purchase Invoice' and not self.update_stock: + if self.doctype == "Purchase Invoice" and not self.update_stock: return self.raw_material_table = raw_material_table @@ -33,13 +33,14 @@ class Subcontracting(): self.__get_backflush_based_on() def __get_backflush_based_on(self): - self.backflush_based_on = frappe.db.get_single_value("Buying Settings", - "backflush_raw_materials_of_subcontract_based_on") + self.backflush_based_on = frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ) def __get_purchase_orders(self): self.purchase_orders = [] - if self.doctype == 'Purchase Order': + if self.doctype == "Purchase Order": return self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] @@ -48,7 +49,7 @@ class Subcontracting(): self.__changed_name = [] self.__reference_name = [] - if self.doctype == 'Purchase Order' or self.is_new(): + if self.doctype == "Purchase Order" or self.is_new(): self.set(self.raw_material_table, []) return @@ -68,20 +69,20 @@ class Subcontracting(): def __get_data_before_save(self): item_dict = {} - if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save: - for row in self._doc_before_save.get('items'): + if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self._doc_before_save: + for row in self._doc_before_save.get("items"): item_dict[row.name] = (row.item_code, row.qty) return item_dict def get_available_materials(self): - ''' Get the available raw materials which has been transferred to the supplier. - available_materials = { - (item_code, subcontracted_item, purchase_order): { - 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details - } - } - ''' + """Get the available raw materials which has been transferred to the supplier. + available_materials = { + (item_code, subcontracted_item, purchase_order): { + 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details + } + } + """ if not self.purchase_orders: return @@ -89,8 +90,17 @@ class Subcontracting(): key = (row.rm_item_code, row.main_item_code, row.purchase_order) if key not in self.available_materials: - self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [], - 'batch_no': defaultdict(float), 'item_details': row, 'po_details': []}) + self.available_materials.setdefault( + key, + frappe._dict( + { + "qty": 0, + "serial_no": [], + "batch_no": defaultdict(float), + "item_details": row, + "po_details": [], + } + ), ) details = self.available_materials[key] @@ -106,17 +116,17 @@ class Subcontracting(): self.__set_alternative_item_details(row) self.__transferred_items = copy.deepcopy(self.available_materials) - for doctype in ['Purchase Receipt', 'Purchase Invoice']: + for doctype in ["Purchase Receipt", "Purchase Invoice"]: self.__update_consumed_materials(doctype) def __update_consumed_materials(self, doctype, return_consumed_items=False): - '''Deduct the consumed materials from the available materials.''' + """Deduct the consumed materials from the available materials.""" pr_items = self.__get_received_items(doctype) if not pr_items: return ([], {}) if return_consumed_items else None - pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items} + pr_items = {d.name: d.get(self.get("po_field") or "purchase_order") for d in pr_items} consumed_materials = self.__get_consumed_items(doctype, pr_items.keys()) if return_consumed_items: @@ -127,97 +137,153 @@ class Subcontracting(): if not self.available_materials.get(key): continue - self.available_materials[key]['qty'] -= row.consumed_qty + self.available_materials[key]["qty"] -= row.consumed_qty if row.serial_no: - self.available_materials[key]['serial_no'] = list( - set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no)) + self.available_materials[key]["serial_no"] = list( + set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no)) ) if row.batch_no: - self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty + self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty def __get_transferred_items(self): - fields = ['`tabStock Entry`.`purchase_order`'] - alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'} + fields = ["`tabStock Entry`.`purchase_order`"] + alias_dict = { + "item_code": "rm_item_code", + "subcontracted_item": "main_item_code", + "basic_rate": "rate", + } - child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount', - 'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor', - 's_warehouse', 't_warehouse', 'item_group', 'po_detail'] + child_table_fields = [ + "item_code", + "item_name", + "description", + "qty", + "basic_rate", + "amount", + "serial_no", + "uom", + "subcontracted_item", + "stock_uom", + "batch_no", + "conversion_factor", + "s_warehouse", + "t_warehouse", + "item_group", + "po_detail", + ] - if self.backflush_based_on == 'BOM': - child_table_fields.append('original_item') + if self.backflush_based_on == "BOM": + child_table_fields.append("original_item") for field in child_table_fields: - fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}') + fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}") - filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'], - ['Stock Entry', 'purchase_order', 'in', self.purchase_orders]] + filters = [ + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purpose", "=", "Send to Subcontractor"], + ["Stock Entry", "purchase_order", "in", self.purchase_orders], + ] - return frappe.get_all('Stock Entry', fields = fields, filters=filters) + return frappe.get_all("Stock Entry", fields=fields, filters=filters) def __get_received_items(self, doctype): fields = [] - self.po_field = 'purchase_order' + self.po_field = "purchase_order" - for field in ['name', self.po_field, 'parent']: - fields.append(f'`tab{doctype} Item`.`{field}`') + for field in ["name", self.po_field, "parent"]: + fields.append(f"`tab{doctype} Item`.`{field}`") - filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]] - if doctype == 'Purchase Invoice': - filters.append(['Purchase Invoice', 'update_stock', "=", 1]) + filters = [ + [doctype, "docstatus", "=", 1], + [f"{doctype} Item", self.po_field, "in", self.purchase_orders], + ] + if doctype == "Purchase Invoice": + filters.append(["Purchase Invoice", "update_stock", "=", 1]) - return frappe.get_all(f'{doctype}', fields = fields, filters = filters) + return frappe.get_all(f"{doctype}", fields=fields, filters=filters) def __get_consumed_items(self, doctype, pr_items): - return frappe.get_all('Purchase Receipt Item Supplied', - fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'], - filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype}) + return frappe.get_all( + "Purchase Receipt Item Supplied", + fields=[ + "serial_no", + "rm_item_code", + "reference_name", + "batch_no", + "consumed_qty", + "main_item_code", + ], + filters={"docstatus": 1, "reference_name": ("in", list(pr_items)), "parenttype": doctype}, + ) def __set_alternative_item_details(self, row): - if row.get('original_item'): - self.alternative_item_details[row.get('original_item')] = row + if row.get("original_item"): + self.alternative_item_details[row.get("original_item")] = row def __get_pending_qty_to_receive(self): - '''Get qty to be received against the purchase order.''' + """Get qty to be received against the purchase order.""" self.qty_to_be_received = defaultdict(float) - if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders: - for row in frappe.get_all('Purchase Order Item', - fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'], - filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}): + if ( + self.doctype != "Purchase Order" and self.backflush_based_on != "BOM" and self.purchase_orders + ): + for row in frappe.get_all( + "Purchase Order Item", + fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], + filters={"docstatus": 1, "parent": ("in", self.purchase_orders)}, + ): self.qty_to_be_received[(row.item_code, row.parent)] += row.qty def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): - doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item' - fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit'] + doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" + fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] - alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'} - for field in ['item_code', 'name', 'rate', 'stock_uom', - 'source_warehouse', 'description', 'item_name', 'stock_uom']: - fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}') + alias_dict = { + "item_code": "rm_item_code", + "name": "bom_detail_no", + "source_warehouse": "reserve_warehouse", + } + for field in [ + "item_code", + "name", + "rate", + "stock_uom", + "source_warehouse", + "description", + "item_name", + "stock_uom", + ]: + fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}") - filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1], - ['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]] + filters = [ + [doctype, "parent", "=", bom_no], + [doctype, "docstatus", "=", 1], + ["BOM", "item", "=", item_code], + [doctype, "sourced_by_supplier", "=", 0], + ] - return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or [] + return ( + frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or [] + ) def __remove_changed_rows(self): if not self.__changed_name: return - i=1 + i = 1 self.set(self.raw_material_table, []) for d in self._doc_before_save.supplied_items: if d.reference_name in self.__changed_name: continue - if (d.reference_name not in self.__reference_name): + if d.reference_name not in self.__reference_name: continue d.idx = i - self.append('supplied_items', d) + self.append("supplied_items", d) i += 1 @@ -226,31 +292,35 @@ class Subcontracting(): has_supplied_items = True if self.get(self.raw_material_table) else False for row in self.items: - if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name) - or (has_supplied_items and not self.__changed_name))): + if self.doctype != "Purchase Order" and ( + (self.__changed_name and row.name not in self.__changed_name) + or (has_supplied_items and not self.__changed_name) + ): continue - if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM': - for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')): - qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor) + if self.doctype == "Purchase Order" or self.backflush_based_on == "BOM": + for bom_item in self.__get_materials_from_bom( + row.item_code, row.bom, row.get("include_exploded_items") + ): + qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor bom_item.main_item_code = row.item_code self.__update_reserve_warehouse(bom_item, row) self.__set_alternative_item(bom_item) self.__add_supplied_item(row, bom_item, qty) - elif self.backflush_based_on != 'BOM': + elif self.backflush_based_on != "BOM": for key, transfer_item in self.available_materials.items(): if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0: qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 transfer_item.qty -= qty - self.__add_supplied_item(row, transfer_item.get('item_details'), qty) + self.__add_supplied_item(row, transfer_item.get("item_details"), qty) if self.qty_to_be_received: self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty def __update_reserve_warehouse(self, row, item): - if self.doctype == 'Purchase Order': - row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse) + if self.doctype == "Purchase Order": + row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse def __get_qty_based_on_material_transfer(self, item_row, transfer_item): key = (item_row.item_code, item_row.purchase_order) @@ -262,8 +332,9 @@ class Subcontracting(): qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) transfer_item.item_details.required_qty = transfer_item.qty - if (transfer_item.serial_no or frappe.get_cached_value('UOM', - transfer_item.item_details.stock_uom, 'must_be_whole_number')): + if transfer_item.serial_no or frappe.get_cached_value( + "UOM", transfer_item.item_details.stock_uom, "must_be_whole_number" + ): return frappe.utils.ceil(qty) return qty @@ -277,7 +348,7 @@ class Subcontracting(): rm_obj = self.append(self.raw_material_table, bom_item) rm_obj.reference_name = item_row.name - if self.doctype == 'Purchase Order': + if self.doctype == "Purchase Order": rm_obj.required_qty = qty else: rm_obj.consumed_qty = 0 @@ -287,12 +358,12 @@ class Subcontracting(): def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) - if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): + if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: new_rm_obj = None - for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): + for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): if batch_qty >= qty: self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) - self.available_materials[key]['batch_no'][batch_no] -= qty + self.available_materials[key]["batch_no"][batch_no] -= qty return elif qty > 0 and batch_qty > 0: @@ -300,7 +371,7 @@ class Subcontracting(): new_rm_obj = self.append(self.raw_material_table, bom_item) new_rm_obj.reference_name = item_row.name self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) - self.available_materials[key]['batch_no'][batch_no] = 0 + self.available_materials[key]["batch_no"][batch_no] = 0 if abs(qty) > 0 and not new_rm_obj: self.__set_consumed_qty(rm_obj, qty) @@ -313,29 +384,35 @@ class Subcontracting(): rm_obj.consumed_qty = consumed_qty def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): - rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, - 'required_qty': qty, 'purchase_order': item_row.purchase_order}) + rm_obj.update( + { + "consumed_qty": qty, + "batch_no": batch_no, + "required_qty": qty, + "purchase_order": item_row.purchase_order, + } + ) self.__set_serial_nos(item_row, rm_obj) def __set_serial_nos(self, item_row, rm_obj): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) - if (self.available_materials.get(key) and self.available_materials[key]['serial_no']): - used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)] - rm_obj.serial_no = '\n'.join(used_serial_nos) + if self.available_materials.get(key) and self.available_materials[key]["serial_no"]: + used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)] + rm_obj.serial_no = "\n".join(used_serial_nos) # Removed the used serial nos from the list for sn in used_serial_nos: - self.available_materials[key]['serial_no'].remove(sn) + self.available_materials[key]["serial_no"].remove(sn) def set_consumed_qty_in_po(self): # Update consumed qty back in the purchase order - if self.is_subcontracted != 'Yes': + if self.is_subcontracted != "Yes": return self.__get_purchase_orders() itemwise_consumed_qty = defaultdict(float) - for doctype in ['Purchase Receipt', 'Purchase Invoice']: + for doctype in ["Purchase Receipt", "Purchase Invoice"]: consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True) for row in consumed_items: @@ -345,10 +422,12 @@ class Subcontracting(): self.__update_consumed_qty_in_po(itemwise_consumed_qty) def __update_consumed_qty_in_po(self, itemwise_consumed_qty): - fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name'] - filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)} + fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"] + filters = {"docstatus": 1, "parent": ("in", self.purchase_orders)} - for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'): + for row in frappe.get_all( + "Purchase Order Item Supplied", fields=fields, filters=filters, order_by="idx" + ): key = (row.rm_item_code, row.main_item_code, row.parent) consumed_qty = itemwise_consumed_qty.get(key, 0) @@ -356,10 +435,10 @@ class Subcontracting(): consumed_qty = row.supplied_qty itemwise_consumed_qty[key] -= consumed_qty - frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) + frappe.db.set_value("Purchase Order Item Supplied", row.name, "consumed_qty", consumed_qty) def __validate_supplied_items(self): - if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']: + if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]: return for row in self.get(self.raw_material_table): @@ -371,18 +450,20 @@ class Subcontracting(): self.__validate_serial_no(row, key) def __validate_batch_no(self, row, key): - if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'): - link = get_link_to_form('Purchase Order', row.purchase_order) + if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( + "batch_no" + ): + link = get_link_to_form("Purchase Order", row.purchase_order) msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}' frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) def __validate_serial_no(self, row, key): - if row.get('serial_no'): - serial_nos = get_serial_nos(row.get('serial_no')) - incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no')) + if row.get("serial_no"): + serial_nos = get_serial_nos(row.get("serial_no")) + incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no")) if incorrect_sn: incorrect_sn = "\n".join(incorrect_sn) - link = get_link_to_form('Purchase Order', row.purchase_order) - msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}' + link = get_link_to_form("Purchase Order", row.purchase_order) + msg = f"The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}" frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 08d1dcea7dc..2afba91b379 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -37,6 +37,8 @@ class calculate_taxes_and_totals(object): self.set_discount_amount() self.apply_discount_amount() + self.calculate_shipping_charges() + if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: self.calculate_total_advance() @@ -50,7 +52,6 @@ class calculate_taxes_and_totals(object): self.initialize_taxes() self.determine_exclusive_rate() self.calculate_net_total() - self.calculate_shipping_charges() self.calculate_taxes() self.manipulate_grand_total_for_inclusive_tax() self.calculate_totals() @@ -58,23 +59,23 @@ class calculate_taxes_and_totals(object): self.calculate_total_net_weight() def validate_item_tax_template(self): - for item in self.doc.get('items'): - if item.item_code and item.get('item_tax_template'): + for item in self.doc.get("items"): + if item.item_code and item.get("item_tax_template"): item_doc = frappe.get_cached_doc("Item", item.item_code) args = { - 'net_rate': item.net_rate or item.rate, - 'tax_category': self.doc.get('tax_category'), - 'posting_date': self.doc.get('posting_date'), - 'bill_date': self.doc.get('bill_date'), - 'transaction_date': self.doc.get('transaction_date'), - 'company': self.doc.get('company') + "net_rate": item.net_rate or item.rate, + "tax_category": self.doc.get("tax_category"), + "posting_date": self.doc.get("posting_date"), + "bill_date": self.doc.get("bill_date"), + "transaction_date": self.doc.get("transaction_date"), + "company": self.doc.get("company"), } item_group = item_doc.item_group item_group_taxes = [] while item_group: - item_group_doc = frappe.get_cached_doc('Item Group', item_group) + item_group_doc = frappe.get_cached_doc("Item Group", item_group) item_group_taxes += item_group_doc.taxes or [] item_group = item_group_doc.parent_item_group @@ -89,9 +90,11 @@ class calculate_taxes_and_totals(object): if taxes: if item.item_tax_template not in taxes: item.item_tax_template = taxes[0] - frappe.msgprint(_("Row {0}: Item Tax template updated as per validity and rate applied").format( - item.idx, frappe.bold(item.item_code) - )) + frappe.msgprint( + _("Row {0}: Item Tax template updated as per validity and rate applied").format( + item.idx, frappe.bold(item.item_code) + ) + ) def validate_conversion_rate(self): # validate conversion rate @@ -100,13 +103,17 @@ class calculate_taxes_and_totals(object): self.doc.currency = company_currency self.doc.conversion_rate = 1.0 else: - validate_conversion_rate(self.doc.currency, self.doc.conversion_rate, - self.doc.meta.get_label("conversion_rate"), self.doc.company) + validate_conversion_rate( + self.doc.currency, + self.doc.conversion_rate, + self.doc.meta.get_label("conversion_rate"), + self.doc.company, + ) self.doc.conversion_rate = flt(self.doc.conversion_rate) def calculate_item_values(self): - if self.doc.get('is_consolidated'): + if self.doc.get("is_consolidated"): return if not self.discount_amount_applied: @@ -117,16 +124,30 @@ class calculate_taxes_and_totals(object): item.rate = 0.0 elif item.price_list_rate: if not item.rate or (item.pricing_rules and item.discount_percentage > 0): - item.rate = flt(item.price_list_rate * - (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) - item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) - elif item.discount_amount and item.pricing_rules: - item.rate = item.price_list_rate - item.discount_amount + item.rate = flt( + item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate") + ) - if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']: + item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) + + elif item.discount_amount and item.pricing_rules: + item.rate = item.price_list_rate - item.discount_amount + + if item.doctype in [ + "Quotation Item", + "Sales Order Item", + "Delivery Note Item", + "Sales Invoice Item", + "POS Invoice Item", + "Purchase Invoice Item", + "Purchase Order Item", + "Purchase Receipt Item", + ]: item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) if flt(item.rate_with_margin) > 0: - item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) + item.rate = flt( + item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate") + ) if item.discount_amount and not item.discount_percentage: item.rate = item.rate_with_margin - item.discount_amount @@ -145,18 +166,22 @@ class calculate_taxes_and_totals(object): elif not item.qty and self.doc.get("is_debit_note"): item.amount = flt(item.rate, item.precision("amount")) else: - item.amount = flt(item.rate * item.qty, item.precision("amount")) + item.amount = flt(item.rate * item.qty, item.precision("amount")) item.net_amount = item.amount - self._set_in_company_currency(item, ["price_list_rate", "rate", "net_rate", "amount", "net_amount"]) + self._set_in_company_currency( + item, ["price_list_rate", "rate", "net_rate", "amount", "net_amount"] + ) item.item_tax_amount = 0.0 def _set_in_company_currency(self, doc, fields): """set values in base currency""" for f in fields: - val = flt(flt(doc.get(f), doc.precision(f)) * self.doc.conversion_rate, doc.precision("base_" + f)) + val = flt( + flt(doc.get(f), doc.precision(f)) * self.doc.conversion_rate, doc.precision("base_" + f) + ) doc.set("base_" + f, val) def initialize_taxes(self): @@ -165,16 +190,22 @@ class calculate_taxes_and_totals(object): validate_taxes_and_charges(tax) validate_inclusive_tax(tax, self.doc) - if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")): + if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")): tax.item_wise_tax_detail = {} - tax_fields = ["total", "tax_amount_after_discount_amount", - "tax_amount_for_current_item", "grand_total_for_current_item", - "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] + tax_fields = [ + "total", + "tax_amount_after_discount_amount", + "tax_amount_for_current_item", + "grand_total_for_current_item", + "tax_fraction_for_current_item", + "grand_total_fraction_for_current_item", + ] - if tax.charge_type != "Actual" and \ - not (self.discount_amount_applied and self.doc.apply_discount_on=="Grand Total"): - tax_fields.append("tax_amount") + if tax.charge_type != "Actual" and not ( + self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total" + ): + tax_fields.append("tax_amount") for fieldname in tax_fields: tax.set(fieldname, 0.0) @@ -190,25 +221,32 @@ class calculate_taxes_and_totals(object): cumulated_tax_fraction = 0 total_inclusive_tax_amount_per_qty = 0 for i, tax in enumerate(self.doc.get("taxes")): - tax.tax_fraction_for_current_item, inclusive_tax_amount_per_qty = self.get_current_tax_fraction(tax, item_tax_map) + ( + tax.tax_fraction_for_current_item, + inclusive_tax_amount_per_qty, + ) = self.get_current_tax_fraction(tax, item_tax_map) - if i==0: + if i == 0: tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item else: - tax.grand_total_fraction_for_current_item = \ - self.doc.get("taxes")[i-1].grand_total_fraction_for_current_item \ + tax.grand_total_fraction_for_current_item = ( + self.doc.get("taxes")[i - 1].grand_total_fraction_for_current_item + tax.tax_fraction_for_current_item + ) cumulated_tax_fraction += tax.tax_fraction_for_current_item total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty) - if not self.discount_amount_applied and item.qty and (cumulated_tax_fraction or total_inclusive_tax_amount_per_qty): + if ( + not self.discount_amount_applied + and item.qty + and (cumulated_tax_fraction or total_inclusive_tax_amount_per_qty) + ): amount = flt(item.amount) - total_inclusive_tax_amount_per_qty item.net_amount = flt(amount / (1 + cumulated_tax_fraction)) item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) - item.discount_percentage = flt(item.discount_percentage, - item.precision("discount_percentage")) + item.discount_percentage = flt(item.discount_percentage, item.precision("discount_percentage")) self._set_in_company_currency(item, ["net_rate", "net_amount"]) @@ -217,8 +255,8 @@ class calculate_taxes_and_totals(object): def get_current_tax_fraction(self, tax, item_tax_map): """ - Get tax fraction for calculating tax exclusive amount - from tax inclusive amount + Get tax fraction for calculating tax exclusive amount + from tax inclusive amount """ current_tax_fraction = 0 inclusive_tax_amount_per_qty = 0 @@ -230,12 +268,14 @@ class calculate_taxes_and_totals(object): current_tax_fraction = tax_rate / 100.0 elif tax.charge_type == "On Previous Row Amount": - current_tax_fraction = (tax_rate / 100.0) * \ - self.doc.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item + current_tax_fraction = (tax_rate / 100.0) * self.doc.get("taxes")[ + cint(tax.row_id) - 1 + ].tax_fraction_for_current_item elif tax.charge_type == "On Previous Row Total": - current_tax_fraction = (tax_rate / 100.0) * \ - self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item + current_tax_fraction = (tax_rate / 100.0) * self.doc.get("taxes")[ + cint(tax.row_id) - 1 + ].grand_total_fraction_for_current_item elif tax.charge_type == "On Item Quantity": inclusive_tax_amount_per_qty = flt(tax_rate) @@ -253,7 +293,9 @@ class calculate_taxes_and_totals(object): return tax.rate def calculate_net_total(self): - self.doc.total_qty = self.doc.total = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 + self.doc.total_qty = ( + self.doc.total + ) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 for item in self.doc.get("items"): self.doc.total += item.amount @@ -265,17 +307,32 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"]) def calculate_shipping_charges(self): + + # Do not apply shipping rule for POS + if self.doc.get("is_pos"): + return + if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule.apply(self.doc) + self._calculate() + def calculate_taxes(self): - if not self.doc.get('is_consolidated'): + rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get( + "rounding_adjustment" + ) + if not rounding_adjustment_computed: self.doc.rounding_adjustment = 0 # maintain actual tax rate based on idx - actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] - for tax in self.doc.get("taxes") if tax.charge_type == "Actual"]) + actual_tax_dict = dict( + [ + [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] + for tax in self.doc.get("taxes") + if tax.charge_type == "Actual" + ] + ) for n, item in enumerate(self.doc.get("items")): item_tax_map = self._load_item_tax_rate(item.item_tax_rate) @@ -290,9 +347,10 @@ class calculate_taxes_and_totals(object): current_tax_amount += actual_tax_dict[tax.idx] # accumulate tax amount into tax.tax_amount - if tax.charge_type != "Actual" and \ - not (self.discount_amount_applied and self.doc.apply_discount_on=="Grand Total"): - tax.tax_amount += current_tax_amount + if tax.charge_type != "Actual" and not ( + self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total" + ): + tax.tax_amount += current_tax_amount # store tax_amount for current item as it will be used for # charge type = 'On Previous Row Amount' @@ -305,17 +363,17 @@ class calculate_taxes_and_totals(object): # note: grand_total_for_current_item contains the contribution of # item's amount, previously applied tax and the current tax on that item - if i==0: + if i == 0: tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount) else: - tax.grand_total_for_current_item = \ - flt(self.doc.get("taxes")[i-1].grand_total_for_current_item + current_tax_amount) + tax.grand_total_for_current_item = flt( + self.doc.get("taxes")[i - 1].grand_total_for_current_item + current_tax_amount + ) # set precision in the last item iteration if n == len(self.doc.get("items")) - 1: self.round_off_totals(tax) - self._set_in_company_currency(tax, - ["tax_amount", "tax_amount_after_discount_amount"]) + self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) self.round_off_base_values(tax) self.set_cumulative_total(i, tax) @@ -323,20 +381,29 @@ class calculate_taxes_and_totals(object): self._set_in_company_currency(tax, ["total"]) # adjust Discount Amount loss in last tax iteration - if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \ - and self.doc.discount_amount \ - and self.doc.apply_discount_on == "Grand Total" \ - and not self.doc.get('is_consolidated'): - self.doc.rounding_adjustment = flt(self.doc.grand_total - - flt(self.doc.discount_amount) - tax.total, - self.doc.precision("rounding_adjustment")) + if ( + i == (len(self.doc.get("taxes")) - 1) + and self.discount_amount_applied + and self.doc.discount_amount + and self.doc.apply_discount_on == "Grand Total" + and not rounding_adjustment_computed + ): + self.doc.rounding_adjustment = flt( + self.doc.grand_total - flt(self.doc.discount_amount) - tax.total, + self.doc.precision("rounding_adjustment"), + ) def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax): # if just for valuation, do not add the tax amount in total # if tax/charges is for deduction, multiply by -1 if getattr(tax, "category", None): tax_amount = 0.0 if (tax.category == "Valuation") else tax_amount - if self.doc.doctype in ["Purchase Order", "Purchase Invoice", "Purchase Receipt", "Supplier Quotation"]: + if self.doc.doctype in [ + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + "Supplier Quotation", + ]: tax_amount *= -1.0 if (tax.add_deduct_tax == "Deduct") else 1.0 return tax_amount @@ -347,7 +414,7 @@ class calculate_taxes_and_totals(object): if row_idx == 0: tax.total = flt(self.doc.net_total + tax_amount, tax.precision("total")) else: - tax.total = flt(self.doc.get("taxes")[row_idx-1].total + tax_amount, tax.precision("total")) + tax.total = flt(self.doc.get("taxes")[row_idx - 1].total + tax_amount, tax.precision("total")) def get_current_tax_amount(self, item, tax, item_tax_map): tax_rate = self._get_tax_rate(tax, item_tax_map) @@ -356,16 +423,20 @@ class calculate_taxes_and_totals(object): if tax.charge_type == "Actual": # distribute the tax amount proportionally to each item row actual = flt(tax.tax_amount, tax.precision("tax_amount")) - current_tax_amount = item.net_amount*actual / self.doc.net_total if self.doc.net_total else 0.0 + current_tax_amount = ( + item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0 + ) elif tax.charge_type == "On Net Total": current_tax_amount = (tax_rate / 100.0) * item.net_amount elif tax.charge_type == "On Previous Row Amount": - current_tax_amount = (tax_rate / 100.0) * \ - self.doc.get("taxes")[cint(tax.row_id) - 1].tax_amount_for_current_item + current_tax_amount = (tax_rate / 100.0) * self.doc.get("taxes")[ + cint(tax.row_id) - 1 + ].tax_amount_for_current_item elif tax.charge_type == "On Previous Row Total": - current_tax_amount = (tax_rate / 100.0) * \ - self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_for_current_item + current_tax_amount = (tax_rate / 100.0) * self.doc.get("taxes")[ + cint(tax.row_id) - 1 + ].grand_total_for_current_item elif tax.charge_type == "On Item Quantity": current_tax_amount = tax_rate * item.qty @@ -377,11 +448,11 @@ class calculate_taxes_and_totals(object): def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount): # store tax breakup for each item key = item.item_code or item.item_name - item_wise_tax_amount = current_tax_amount*self.doc.conversion_rate + item_wise_tax_amount = current_tax_amount * self.doc.conversion_rate if tax.item_wise_tax_detail.get(key): item_wise_tax_amount += tax.item_wise_tax_detail[key][1] - tax.item_wise_tax_detail[key] = [tax_rate,flt(item_wise_tax_amount)] + tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)] def round_off_totals(self, tax): if tax.account_head in frappe.flags.round_off_applicable_accounts: @@ -389,8 +460,9 @@ class calculate_taxes_and_totals(object): tax.tax_amount_after_discount_amount = round(tax.tax_amount_after_discount_amount, 0) tax.tax_amount = flt(tax.tax_amount, tax.precision("tax_amount")) - tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, - tax.precision("tax_amount")) + tax.tax_amount_after_discount_amount = flt( + tax.tax_amount_after_discount_amount, tax.precision("tax_amount") + ) def round_off_base_values(self, tax): # Round off to nearest integer based on regional settings @@ -402,11 +474,15 @@ class calculate_taxes_and_totals(object): # if fully inclusive taxes and diff if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")): last_tax = self.doc.get("taxes")[-1] - non_inclusive_tax_amount = sum(flt(d.tax_amount_after_discount_amount) - for d in self.doc.get("taxes") if not d.included_in_print_rate) + non_inclusive_tax_amount = sum( + flt(d.tax_amount_after_discount_amount) + for d in self.doc.get("taxes") + if not d.included_in_print_rate + ) - diff = self.doc.total + non_inclusive_tax_amount \ - - flt(last_tax.total, last_tax.precision("total")) + diff = ( + self.doc.total + non_inclusive_tax_amount - flt(last_tax.total, last_tax.precision("total")) + ) # If discount amount applied, deduct the discount amount # because self.doc.total is always without discount, but last_tax.total is after discount @@ -415,7 +491,7 @@ class calculate_taxes_and_totals(object): diff = flt(diff, self.doc.precision("rounding_adjustment")) - if diff and abs(diff) <= (5.0 / 10**last_tax.precision("tax_amount")): + if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")): self.doc.rounding_adjustment = diff def calculate_totals(self): @@ -425,16 +501,27 @@ class calculate_taxes_and_totals(object): self.doc.grand_total = flt(self.doc.net_total) if self.doc.get("taxes"): - self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total - - flt(self.doc.rounding_adjustment), self.doc.precision("total_taxes_and_charges")) + self.doc.total_taxes_and_charges = flt( + self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), + self.doc.precision("total_taxes_and_charges"), + ) else: self.doc.total_taxes_and_charges = 0.0 self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"]) - if self.doc.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"]: - self.doc.base_grand_total = flt(self.doc.grand_total * self.doc.conversion_rate, self.doc.precision("base_grand_total")) \ - if self.doc.total_taxes_and_charges else self.doc.base_net_total + if self.doc.doctype in [ + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", + "POS Invoice", + ]: + self.doc.base_grand_total = ( + flt(self.doc.grand_total * self.doc.conversion_rate, self.doc.precision("base_grand_total")) + if self.doc.total_taxes_and_charges + else self.doc.base_net_total + ) else: self.doc.taxes_and_charges_added = self.doc.taxes_and_charges_deducted = 0.0 for tax in self.doc.get("taxes"): @@ -446,58 +533,70 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["taxes_and_charges_added", "taxes_and_charges_deducted"]) - self.doc.base_grand_total = flt(self.doc.grand_total * self.doc.conversion_rate) \ - if (self.doc.taxes_and_charges_added or self.doc.taxes_and_charges_deducted) \ + self.doc.base_grand_total = ( + flt(self.doc.grand_total * self.doc.conversion_rate) + if (self.doc.taxes_and_charges_added or self.doc.taxes_and_charges_deducted) else self.doc.base_net_total + ) - self._set_in_company_currency(self.doc, - ["taxes_and_charges_added", "taxes_and_charges_deducted"]) + self._set_in_company_currency( + self.doc, ["taxes_and_charges_added", "taxes_and_charges_deducted"] + ) self.doc.round_floats_in(self.doc, ["grand_total", "base_grand_total"]) self.set_rounded_total() def calculate_total_net_weight(self): - if self.doc.meta.get_field('total_net_weight'): + if self.doc.meta.get_field("total_net_weight"): self.doc.total_net_weight = 0.0 for d in self.doc.items: if d.total_weight: self.doc.total_net_weight += d.total_weight def set_rounded_total(self): - if not self.doc.get('is_consolidated'): - if self.doc.meta.get_field("rounded_total"): - if self.doc.is_rounded_total_disabled(): - self.doc.rounded_total = self.doc.base_rounded_total = 0 - return + if self.doc.get("is_consolidated") and self.doc.get("rounding_adjustment"): + return - self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, - self.doc.currency, self.doc.precision("rounded_total")) + if self.doc.meta.get_field("rounded_total"): + if self.doc.is_rounded_total_disabled(): + self.doc.rounded_total = self.doc.base_rounded_total = 0 + return - #if print_in_rate is set, we would have already calculated rounding adjustment - self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total, - self.doc.precision("rounding_adjustment")) + self.doc.rounded_total = round_based_on_smallest_currency_fraction( + self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total") + ) - self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) + # if print_in_rate is set, we would have already calculated rounding adjustment + self.doc.rounding_adjustment += flt( + self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment") + ) + + self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) def _cleanup(self): - if not self.doc.get('is_consolidated'): + if not self.doc.get("is_consolidated"): for tax in self.doc.get("taxes"): if not tax.get("dont_recompute_tax"): - tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) + tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(",", ":")) def set_discount_amount(self): if self.doc.additional_discount_percentage: - self.doc.discount_amount = flt(flt(self.doc.get(scrub(self.doc.apply_discount_on))) - * self.doc.additional_discount_percentage / 100, self.doc.precision("discount_amount")) + self.doc.discount_amount = flt( + flt(self.doc.get(scrub(self.doc.apply_discount_on))) + * self.doc.additional_discount_percentage + / 100, + self.doc.precision("discount_amount"), + ) def apply_discount_amount(self): if self.doc.discount_amount: if not self.doc.apply_discount_on: frappe.throw(_("Please select Apply Discount On")) - self.doc.base_discount_amount = flt(self.doc.discount_amount * self.doc.conversion_rate, - self.doc.precision("base_discount_amount")) + self.doc.base_discount_amount = flt( + self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") + ) total_for_discount_amount = self.get_total_for_discount_amount() taxes = self.doc.get("taxes") @@ -506,20 +605,24 @@ class calculate_taxes_and_totals(object): if total_for_discount_amount: # calculate item amount after Discount Amount for i, item in enumerate(self.doc.get("items")): - distributed_amount = flt(self.doc.discount_amount) * \ - item.net_amount / total_for_discount_amount + distributed_amount = ( + flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount + ) item.net_amount = flt(item.net_amount - distributed_amount, item.precision("net_amount")) net_total += item.net_amount # discount amount rounding loss adjustment if no taxes - if (self.doc.apply_discount_on == "Net Total" or not taxes or total_for_discount_amount==self.doc.net_total) \ - and i == len(self.doc.get("items")) - 1: - discount_amount_loss = flt(self.doc.net_total - net_total - self.doc.discount_amount, - self.doc.precision("net_total")) + if ( + self.doc.apply_discount_on == "Net Total" + or not taxes + or total_for_discount_amount == self.doc.net_total + ) and i == len(self.doc.get("items")) - 1: + discount_amount_loss = flt( + self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total") + ) - item.net_amount = flt(item.net_amount + discount_amount_loss, - item.precision("net_amount")) + item.net_amount = flt(item.net_amount + discount_amount_loss, item.precision("net_amount")) item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0 @@ -544,42 +647,56 @@ class calculate_taxes_and_totals(object): actual_tax_amount = flt(actual_taxes_dict.get(tax.row_id, 0)) * flt(tax.rate) / 100 actual_taxes_dict.setdefault(tax.idx, actual_tax_amount) - return flt(self.doc.grand_total - sum(actual_taxes_dict.values()), - self.doc.precision("grand_total")) - + return flt( + self.doc.grand_total - sum(actual_taxes_dict.values()), self.doc.precision("grand_total") + ) def calculate_total_advance(self): if self.doc.docstatus < 2: - total_allocated_amount = sum(flt(adv.allocated_amount, adv.precision("allocated_amount")) - for adv in self.doc.get("advances")) + total_allocated_amount = sum( + flt(adv.allocated_amount, adv.precision("allocated_amount")) + for adv in self.doc.get("advances") + ) self.doc.total_advance = flt(total_allocated_amount, self.doc.precision("total_advance")) grand_total = self.doc.rounded_total or self.doc.grand_total if self.doc.party_account_currency == self.doc.currency: - invoice_total = flt(grand_total - flt(self.doc.write_off_amount), - self.doc.precision("grand_total")) + invoice_total = flt( + grand_total - flt(self.doc.write_off_amount), self.doc.precision("grand_total") + ) else: - base_write_off_amount = flt(flt(self.doc.write_off_amount) * self.doc.conversion_rate, - self.doc.precision("base_write_off_amount")) - invoice_total = flt(grand_total * self.doc.conversion_rate, - self.doc.precision("grand_total")) - base_write_off_amount + base_write_off_amount = flt( + flt(self.doc.write_off_amount) * self.doc.conversion_rate, + self.doc.precision("base_write_off_amount"), + ) + invoice_total = ( + flt(grand_total * self.doc.conversion_rate, self.doc.precision("grand_total")) + - base_write_off_amount + ) if invoice_total > 0 and self.doc.total_advance > invoice_total: - frappe.throw(_("Advance amount cannot be greater than {0} {1}") - .format(self.doc.party_account_currency, invoice_total)) + frappe.throw( + _("Advance amount cannot be greater than {0} {1}").format( + self.doc.party_account_currency, invoice_total + ) + ) if self.doc.docstatus == 0: + if self.doc.get("write_off_outstanding_amount_automatically"): + self.doc.write_off_amount = 0 + self.calculate_outstanding_amount() + self.calculate_write_off_amount() def is_internal_invoice(self): """ - Checks if its an internal transfer invoice - and decides if to calculate any out standing amount or not + Checks if its an internal transfer invoice + and decides if to calculate any out standing amount or not """ - if self.doc.doctype in ('Sales Invoice', 'Purchase Invoice') and self.doc.is_internal_transfer(): + if self.doc.doctype in ("Sales Invoice", "Purchase Invoice") and self.doc.is_internal_transfer(): return True return False @@ -591,57 +708,81 @@ class calculate_taxes_and_totals(object): if self.doc.doctype == "Sales Invoice": self.calculate_paid_amount() - if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos') or \ - self.is_internal_invoice(): return + if ( + self.doc.is_return + and self.doc.return_against + and not self.doc.get("is_pos") + or self.is_internal_invoice() + ): + return self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"]) - self._set_in_company_currency(self.doc, ['write_off_amount']) + self._set_in_company_currency(self.doc, ["write_off_amount"]) if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: grand_total = self.doc.rounded_total or self.doc.grand_total base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total if self.doc.party_account_currency == self.doc.currency: - total_amount_to_pay = flt(grand_total - self.doc.total_advance - - flt(self.doc.write_off_amount), self.doc.precision("grand_total")) + total_amount_to_pay = flt( + grand_total - self.doc.total_advance - flt(self.doc.write_off_amount), + self.doc.precision("grand_total"), + ) else: - total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance - - flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total")) + total_amount_to_pay = flt( + flt(base_grand_total, self.doc.precision("base_grand_total")) + - self.doc.total_advance + - flt(self.doc.base_write_off_amount), + self.doc.precision("base_grand_total"), + ) self.doc.round_floats_in(self.doc, ["paid_amount"]) change_amount = 0 - if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'): - self.calculate_write_off_amount() + if self.doc.doctype == "Sales Invoice" and not self.doc.get("is_return"): self.calculate_change_amount() - change_amount = self.doc.change_amount \ - if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount + change_amount = ( + self.doc.change_amount + if self.doc.party_account_currency == self.doc.currency + else self.doc.base_change_amount + ) - paid_amount = self.doc.paid_amount \ - if self.doc.party_account_currency == self.doc.currency else self.doc.base_paid_amount + paid_amount = ( + self.doc.paid_amount + if self.doc.party_account_currency == self.doc.currency + else self.doc.base_paid_amount + ) - self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), - self.doc.precision("outstanding_amount")) + self.doc.outstanding_amount = flt( + total_amount_to_pay - flt(paid_amount) + flt(change_amount), + self.doc.precision("outstanding_amount"), + ) - if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): - self.update_paid_amount_for_return(total_amount_to_pay) + if ( + self.doc.doctype == "Sales Invoice" + and self.doc.get("is_pos") + and self.doc.get("is_return") + and not self.doc.get("is_consolidated") + ): + self.set_total_amount_to_default_mop(total_amount_to_pay) + self.calculate_paid_amount() def calculate_paid_amount(self): paid_amount = base_paid_amount = 0.0 if self.doc.is_pos: - for payment in self.doc.get('payments'): + for payment in self.doc.get("payments"): payment.amount = flt(payment.amount) payment.base_amount = payment.amount * flt(self.doc.conversion_rate) paid_amount += payment.amount base_paid_amount += payment.base_amount elif not self.doc.is_return: - self.doc.set('payments', []) + self.doc.set("payments", []) if self.doc.redeem_loyalty_points and self.doc.loyalty_amount: base_paid_amount += self.doc.loyalty_amount - paid_amount += (self.doc.loyalty_amount / flt(self.doc.conversion_rate)) + paid_amount += self.doc.loyalty_amount / flt(self.doc.conversion_rate) self.doc.paid_amount = flt(paid_amount, self.doc.precision("paid_amount")) self.doc.base_paid_amount = flt(base_paid_amount, self.doc.precision("base_paid_amount")) @@ -652,22 +793,32 @@ class calculate_taxes_and_totals(object): grand_total = self.doc.rounded_total or self.doc.grand_total base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total - if self.doc.doctype == "Sales Invoice" \ - and self.doc.paid_amount > grand_total and not self.doc.is_return \ - and any(d.type == "Cash" for d in self.doc.payments): + if ( + self.doc.doctype == "Sales Invoice" + and self.doc.paid_amount > grand_total + and not self.doc.is_return + and any(d.type == "Cash" for d in self.doc.payments) + ): - self.doc.change_amount = flt(self.doc.paid_amount - grand_total + - self.doc.write_off_amount, self.doc.precision("change_amount")) + self.doc.change_amount = flt( + self.doc.paid_amount - grand_total, self.doc.precision("change_amount") + ) - self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total + - self.doc.base_write_off_amount, self.doc.precision("base_change_amount")) + self.doc.base_change_amount = flt( + self.doc.base_paid_amount - base_grand_total, self.doc.precision("base_change_amount") + ) def calculate_write_off_amount(self): - if flt(self.doc.change_amount) > 0: - self.doc.write_off_amount = flt(self.doc.grand_total - self.doc.paid_amount - + self.doc.change_amount, self.doc.precision("write_off_amount")) - self.doc.base_write_off_amount = flt(self.doc.write_off_amount * self.doc.conversion_rate, - self.doc.precision("base_write_off_amount")) + if self.doc.get("write_off_outstanding_amount_automatically"): + self.doc.write_off_amount = flt( + self.doc.outstanding_amount, self.doc.precision("write_off_amount") + ) + self.doc.base_write_off_amount = flt( + self.doc.write_off_amount * self.doc.conversion_rate, + self.doc.precision("base_write_off_amount"), + ) + + self.calculate_outstanding_amount() def calculate_margin(self, item): rate_with_margin = 0.0 @@ -676,10 +827,15 @@ class calculate_taxes_and_totals(object): if item.pricing_rules and not self.doc.ignore_pricing_rule: has_margin = False for d in get_applied_pricing_rules(item.pricing_rules): - pricing_rule = frappe.get_cached_doc('Pricing Rule', d) + pricing_rule = frappe.get_cached_doc("Pricing Rule", d) - if pricing_rule.margin_rate_or_amount and ((pricing_rule.currency == self.doc.currency and - pricing_rule.margin_type in ['Amount', 'Percentage']) or pricing_rule.margin_type == 'Percentage'): + if pricing_rule.margin_rate_or_amount and ( + ( + pricing_rule.currency == self.doc.currency + and pricing_rule.margin_type in ["Amount", "Percentage"] + ) + or pricing_rule.margin_type == "Percentage" + ): item.margin_type = pricing_rule.margin_type item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount has_margin = True @@ -690,12 +846,17 @@ class calculate_taxes_and_totals(object): if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate): item.margin_type = "Amount" - item.margin_rate_or_amount = flt(item.rate - item.price_list_rate, - item.precision("margin_rate_or_amount")) + item.margin_rate_or_amount = flt( + item.rate - item.price_list_rate, item.precision("margin_rate_or_amount") + ) item.rate_with_margin = item.rate elif item.margin_type and item.margin_rate_or_amount: - margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 + margin_value = ( + item.margin_rate_or_amount + if item.margin_type == "Amount" + else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 + ) rate_with_margin = flt(item.price_list_rate) + flt(margin_value) base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) @@ -704,19 +865,25 @@ class calculate_taxes_and_totals(object): def set_item_wise_tax_breakup(self): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) - def update_paid_amount_for_return(self, total_amount_to_pay): - default_mode_of_payment = frappe.db.get_value('POS Payment Method', - {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1) + def set_total_amount_to_default_mop(self, total_amount_to_pay): + default_mode_of_payment = frappe.db.get_value( + "POS Payment Method", + {"parent": self.doc.pos_profile, "default": 1}, + ["mode_of_payment"], + as_dict=1, + ) if default_mode_of_payment: self.doc.payments = [] - self.doc.append('payments', { - 'mode_of_payment': default_mode_of_payment.mode_of_payment, - 'amount': total_amount_to_pay, - 'default': 1 - }) + self.doc.append( + "payments", + { + "mode_of_payment": default_mode_of_payment.mode_of_payment, + "amount": total_amount_to_pay, + "default": 1, + }, + ) - self.calculate_paid_amount() def get_itemised_tax_breakup_html(doc): if not doc.taxes: @@ -726,7 +893,7 @@ def get_itemised_tax_breakup_html(doc): # get headers tax_accounts = [] for tax in doc.taxes: - if getattr(tax, "category", None) and tax.category=="Valuation": + if getattr(tax, "category", None) and tax.category == "Valuation": continue if tax.description not in tax_accounts: tax_accounts.append(tax.description) @@ -742,34 +909,40 @@ def get_itemised_tax_breakup_html(doc): frappe.flags.company = None return frappe.render_template( - "templates/includes/itemised_tax_breakup.html", dict( + "templates/includes/itemised_tax_breakup.html", + dict( headers=headers, itemised_tax=itemised_tax, itemised_taxable_amount=itemised_taxable_amount, tax_accounts=tax_accounts, - doc=doc - ) + doc=doc, + ), ) + @frappe.whitelist() def get_round_off_applicable_accounts(company, account_list): account_list = get_regional_round_off_accounts(company, account_list) return account_list + @erpnext.allow_regional def get_regional_round_off_accounts(company, account_list): pass + @erpnext.allow_regional def update_itemised_tax_data(doc): - #Don't delete this method, used for localization + # Don't delete this method, used for localization pass + @erpnext.allow_regional def get_itemised_tax_breakup_header(item_doctype, tax_accounts): return [_("Item"), _("Taxable Amount")] + tax_accounts + @erpnext.allow_regional def get_itemised_tax_breakup_data(doc): itemised_tax = get_itemised_tax(doc.taxes) @@ -778,10 +951,11 @@ def get_itemised_tax_breakup_data(doc): return itemised_tax, itemised_taxable_amount + def get_itemised_tax(taxes, with_tax_account=False): itemised_tax = {} for tax in taxes: - if getattr(tax, "category", None) and tax.category=="Valuation": + if getattr(tax, "category", None) and tax.category == "Valuation": continue item_tax_map = json.loads(tax.item_wise_tax_detail) if tax.item_wise_tax_detail else {} @@ -798,16 +972,16 @@ def get_itemised_tax(taxes, with_tax_account=False): else: tax_rate = flt(tax_data) - itemised_tax[item_code][tax.description] = frappe._dict(dict( - tax_rate = tax_rate, - tax_amount = tax_amount - )) + itemised_tax[item_code][tax.description] = frappe._dict( + dict(tax_rate=tax_rate, tax_amount=tax_amount) + ) if with_tax_account: itemised_tax[item_code][tax.description].tax_account = tax.account_head return itemised_tax + def get_itemised_taxable_amount(items): itemised_taxable_amount = frappe._dict() for item in items: @@ -817,16 +991,18 @@ def get_itemised_taxable_amount(items): return itemised_taxable_amount + def get_rounded_tax_amount(itemised_tax, precision): # Rounding based on tax_amount precision for taxes in itemised_tax.values(): for tax_account in taxes: taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) + class init_landed_taxes_and_totals(object): def __init__(self, doc): self.doc = doc - self.tax_field = 'taxes' if self.doc.doctype == 'Landed Cost Voucher' else 'additional_costs' + self.tax_field = "taxes" if self.doc.doctype == "Landed Cost Voucher" else "additional_costs" self.set_account_currency() self.set_exchange_rate() self.set_amounts_in_company_currency() @@ -835,7 +1011,7 @@ class init_landed_taxes_and_totals(object): company_currency = erpnext.get_company_currency(self.doc.company) for d in self.doc.get(self.tax_field): if not d.account_currency: - account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency') + account_currency = frappe.db.get_value("Account", d.expense_account, "account_currency") d.account_currency = account_currency or company_currency def set_exchange_rate(self): @@ -844,8 +1020,12 @@ class init_landed_taxes_and_totals(object): if d.account_currency == company_currency: d.exchange_rate = 1 elif not d.exchange_rate: - d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account, - account_currency=d.account_currency, company=self.doc.company) + d.exchange_rate = get_exchange_rate( + self.doc.posting_date, + account=d.expense_account, + account_currency=d.account_currency, + company=self.doc.company, + ) if not d.exchange_rate: frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) diff --git a/erpnext/controllers/tests/test_item_variant.py b/erpnext/controllers/tests/test_item_variant.py index 391b5ec8819..c57e54f7fc8 100644 --- a/erpnext/controllers/tests/test_item_variant.py +++ b/erpnext/controllers/tests/test_item_variant.py @@ -1,4 +1,3 @@ - import json import unittest @@ -14,11 +13,12 @@ from erpnext.stock.doctype.quality_inspection.test_quality_inspection import ( class TestItemVariant(unittest.TestCase): def test_tables_in_template_copied_to_variant(self): - fields = [{'field_name': 'quality_inspection_template'}] + fields = [{"field_name": "quality_inspection_template"}] set_item_variant_settings(fields) variant = make_item_variant() self.assertEqual(variant.get("quality_inspection_template"), "_Test QC Template") + def create_variant_with_tables(item, args): if isinstance(args, string_types): args = json.loads(args) @@ -29,14 +29,11 @@ def create_variant_with_tables(item, args): template.save() variant = frappe.new_doc("Item") - variant.variant_based_on = 'Item Attribute' + variant.variant_based_on = "Item Attribute" variant_attributes = [] for d in template.attributes: - variant_attributes.append({ - "attribute": d.attribute, - "attribute_value": args.get(d.attribute) - }) + variant_attributes.append({"attribute": d.attribute, "attribute_value": args.get(d.attribute)}) variant.set("attributes", variant_attributes) copy_attributes_to_variant(template, variant) @@ -44,6 +41,7 @@ def create_variant_with_tables(item, args): return variant + def make_item_variant(): frappe.delete_doc_if_exists("Item", "_Test Variant Item-XSL", force=1) variant = create_variant_with_tables("_Test Variant Item", '{"Test Size": "Extra Small"}') @@ -52,6 +50,7 @@ def make_item_variant(): variant.save() return variant + def make_quality_inspection_template(): qc_template = "_Test QC Template" if frappe.db.exists("Quality Inspection Template", qc_template): @@ -61,10 +60,13 @@ def make_quality_inspection_template(): qc.quality_inspection_template_name = qc_template create_quality_inspection_parameter("Moisture") - qc.append('item_quality_inspection_parameter', { - "specification": "Moisture", - "value": "< 5%", - }) + qc.append( + "item_quality_inspection_parameter", + { + "specification": "Moisture", + "value": "< 5%", + }, + ) qc.insert() return qc.name diff --git a/erpnext/controllers/tests/test_mapper.py b/erpnext/controllers/tests/test_mapper.py index 0d8789abef6..919bcdab660 100644 --- a/erpnext/controllers/tests/test_mapper.py +++ b/erpnext/controllers/tests/test_mapper.py @@ -1,4 +1,3 @@ - import json import unittest @@ -11,14 +10,14 @@ from frappe.utils import add_months, nowdate class TestMapper(unittest.TestCase): def test_map_docs(self): - '''Test mapping of multiple source docs on a single target doc''' + """Test mapping of multiple source docs on a single target doc""" make_test_records("Item") - items = ['_Test Item', '_Test Item 2', '_Test FG Item'] + items = ["_Test Item", "_Test Item 2", "_Test FG Item"] # Make source docs (quotations) and a target doc (sales order) - qtn1, item_list_1 = self.make_quotation(items, '_Test Customer') - qtn2, item_list_2 = self.make_quotation(items, '_Test Customer') + qtn1, item_list_1 = self.make_quotation(items, "_Test Customer") + qtn2, item_list_2 = self.make_quotation(items, "_Test Customer") so, item_list_3 = self.make_sales_order() # Map source docs to target with corresponding mapper method @@ -27,20 +26,20 @@ class TestMapper(unittest.TestCase): # Assert that all inserted items are present in updated sales order src_items = item_list_1 + item_list_2 + item_list_3 - self.assertEqual(set(d for d in src_items), - set(d.item_code for d in updated_so.items)) - + self.assertEqual(set(d for d in src_items), set(d.item_code for d in updated_so.items)) def make_quotation(self, item_list, customer): - qtn = frappe.get_doc({ - "doctype": "Quotation", - "quotation_to": "Customer", - "party_name": customer, - "order_type": "Sales", - "transaction_date" : nowdate(), - "valid_till" : add_months(nowdate(), 1) - }) + qtn = frappe.get_doc( + { + "doctype": "Quotation", + "quotation_to": "Customer", + "party_name": customer, + "order_type": "Sales", + "transaction_date": nowdate(), + "valid_till": add_months(nowdate(), 1), + } + ) for item in item_list: qtn.append("items", {"qty": "2", "item_code": item}) @@ -48,21 +47,23 @@ class TestMapper(unittest.TestCase): return qtn, item_list def make_sales_order(self): - item = frappe.get_doc({ - "base_amount": 1000.0, - "base_rate": 100.0, - "description": "CPU", - "doctype": "Sales Order Item", - "item_code": "_Test Item", - "item_name": "CPU", - "parentfield": "items", - "qty": 10.0, - "rate": 100.0, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "_Test UOM", - "conversion_factor": 1.0, - "uom": "_Test UOM" - }) - so = frappe.get_doc(frappe.get_test_records('Sales Order')[0]) + item = frappe.get_doc( + { + "base_amount": 1000.0, + "base_rate": 100.0, + "description": "CPU", + "doctype": "Sales Order Item", + "item_code": "_Test Item", + "item_name": "CPU", + "parentfield": "items", + "qty": 10.0, + "rate": 100.0, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "_Test UOM", + "conversion_factor": 1.0, + "uom": "_Test UOM", + } + ) + so = frappe.get_doc(frappe.get_test_records("Sales Order")[0]) so.insert(ignore_permissions=True) return so, [item.item_code] diff --git a/erpnext/controllers/tests/test_qty_based_taxes.py b/erpnext/controllers/tests/test_qty_based_taxes.py index 226778db014..2e9dfd2faa0 100644 --- a/erpnext/controllers/tests/test_qty_based_taxes.py +++ b/erpnext/controllers/tests/test_qty_based_taxes.py @@ -1,4 +1,3 @@ - import unittest from uuid import uuid4 as _uuid4 @@ -6,104 +5,131 @@ import frappe def uuid4(): - return str(_uuid4()) + return str(_uuid4()) + class TestTaxes(unittest.TestCase): - def setUp(self): - self.company = frappe.get_doc({ - 'doctype': 'Company', - 'company_name': uuid4(), - 'abbr': ''.join(s[0] for s in uuid4().split('-')), - 'default_currency': 'USD', - 'country': 'United States', - }).insert() - self.account = frappe.get_doc({ - 'doctype': 'Account', - 'account_name': uuid4(), - 'account_type': 'Tax', - 'company': self.company.name, - 'parent_account': 'Duties and Taxes - {self.company.abbr}'.format(self=self) - }).insert() - self.item_group = frappe.get_doc({ - 'doctype': 'Item Group', - 'item_group_name': uuid4(), - 'parent_item_group': 'All Item Groups', - }).insert() - self.item_tax_template = frappe.get_doc({ - 'doctype': 'Item Tax Template', - 'title': uuid4(), - 'company': self.company.name, - 'taxes': [ - { - 'tax_type': self.account.name, - 'tax_rate': 2, - } - ] - }).insert() - self.item = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': uuid4(), - 'item_group': self.item_group.name, - 'is_stock_item': 0, - 'taxes': [ - { - 'item_tax_template': self.item_tax_template.name, - 'tax_category': '', - } - ], - }).insert() - self.customer = frappe.get_doc({ - 'doctype': 'Customer', - 'customer_name': uuid4(), - 'customer_group': 'All Customer Groups', - }).insert() - self.supplier = frappe.get_doc({ - 'doctype': 'Supplier', - 'supplier_name': uuid4(), - 'supplier_group': 'All Supplier Groups', - }).insert() + def setUp(self): + self.company = frappe.get_doc( + { + "doctype": "Company", + "company_name": uuid4(), + "abbr": "".join(s[0] for s in uuid4().split("-")), + "default_currency": "USD", + "country": "United States", + } + ).insert() + self.account = frappe.get_doc( + { + "doctype": "Account", + "account_name": uuid4(), + "account_type": "Tax", + "company": self.company.name, + "parent_account": "Duties and Taxes - {self.company.abbr}".format(self=self), + } + ).insert() + self.item_group = frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": uuid4(), + "parent_item_group": "All Item Groups", + } + ).insert() + self.item_tax_template = frappe.get_doc( + { + "doctype": "Item Tax Template", + "title": uuid4(), + "company": self.company.name, + "taxes": [ + { + "tax_type": self.account.name, + "tax_rate": 2, + } + ], + } + ).insert() + self.item = frappe.get_doc( + { + "doctype": "Item", + "item_code": uuid4(), + "item_group": self.item_group.name, + "is_stock_item": 0, + "taxes": [ + { + "item_tax_template": self.item_tax_template.name, + "tax_category": "", + } + ], + } + ).insert() + self.customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": uuid4(), + "customer_group": "All Customer Groups", + } + ).insert() + self.supplier = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": uuid4(), + "supplier_group": "All Supplier Groups", + } + ).insert() - def test_taxes(self): - self.created_docs = [] - for dt in ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice', - 'Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']: - doc = frappe.get_doc({ - 'doctype': dt, - 'company': self.company.name, - 'supplier': self.supplier.name, - 'currency': "USD", - 'schedule_date': frappe.utils.nowdate(), - 'delivery_date': frappe.utils.nowdate(), - 'customer': self.customer.name, - 'buying_price_list' if dt.startswith('Purchase') else 'selling_price_list' - : 'Standard Buying' if dt.startswith('Purchase') else 'Standard Selling', - 'items': [ - { - 'item_code': self.item.name, - 'qty': 300, - 'rate': 100, - } - ], - 'taxes': [ - { - 'charge_type': 'On Item Quantity', - 'account_head': self.account.name, - 'description': 'N/A', - 'rate': 0, - }, - ], - }) - doc.run_method('set_missing_values') - doc.run_method('calculate_taxes_and_totals') - doc.insert() - self.assertEqual(doc.taxes[0].tax_amount, 600) - self.created_docs.append(doc) + def test_taxes(self): + self.created_docs = [] + for dt in [ + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", + ]: + doc = frappe.get_doc( + { + "doctype": dt, + "company": self.company.name, + "supplier": self.supplier.name, + "currency": "USD", + "schedule_date": frappe.utils.nowdate(), + "delivery_date": frappe.utils.nowdate(), + "customer": self.customer.name, + "buying_price_list" + if dt.startswith("Purchase") + else "selling_price_list": "Standard Buying" + if dt.startswith("Purchase") + else "Standard Selling", + "items": [ + { + "item_code": self.item.name, + "qty": 300, + "rate": 100, + } + ], + "taxes": [ + { + "charge_type": "On Item Quantity", + "account_head": self.account.name, + "description": "N/A", + "rate": 0, + }, + ], + } + ) + doc.run_method("set_missing_values") + doc.run_method("calculate_taxes_and_totals") + doc.insert() + self.assertEqual(doc.taxes[0].tax_amount, 600) + self.created_docs.append(doc) - def tearDown(self): - for doc in self.created_docs: - doc.delete() - self.item.delete() - self.item_group.delete() - self.item_tax_template.delete() - self.account.delete() - self.company.delete() + def tearDown(self): + for doc in self.created_docs: + doc.delete() + self.item.delete() + self.item_group.delete() + self.item_tax_template.delete() + self.account.delete() + self.company.delete() diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py index f4d3f97ef0d..1471543f1b2 100644 --- a/erpnext/controllers/tests/test_transaction_base.py +++ b/erpnext/controllers/tests/test_transaction_base.py @@ -5,18 +5,28 @@ import frappe class TestUtils(unittest.TestCase): def test_reset_default_field_value(self): - doc = frappe.get_doc({ - "doctype": "Purchase Receipt", - "set_warehouse": "Warehouse 1", - }) + doc = frappe.get_doc( + { + "doctype": "Purchase Receipt", + "set_warehouse": "Warehouse 1", + } + ) # Same values - doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] + doc.items = [ + {"warehouse": "Warehouse 1"}, + {"warehouse": "Warehouse 1"}, + {"warehouse": "Warehouse 1"}, + ] doc.reset_default_field_value("set_warehouse", "items", "warehouse") self.assertEqual(doc.set_warehouse, "Warehouse 1") # Mixed values - doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] + doc.items = [ + {"warehouse": "Warehouse 1"}, + {"warehouse": "Warehouse 2"}, + {"warehouse": "Warehouse 1"}, + ] doc.reset_default_field_value("set_warehouse", "items", "warehouse") self.assertEqual(doc.set_warehouse, None) @@ -30,9 +40,13 @@ class TestUtils(unittest.TestCase): from_warehouse="_Test Warehouse - _TC", to_warehouse="_Test Warehouse 1 - _TC", items=[ - frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), - frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1) - ] + frappe._dict( + item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC" + ), + frappe._dict( + item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1 + ), + ], ) se.save() @@ -43,18 +57,20 @@ class TestUtils(unittest.TestCase): se.delete() def test_reset_default_field_value_in_transfer_stock_entry(self): - doc = frappe.get_doc({ - "doctype": "Stock Entry", - "purpose": "Material Receipt", - "from_warehouse": "Warehouse 1", - "to_warehouse": "Warehouse 2", - }) + doc = frappe.get_doc( + { + "doctype": "Stock Entry", + "purpose": "Material Receipt", + "from_warehouse": "Warehouse 1", + "to_warehouse": "Warehouse 2", + } + ) # Same values doc.items = [ {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, - {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"} + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, ] doc.reset_default_field_value("from_warehouse", "items", "s_warehouse") @@ -66,10 +82,10 @@ class TestUtils(unittest.TestCase): doc.items = [ {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, {"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"}, - {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"} + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, ] doc.reset_default_field_value("from_warehouse", "items", "s_warehouse") doc.reset_default_field_value("to_warehouse", "items", "t_warehouse") self.assertEqual(doc.from_warehouse, None) - self.assertEqual(doc.to_warehouse, "Warehouse 2") \ No newline at end of file + self.assertEqual(doc.to_warehouse, "Warehouse 2") diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 1cb101f214a..1d6c5dc0be0 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -17,17 +17,33 @@ def get_columns(filters, trans): # get conditions for grouping filter cond group_by_cols = group_wise_column(filters.get("group_by")) - columns = based_on_details["based_on_cols"] + period_cols + [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"] + columns = ( + based_on_details["based_on_cols"] + + period_cols + + [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"] + ) if group_by_cols: - columns = based_on_details["based_on_cols"] + group_by_cols + period_cols + \ - [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"] + columns = ( + based_on_details["based_on_cols"] + + group_by_cols + + period_cols + + [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"] + ) - conditions = {"based_on_select": based_on_details["based_on_select"], "period_wise_select": period_select, - "columns": columns, "group_by": based_on_details["based_on_group_by"], "grbc": group_by_cols, "trans": trans, - "addl_tables": based_on_details["addl_tables"], "addl_tables_relational_cond": based_on_details.get("addl_tables_relational_cond", "")} + conditions = { + "based_on_select": based_on_details["based_on_select"], + "period_wise_select": period_select, + "columns": columns, + "group_by": based_on_details["based_on_group_by"], + "grbc": group_by_cols, + "trans": trans, + "addl_tables": based_on_details["addl_tables"], + "addl_tables_relational_cond": based_on_details.get("addl_tables_relational_cond", ""), + } return conditions + def validate_filters(filters): for f in ["Fiscal Year", "Based On", "Period", "Company"]: if not filters.get(f.lower().replace(" ", "_")): @@ -39,153 +55,231 @@ def validate_filters(filters): if filters.get("based_on") == filters.get("group_by"): frappe.throw(_("'Based On' and 'Group By' can not be same")) + def get_data(filters, conditions): data = [] - inc, cond= '','' - query_details = conditions["based_on_select"] + conditions["period_wise_select"] + inc, cond = "", "" + query_details = conditions["based_on_select"] + conditions["period_wise_select"] - posting_date = 't1.transaction_date' - if conditions.get('trans') in ['Sales Invoice', 'Purchase Invoice', 'Purchase Receipt', 'Delivery Note']: - posting_date = 't1.posting_date' + posting_date = "t1.transaction_date" + if conditions.get("trans") in [ + "Sales Invoice", + "Purchase Invoice", + "Purchase Receipt", + "Delivery Note", + ]: + posting_date = "t1.posting_date" if filters.period_based_on: - posting_date = 't1.'+filters.period_based_on + posting_date = "t1." + filters.period_based_on if conditions["based_on_select"] in ["t1.project,", "t2.project,"]: - cond = ' and '+ conditions["based_on_select"][:-1] +' IS Not NULL' - if conditions.get('trans') in ['Sales Order', 'Purchase Order']: + cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL" + if conditions.get("trans") in ["Sales Order", "Purchase Order"]: cond += " and t1.status != 'Closed'" - if conditions.get('trans') == 'Quotation' and filters.get("group_by") == 'Customer': + if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer": cond += " and t1.quotation_to = 'Customer'" - year_start_date, year_end_date = frappe.db.get_value("Fiscal Year", - filters.get('fiscal_year'), ["year_start_date", "year_end_date"]) + year_start_date, year_end_date = frappe.db.get_value( + "Fiscal Year", filters.get("fiscal_year"), ["year_start_date", "year_end_date"] + ) if filters.get("group_by"): - sel_col = '' + sel_col = "" ind = conditions["columns"].index(conditions["grbc"][0]) - if filters.get("group_by") == 'Item': - sel_col = 't2.item_code' - elif filters.get("group_by") == 'Customer': - sel_col = 't1.party_name' if conditions.get('trans') == 'Quotation' else 't1.customer' - elif filters.get("group_by") == 'Supplier': - sel_col = 't1.supplier' + if filters.get("group_by") == "Item": + sel_col = "t2.item_code" + elif filters.get("group_by") == "Customer": + sel_col = "t1.party_name" if conditions.get("trans") == "Quotation" else "t1.customer" + elif filters.get("group_by") == "Supplier": + sel_col = "t1.supplier" - if filters.get('based_on') in ['Item','Customer','Supplier']: + if filters.get("based_on") in ["Item", "Customer", "Supplier"]: inc = 2 - else : + else: inc = 1 - data1 = frappe.db.sql(""" select %s from `tab%s` t1, `tab%s Item` t2 %s + data1 = frappe.db.sql( + """ select %s from `tab%s` t1, `tab%s Item` t2 %s where t2.parent = t1.name and t1.company = %s and %s between %s and %s and t1.docstatus = 1 %s %s group by %s - """ % (query_details, conditions["trans"], conditions["trans"], conditions["addl_tables"], "%s", - posting_date, "%s", "%s", conditions.get("addl_tables_relational_cond"), cond, conditions["group_by"]), (filters.get("company"), - year_start_date, year_end_date),as_list=1) + """ + % ( + query_details, + conditions["trans"], + conditions["trans"], + conditions["addl_tables"], + "%s", + posting_date, + "%s", + "%s", + conditions.get("addl_tables_relational_cond"), + cond, + conditions["group_by"], + ), + (filters.get("company"), year_start_date, year_end_date), + as_list=1, + ) for d in range(len(data1)): - #to add blanck column + # to add blanck column dt = data1[d] - dt.insert(ind,'') + dt.insert(ind, "") data.append(dt) - #to get distinct value of col specified by group_by in filter - row = frappe.db.sql("""select DISTINCT(%s) from `tab%s` t1, `tab%s Item` t2 %s + # to get distinct value of col specified by group_by in filter + row = frappe.db.sql( + """select DISTINCT(%s) from `tab%s` t1, `tab%s Item` t2 %s where t2.parent = t1.name and t1.company = %s and %s between %s and %s and t1.docstatus = 1 and %s = %s %s %s - """ % - (sel_col, conditions["trans"], conditions["trans"], conditions["addl_tables"], - "%s", posting_date, "%s", "%s", conditions["group_by"], "%s", conditions.get("addl_tables_relational_cond"), cond), - (filters.get("company"), year_start_date, year_end_date, data1[d][0]), as_list=1) + """ + % ( + sel_col, + conditions["trans"], + conditions["trans"], + conditions["addl_tables"], + "%s", + posting_date, + "%s", + "%s", + conditions["group_by"], + "%s", + conditions.get("addl_tables_relational_cond"), + cond, + ), + (filters.get("company"), year_start_date, year_end_date, data1[d][0]), + as_list=1, + ) for i in range(len(row)): - des = ['' for q in range(len(conditions["columns"]))] + des = ["" for q in range(len(conditions["columns"]))] - #get data for group_by filter - row1 = frappe.db.sql(""" select %s , %s from `tab%s` t1, `tab%s Item` t2 %s + # get data for group_by filter + row1 = frappe.db.sql( + """ select %s , %s from `tab%s` t1, `tab%s Item` t2 %s where t2.parent = t1.name and t1.company = %s and %s between %s and %s and t1.docstatus = 1 and %s = %s and %s = %s %s %s - """ % - (sel_col, conditions["period_wise_select"], conditions["trans"], - conditions["trans"], conditions["addl_tables"], "%s", posting_date, "%s","%s", sel_col, - "%s", conditions["group_by"], "%s", conditions.get("addl_tables_relational_cond"), cond), - (filters.get("company"), year_start_date, year_end_date, row[i][0], - data1[d][0]), as_list=1) + """ + % ( + sel_col, + conditions["period_wise_select"], + conditions["trans"], + conditions["trans"], + conditions["addl_tables"], + "%s", + posting_date, + "%s", + "%s", + sel_col, + "%s", + conditions["group_by"], + "%s", + conditions.get("addl_tables_relational_cond"), + cond, + ), + (filters.get("company"), year_start_date, year_end_date, row[i][0], data1[d][0]), + as_list=1, + ) des[ind] = row[i][0] - for j in range(1,len(conditions["columns"])-inc): - des[j+inc] = row1[0][j] + for j in range(1, len(conditions["columns"]) - inc): + des[j + inc] = row1[0][j] data.append(des) else: - data = frappe.db.sql(""" select %s from `tab%s` t1, `tab%s Item` t2 %s + data = frappe.db.sql( + """ select %s from `tab%s` t1, `tab%s Item` t2 %s where t2.parent = t1.name and t1.company = %s and %s between %s and %s and t1.docstatus = 1 %s %s group by %s - """ % - (query_details, conditions["trans"], conditions["trans"], conditions["addl_tables"], - "%s", posting_date, "%s", "%s", cond, conditions.get("addl_tables_relational_cond", ""), conditions["group_by"]), - (filters.get("company"), year_start_date, year_end_date), as_list=1) + """ + % ( + query_details, + conditions["trans"], + conditions["trans"], + conditions["addl_tables"], + "%s", + posting_date, + "%s", + "%s", + cond, + conditions.get("addl_tables_relational_cond", ""), + conditions["group_by"], + ), + (filters.get("company"), year_start_date, year_end_date), + as_list=1, + ) return data + def get_mon(dt): return getdate(dt).strftime("%b") + def period_wise_columns_query(filters, trans): - query_details = '' + query_details = "" pwc = [] bet_dates = get_period_date_ranges(filters.get("period"), filters.get("fiscal_year")) - if trans in ['Purchase Receipt', 'Delivery Note', 'Purchase Invoice', 'Sales Invoice']: - trans_date = 'posting_date' + if trans in ["Purchase Receipt", "Delivery Note", "Purchase Invoice", "Sales Invoice"]: + trans_date = "posting_date" if filters.period_based_on: trans_date = filters.period_based_on else: - trans_date = 'transaction_date' + trans_date = "transaction_date" - if filters.get("period") != 'Yearly': + if filters.get("period") != "Yearly": for dt in bet_dates: get_period_wise_columns(dt, filters.get("period"), pwc) query_details = get_period_wise_query(dt, trans_date, query_details) else: - pwc = [_(filters.get("fiscal_year")) + " ("+_("Qty") + "):Float:120", - _(filters.get("fiscal_year")) + " ("+ _("Amt") + "):Currency:120"] + pwc = [ + _(filters.get("fiscal_year")) + " (" + _("Qty") + "):Float:120", + _(filters.get("fiscal_year")) + " (" + _("Amt") + "):Currency:120", + ] query_details = " SUM(t2.stock_qty), SUM(t2.base_net_amount)," - query_details += 'SUM(t2.stock_qty), SUM(t2.base_net_amount)' + query_details += "SUM(t2.stock_qty), SUM(t2.base_net_amount)" return pwc, query_details + def get_period_wise_columns(bet_dates, period, pwc): - if period == 'Monthly': - pwc += [_(get_mon(bet_dates[0])) + " (" + _("Qty") + "):Float:120", - _(get_mon(bet_dates[0])) + " (" + _("Amt") + "):Currency:120"] + if period == "Monthly": + pwc += [ + _(get_mon(bet_dates[0])) + " (" + _("Qty") + "):Float:120", + _(get_mon(bet_dates[0])) + " (" + _("Amt") + "):Currency:120", + ] else: - pwc += [_(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Qty") + "):Float:120", - _(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Amt") + "):Currency:120"] + pwc += [ + _(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Qty") + "):Float:120", + _(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Amt") + "):Currency:120", + ] + def get_period_wise_query(bet_dates, trans_date, query_details): query_details += """SUM(IF(t1.%(trans_date)s BETWEEN '%(sd)s' AND '%(ed)s', t2.stock_qty, NULL)), SUM(IF(t1.%(trans_date)s BETWEEN '%(sd)s' AND '%(ed)s', t2.base_net_amount, NULL)), - """ % {"trans_date": trans_date, "sd": bet_dates[0],"ed": bet_dates[1]} + """ % { + "trans_date": trans_date, + "sd": bet_dates[0], + "ed": bet_dates[1], + } return query_details + @frappe.whitelist(allow_guest=True) def get_period_date_ranges(period, fiscal_year=None, year_start_date=None): from dateutil.relativedelta import relativedelta if not year_start_date: - year_start_date, year_end_date = frappe.db.get_value("Fiscal Year", - fiscal_year, ["year_start_date", "year_end_date"]) + year_start_date, year_end_date = frappe.db.get_value( + "Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"] + ) - increment = { - "Monthly": 1, - "Quarterly": 3, - "Half-Yearly": 6, - "Yearly": 12 - }.get(period) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(period) period_date_ranges = [] for i in range(1, 13, increment): @@ -199,8 +293,10 @@ def get_period_date_ranges(period, fiscal_year=None, year_start_date=None): return period_date_ranges + def get_period_month_ranges(period, fiscal_year): from dateutil.relativedelta import relativedelta + period_month_ranges = [] for start_date, end_date in get_period_date_ranges(period, fiscal_year): @@ -212,6 +308,7 @@ def get_period_month_ranges(period, fiscal_year): return period_month_ranges + def based_wise_columns_query(based_on, trans): based_on_details = {} @@ -219,65 +316,74 @@ def based_wise_columns_query(based_on, trans): if based_on == "Item": based_on_details["based_on_cols"] = ["Item:Link/Item:120", "Item Name:Data:120"] based_on_details["based_on_select"] = "t2.item_code, t2.item_name," - based_on_details["based_on_group_by"] = 't2.item_code' - based_on_details["addl_tables"] = '' + based_on_details["based_on_group_by"] = "t2.item_code" + based_on_details["addl_tables"] = "" elif based_on == "Item Group": based_on_details["based_on_cols"] = ["Item Group:Link/Item Group:120"] based_on_details["based_on_select"] = "t2.item_group," - based_on_details["based_on_group_by"] = 't2.item_group' - based_on_details["addl_tables"] = '' + based_on_details["based_on_group_by"] = "t2.item_group" + based_on_details["addl_tables"] = "" elif based_on == "Customer": - based_on_details["based_on_cols"] = ["Customer:Link/Customer:120", "Territory:Link/Territory:120"] + based_on_details["based_on_cols"] = [ + "Customer:Link/Customer:120", + "Territory:Link/Territory:120", + ] based_on_details["based_on_select"] = "t1.customer_name, t1.territory, " - based_on_details["based_on_group_by"] = 't1.party_name' if trans == 'Quotation' else 't1.customer' - based_on_details["addl_tables"] = '' + based_on_details["based_on_group_by"] = ( + "t1.party_name" if trans == "Quotation" else "t1.customer" + ) + based_on_details["addl_tables"] = "" elif based_on == "Customer Group": based_on_details["based_on_cols"] = ["Customer Group:Link/Customer Group"] based_on_details["based_on_select"] = "t1.customer_group," - based_on_details["based_on_group_by"] = 't1.customer_group' - based_on_details["addl_tables"] = '' + based_on_details["based_on_group_by"] = "t1.customer_group" + based_on_details["addl_tables"] = "" - elif based_on == 'Supplier': - based_on_details["based_on_cols"] = ["Supplier:Link/Supplier:120", "Supplier Group:Link/Supplier Group:140"] + elif based_on == "Supplier": + based_on_details["based_on_cols"] = [ + "Supplier:Link/Supplier:120", + "Supplier Group:Link/Supplier Group:140", + ] based_on_details["based_on_select"] = "t1.supplier, t3.supplier_group," - based_on_details["based_on_group_by"] = 't1.supplier' - based_on_details["addl_tables"] = ',`tabSupplier` t3' + based_on_details["based_on_group_by"] = "t1.supplier" + based_on_details["addl_tables"] = ",`tabSupplier` t3" based_on_details["addl_tables_relational_cond"] = " and t1.supplier = t3.name" - elif based_on == 'Supplier Group': + elif based_on == "Supplier Group": based_on_details["based_on_cols"] = ["Supplier Group:Link/Supplier Group:140"] based_on_details["based_on_select"] = "t3.supplier_group," - based_on_details["based_on_group_by"] = 't3.supplier_group' - based_on_details["addl_tables"] = ',`tabSupplier` t3' + based_on_details["based_on_group_by"] = "t3.supplier_group" + based_on_details["addl_tables"] = ",`tabSupplier` t3" based_on_details["addl_tables_relational_cond"] = " and t1.supplier = t3.name" elif based_on == "Territory": based_on_details["based_on_cols"] = ["Territory:Link/Territory:120"] based_on_details["based_on_select"] = "t1.territory," - based_on_details["based_on_group_by"] = 't1.territory' - based_on_details["addl_tables"] = '' + based_on_details["based_on_group_by"] = "t1.territory" + based_on_details["addl_tables"] = "" elif based_on == "Project": - if trans in ['Sales Invoice', 'Delivery Note', 'Sales Order']: + if trans in ["Sales Invoice", "Delivery Note", "Sales Order"]: based_on_details["based_on_cols"] = ["Project:Link/Project:120"] based_on_details["based_on_select"] = "t1.project," - based_on_details["based_on_group_by"] = 't1.project' - based_on_details["addl_tables"] = '' - elif trans in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']: + based_on_details["based_on_group_by"] = "t1.project" + based_on_details["addl_tables"] = "" + elif trans in ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]: based_on_details["based_on_cols"] = ["Project:Link/Project:120"] based_on_details["based_on_select"] = "t2.project," - based_on_details["based_on_group_by"] = 't2.project' - based_on_details["addl_tables"] = '' + based_on_details["based_on_group_by"] = "t2.project" + based_on_details["addl_tables"] = "" else: frappe.throw(_("Project-wise data is not available for Quotation")) return based_on_details + def group_wise_column(group_by): if group_by: - return [group_by+":Link/"+group_by+":120"] + return [group_by + ":Link/" + group_by + ":120"] else: return [] diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index 23463abe0ae..467323035ea 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -15,21 +15,29 @@ def get_list_context(context=None): return { "global_number_format": frappe.db.get_default("number_format") or "#,###.##", "currency": frappe.db.get_default("currency"), - "currency_symbols": json.dumps(dict(frappe.db.sql("""select name, symbol - from tabCurrency where enabled=1"""))), + "currency_symbols": json.dumps( + dict( + frappe.db.sql( + """select name, symbol + from tabCurrency where enabled=1""" + ) + ) + ), "row_template": "templates/includes/transaction_row.html", - "get_list": get_transaction_list + "get_list": get_transaction_list, } + def get_webform_list_context(module): - if get_module_app(module) != 'erpnext': + if get_module_app(module) != "erpnext": return - return { - "get_list": get_webform_transaction_list - } + return {"get_list": get_webform_transaction_list} -def get_webform_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"): - """ Get List of transactions for custom doctypes """ + +def get_webform_transaction_list( + doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified" +): + """Get List of transactions for custom doctypes""" from frappe.www.list import get_list if not filters: @@ -38,42 +46,62 @@ def get_webform_transaction_list(doctype, txt=None, filters=None, limit_start=0, meta = frappe.get_meta(doctype) for d in meta.fields: - if d.fieldtype == 'Link' and d.fieldname != 'amended_from': + if d.fieldtype == "Link" and d.fieldname != "amended_from": allowed_docs = [d.name for d in get_transaction_list(doctype=d.options, custom=True)] - allowed_docs.append('') - filters.append((d.fieldname, 'in', allowed_docs)) + allowed_docs.append("") + filters.append((d.fieldname, "in", allowed_docs)) - return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=False, - fields=None, order_by="modified") + return get_list( + doctype, + txt, + filters, + limit_start, + limit_page_length, + ignore_permissions=False, + fields=None, + order_by="modified", + ) -def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified", custom=False): + +def get_transaction_list( + doctype, + txt=None, + filters=None, + limit_start=0, + limit_page_length=20, + order_by="modified", + custom=False, +): user = frappe.session.user ignore_permissions = False - if not filters: filters = [] + if not filters: + filters = [] - if doctype in ['Supplier Quotation', 'Purchase Invoice']: - filters.append((doctype, 'docstatus', '<', 2)) + if doctype in ["Supplier Quotation", "Purchase Invoice"]: + filters.append((doctype, "docstatus", "<", 2)) else: - filters.append((doctype, 'docstatus', '=', 1)) + filters.append((doctype, "docstatus", "=", 1)) - if (user != 'Guest' and is_website_user()) or doctype == 'Request for Quotation': - parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype + if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation": + parties_doctype = ( + "Request for Quotation Supplier" if doctype == "Request for Quotation" else doctype + ) # find party for this contact customers, suppliers = get_customers_suppliers(parties_doctype, user) if customers: - if doctype == 'Quotation': - filters.append(('quotation_to', '=', 'Customer')) - filters.append(('party_name', 'in', customers)) + if doctype == "Quotation": + filters.append(("quotation_to", "=", "Customer")) + filters.append(("party_name", "in", customers)) else: - filters.append(('customer', 'in', customers)) + filters.append(("customer", "in", customers)) elif suppliers: - filters.append(('supplier', 'in', suppliers)) + filters.append(("supplier", "in", suppliers)) elif not custom: return [] - if doctype == 'Request for Quotation': + if doctype == "Request for Quotation": parties = customers or suppliers return rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length) @@ -84,49 +112,88 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p ignore_permissions = False filters = [] - transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length, - fields='name', ignore_permissions=ignore_permissions, order_by='modified desc') + transactions = get_list_for_transactions( + doctype, + txt, + filters, + limit_start, + limit_page_length, + fields="name", + ignore_permissions=ignore_permissions, + order_by="modified desc", + ) if custom: return transactions return post_process(doctype, transactions) -def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20, - ignore_permissions=False, fields=None, order_by=None): - """ Get List of transactions like Invoices, Orders """ + +def get_list_for_transactions( + doctype, + txt, + filters, + limit_start, + limit_page_length=20, + ignore_permissions=False, + fields=None, + order_by=None, +): + """Get List of transactions like Invoices, Orders""" from frappe.www.list import get_list + meta = frappe.get_meta(doctype) data = [] or_filters = [] - for d in get_list(doctype, txt, filters=filters, fields="name", limit_start=limit_start, - limit_page_length=limit_page_length, ignore_permissions=ignore_permissions, order_by="modified desc"): + for d in get_list( + doctype, + txt, + filters=filters, + fields="name", + limit_start=limit_start, + limit_page_length=limit_page_length, + ignore_permissions=ignore_permissions, + order_by="modified desc", + ): data.append(d) if txt: - if meta.get_field('items'): - if meta.get_field('items').options: - child_doctype = meta.get_field('items').options - for item in frappe.get_all(child_doctype, {"item_name": ['like', "%" + txt + "%"]}): + if meta.get_field("items"): + if meta.get_field("items").options: + child_doctype = meta.get_field("items").options + for item in frappe.get_all(child_doctype, {"item_name": ["like", "%" + txt + "%"]}): child = frappe.get_doc(child_doctype, item.name) or_filters.append([doctype, "name", "=", child.parent]) if or_filters: - for r in frappe.get_list(doctype, fields=fields,filters=filters, or_filters=or_filters, - limit_start=limit_start, limit_page_length=limit_page_length, - ignore_permissions=ignore_permissions, order_by=order_by): + for r in frappe.get_list( + doctype, + fields=fields, + filters=filters, + or_filters=or_filters, + limit_start=limit_start, + limit_page_length=limit_page_length, + ignore_permissions=ignore_permissions, + order_by=order_by, + ): data.append(r) return data + def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length): - data = frappe.db.sql("""select distinct parent as name, supplier from `tab{doctype}` - where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""". - format(doctype=parties_doctype, supplier=parties[0], start=limit_start, len = limit_page_length), as_dict=1) + data = frappe.db.sql( + """select distinct parent as name, supplier from `tab{doctype}` + where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""".format( + doctype=parties_doctype, supplier=parties[0], start=limit_start, len=limit_page_length + ), + as_dict=1, + ) return post_process(doctype, data) + def post_process(doctype, data): result = [] for d in data: @@ -137,11 +204,15 @@ def post_process(doctype, data): if doc.get("per_billed"): doc.status_percent += flt(doc.per_billed) - doc.status_display.append(_("Billed") if doc.per_billed==100 else _("{0}% Billed").format(doc.per_billed)) + doc.status_display.append( + _("Billed") if doc.per_billed == 100 else _("{0}% Billed").format(doc.per_billed) + ) if doc.get("per_delivered"): doc.status_percent += flt(doc.per_delivered) - doc.status_display.append(_("Delivered") if doc.per_delivered==100 else _("{0}% Delivered").format(doc.per_delivered)) + doc.status_display.append( + _("Delivered") if doc.per_delivered == 100 else _("{0}% Delivered").format(doc.per_delivered) + ) if hasattr(doc, "set_indicator"): doc.set_indicator() @@ -152,6 +223,7 @@ def post_process(doctype, data): return result + def get_customers_suppliers(doctype, user): customers = [] suppliers = [] @@ -160,10 +232,11 @@ def get_customers_suppliers(doctype, user): customer_field_name = get_customer_field_name(doctype) has_customer_field = meta.has_field(customer_field_name) - has_supplier_field = meta.has_field('supplier') + has_supplier_field = meta.has_field("supplier") if has_common(["Supplier", "Customer"], frappe.get_roles(user)): - contacts = frappe.db.sql(""" + contacts = frappe.db.sql( + """ select `tabContact`.email_id, `tabDynamic Link`.link_doctype, @@ -172,15 +245,18 @@ def get_customers_suppliers(doctype, user): `tabContact`, `tabDynamic Link` where `tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s - """, user, as_dict=1) - customers = [c.link_name for c in contacts if c.link_doctype == 'Customer'] - suppliers = [c.link_name for c in contacts if c.link_doctype == 'Supplier'] - elif frappe.has_permission(doctype, 'read', user=user): + """, + user, + as_dict=1, + ) + customers = [c.link_name for c in contacts if c.link_doctype == "Customer"] + suppliers = [c.link_name for c in contacts if c.link_doctype == "Supplier"] + elif frappe.has_permission(doctype, "read", user=user): customer_list = frappe.get_list("Customer") customers = suppliers = [customer.name for customer in customer_list] - return customers if has_customer_field else None, \ - suppliers if has_supplier_field else None + return customers if has_customer_field else None, suppliers if has_supplier_field else None + def has_website_permission(doc, ptype, user, verbose=False): doctype = doc.doctype @@ -188,25 +264,24 @@ def has_website_permission(doc, ptype, user, verbose=False): if customers: return frappe.db.exists(doctype, get_customer_filter(doc, customers)) elif suppliers: - fieldname = 'suppliers' if doctype == 'Request for Quotation' else 'supplier' - return frappe.db.exists(doctype, { - 'name': doc.name, - fieldname: ["in", suppliers] - }) + fieldname = "suppliers" if doctype == "Request for Quotation" else "supplier" + return frappe.db.exists(doctype, {"name": doc.name, fieldname: ["in", suppliers]}) else: return False + def get_customer_filter(doc, customers): doctype = doc.doctype filters = frappe._dict() filters.name = doc.name - filters[get_customer_field_name(doctype)] = ['in', customers] - if doctype == 'Quotation': - filters.quotation_to = 'Customer' + filters[get_customer_field_name(doctype)] = ["in", customers] + if doctype == "Quotation": + filters.quotation_to = "Customer" return filters + def get_customer_field_name(doctype): - if doctype == 'Quotation': - return 'party_name' + if doctype == "Quotation": + return "party_name" else: - return 'customer' + return "customer" diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 20fb987c601..69b8c324e00 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -12,17 +12,17 @@ from frappe.utils.verified_command import get_signed_params class Appointment(Document): - def find_lead_by_email(self): lead_list = frappe.get_list( - 'Lead', filters={'email_id': self.customer_email}, ignore_permissions=True) + "Lead", filters={"email_id": self.customer_email}, ignore_permissions=True + ) if lead_list: return lead_list[0].name return None def find_customer_by_email(self): customer_list = frappe.get_list( - 'Customer', filters={'email_id': self.customer_email}, ignore_permissions=True + "Customer", filters={"email_id": self.customer_email}, ignore_permissions=True ) if customer_list: return customer_list[0].name @@ -30,11 +30,12 @@ class Appointment(Document): def before_insert(self): number_of_appointments_in_same_slot = frappe.db.count( - 'Appointment', filters={'scheduled_time': self.scheduled_time}) - number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents') + "Appointment", filters={"scheduled_time": self.scheduled_time} + ) + number_of_agents = frappe.db.get_single_value("Appointment Booking Settings", "number_of_agents") if not number_of_agents == 0: - if (number_of_appointments_in_same_slot >= number_of_agents): - frappe.throw(_('Time slot is not available')) + if number_of_appointments_in_same_slot >= number_of_agents: + frappe.throw(_("Time slot is not available")) # Link lead if not self.party: lead = self.find_lead_by_email() @@ -53,45 +54,46 @@ class Appointment(Document): self.create_calendar_event() else: # Set status to unverified - self.status = 'Unverified' + self.status = "Unverified" # Send email to confirm self.send_confirmation_email() def send_confirmation_email(self): verify_url = self._get_verify_url() - template = 'confirm_appointment' + template = "confirm_appointment" args = { - "link":verify_url, - "site_url":frappe.utils.get_url(), - "full_name":self.customer_name, + "link": verify_url, + "site_url": frappe.utils.get_url(), + "full_name": self.customer_name, } - frappe.sendmail(recipients=[self.customer_email], - template=template, - args=args, - subject=_('Appointment Confirmation')) + frappe.sendmail( + recipients=[self.customer_email], + template=template, + args=args, + subject=_("Appointment Confirmation"), + ) if frappe.session.user == "Guest": + frappe.msgprint(_("Please check your email to confirm the appointment")) + else: frappe.msgprint( - _('Please check your email to confirm the appointment')) - else : - frappe.msgprint( - _('Appointment was created. But no lead was found. Please check the email to confirm')) + _("Appointment was created. But no lead was found. Please check the email to confirm") + ) def on_change(self): # Sync Calendar if not self.calendar_event: return - cal_event = frappe.get_doc('Event', self.calendar_event) + cal_event = frappe.get_doc("Event", self.calendar_event) cal_event.starts_on = self.scheduled_time cal_event.save(ignore_permissions=True) - def set_verified(self, email): if not email == self.customer_email: - frappe.throw(_('Email verification failed.')) + frappe.throw(_("Email verification failed.")) # Create new lead self.create_lead_and_link() # Remove unverified status - self.status = 'Open' + self.status = "Open" # Create calender event self.auto_assign() self.create_calendar_event() @@ -102,58 +104,53 @@ class Appointment(Document): # Return if already linked if self.party: return - lead = frappe.get_doc({ - 'doctype': 'Lead', - 'lead_name': self.customer_name, - 'email_id': self.customer_email, - 'notes': self.customer_details, - 'phone': self.customer_phone_number, - }) + lead = frappe.get_doc( + { + "doctype": "Lead", + "lead_name": self.customer_name, + "email_id": self.customer_email, + "notes": self.customer_details, + "phone": self.customer_phone_number, + } + ) lead.insert(ignore_permissions=True) # Link lead self.party = lead.name def auto_assign(self): from frappe.desk.form.assign_to import add as add_assignemnt + existing_assignee = self.get_assignee_from_latest_opportunity() if existing_assignee: # If the latest opportunity is assigned to someone # Assign the appointment to the same - add_assignemnt({ - 'doctype': self.doctype, - 'name': self.name, - 'assign_to': [existing_assignee] - }) + add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]}) return if self._assign: return - available_agents = _get_agents_sorted_by_asc_workload( - getdate(self.scheduled_time)) + available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time)) for agent in available_agents: - if(_check_agent_availability(agent, self.scheduled_time)): + if _check_agent_availability(agent, self.scheduled_time): agent = agent[0] - add_assignemnt({ - 'doctype': self.doctype, - 'name': self.name, - 'assign_to': [agent] - }) + add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) break def get_assignee_from_latest_opportunity(self): if not self.party: return None - if not frappe.db.exists('Lead', self.party): + if not frappe.db.exists("Lead", self.party): return None opporutnities = frappe.get_list( - 'Opportunity', + "Opportunity", filters={ - 'party_name': self.party, + "party_name": self.party, }, ignore_permissions=True, - order_by='creation desc') + order_by="creation desc", + ) if not opporutnities: return None - latest_opportunity = frappe.get_doc('Opportunity', opporutnities[0].name ) + latest_opportunity = frappe.get_doc("Opportunity", opporutnities[0].name) assignee = latest_opportunity._assign if not assignee: return None @@ -163,35 +160,36 @@ class Appointment(Document): def create_calendar_event(self): if self.calendar_event: return - appointment_event = frappe.get_doc({ - 'doctype': 'Event', - 'subject': ' '.join(['Appointment with', self.customer_name]), - 'starts_on': self.scheduled_time, - 'status': 'Open', - 'type': 'Public', - 'send_reminder': frappe.db.get_single_value('Appointment Booking Settings', 'email_reminders'), - 'event_participants': [dict(reference_doctype=self.appointment_with, reference_docname=self.party)] - }) + appointment_event = frappe.get_doc( + { + "doctype": "Event", + "subject": " ".join(["Appointment with", self.customer_name]), + "starts_on": self.scheduled_time, + "status": "Open", + "type": "Public", + "send_reminder": frappe.db.get_single_value("Appointment Booking Settings", "email_reminders"), + "event_participants": [ + dict(reference_doctype=self.appointment_with, reference_docname=self.party) + ], + } + ) employee = _get_employee_from_user(self._assign) if employee: - appointment_event.append('event_participants', dict( - reference_doctype='Employee', - reference_docname=employee.name)) + appointment_event.append( + "event_participants", dict(reference_doctype="Employee", reference_docname=employee.name) + ) appointment_event.insert(ignore_permissions=True) self.calendar_event = appointment_event.name self.save(ignore_permissions=True) def _get_verify_url(self): - verify_route = '/book_appointment/verify' - params = { - 'email': self.customer_email, - 'appointment': self.name - } - return get_url(verify_route + '?' + get_signed_params(params)) + verify_route = "/book_appointment/verify" + params = {"email": self.customer_email, "appointment": self.name} + return get_url(verify_route + "?" + get_signed_params(params)) def _get_agents_sorted_by_asc_workload(date): - appointments = frappe.db.get_list('Appointment', fields='*') + appointments = frappe.db.get_list("Appointment", fields="*") agent_list = _get_agent_list_as_strings() if not appointments: return agent_list @@ -209,7 +207,7 @@ def _get_agents_sorted_by_asc_workload(date): def _get_agent_list_as_strings(): agent_list_as_strings = [] - agent_list = frappe.get_doc('Appointment Booking Settings').agent_list + agent_list = frappe.get_doc("Appointment Booking Settings").agent_list for agent in agent_list: agent_list_as_strings.append(agent.user) return agent_list_as_strings @@ -217,7 +215,8 @@ def _get_agent_list_as_strings(): def _check_agent_availability(agent_email, scheduled_time): appointemnts_at_scheduled_time = frappe.get_list( - 'Appointment', filters={'scheduled_time': scheduled_time}) + "Appointment", filters={"scheduled_time": scheduled_time} + ) for appointment in appointemnts_at_scheduled_time: if appointment._assign == agent_email: return False @@ -225,9 +224,8 @@ def _check_agent_availability(agent_email, scheduled_time): def _get_employee_from_user(user): - employee_docname = frappe.db.exists( - {'doctype': 'Employee', 'user_id': user}) + employee_docname = frappe.db.exists({"doctype": "Employee", "user_id": user}) if employee_docname: # frappe.db.exists returns a tuple of a tuple - return frappe.get_doc('Employee', employee_docname[0][0]) + return frappe.get_doc("Employee", employee_docname[0][0]) return None diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 8335f756ca8..6fca5da9ef2 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -8,50 +8,53 @@ import frappe def create_test_lead(): - test_lead = frappe.db.exists({'doctype': 'Lead', 'lead_name': 'Test Lead'}) - if test_lead: - return frappe.get_doc('Lead', test_lead[0][0]) - test_lead = frappe.get_doc({ - 'doctype': 'Lead', - 'lead_name': 'Test Lead', - 'email_id': 'test@example.com' - }) - test_lead.insert(ignore_permissions=True) - return test_lead + test_lead = frappe.db.exists({"doctype": "Lead", "lead_name": "Test Lead"}) + if test_lead: + return frappe.get_doc("Lead", test_lead[0][0]) + test_lead = frappe.get_doc( + {"doctype": "Lead", "lead_name": "Test Lead", "email_id": "test@example.com"} + ) + test_lead.insert(ignore_permissions=True) + return test_lead def create_test_appointments(): - test_appointment = frappe.db.exists( - {'doctype': 'Appointment', 'scheduled_time':datetime.datetime.now(),'email':'test@example.com'}) - if test_appointment: - return frappe.get_doc('Appointment', test_appointment[0][0]) - test_appointment = frappe.get_doc({ - 'doctype': 'Appointment', - 'email': 'test@example.com', - 'status': 'Open', - 'customer_name': 'Test Lead', - 'customer_phone_number': '666', - 'customer_skype': 'test', - 'customer_email': 'test@example.com', - 'scheduled_time': datetime.datetime.now() - }) - test_appointment.insert() - return test_appointment + test_appointment = frappe.db.exists( + { + "doctype": "Appointment", + "scheduled_time": datetime.datetime.now(), + "email": "test@example.com", + } + ) + if test_appointment: + return frappe.get_doc("Appointment", test_appointment[0][0]) + test_appointment = frappe.get_doc( + { + "doctype": "Appointment", + "email": "test@example.com", + "status": "Open", + "customer_name": "Test Lead", + "customer_phone_number": "666", + "customer_skype": "test", + "customer_email": "test@example.com", + "scheduled_time": datetime.datetime.now(), + } + ) + test_appointment.insert() + return test_appointment class TestAppointment(unittest.TestCase): - test_appointment = test_lead = None + test_appointment = test_lead = None - def setUp(self): - self.test_lead = create_test_lead() - self.test_appointment = create_test_appointments() + def setUp(self): + self.test_lead = create_test_lead() + self.test_appointment = create_test_appointments() - def test_calendar_event_created(self): - cal_event = frappe.get_doc( - 'Event', self.test_appointment.calendar_event) - self.assertEqual(cal_event.starts_on, - self.test_appointment.scheduled_time) + def test_calendar_event_created(self): + cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event) + self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time) - def test_lead_linked(self): - lead = frappe.get_doc('Lead', self.test_lead.name) - self.assertIsNotNone(lead) + def test_lead_linked(self): + lead = frappe.get_doc("Lead", self.test_lead.name) + self.assertIsNotNone(lead) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 1431b03a2ef..e43f4601e9c 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -10,8 +10,8 @@ from frappe.model.document import Document class AppointmentBookingSettings(Document): - agent_list = [] #Hack - min_date = '01/01/1970 ' + agent_list = [] # Hack + min_date = "01/01/1970 " format_string = "%d/%m/%Y %H:%M:%S" def validate(self): @@ -23,21 +23,22 @@ class AppointmentBookingSettings(Document): def validate_availability_of_slots(self): for record in self.availability_of_slots: - from_time = datetime.datetime.strptime( - self.min_date+record.from_time, self.format_string) - to_time = datetime.datetime.strptime( - self.min_date+record.to_time, self.format_string) - timedelta = to_time-from_time + from_time = datetime.datetime.strptime(self.min_date + record.from_time, self.format_string) + to_time = datetime.datetime.strptime(self.min_date + record.to_time, self.format_string) + timedelta = to_time - from_time self.validate_from_and_to_time(from_time, to_time, record) self.duration_is_divisible(from_time, to_time) def validate_from_and_to_time(self, from_time, to_time, record): if from_time > to_time: - err_msg = _('From Time cannot be later than To Time for {0}').format(record.day_of_week) + err_msg = _("From Time cannot be later than To Time for {0}").format( + record.day_of_week + ) frappe.throw(_(err_msg)) def duration_is_divisible(self, from_time, to_time): timedelta = to_time - from_time if timedelta.total_seconds() % (self.appointment_duration * 60): frappe.throw( - _('The difference between from time and To Time must be a multiple of Appointment')) + _("The difference between from time and To Time must be a multiple of Appointment") + ) diff --git a/erpnext/crm/doctype/contract/contract.js b/erpnext/crm/doctype/contract/contract.js index 7848de7a727..8751edbc9b2 100644 --- a/erpnext/crm/doctype/contract/contract.js +++ b/erpnext/crm/doctype/contract/contract.js @@ -2,6 +2,17 @@ // For license information, please see license.txt frappe.ui.form.on("Contract", { + onload: function(frm) { + frappe.db.get_value( + "Selling Settings", + "Selling Settings", + "contract_naming_by", + (r) => { + frm.toggle_display("naming_series", r.contract_naming_by === "Naming Series"); + } + ); + }, + contract_template: function (frm) { if (frm.doc.contract_template) { frappe.call({ diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index de3230f0e67..8a65ca5f20d 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -2,11 +2,13 @@ "actions": [], "allow_import": 1, "allow_rename": 1, + "autoname": "naming_series:", "creation": "2018-04-12 06:32:04.582486", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "naming_series", "party_type", "is_signed", "cb_party", @@ -244,11 +246,20 @@ "fieldname": "authorised_by_section", "fieldtype": "Section Break", "label": "Authorised By" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "no_copy": 1, + "options": "CRM-CONTR-.YYYY.-", + "reqd": 1, + "set_only_once": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-12-07 11:15:58.385521", + "modified": "2022-03-28 10:22:11.156658", "modified_by": "Administrator", "module": "CRM", "name": "Contract", diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index e21f46a3837..8a2c2e966e9 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -5,22 +5,32 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.model.naming import set_name_by_naming_series from frappe.utils import getdate, nowdate class Contract(Document): def autoname(self): - name = self.party_name + if frappe.db.get_single_value("Selling Settings", "contract_naming_by") == "Naming Series": + set_name_by_naming_series(self) - if self.contract_template: - name += " - {} Agreement".format(self.contract_template) + else: + name = self.party_name - # If identical, append contract name with the next number in the iteration - if frappe.db.exists("Contract", name): - count = len(frappe.get_all("Contract", filters={"name": ["like", "%{}%".format(name)]})) - name = "{} - {}".format(name, count) + if self.contract_template: + name = f"{name} - {self.contract_template} Agreement" - self.name = _(name) + # If identical, append contract name with the next number in the iteration + if frappe.db.exists("Contract", name): + count = frappe.db.count( + "Contract", + filters={ + "name": ("like", f"%{name}%"), + }, + ) + name = f"{name} - {count}" + + self.name = _(name) def validate(self): self.validate_dates() @@ -75,11 +85,11 @@ def get_status(start_date, end_date): Get a Contract's status based on the start, current and end dates Args: - start_date (str): The start date of the contract - end_date (str): The end date of the contract + start_date (str): The start date of the contract + end_date (str): The end date of the contract Returns: - str: 'Active' if within range, otherwise 'Inactive' + str: 'Active' if within range, otherwise 'Inactive' """ if not end_date: @@ -98,13 +108,13 @@ def update_status_for_contracts(): and submitted Contracts """ - contracts = frappe.get_all("Contract", - filters={"is_signed": True, - "docstatus": 1}, - fields=["name", "start_date", "end_date"]) + contracts = frappe.get_all( + "Contract", + filters={"is_signed": True, "docstatus": 1}, + fields=["name", "start_date", "end_date"], + ) for contract in contracts: - status = get_status(contract.get("start_date"), - contract.get("end_date")) + status = get_status(contract.get("start_date"), contract.get("end_date")) frappe.db.set_value("Contract", contract.get("name"), "status", status) diff --git a/erpnext/crm/doctype/contract/test_contract.py b/erpnext/crm/doctype/contract/test_contract.py index e685362a494..13901683de8 100644 --- a/erpnext/crm/doctype/contract/test_contract.py +++ b/erpnext/crm/doctype/contract/test_contract.py @@ -8,7 +8,6 @@ from frappe.utils import add_days, nowdate class TestContract(unittest.TestCase): - def setUp(self): frappe.db.sql("delete from `tabContract`") self.contract_doc = get_contract() @@ -65,10 +64,7 @@ class TestContract(unittest.TestCase): # Mark all the terms as fulfilled self.contract_doc.requires_fulfilment = 1 fulfilment_terms = [] - fulfilment_terms.append({ - "requirement": "This is a test requirement.", - "fulfilled": 0 - }) + fulfilment_terms.append({"requirement": "This is a test requirement.", "fulfilled": 0}) self.contract_doc.set("fulfilment_terms", fulfilment_terms) for term in self.contract_doc.fulfilment_terms: @@ -85,14 +81,8 @@ class TestContract(unittest.TestCase): # Mark only the first term as fulfilled self.contract_doc.save() fulfilment_terms = [] - fulfilment_terms.append({ - "requirement": "This is a test requirement.", - "fulfilled": 0 - }) - fulfilment_terms.append({ - "requirement": "This is another test requirement.", - "fulfilled": 0 - }) + fulfilment_terms.append({"requirement": "This is a test requirement.", "fulfilled": 0}) + fulfilment_terms.append({"requirement": "This is another test requirement.", "fulfilled": 0}) self.contract_doc.set("fulfilment_terms", fulfilment_terms) self.contract_doc.fulfilment_terms[0].fulfilled = 1 @@ -110,6 +100,7 @@ class TestContract(unittest.TestCase): self.assertEqual(self.contract_doc.fulfilment_status, "Lapsed") + def get_contract(): doc = frappe.new_doc("Contract") doc.party_type = "Customer" diff --git a/erpnext/crm/doctype/contract_template/contract_template.py b/erpnext/crm/doctype/contract_template/contract_template.py index 8adbb4e25e4..f417bf0c209 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.py +++ b/erpnext/crm/doctype/contract_template/contract_template.py @@ -15,6 +15,7 @@ class ContractTemplate(Document): if self.contract_terms: validate_template(self.contract_terms) + @frappe.whitelist() def get_contract_template(template_name, doc): if isinstance(doc, string_types): @@ -26,7 +27,4 @@ def get_contract_template(template_name, doc): if contract_template.contract_terms: contract_terms = frappe.render_template(contract_template.contract_terms, doc) - return { - 'contract_template': contract_template, - 'contract_terms': contract_terms - } + return {"contract_template": contract_template, "contract_terms": contract_terms} diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index d44443237e8..9ec54ffc1ef 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -12,7 +12,7 @@ from frappe.utils import add_days, getdate, today class EmailCampaign(Document): def validate(self): self.set_date() - #checking if email is set for lead. Not checking for contact as email is a mandatory field for contact. + # checking if email is set for lead. Not checking for contact as email is a mandatory field for contact. if self.email_campaign_for == "Lead": self.validate_lead() self.validate_email_campaign_already_exists() @@ -21,7 +21,7 @@ class EmailCampaign(Document): def set_date(self): if getdate(self.start_date) < getdate(today()): frappe.throw(_("Start Date cannot be before the current date")) - #set the end date as start date + max(send after days) in campaign schedule + # set the end date as start date + max(send after days) in campaign schedule send_after_days = [] campaign = frappe.get_doc("Campaign", self.campaign_name) for entry in campaign.get("campaign_schedules"): @@ -29,23 +29,32 @@ class EmailCampaign(Document): try: self.end_date = add_days(getdate(self.start_date), max(send_after_days)) except ValueError: - frappe.throw(_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name)) + frappe.throw( + _("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name) + ) def validate_lead(self): - lead_email_id = frappe.db.get_value("Lead", self.recipient, 'email_id') + lead_email_id = frappe.db.get_value("Lead", self.recipient, "email_id") if not lead_email_id: - lead_name = frappe.db.get_value("Lead", self.recipient, 'lead_name') + lead_name = frappe.db.get_value("Lead", self.recipient, "lead_name") frappe.throw(_("Please set an email id for the Lead {0}").format(lead_name)) def validate_email_campaign_already_exists(self): - email_campaign_exists = frappe.db.exists("Email Campaign", { - "campaign_name": self.campaign_name, - "recipient": self.recipient, - "status": ("in", ["In Progress", "Scheduled"]), - "name": ("!=", self.name) - }) + email_campaign_exists = frappe.db.exists( + "Email Campaign", + { + "campaign_name": self.campaign_name, + "recipient": self.recipient, + "status": ("in", ["In Progress", "Scheduled"]), + "name": ("!=", self.name), + }, + ) if email_campaign_exists: - frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) + frappe.throw( + _("The Campaign '{0}' already exists for the {1} '{2}'").format( + self.campaign_name, self.email_campaign_for, self.recipient + ) + ) def update_status(self): start_date = getdate(self.start_date) @@ -58,51 +67,63 @@ class EmailCampaign(Document): elif end_date < today_date: self.status = "Completed" -#called through hooks to send campaign mails to leads + +# called through hooks to send campaign mails to leads def send_email_to_leads_or_contacts(): - email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('not in', ['Unsubscribed', 'Completed', 'Scheduled']) }) + email_campaigns = frappe.get_all( + "Email Campaign", filters={"status": ("not in", ["Unsubscribed", "Completed", "Scheduled"])} + ) for camp in email_campaigns: email_campaign = frappe.get_doc("Email Campaign", camp.name) campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name) for entry in campaign.get("campaign_schedules"): - scheduled_date = add_days(email_campaign.get('start_date'), entry.get('send_after_days')) + scheduled_date = add_days(email_campaign.get("start_date"), entry.get("send_after_days")) if scheduled_date == getdate(today()): send_mail(entry, email_campaign) + def send_mail(entry, email_campaign): recipient_list = [] if email_campaign.email_campaign_for == "Email Group": - for member in frappe.db.get_list("Email Group Member", filters={"email_group": email_campaign.get("recipient")}, fields=["email"]): - recipient_list.append(member['email']) + for member in frappe.db.get_list( + "Email Group Member", filters={"email_group": email_campaign.get("recipient")}, fields=["email"] + ): + recipient_list.append(member["email"]) else: - recipient_list.append(frappe.db.get_value(email_campaign.email_campaign_for, email_campaign.get("recipient"), "email_id")) + recipient_list.append( + frappe.db.get_value( + email_campaign.email_campaign_for, email_campaign.get("recipient"), "email_id" + ) + ) email_template = frappe.get_doc("Email Template", entry.get("email_template")) sender = frappe.db.get_value("User", email_campaign.get("sender"), "email") context = {"doc": frappe.get_doc(email_campaign.email_campaign_for, email_campaign.recipient)} # send mail and link communication to document comm = make( - doctype = "Email Campaign", - name = email_campaign.name, - subject = frappe.render_template(email_template.get("subject"), context), - content = frappe.render_template(email_template.get("response"), context), - sender = sender, - recipients = recipient_list, - communication_medium = "Email", - sent_or_received = "Sent", - send_email = True, - email_template = email_template.name + doctype="Email Campaign", + name=email_campaign.name, + subject=frappe.render_template(email_template.get("subject"), context), + content=frappe.render_template(email_template.get("response"), context), + sender=sender, + recipients=recipient_list, + communication_medium="Email", + sent_or_received="Sent", + send_email=True, + email_template=email_template.name, ) return comm -#called from hooks on doc_event Email Unsubscribe + +# called from hooks on doc_event Email Unsubscribe def unsubscribe_recipient(unsubscribe, method): - if unsubscribe.reference_doctype == 'Email Campaign': + if unsubscribe.reference_doctype == "Email Campaign": frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed") -#called through hooks to update email campaign status daily + +# called through hooks to update email campaign status daily def set_email_campaign_status(): - email_campaigns = frappe.get_all("Email Campaign", filters = { 'status': ('!=', 'Unsubscribed')}) + email_campaigns = frappe.get_all("Email Campaign", filters={"status": ("!=", "Unsubscribed")}) for entry in email_campaigns: email_campaign = frappe.get_doc("Email Campaign", entry.name) email_campaign.update_status() diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 809a9d29138..36ec225ce89 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -15,7 +15,7 @@ from erpnext.controllers.selling_controller import SellingController class Lead(SellingController): def get_feed(self): - return '{0}: {1}'.format(_(self.status), self.lead_name) + return "{0}: {1}".format(_(self.status), self.lead_name) def onload(self): customer = frappe.db.get_value("Customer", {"lead_name": self.name}) @@ -57,8 +57,7 @@ class Lead(SellingController): if self.contact_date and getdate(self.contact_date) < getdate(nowdate()): frappe.throw(_("Next Contact Date cannot be in the past")) - if (self.ends_on and self.contact_date and - (getdate(self.ends_on) < getdate(self.contact_date))): + if self.ends_on and self.contact_date and (getdate(self.ends_on) < getdate(self.contact_date)): frappe.throw(_("Ends On date cannot be before Next Contact Date.")) def on_update(self): @@ -66,32 +65,38 @@ class Lead(SellingController): def set_prev(self): if self.is_new(): - self._prev = frappe._dict({ - "contact_date": None, - "ends_on": None, - "contact_by": None - }) + self._prev = frappe._dict({"contact_date": None, "ends_on": None, "contact_by": None}) else: - self._prev = frappe.db.get_value("Lead", self.name, ["contact_date", "ends_on", "contact_by"], as_dict=1) + self._prev = frappe.db.get_value( + "Lead", self.name, ["contact_date", "ends_on", "contact_by"], as_dict=1 + ) def add_calendar_event(self, opts=None, force=False): - super(Lead, self).add_calendar_event({ - "owner": self.lead_owner, - "starts_on": self.contact_date, - "ends_on": self.ends_on or "", - "subject": ('Contact ' + cstr(self.lead_name)), - "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') - }, force) + super(Lead, self).add_calendar_event( + { + "owner": self.lead_owner, + "starts_on": self.contact_date, + "ends_on": self.ends_on or "", + "subject": ("Contact " + cstr(self.lead_name)), + "description": ("Contact " + cstr(self.lead_name)) + + (self.contact_by and (". By : " + cstr(self.contact_by)) or ""), + }, + force, + ) def check_email_id_is_unique(self): if self.email_id: # validate email is unique - duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) + duplicate_leads = frappe.get_all( + "Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]} + ) duplicate_leads = [lead.name for lead in duplicate_leads] if duplicate_leads: - frappe.throw(_("Email Address must be unique, already exists for {0}") - .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) + frappe.throw( + _("Email Address must be unique, already exists for {0}").format(comma_and(duplicate_leads)), + frappe.DuplicateEntryError, + ) def on_trash(self): frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) @@ -105,19 +110,14 @@ class Lead(SellingController): return frappe.db.get_value("Opportunity", {"party_name": self.name, "status": ["!=", "Lost"]}) def has_quotation(self): - return frappe.db.get_value("Quotation", { - "party_name": self.name, - "docstatus": 1, - "status": ["!=", "Lost"] - - }) + return frappe.db.get_value( + "Quotation", {"party_name": self.name, "docstatus": 1, "status": ["!=", "Lost"]} + ) def has_lost_quotation(self): - return frappe.db.get_value("Quotation", { - "party_name": self.name, - "docstatus": 1, - "status": "Lost" - }) + return frappe.db.get_value( + "Quotation", {"party_name": self.name, "docstatus": 1, "status": "Lost"} + ) def set_lead_name(self): if not self.lead_name: @@ -136,8 +136,17 @@ class Lead(SellingController): self.title = self.lead_name def create_address(self): - address_fields = ["address_type", "address_title", "address_line1", "address_line2", - "city", "county", "state", "country", "pincode"] + address_fields = [ + "address_type", + "address_title", + "address_line1", + "address_line2", + "city", + "county", + "state", + "country", + "pincode", + ] info_fields = ["email_id", "phone", "fax"] # do not create an address if no fields are available, @@ -161,56 +170,47 @@ class Lead(SellingController): first_name, last_name = self.lead_name, None contact = frappe.new_doc("Contact") - contact.update({ - "first_name": first_name, - "last_name": last_name, - "salutation": self.salutation, - "gender": self.gender, - "designation": self.designation, - "company_name": self.company_name, - }) + contact.update( + { + "first_name": first_name, + "last_name": last_name, + "salutation": self.salutation, + "gender": self.gender, + "designation": self.designation, + "company_name": self.company_name, + } + ) if self.email_id: - contact.append("email_ids", { - "email_id": self.email_id, - "is_primary": 1 - }) + contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1}) if self.phone: - contact.append("phone_nos", { - "phone": self.phone, - "is_primary_phone": 1 - }) + contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1}) if self.mobile_no: - contact.append("phone_nos", { - "phone": self.mobile_no, - "is_primary_mobile_no":1 - }) + contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1}) contact.insert(ignore_permissions=True) + contact.reload() # load changes by hooks on contact return contact def update_links(self): # update address links - if hasattr(self, 'address_doc'): - self.address_doc.append("links", { - "link_doctype": "Lead", - "link_name": self.name, - "link_title": self.lead_name - }) + if hasattr(self, "address_doc"): + self.address_doc.append( + "links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name} + ) self.address_doc.save() # update contact links if self.contact_doc: - self.contact_doc.append("links", { - "link_doctype": "Lead", - "link_name": self.name, - "link_title": self.lead_name - }) + self.contact_doc.append( + "links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name} + ) self.contact_doc.save() + @frappe.whitelist() def make_customer(source_name, target_doc=None): return _make_customer(source_name, target_doc) @@ -227,16 +227,24 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False): target.customer_group = frappe.db.get_default("Customer Group") - doclist = get_mapped_doc("Lead", source_name, - {"Lead": { - "doctype": "Customer", - "field_map": { - "name": "lead_name", - "company_name": "customer_name", - "contact_no": "phone_1", - "fax": "fax_1" + doclist = get_mapped_doc( + "Lead", + source_name, + { + "Lead": { + "doctype": "Customer", + "field_map": { + "name": "lead_name", + "company_name": "customer_name", + "contact_no": "phone_1", + "fax": "fax_1", + }, } - }}, target_doc, set_missing_values, ignore_permissions=ignore_permissions) + }, + target_doc, + set_missing_values, + ignore_permissions=ignore_permissions, + ) return doclist @@ -246,19 +254,26 @@ def make_opportunity(source_name, target_doc=None): def set_missing_values(source, target): _set_missing_values(source, target) - target_doc = get_mapped_doc("Lead", source_name, - {"Lead": { - "doctype": "Opportunity", - "field_map": { - "campaign_name": "campaign", - "doctype": "opportunity_from", - "name": "party_name", - "lead_name": "contact_display", - "company_name": "customer_name", - "email_id": "contact_email", - "mobile_no": "contact_mobile" + target_doc = get_mapped_doc( + "Lead", + source_name, + { + "Lead": { + "doctype": "Opportunity", + "field_map": { + "campaign_name": "campaign", + "doctype": "opportunity_from", + "name": "party_name", + "lead_name": "contact_display", + "company_name": "customer_name", + "email_id": "contact_email", + "mobile_no": "contact_mobile", + }, } - }}, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return target_doc @@ -268,13 +283,13 @@ def make_quotation(source_name, target_doc=None): def set_missing_values(source, target): _set_missing_values(source, target) - target_doc = get_mapped_doc("Lead", source_name, - {"Lead": { - "doctype": "Quotation", - "field_map": { - "name": "party_name" - } - }}, target_doc, set_missing_values) + target_doc = get_mapped_doc( + "Lead", + source_name, + {"Lead": {"doctype": "Quotation", "field_map": {"name": "party_name"}}}, + target_doc, + set_missing_values, + ) target_doc.quotation_to = "Lead" target_doc.run_method("set_missing_values") @@ -283,18 +298,29 @@ def make_quotation(source_name, target_doc=None): return target_doc -def _set_missing_values(source, target): - address = frappe.get_all('Dynamic Link', { - 'link_doctype': source.doctype, - 'link_name': source.name, - 'parenttype': 'Address', - }, ['parent'], limit=1) - contact = frappe.get_all('Dynamic Link', { - 'link_doctype': source.doctype, - 'link_name': source.name, - 'parenttype': 'Contact', - }, ['parent'], limit=1) +def _set_missing_values(source, target): + address = frappe.get_all( + "Dynamic Link", + { + "link_doctype": source.doctype, + "link_name": source.name, + "parenttype": "Address", + }, + ["parent"], + limit=1, + ) + + contact = frappe.get_all( + "Dynamic Link", + { + "link_doctype": source.doctype, + "link_name": source.name, + "parenttype": "Contact", + }, + ["parent"], + limit=1, + ) if address: target.customer_address = address[0].parent @@ -302,39 +328,49 @@ def _set_missing_values(source, target): if contact: target.contact_person = contact[0].parent + @frappe.whitelist() def get_lead_details(lead, posting_date=None, company=None): if not lead: return {} from erpnext.accounts.party import set_address_details + out = frappe._dict() lead_doc = frappe.get_doc("Lead", lead) lead = lead_doc - out.update({ - "territory": lead.territory, - "customer_name": lead.company_name or lead.lead_name, - "contact_display": " ".join(filter(None, [lead.salutation, lead.lead_name])), - "contact_email": lead.email_id, - "contact_mobile": lead.mobile_no, - "contact_phone": lead.phone, - }) + out.update( + { + "territory": lead.territory, + "customer_name": lead.company_name or lead.lead_name, + "contact_display": " ".join(filter(None, [lead.salutation, lead.lead_name])), + "contact_email": lead.email_id, + "contact_mobile": lead.mobile_no, + "contact_phone": lead.phone, + } + ) set_address_details(out, lead, "Lead") - taxes_and_charges = set_taxes(None, 'Lead', posting_date, company, - billing_address=out.get('customer_address'), shipping_address=out.get('shipping_address_name')) + taxes_and_charges = set_taxes( + None, + "Lead", + posting_date, + company, + billing_address=out.get("customer_address"), + shipping_address=out.get("shipping_address_name"), + ) if taxes_and_charges: - out['taxes_and_charges'] = taxes_and_charges + out["taxes_and_charges"] = taxes_and_charges return out @frappe.whitelist() def make_lead_from_communication(communication, ignore_communication_links=False): - """ raise a issue from email """ + """raise a issue from email""" doc = frappe.get_doc("Communication", communication) lead_name = None @@ -343,12 +379,14 @@ def make_lead_from_communication(communication, ignore_communication_links=False if not lead_name and doc.phone_no: lead_name = frappe.db.get_value("Lead", {"mobile_no": doc.phone_no}) if not lead_name: - lead = frappe.get_doc({ - "doctype": "Lead", - "lead_name": doc.sender_full_name, - "email_id": doc.sender, - "mobile_no": doc.phone_no - }) + lead = frappe.get_doc( + { + "doctype": "Lead", + "lead_name": doc.sender_full_name, + "email_id": doc.sender, + "mobile_no": doc.phone_no, + } + ) lead.flags.ignore_mandatory = True lead.flags.ignore_permissions = True lead.insert() @@ -358,19 +396,27 @@ def make_lead_from_communication(communication, ignore_communication_links=False link_communication_to_document(doc, "Lead", lead_name, ignore_communication_links) return lead_name -def get_lead_with_phone_number(number): - if not number: return - leads = frappe.get_all('Lead', or_filters={ - 'phone': ['like', '%{}'.format(number)], - 'mobile_no': ['like', '%{}'.format(number)] - }, limit=1, order_by="creation DESC") +def get_lead_with_phone_number(number): + if not number: + return + + leads = frappe.get_all( + "Lead", + or_filters={ + "phone": ["like", "%{}".format(number)], + "mobile_no": ["like", "%{}".format(number)], + }, + limit=1, + order_by="creation DESC", + ) lead = leads[0].name if leads else None return lead + def daily_open_lead(): - leads = frappe.get_all("Lead", filters = [["contact_date", "Between", [nowdate(), nowdate()]]]) + leads = frappe.get_all("Lead", filters=[["contact_date", "Between", [nowdate(), nowdate()]]]) for lead in leads: frappe.db.set_value("Lead", lead.name, "status", "Open") diff --git a/erpnext/crm/doctype/lead/lead_dashboard.py b/erpnext/crm/doctype/lead/lead_dashboard.py index 87a8c51bb84..c9ea88950d4 100644 --- a/erpnext/crm/doctype/lead/lead_dashboard.py +++ b/erpnext/crm/doctype/lead/lead_dashboard.py @@ -1,18 +1,9 @@ - - def get_data(): return { - 'fieldname': 'lead', - 'non_standard_fieldnames': { - 'Quotation': 'party_name', - 'Opportunity': 'party_name' - }, - 'dynamic_links': { - 'party_name': ['Lead', 'quotation_to'] - }, - 'transactions': [ - { - 'items': ['Opportunity', 'Quotation'] - }, - ] + "fieldname": "lead", + "non_standard_fieldnames": {"Quotation": "party_name", "Opportunity": "party_name"}, + "dynamic_links": {"party_name": ["Lead", "quotation_to"]}, + "transactions": [ + {"items": ["Opportunity", "Quotation"]}, + ], } diff --git a/erpnext/crm/doctype/lead/test_lead.py b/erpnext/crm/doctype/lead/test_lead.py index d4157db848d..fc48363c293 100644 --- a/erpnext/crm/doctype/lead/test_lead.py +++ b/erpnext/crm/doctype/lead/test_lead.py @@ -6,7 +6,8 @@ import unittest import frappe -test_records = frappe.get_test_records('Lead') +test_records = frappe.get_test_records("Lead") + class TestLead(unittest.TestCase): def test_make_customer(self): diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index 8fd4978715f..e50c13dc5e1 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -14,12 +14,16 @@ from six.moves.urllib.parse import urlencode class LinkedInSettings(Document): @frappe.whitelist() def get_authorization_url(self): - params = urlencode({ - "response_type":"code", - "client_id": self.consumer_key, - "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(frappe.utils.get_url()), - "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" - }) + params = urlencode( + { + "response_type": "code", + "client_id": self.consumer_key, + "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format( + frappe.utils.get_url() + ), + "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social", + } + ) url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params) @@ -32,11 +36,11 @@ class LinkedInSettings(Document): "code": code, "client_id": self.consumer_key, "client_secret": self.get_password(fieldname="consumer_secret"), - "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(frappe.utils.get_url()), - } - headers = { - "Content-Type": "application/x-www-form-urlencoded" + "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format( + frappe.utils.get_url() + ), } + headers = {"Content-Type": "application/x-www-form-urlencoded"} response = self.http_post(url=url, data=body, headers=headers) response = frappe.parse_json(response.content.decode()) @@ -46,11 +50,15 @@ class LinkedInSettings(Document): response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers()) response = frappe.parse_json(response.content.decode()) - frappe.db.set_value(self.doctype, self.name, { - "person_urn": response["id"], - "account_name": response["vanityName"], - "session_status": "Active" - }) + frappe.db.set_value( + self.doctype, + self.name, + { + "person_urn": response["id"], + "account_name": response["vanityName"], + "session_status": "Active", + }, + ) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings") @@ -63,8 +71,7 @@ class LinkedInSettings(Document): if media_id: return self.post_text(text, title, media_id=media_id) else: - frappe.log_error("Failed to upload media.","LinkedIn Upload Error") - + frappe.log_error("Failed to upload media.", "LinkedIn Upload Error") def upload_image(self, media): media = get_file_path(media) @@ -73,10 +80,9 @@ class LinkedInSettings(Document): "registerUploadRequest": { "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], "owner": "urn:li:organization:{0}".format(self.company_id), - "serviceRelationships": [{ - "relationshipType": "OWNER", - "identifier": "urn:li:userGeneratedContent" - }] + "serviceRelationships": [ + {"relationshipType": "OWNER", "identifier": "urn:li:userGeneratedContent"} + ], } } headers = self.get_headers() @@ -85,11 +91,16 @@ class LinkedInSettings(Document): if response.status_code == 200: response = response.json() asset = response["value"]["asset"] - upload_url = response["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"] - headers['Content-Type']='image/jpeg' - response = self.http_post(upload_url, headers=headers, data=open(media,"rb")) + upload_url = response["value"]["uploadMechanism"][ + "com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest" + ]["uploadUrl"] + headers["Content-Type"] = "image/jpeg" + response = self.http_post(upload_url, headers=headers, data=open(media, "rb")) if response.status_code < 200 and response.status_code > 299: - frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason)) + frappe.throw( + _("Error While Uploading Image"), + title="{0} {1}".format(response.status_code, response.reason), + ) return None return asset @@ -102,46 +113,26 @@ class LinkedInSettings(Document): headers["Content-Type"] = "application/json; charset=UTF-8" body = { - "distribution": { - "linkedInDistributionTarget": {} - }, - "owner":"urn:li:organization:{0}".format(self.company_id), + "distribution": {"linkedInDistributionTarget": {}}, + "owner": "urn:li:organization:{0}".format(self.company_id), "subject": title, - "text": { - "text": text - } + "text": {"text": text}, } reference_url = self.get_reference_url(text) if reference_url: - body["content"] = { - "contentEntities": [ - { - "entityLocation": reference_url - } - ] - } + body["content"] = {"contentEntities": [{"entityLocation": reference_url}]} if media_id: - body["content"]= { - "contentEntities": [{ - "entity": media_id - }], - "shareMediaCategory": "IMAGE" - } + body["content"] = {"contentEntities": [{"entity": media_id}], "shareMediaCategory": "IMAGE"} response = self.http_post(url=url, headers=headers, body=body) return response def http_post(self, url, headers=None, body=None, data=None): try: - response = requests.post( - url = url, - json = body, - data = data, - headers = headers - ) - if response.status_code not in [201,200]: + response = requests.post(url=url, json=body, data=data, headers=headers) + if response.status_code not in [201, 200]: raise except Exception as e: @@ -150,12 +141,11 @@ class LinkedInSettings(Document): return response def get_headers(self): - return { - "Authorization": "Bearer {}".format(self.access_token) - } + return {"Authorization": "Bearer {}".format(self.access_token)} def get_reference_url(self, text): import re + regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" urls = re.findall(regex_url, text) if urls: @@ -163,18 +153,23 @@ class LinkedInSettings(Document): def delete_post(self, post_id): try: - response = requests.delete(url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), headers=self.get_headers()) - if response.status_code !=200: + response = requests.delete( + url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), + headers=self.get_headers(), + ) + if response.status_code != 200: raise except Exception: self.api_error(response) def get_post(self, post_id): - url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(self.company_id, post_id) + url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format( + self.company_id, post_id + ) try: response = requests.get(url=url, headers=self.get_headers()) - if response.status_code !=200: + if response.status_code != 200: raise except Exception: @@ -199,6 +194,7 @@ class LinkedInSettings(Document): else: frappe.throw(response.reason, title=response.status_code) + @frappe.whitelist(allow_guest=True) def callback(code=None, error=None, error_description=None): if not error: @@ -208,4 +204,4 @@ def callback(code=None, error=None, error_description=None): frappe.db.commit() else: frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings") diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index ca670190c92..fc205343924 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -21,12 +21,16 @@ class Opportunity(TransactionBase): frappe.get_doc("Lead", self.party_name).set_status(update=True) def validate(self): - self._prev = frappe._dict({ - "contact_date": frappe.db.get_value("Opportunity", self.name, "contact_date") if \ - (not cint(self.get("__islocal"))) else None, - "contact_by": frappe.db.get_value("Opportunity", self.name, "contact_by") if \ - (not cint(self.get("__islocal"))) else None, - }) + self._prev = frappe._dict( + { + "contact_date": frappe.db.get_value("Opportunity", self.name, "contact_date") + if (not cint(self.get("__islocal"))) + else None, + "contact_by": frappe.db.get_value("Opportunity", self.name, "contact_by") + if (not cint(self.get("__islocal"))) + else None, + } + ) self.make_new_lead_if_required() @@ -54,7 +58,8 @@ class Opportunity(TransactionBase): """Set lead against new opportunity""" if (not self.get("party_name")) and self.contact_email: # check if customer is already created agains the self.contact_email - customer = frappe.db.sql("""select + customer = frappe.db.sql( + """select distinct `tabDynamic Link`.link_name as customer from `tabContact`, @@ -66,7 +71,11 @@ class Opportunity(TransactionBase): ifnull(`tabDynamic Link`.link_name, '')<>'' and `tabDynamic Link`.link_doctype='Customer' - """.format(self.contact_email), as_dict=True) + """.format( + self.contact_email + ), + as_dict=True, + ) if customer and customer[0].customer: self.party_name = customer[0].customer self.opportunity_from = "Customer" @@ -78,19 +87,17 @@ class Opportunity(TransactionBase): if sender_name == self.contact_email: sender_name = None - if not sender_name and ('@' in self.contact_email): - email_name = self.contact_email.split('@')[0] + if not sender_name and ("@" in self.contact_email): + email_name = self.contact_email.split("@")[0] - email_split = email_name.split('.') - sender_name = '' + email_split = email_name.split(".") + sender_name = "" for s in email_split: - sender_name += s.capitalize() + ' ' + sender_name += s.capitalize() + " " - lead = frappe.get_doc({ - "doctype": "Lead", - "email_id": self.contact_email, - "lead_name": sender_name or 'Unknown' - }) + lead = frappe.get_doc( + {"doctype": "Lead", "email_id": self.contact_email, "lead_name": sender_name or "Unknown"} + ) lead.flags.ignore_email_validation = True lead.insert(ignore_permissions=True) @@ -102,13 +109,13 @@ class Opportunity(TransactionBase): @frappe.whitelist() def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): if not self.has_active_quotation(): - frappe.db.set(self, 'status', 'Lost') + frappe.db.set(self, "status", "Lost") if detailed_reason: - frappe.db.set(self, 'order_lost_reason', detailed_reason) + frappe.db.set(self, "order_lost_reason", detailed_reason) for reason in lost_reasons_list: - self.append('lost_reasons', reason) + self.append("lost_reasons", reason) self.save() @@ -120,51 +127,58 @@ class Opportunity(TransactionBase): def has_active_quotation(self): if not self.with_items: - return frappe.get_all('Quotation', - { - 'opportunity': self.name, - 'status': ("not in", ['Lost', 'Closed']), - 'docstatus': 1 - }, 'name') + return frappe.get_all( + "Quotation", + {"opportunity": self.name, "status": ("not in", ["Lost", "Closed"]), "docstatus": 1}, + "name", + ) else: - return frappe.db.sql(""" + return frappe.db.sql( + """ select q.name from `tabQuotation` q, `tabQuotation Item` qi where q.name = qi.parent and q.docstatus=1 and qi.prevdoc_docname =%s - and q.status not in ('Lost', 'Closed')""", self.name) + and q.status not in ('Lost', 'Closed')""", + self.name, + ) def has_ordered_quotation(self): if not self.with_items: - return frappe.get_all('Quotation', - { - 'opportunity': self.name, - 'status': 'Ordered', - 'docstatus': 1 - }, 'name') + return frappe.get_all( + "Quotation", {"opportunity": self.name, "status": "Ordered", "docstatus": 1}, "name" + ) else: - return frappe.db.sql(""" + return frappe.db.sql( + """ select q.name from `tabQuotation` q, `tabQuotation Item` qi where q.name = qi.parent and q.docstatus=1 and qi.prevdoc_docname =%s - and q.status = 'Ordered'""", self.name) + and q.status = 'Ordered'""", + self.name, + ) def has_lost_quotation(self): - lost_quotation = frappe.db.sql(""" + lost_quotation = frappe.db.sql( + """ select name from `tabQuotation` where docstatus=1 and opportunity =%s and status = 'Lost' - """, self.name) + """, + self.name, + ) if lost_quotation: if self.has_active_quotation(): return False return True def validate_cust_name(self): - if self.party_name and self.opportunity_from == 'Customer': + if self.party_name and self.opportunity_from == "Customer": self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name") - elif self.party_name and self.opportunity_from == 'Lead': - lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"]) + elif self.party_name and self.opportunity_from == "Lead": + lead_name, company_name = frappe.db.get_value( + "Lead", self.party_name, ["lead_name", "company_name"] + ) self.customer_name = company_name or lead_name def on_update(self): @@ -177,27 +191,27 @@ class Opportunity(TransactionBase): opts.description = "" opts.contact_date = self.contact_date - if self.party_name and self.opportunity_from == 'Customer': + if self.party_name and self.opportunity_from == "Customer": if self.contact_person: - opts.description = 'Contact '+cstr(self.contact_person) + opts.description = "Contact " + cstr(self.contact_person) else: - opts.description = 'Contact customer '+cstr(self.party_name) - elif self.party_name and self.opportunity_from == 'Lead': + opts.description = "Contact customer " + cstr(self.party_name) + elif self.party_name and self.opportunity_from == "Lead": if self.contact_display: - opts.description = 'Contact '+cstr(self.contact_display) + opts.description = "Contact " + cstr(self.contact_display) else: - opts.description = 'Contact lead '+cstr(self.party_name) + opts.description = "Contact lead " + cstr(self.party_name) opts.subject = opts.description - opts.description += '. By : ' + cstr(self.contact_by) + opts.description += ". By : " + cstr(self.contact_by) if self.to_discuss: - opts.description += ' To Discuss : ' + cstr(self.to_discuss) + opts.description += " To Discuss : " + cstr(self.to_discuss) super(Opportunity, self).add_calendar_event(opts, force) def validate_item_details(self): - if not self.get('items'): + if not self.get("items"): return # set missing values @@ -209,32 +223,41 @@ class Opportunity(TransactionBase): item = frappe.db.get_value("Item", d.item_code, item_fields, as_dict=True) for key in item_fields: - if not d.get(key): d.set(key, item.get(key)) + if not d.get(key): + d.set(key, item.get(key)) @frappe.whitelist() def get_item_details(item_code): - item = frappe.db.sql("""select item_name, stock_uom, image, description, item_group, brand - from `tabItem` where name = %s""", item_code, as_dict=1) + item = frappe.db.sql( + """select item_name, stock_uom, image, description, item_group, brand + from `tabItem` where name = %s""", + item_code, + as_dict=1, + ) return { - 'item_name': item and item[0]['item_name'] or '', - 'uom': item and item[0]['stock_uom'] or '', - 'description': item and item[0]['description'] or '', - 'image': item and item[0]['image'] or '', - 'item_group': item and item[0]['item_group'] or '', - 'brand': item and item[0]['brand'] or '' + "item_name": item and item[0]["item_name"] or "", + "uom": item and item[0]["stock_uom"] or "", + "description": item and item[0]["description"] or "", + "image": item and item[0]["image"] or "", + "item_group": item and item[0]["item_group"] or "", + "brand": item and item[0]["brand"] or "", } + @frappe.whitelist() def make_quotation(source_name, target_doc=None): def set_missing_values(source, target): from erpnext.controllers.accounts_controller import get_default_taxes_and_charges + quotation = frappe.get_doc(target) - company_currency = frappe.get_cached_value('Company', quotation.company, "default_currency") + company_currency = frappe.get_cached_value("Company", quotation.company, "default_currency") - if quotation.quotation_to == 'Customer' and quotation.party_name: - party_account_currency = get_party_account_currency("Customer", quotation.party_name, quotation.company) + if quotation.quotation_to == "Customer" and quotation.party_name: + party_account_currency = get_party_account_currency( + "Customer", quotation.party_name, quotation.company + ) else: party_account_currency = company_currency @@ -243,14 +266,17 @@ def make_quotation(source_name, target_doc=None): if company_currency == quotation.currency: exchange_rate = 1 else: - exchange_rate = get_exchange_rate(quotation.currency, company_currency, - quotation.transaction_date, args="for_selling") + exchange_rate = get_exchange_rate( + quotation.currency, company_currency, quotation.transaction_date, args="for_selling" + ) quotation.conversion_rate = exchange_rate # get default taxes - taxes = get_default_taxes_and_charges("Sales Taxes and Charges Template", company=quotation.company) - if taxes.get('taxes'): + taxes = get_default_taxes_and_charges( + "Sales Taxes and Charges Template", company=quotation.company + ) + if taxes.get("taxes"): quotation.update(taxes) quotation.run_method("set_missing_values") @@ -258,49 +284,56 @@ def make_quotation(source_name, target_doc=None): if not source.with_items: quotation.opportunity = source.name - doclist = get_mapped_doc("Opportunity", source_name, { - "Opportunity": { - "doctype": "Quotation", - "field_map": { - "opportunity_from": "quotation_to", - "name": "enq_no", - } - }, - "Opportunity Item": { - "doctype": "Quotation Item", - "field_map": { - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", - "uom": "stock_uom" + doclist = get_mapped_doc( + "Opportunity", + source_name, + { + "Opportunity": { + "doctype": "Quotation", + "field_map": { + "opportunity_from": "quotation_to", + "name": "enq_no", + }, }, - "add_if_empty": True - } - }, target_doc, set_missing_values) + "Opportunity Item": { + "doctype": "Quotation Item", + "field_map": { + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + "uom": "stock_uom", + }, + "add_if_empty": True, + }, + }, + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): def update_item(obj, target, source_parent): target.conversion_factor = 1.0 - doclist = get_mapped_doc("Opportunity", source_name, { - "Opportunity": { - "doctype": "Request for Quotation" + doclist = get_mapped_doc( + "Opportunity", + source_name, + { + "Opportunity": {"doctype": "Request for Quotation"}, + "Opportunity Item": { + "doctype": "Request for Quotation Item", + "field_map": [["name", "opportunity_item"], ["parent", "opportunity"], ["uom", "uom"]], + "postprocess": update_item, + }, }, - "Opportunity Item": { - "doctype": "Request for Quotation Item", - "field_map": [ - ["name", "opportunity_item"], - ["parent", "opportunity"], - ["uom", "uom"] - ], - "postprocess": update_item - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_customer(source_name, target_doc=None): def set_missing_values(source, target): @@ -309,37 +342,37 @@ def make_customer(source_name, target_doc=None): if source.opportunity_from == "Lead": target.lead_name = source.party_name - doclist = get_mapped_doc("Opportunity", source_name, { - "Opportunity": { - "doctype": "Customer", - "field_map": { - "currency": "default_currency", - "customer_name": "customer_name" + doclist = get_mapped_doc( + "Opportunity", + source_name, + { + "Opportunity": { + "doctype": "Customer", + "field_map": {"currency": "default_currency", "customer_name": "customer_name"}, } - } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def make_supplier_quotation(source_name, target_doc=None): - doclist = get_mapped_doc("Opportunity", source_name, { - "Opportunity": { - "doctype": "Supplier Quotation", - "field_map": { - "name": "opportunity" - } + doclist = get_mapped_doc( + "Opportunity", + source_name, + { + "Opportunity": {"doctype": "Supplier Quotation", "field_map": {"name": "opportunity"}}, + "Opportunity Item": {"doctype": "Supplier Quotation Item", "field_map": {"uom": "stock_uom"}}, }, - "Opportunity Item": { - "doctype": "Supplier Quotation Item", - "field_map": { - "uom": "stock_uom" - } - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def set_multiple_status(names, status): names = json.loads(names) @@ -348,12 +381,19 @@ def set_multiple_status(names, status): opp.status = status opp.save() -def auto_close_opportunity(): - """ auto close the `Replied` Opportunities after 7 days """ - auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15 - opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and - modified start and post_time <= end: - sm_post = frappe.get_doc('Social Media Post', post.name) + sm_post = frappe.get_doc("Social Media Post", post.name) sm_post.post() diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index be7d9145c53..42874ddeea5 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -16,22 +16,26 @@ from tweepy.error import TweepError class TwitterSettings(Document): @frappe.whitelist() def get_authorize_url(self): - callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url()) - auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) + callback_url = ( + "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format( + frappe.utils.get_url() + ) + ) + auth = tweepy.OAuthHandler( + self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url + ) try: redirect_url = auth.get_authorization_url() return redirect_url except tweepy.TweepError as e: frappe.msgprint(_("Error! Failed to get request token.")) - frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) - + frappe.throw( + _("Invalid {0} or {1}").format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")) + ) def get_access_token(self, oauth_token, oauth_verifier): auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) - auth.request_token = { - 'oauth_token' : oauth_token, - 'oauth_token_secret' : oauth_verifier - } + auth.request_token = {"oauth_token": oauth_token, "oauth_token_secret": oauth_verifier} try: auth.get_access_token(oauth_verifier) @@ -39,21 +43,25 @@ class TwitterSettings(Document): self.access_token_secret = auth.access_token_secret api = self.get_api() user = api.me() - profile_pic = (user._json["profile_image_url"]).replace("_normal","") + profile_pic = (user._json["profile_image_url"]).replace("_normal", "") - frappe.db.set_value(self.doctype, self.name, { - "access_token" : auth.access_token, - "access_token_secret" : auth.access_token_secret, - "account_name" : user._json["screen_name"], - "profile_pic" : profile_pic, - "session_status" : "Active" - }) + frappe.db.set_value( + self.doctype, + self.name, + { + "access_token": auth.access_token, + "access_token_secret": auth.access_token_secret, + "account_name": user._json["screen_name"], + "profile_pic": profile_pic, + "session_status": "Active", + }, + ) frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") + frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings") except TweepError as e: frappe.msgprint(_("Error! Failed to get access token.")) - frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) + frappe.throw(_("Invalid Consumer Key or Consumer Secret Key")) def get_api(self): # authentication of consumer key and secret @@ -82,9 +90,9 @@ class TwitterSettings(Document): api = self.get_api() try: if media_id: - response = api.update_status(status = text, media_ids = [media_id]) + response = api.update_status(status=text, media_ids=[media_id]) else: - response = api.update_status(status = text) + response = api.update_status(status=text) return response @@ -113,15 +121,18 @@ class TwitterSettings(Document): if e.response.status_code == 401: self.db_set("session_status", "Expired") frappe.db.commit() - frappe.throw(content["message"],title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason)) + frappe.throw( + content["message"], + title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason), + ) @frappe.whitelist(allow_guest=True) -def callback(oauth_token = None, oauth_verifier = None): +def callback(oauth_token=None, oauth_verifier=None): if oauth_token and oauth_verifier: twitter_settings = frappe.get_single("Twitter Settings") - twitter_settings.get_access_token(oauth_token,oauth_verifier) + twitter_settings.get_access_token(oauth_token, oauth_verifier) frappe.db.commit() else: frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") + frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings") diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 0da0e0e71af..6bcfcb7e626 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -1,20 +1,20 @@ - import frappe @frappe.whitelist() def get_last_interaction(contact=None, lead=None): - if not contact and not lead: return + if not contact and not lead: + return last_communication = None last_issue = None if contact: - query_condition = '' + query_condition = "" values = [] - contact = frappe.get_doc('Contact', contact) + contact = frappe.get_doc("Contact", contact) for link in contact.links: - if link.link_doctype == 'Customer': + if link.link_doctype == "Customer": last_issue = get_last_issue_from_customer(link.link_name) query_condition += "(`reference_doctype`=%s AND `reference_name`=%s) OR" values += [link.link_doctype, link.link_name] @@ -22,65 +22,82 @@ def get_last_interaction(contact=None, lead=None): if query_condition: # remove extra appended 'OR' query_condition = query_condition[:-2] - last_communication = frappe.db.sql(""" + last_communication = frappe.db.sql( + """ SELECT `name`, `content` FROM `tabCommunication` WHERE `sent_or_received`='Received' AND ({}) ORDER BY `modified` LIMIT 1 - """.format(query_condition), values, as_dict=1) # nosec + """.format( + query_condition + ), + values, + as_dict=1, + ) # nosec if lead: - last_communication = frappe.get_all('Communication', filters={ - 'reference_doctype': 'Lead', - 'reference_name': lead, - 'sent_or_received': 'Received' - }, fields=['name', 'content'], order_by='`creation` DESC', limit=1) + last_communication = frappe.get_all( + "Communication", + filters={"reference_doctype": "Lead", "reference_name": lead, "sent_or_received": "Received"}, + fields=["name", "content"], + order_by="`creation` DESC", + limit=1, + ) last_communication = last_communication[0] if last_communication else None - return { - 'last_communication': last_communication, - 'last_issue': last_issue - } + return {"last_communication": last_communication, "last_issue": last_issue} + def get_last_issue_from_customer(customer_name): - issues = frappe.get_all('Issue', { - 'customer': customer_name - }, ['name', 'subject', 'customer'], order_by='`creation` DESC', limit=1) + issues = frappe.get_all( + "Issue", + {"customer": customer_name}, + ["name", "subject", "customer"], + order_by="`creation` DESC", + limit=1, + ) return issues[0] if issues else None def get_scheduled_employees_for_popup(communication_medium): - if not communication_medium: return [] + if not communication_medium: + return [] now_time = frappe.utils.nowtime() weekday = frappe.utils.get_weekday() - available_employee_groups = frappe.get_all("Communication Medium Timeslot", filters={ - 'day_of_week': weekday, - 'parent': communication_medium, - 'from_time': ['<=', now_time], - 'to_time': ['>=', now_time], - }, fields=['employee_group']) + available_employee_groups = frappe.get_all( + "Communication Medium Timeslot", + filters={ + "day_of_week": weekday, + "parent": communication_medium, + "from_time": ["<=", now_time], + "to_time": [">=", now_time], + }, + fields=["employee_group"], + ) available_employee_groups = tuple([emp.employee_group for emp in available_employee_groups]) - employees = frappe.get_all('Employee Group Table', filters={ - 'parent': ['in', available_employee_groups] - }, fields=['user_id']) + employees = frappe.get_all( + "Employee Group Table", filters={"parent": ["in", available_employee_groups]}, fields=["user_id"] + ) employee_emails = set([employee.user_id for employee in employees]) return employee_emails + def strip_number(number): - if not number: return + if not number: + return # strip + and 0 from the start of the number for proper number comparisions # eg. +7888383332 should match with 7888383332 # eg. 07888383332 should match with 7888383332 - number = number.lstrip('+') - number = number.lstrip('0') + number = number.lstrip("+") + number = number.lstrip("0") return number diff --git a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py index 6f3e311f392..be7f5ca29b3 100644 --- a/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py +++ b/erpnext/crm/report/campaign_efficiency/campaign_efficiency.py @@ -9,77 +9,40 @@ from frappe.utils import flt def execute(filters=None): columns, data = [], [] - columns=get_columns("Campaign Name") - data=get_lead_data(filters or {}, "Campaign Name") + columns = get_columns("Campaign Name") + data = get_lead_data(filters or {}, "Campaign Name") return columns, data + def get_columns(based_on): return [ - { - "fieldname": frappe.scrub(based_on), - "label": _(based_on), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "lead_count", - "label": _("Lead Count"), - "fieldtype": "Int", - "width": 80 - }, - { - "fieldname": "opp_count", - "label": _("Opp Count"), - "fieldtype": "Int", - "width": 80 - }, - { - "fieldname": "quot_count", - "label": _("Quot Count"), - "fieldtype": "Int", - "width": 80 - }, - { - "fieldname": "order_count", - "label": _("Order Count"), - "fieldtype": "Int", - "width": 100 - }, - { - "fieldname": "order_value", - "label": _("Order Value"), - "fieldtype": "Float", - "width": 100 - }, - { - "fieldname": "opp_lead", - "label": _("Opp/Lead %"), - "fieldtype": "Float", - "width": 100 - }, - { - "fieldname": "quot_lead", - "label": _("Quot/Lead %"), - "fieldtype": "Float", - "width": 100 - }, - { - "fieldname": "order_quot", - "label": _("Order/Quot %"), - "fieldtype": "Float", - "width": 100 - } + {"fieldname": frappe.scrub(based_on), "label": _(based_on), "fieldtype": "Data", "width": 150}, + {"fieldname": "lead_count", "label": _("Lead Count"), "fieldtype": "Int", "width": 80}, + {"fieldname": "opp_count", "label": _("Opp Count"), "fieldtype": "Int", "width": 80}, + {"fieldname": "quot_count", "label": _("Quot Count"), "fieldtype": "Int", "width": 80}, + {"fieldname": "order_count", "label": _("Order Count"), "fieldtype": "Int", "width": 100}, + {"fieldname": "order_value", "label": _("Order Value"), "fieldtype": "Float", "width": 100}, + {"fieldname": "opp_lead", "label": _("Opp/Lead %"), "fieldtype": "Float", "width": 100}, + {"fieldname": "quot_lead", "label": _("Quot/Lead %"), "fieldtype": "Float", "width": 100}, + {"fieldname": "order_quot", "label": _("Order/Quot %"), "fieldtype": "Float", "width": 100}, ] + def get_lead_data(filters, based_on): based_on_field = frappe.scrub(based_on) conditions = get_filter_conditions(filters) - lead_details = frappe.db.sql(""" + lead_details = frappe.db.sql( + """ select {based_on_field}, name from `tabLead` where {based_on_field} is not null and {based_on_field} != '' {conditions} - """.format(based_on_field=based_on_field, conditions=conditions), filters, as_dict=1) + """.format( + based_on_field=based_on_field, conditions=conditions + ), + filters, + as_dict=1, + ) lead_map = frappe._dict() for d in lead_details: @@ -87,11 +50,8 @@ def get_lead_data(filters, based_on): data = [] for based_on_value, leads in lead_map.items(): - row = { - based_on_field: based_on_value, - "lead_count": len(leads) - } - row["quot_count"]= get_lead_quotation_count(leads) + row = {based_on_field: based_on_value, "lead_count": len(leads)} + row["quot_count"] = get_lead_quotation_count(leads) row["opp_count"] = get_lead_opp_count(leads) row["order_count"] = get_quotation_ordered_count(leads) row["order_value"] = get_order_amount(leads) or 0 @@ -105,8 +65,9 @@ def get_lead_data(filters, based_on): return data + def get_filter_conditions(filters): - conditions="" + conditions = "" if filters.from_date: conditions += " and date(creation) >= %(from_date)s" if filters.to_date: @@ -114,23 +75,45 @@ def get_filter_conditions(filters): return conditions + def get_lead_quotation_count(leads): - return frappe.db.sql("""select count(name) from `tabQuotation` - where quotation_to = 'Lead' and party_name in (%s)""" % ', '.join(["%s"]*len(leads)), tuple(leads))[0][0] #nosec + return frappe.db.sql( + """select count(name) from `tabQuotation` + where quotation_to = 'Lead' and party_name in (%s)""" + % ", ".join(["%s"] * len(leads)), + tuple(leads), + )[0][ + 0 + ] # nosec + def get_lead_opp_count(leads): - return frappe.db.sql("""select count(name) from `tabOpportunity` - where opportunity_from = 'Lead' and party_name in (%s)""" % ', '.join(["%s"]*len(leads)), tuple(leads))[0][0] + return frappe.db.sql( + """select count(name) from `tabOpportunity` + where opportunity_from = 'Lead' and party_name in (%s)""" + % ", ".join(["%s"] * len(leads)), + tuple(leads), + )[0][0] + def get_quotation_ordered_count(leads): - return frappe.db.sql("""select count(name) + return frappe.db.sql( + """select count(name) from `tabQuotation` where status = 'Ordered' and quotation_to = 'Lead' - and party_name in (%s)""" % ', '.join(["%s"]*len(leads)), tuple(leads))[0][0] + and party_name in (%s)""" + % ", ".join(["%s"] * len(leads)), + tuple(leads), + )[0][0] + def get_order_amount(leads): - return frappe.db.sql("""select sum(base_net_amount) + return frappe.db.sql( + """select sum(base_net_amount) from `tabSales Order Item` where prevdoc_docname in ( select name from `tabQuotation` where status = 'Ordered' and quotation_to = 'Lead' and party_name in (%s) - )""" % ', '.join(["%s"]*len(leads)), tuple(leads))[0][0] + )""" + % ", ".join(["%s"] * len(leads)), + tuple(leads), + )[0][0] diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py index ed6cefb2a32..9dae1d50f68 100644 --- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py @@ -7,21 +7,17 @@ import frappe def execute(filters=None): columns = [ + {"fieldname": "creation_date", "label": "Date", "fieldtype": "Date", "width": 300}, { - 'fieldname': 'creation_date', - 'label': 'Date', - 'fieldtype': 'Date', - 'width': 300 - }, - { - 'fieldname': 'first_response_time', - 'fieldtype': 'Duration', - 'label': 'First Response Time', - 'width': 300 + "fieldname": "first_response_time", + "fieldtype": "Duration", + "label": "First Response Time", + "width": 300, }, ] - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT date(creation) as creation_date, avg(first_response_time) as avg_response_time @@ -31,6 +27,8 @@ def execute(filters=None): and first_response_time > 0 GROUP BY creation_date ORDER BY creation_date desc - ''', (filters.from_date, filters.to_date)) + """, + (filters.from_date, filters.to_date), + ) return columns, data diff --git a/erpnext/crm/report/lead_conversion_time/lead_conversion_time.py b/erpnext/crm/report/lead_conversion_time/lead_conversion_time.py index 1f43fa0c476..d7d964d690a 100644 --- a/erpnext/crm/report/lead_conversion_time/lead_conversion_time.py +++ b/erpnext/crm/report/lead_conversion_time/lead_conversion_time.py @@ -8,7 +8,8 @@ from frappe.utils import date_diff, flt def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} communication_list = get_communication_details(filters) columns = get_columns() @@ -19,8 +20,12 @@ def execute(filters=None): data = [] for communication in communication_list: - row = [communication.get('customer'), communication.get('interactions'),\ - communication.get('duration'), communication.get('support_tickets')] + row = [ + communication.get("customer"), + communication.get("interactions"), + communication.get("duration"), + communication.get("support_tickets"), + ] data.append(row) # add the average row @@ -32,9 +37,17 @@ def execute(filters=None): total_interactions += row[1] total_duration += row[2] total_tickets += row[3] - data.append(['Average', total_interactions/len(data), total_duration/len(data), total_tickets/len(data)]) + data.append( + [ + "Average", + total_interactions / len(data), + total_duration / len(data), + total_tickets / len(data), + ] + ) return columns, data + def get_columns(): return [ { @@ -42,36 +55,37 @@ def get_columns(): "fieldname": "customer", "fieldtype": "Link", "options": "Customer", - "width": 120 + "width": 120, }, { "label": _("No of Interactions"), "fieldname": "interactions", "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Duration in Days"), - "fieldname": "duration", - "fieldtype": "Float", - "width": 120 + "width": 120, }, + {"label": _("Duration in Days"), "fieldname": "duration", "fieldtype": "Float", "width": 120}, { "label": _("Support Tickets"), "fieldname": "support_tickets", "fieldtype": "Float", - "width": 120 - } + "width": 120, + }, ] + def get_communication_details(filters): communication_count = None communication_list = [] - opportunities = frappe.db.get_values('Opportunity', {'opportunity_from': 'Lead'},\ - ['name', 'customer_name', 'contact_email'], as_dict=1) + opportunities = frappe.db.get_values( + "Opportunity", + {"opportunity_from": "Lead"}, + ["name", "customer_name", "contact_email"], + as_dict=1, + ) for d in opportunities: - invoice = frappe.db.sql(''' + invoice = frappe.db.sql( + """ SELECT date(creation) FROM @@ -81,22 +95,30 @@ def get_communication_details(filters): ORDER BY creation LIMIT 1 - ''', (d.contact_email, filters.from_date, filters.to_date)) + """, + (d.contact_email, filters.from_date, filters.to_date), + ) - if not invoice: continue + if not invoice: + continue - communication_count = frappe.db.sql(''' + communication_count = frappe.db.sql( + """ SELECT count(*) FROM `tabCommunication` WHERE sender = %s AND date(communication_date) <= %s - ''', (d.contact_email, invoice))[0][0] + """, + (d.contact_email, invoice), + )[0][0] - if not communication_count: continue + if not communication_count: + continue - first_contact = frappe.db.sql(''' + first_contact = frappe.db.sql( + """ SELECT date(communication_date) FROM @@ -106,10 +128,19 @@ def get_communication_details(filters): ORDER BY communication_date LIMIT 1 - ''', (d.contact_email))[0][0] + """, + (d.contact_email), + )[0][0] duration = flt(date_diff(invoice[0][0], first_contact)) - support_tickets = len(frappe.db.get_all('Issue', {'raised_by': d.contact_email})) - communication_list.append({'customer': d.customer_name, 'interactions': communication_count, 'duration': duration, 'support_tickets': support_tickets}) + support_tickets = len(frappe.db.get_all("Issue", {"raised_by": d.contact_email})) + communication_list.append( + { + "customer": d.customer_name, + "interactions": communication_count, + "duration": duration, + "support_tickets": support_tickets, + } + ) return communication_list diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 09eba7c38a6..8660c733103 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -10,6 +10,7 @@ def execute(filters=None): columns, data = get_columns(), get_data(filters) return columns, data + def get_columns(): columns = [ { @@ -19,101 +20,57 @@ def get_columns(): "options": "Lead", "width": 150, }, + {"label": _("Lead Name"), "fieldname": "lead_name", "fieldtype": "Data", "width": 120}, + {"fieldname": "status", "label": _("Status"), "fieldtype": "Data", "width": 100}, { - "label": _("Lead Name"), - "fieldname": "lead_name", - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname":"status", - "label": _("Status"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname":"lead_owner", + "fieldname": "lead_owner", "label": _("Lead Owner"), "fieldtype": "Link", "options": "User", - "width": 100 + "width": 100, }, { "label": _("Territory"), "fieldname": "territory", "fieldtype": "Link", "options": "Territory", - "width": 100 - }, - { - "label": _("Source"), - "fieldname": "source", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Email"), - "fieldname": "email_id", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Mobile"), - "fieldname": "mobile_no", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Phone"), - "fieldname": "phone", - "fieldtype": "Data", - "width": 120 + "width": 100, }, + {"label": _("Source"), "fieldname": "source", "fieldtype": "Data", "width": 120}, + {"label": _("Email"), "fieldname": "email_id", "fieldtype": "Data", "width": 120}, + {"label": _("Mobile"), "fieldname": "mobile_no", "fieldtype": "Data", "width": 120}, + {"label": _("Phone"), "fieldname": "phone", "fieldtype": "Data", "width": 120}, { "label": _("Owner"), "fieldname": "owner", "fieldtype": "Link", "options": "user", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 + "width": 120, }, + {"fieldname": "address", "label": _("Address"), "fieldtype": "Data", "width": 130}, + {"fieldname": "state", "label": _("State"), "fieldtype": "Data", "width": 100}, + {"fieldname": "pincode", "label": _("Postal Code"), "fieldtype": "Data", "width": 90}, { - "fieldname":"address", - "label": _("Address"), - "fieldtype": "Data", - "width": 130 - }, - { - "fieldname":"state", - "label": _("State"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname":"pincode", - "label": _("Postal Code"), - "fieldtype": "Data", - "width": 90 - }, - { - "fieldname":"country", + "fieldname": "country", "label": _("Country"), "fieldtype": "Link", "options": "Country", - "width": 100 + "width": 100, }, - ] return columns + def get_data(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `tabLead`.name, `tabLead`.lead_name, @@ -144,9 +101,15 @@ def get_data(filters): AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s {conditions} ORDER BY - `tabLead`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) + `tabLead`.creation asc """.format( + conditions=get_conditions(filters) + ), + filters, + as_dict=1, + ) -def get_conditions(filters) : + +def get_conditions(filters): conditions = [] if filters.get("territory"): diff --git a/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.py b/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.py index 29322119ee4..996b1b47172 100644 --- a/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.py +++ b/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.py @@ -9,10 +9,11 @@ from erpnext.crm.report.campaign_efficiency.campaign_efficiency import get_lead_ def execute(filters=None): columns, data = [], [] - columns=get_columns() - data=get_lead_data(filters, "Lead Owner") + columns = get_columns() + data = get_lead_data(filters, "Lead Owner") return columns, data + def get_columns(): return [ { @@ -20,54 +21,14 @@ def get_columns(): "label": _("Lead Owner"), "fieldtype": "Link", "options": "User", - "width": "130" + "width": "130", }, - { - "fieldname": "lead_count", - "label": _("Lead Count"), - "fieldtype": "Int", - "width": "80" - }, - { - "fieldname": "opp_count", - "label": _("Opp Count"), - "fieldtype": "Int", - "width": "80" - }, - { - "fieldname": "quot_count", - "label": _("Quot Count"), - "fieldtype": "Int", - "width": "80" - }, - { - "fieldname": "order_count", - "label": _("Order Count"), - "fieldtype": "Int", - "width": "100" - }, - { - "fieldname": "order_value", - "label": _("Order Value"), - "fieldtype": "Float", - "width": "100" - }, - { - "fieldname": "opp_lead", - "label": _("Opp/Lead %"), - "fieldtype": "Float", - "width": "100" - }, - { - "fieldname": "quot_lead", - "label": _("Quot/Lead %"), - "fieldtype": "Float", - "width": "100" - }, - { - "fieldname": "order_quot", - "label": _("Order/Quot %"), - "fieldtype": "Float", - "width": "100" - } + {"fieldname": "lead_count", "label": _("Lead Count"), "fieldtype": "Int", "width": "80"}, + {"fieldname": "opp_count", "label": _("Opp Count"), "fieldtype": "Int", "width": "80"}, + {"fieldname": "quot_count", "label": _("Quot Count"), "fieldtype": "Int", "width": "80"}, + {"fieldname": "order_count", "label": _("Order Count"), "fieldtype": "Int", "width": "100"}, + {"fieldname": "order_value", "label": _("Order Value"), "fieldtype": "Float", "width": "100"}, + {"fieldname": "opp_lead", "label": _("Opp/Lead %"), "fieldtype": "Float", "width": "100"}, + {"fieldname": "quot_lead", "label": _("Quot/Lead %"), "fieldtype": "Float", "width": "100"}, + {"fieldname": "order_quot", "label": _("Order/Quot %"), "fieldtype": "Float", "width": "100"}, ] diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index 60d4be85648..a57b44be477 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -10,6 +10,7 @@ def execute(filters=None): columns, data = get_columns(), get_data(filters) return columns, data + def get_columns(): columns = [ { @@ -24,59 +25,56 @@ def get_columns(): "fieldname": "opportunity_from", "fieldtype": "Link", "options": "DocType", - "width": 130 + "width": 130, }, { "label": _("Party"), - "fieldname":"party_name", + "fieldname": "party_name", "fieldtype": "Dynamic Link", "options": "opportunity_from", - "width": 160 + "width": 160, }, { "label": _("Customer/Lead Name"), - "fieldname":"customer_name", + "fieldname": "customer_name", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": _("Opportunity Type"), "fieldname": "opportunity_type", "fieldtype": "Data", - "width": 130 - }, - { - "label": _("Lost Reasons"), - "fieldname": "lost_reason", - "fieldtype": "Data", - "width": 220 + "width": 130, }, + {"label": _("Lost Reasons"), "fieldname": "lost_reason", "fieldtype": "Data", "width": 220}, { "label": _("Sales Stage"), "fieldname": "sales_stage", "fieldtype": "Link", "options": "Sales Stage", - "width": 150 + "width": 150, }, { "label": _("Territory"), "fieldname": "territory", "fieldtype": "Link", "options": "Territory", - "width": 150 + "width": 150, }, { "label": _("Next Contact By"), "fieldname": "contact_by", "fieldtype": "Link", "options": "User", - "width": 150 - } + "width": 150, + }, ] return columns + def get_data(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `tabOpportunity`.name, `tabOpportunity`.opportunity_from, @@ -97,7 +95,12 @@ def get_data(filters): GROUP BY `tabOpportunity`.name ORDER BY - `tabOpportunity`.creation asc """.format(conditions=get_conditions(filters), join=get_join(filters)), filters, as_dict=1) + `tabOpportunity`.creation asc """.format( + conditions=get_conditions(filters), join=get_join(filters) + ), + filters, + as_dict=1, + ) def get_conditions(filters): @@ -117,6 +120,7 @@ def get_conditions(filters): return " ".join(conditions) if conditions else "" + def get_join(filters): join = """LEFT JOIN `tabOpportunity Lost Reason Detail` ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and @@ -127,6 +131,8 @@ def get_join(filters): ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and `tabOpportunity Lost Reason Detail`.lost_reason = '{0}' - """.format(filters.get("lost_reason")) + """.format( + filters.get("lost_reason") + ) return join diff --git a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py index 41cb4422a59..50c42efe3c5 100644 --- a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py +++ b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py @@ -15,62 +15,58 @@ def execute(filters=None): return columns, data + def set_defaut_value_for_filters(filters): - if not filters.get('no_of_interaction'): filters["no_of_interaction"] = 1 - if not filters.get('lead_age'): filters["lead_age"] = 60 + if not filters.get("no_of_interaction"): + filters["no_of_interaction"] = 1 + if not filters.get("lead_age"): + filters["lead_age"] = 60 + def get_columns(): - columns = [{ - "label": _("Lead"), - "fieldname": "lead", - "fieldtype": "Link", - "options": "Lead", - "width": 130 - }, - { - "label": _("Name"), - "fieldname": "name", - "width": 120 - }, - { - "label": _("Organization"), - "fieldname": "organization", - "width": 120 - }, + columns = [ + {"label": _("Lead"), "fieldname": "lead", "fieldtype": "Link", "options": "Lead", "width": 130}, + {"label": _("Name"), "fieldname": "name", "width": 120}, + {"label": _("Organization"), "fieldname": "organization", "width": 120}, { "label": _("Reference Document Type"), "fieldname": "reference_document_type", "fieldtype": "Link", "options": "Doctype", - "width": 100 + "width": 100, }, { "label": _("Reference Name"), "fieldname": "reference_name", "fieldtype": "Dynamic Link", "options": "reference_document_type", - "width": 140 + "width": 140, }, { "label": _("Last Communication"), "fieldname": "last_communication", "fieldtype": "Data", - "width": 200 + "width": 200, }, { "label": _("Last Communication Date"), "fieldname": "last_communication_date", "fieldtype": "Date", - "width": 100 - }] + "width": 100, + }, + ] return columns + def get_data(filters): lead_details = [] lead_filters = get_lead_filters(filters) - for lead in frappe.get_all('Lead', fields = ['name', 'lead_name', 'company_name'], filters=lead_filters): - data = frappe.db.sql(""" + for lead in frappe.get_all( + "Lead", fields=["name", "lead_name", "company_name"], filters=lead_filters + ): + data = frappe.db.sql( + """ select `tabCommunication`.reference_doctype, `tabCommunication`.reference_name, `tabCommunication`.content, `tabCommunication`.communication_date @@ -90,7 +86,8 @@ def get_data(filters): `tabCommunication`.sent_or_received = 'Received' order by ref_document.lead, `tabCommunication`.creation desc limit %(limit)s""", - {'lead': lead.name, 'limit': filters.get('no_of_interaction')}) + {"lead": lead.name, "limit": filters.get("no_of_interaction")}, + ) for lead_info in data: lead_data = [lead.name, lead.lead_name, lead.company_name] + list(lead_info) @@ -98,13 +95,15 @@ def get_data(filters): return lead_details + def get_lead_filters(filters): lead_creation_date = get_creation_date_based_on_lead_age(filters) lead_filters = [["status", "!=", "Converted"], ["creation", ">", lead_creation_date]] - if filters.get('lead'): - lead_filters.append(["name", "=", filters.get('lead')]) + if filters.get("lead"): + lead_filters.append(["name", "=", filters.get("lead")]) return lead_filters + def get_creation_date_based_on_lead_age(filters): - return add_days(now(), (filters.get('lead_age') * -1)) + return add_days(now(), (filters.get("lead_age") * -1)) diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 95b19ec21ec..be1e7077f0a 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -9,12 +9,16 @@ def update_lead_phone_numbers(contact, method): if len(contact.phone_nos) > 1: # get the default phone number - primary_phones = [phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_phone] + primary_phones = [ + phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_phone + ] if primary_phones: phone = primary_phones[0] # get the default mobile number - primary_mobile_nos = [phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_mobile_no] + primary_mobile_nos = [ + phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_mobile_no + ] if primary_mobile_nos: mobile_no = primary_mobile_nos[0] diff --git a/erpnext/demo/demo.py b/erpnext/demo/demo.py index edcd5880742..fbcfca0ad54 100644 --- a/erpnext/demo/demo.py +++ b/erpnext/demo/demo.py @@ -1,4 +1,3 @@ - import sys import frappe @@ -27,17 +26,18 @@ bench --site demo.erpnext.dev execute erpnext.demo.demo.simulate """ -def make(domain='Manufacturing', days=100): + +def make(domain="Manufacturing", days=100): frappe.flags.domain = domain frappe.flags.mute_emails = True setup_data.setup(domain) - if domain== 'Manufacturing': + if domain == "Manufacturing": manufacture.setup_data() elif domain == "Retail": retail.setup_data() - elif domain== 'Education': + elif domain == "Education": education.setup_data() - elif domain== 'Healthcare': + elif domain == "Healthcare": healthcare.setup_data() site = frappe.local.site @@ -47,20 +47,20 @@ def make(domain='Manufacturing', days=100): simulate(domain, days) -def simulate(domain='Manufacturing', days=100): + +def simulate(domain="Manufacturing", days=100): runs_for = frappe.flags.runs_for or days frappe.flags.company = erpnext.get_default_company() frappe.flags.mute_emails = True if not frappe.flags.start_date: # start date = 100 days back - frappe.flags.start_date = frappe.utils.add_days(frappe.utils.nowdate(), - -1 * runs_for) + frappe.flags.start_date = frappe.utils.add_days(frappe.utils.nowdate(), -1 * runs_for) current_date = frappe.utils.getdate(frappe.flags.start_date) # continue? - demo_last_date = frappe.db.get_global('demo_last_date') + demo_last_date = frappe.db.get_global("demo_last_date") if demo_last_date: current_date = frappe.utils.add_days(frappe.utils.getdate(demo_last_date), 1) @@ -71,8 +71,7 @@ def simulate(domain='Manufacturing', days=100): fixed_asset.work() for i in range(runs_for): - sys.stdout.write("\rSimulating {0}: Day {1}".format( - current_date.strftime("%Y-%m-%d"), i)) + sys.stdout.write("\rSimulating {0}: Day {1}".format(current_date.strftime("%Y-%m-%d"), i)) sys.stdout.flush() frappe.flags.current_date = current_date if current_date.weekday() in (5, 6): @@ -87,13 +86,13 @@ def simulate(domain='Manufacturing', days=100): sales.work(domain) # run_messages() - if domain=='Manufacturing': + if domain == "Manufacturing": manufacturing.work() - elif domain=='Education': + elif domain == "Education": edu.work() except Exception: - frappe.db.set_global('demo_last_date', current_date) + frappe.db.set_global("demo_last_date", current_date) raise finally: current_date = frappe.utils.add_days(current_date, 1) diff --git a/erpnext/demo/domains.py b/erpnext/demo/domains.py index 956f36392e9..a15becf10cb 100644 --- a/erpnext/demo/domains.py +++ b/erpnext/demo/domains.py @@ -1,27 +1,14 @@ - data = { - 'Manufacturing': { - 'company_name': 'Wind Power LLC' + "Manufacturing": {"company_name": "Wind Power LLC"}, + "Retail": { + "company_name": "Mobile Next", }, - 'Retail': { - 'company_name': 'Mobile Next', + "Distribution": { + "company_name": "Soltice Hardware", }, - 'Distribution': { - 'company_name': 'Soltice Hardware', - }, - 'Services': { - 'company_name': 'Acme Consulting' - }, - 'Education': { - 'company_name': 'Whitmore College' - }, - 'Healthcare': { - 'company_name': 'ABC Hospital Ltd.' - }, - 'Agriculture': { - 'company_name': 'Schrute Farms' - }, - 'Non Profit': { - 'company_name': 'Erpnext Foundation' - } + "Services": {"company_name": "Acme Consulting"}, + "Education": {"company_name": "Whitmore College"}, + "Healthcare": {"company_name": "ABC Hospital Ltd."}, + "Agriculture": {"company_name": "Schrute Farms"}, + "Non Profit": {"company_name": "Erpnext Foundation"}, } diff --git a/erpnext/demo/setup/education.py b/erpnext/demo/setup/education.py index eb833f4e0c0..39dc41fc3fb 100644 --- a/erpnext/demo/setup/education.py +++ b/erpnext/demo/setup/education.py @@ -23,6 +23,7 @@ def setup_data(): frappe.db.commit() frappe.clear_cache() + def make_masters(): import_json("Room") import_json("Department") @@ -34,16 +35,21 @@ def make_masters(): import_json("Grading Scale") frappe.db.commit() + def setup_item(): - items = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'item_education.json')).read()) + items = json.loads( + open(frappe.get_app_path("erpnext", "demo", "data", "item_education.json")).read() + ) for i in items: - item = frappe.new_doc('Item') + item = frappe.new_doc("Item") item.update(i) item.min_order_qty = random.randint(10, 30) - item.item_defaults[0].default_warehouse = frappe.get_all('Warehouse', - filters={'warehouse_name': item.item_defaults[0].default_warehouse}, limit=1)[0].name + item.item_defaults[0].default_warehouse = frappe.get_all( + "Warehouse", filters={"warehouse_name": item.item_defaults[0].default_warehouse}, limit=1 + )[0].name item.insert() + def make_student_applicants(): blood_group = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"] male_names = [] @@ -55,39 +61,46 @@ def make_student_applicants(): count = 1 for d in random_student_data: - if d.get('gender') == "Male": - male_names.append(d.get('first_name').title()) + if d.get("gender") == "Male": + male_names.append(d.get("first_name").title()) - if d.get('gender') == "Female": - female_names.append(d.get('first_name').title()) + if d.get("gender") == "Female": + female_names.append(d.get("first_name").title()) for idx, d in enumerate(random_student_data): student_applicant = frappe.new_doc("Student Applicant") - student_applicant.first_name = d.get('first_name').title() - student_applicant.last_name = d.get('last_name').title() - student_applicant.image = d.get('image') - student_applicant.gender = d.get('gender') + student_applicant.first_name = d.get("first_name").title() + student_applicant.last_name = d.get("last_name").title() + student_applicant.image = d.get("image") + student_applicant.gender = d.get("gender") student_applicant.program = get_random("Program") student_applicant.blood_group = random.choice(blood_group) year = random.randint(1990, 1998) month = random.randint(1, 12) day = random.randint(1, 28) student_applicant.date_of_birth = datetime(year, month, day) - student_applicant.mother_name = random.choice(female_names) + " " + d.get('last_name').title() - student_applicant.father_name = random.choice(male_names) + " " + d.get('last_name').title() + student_applicant.mother_name = random.choice(female_names) + " " + d.get("last_name").title() + student_applicant.father_name = random.choice(male_names) + " " + d.get("last_name").title() if student_applicant.gender == "Male": student_applicant.middle_name = random.choice(male_names) else: student_applicant.middle_name = random.choice(female_names) - student_applicant.student_email_id = d.get('first_name') + "_" + \ - student_applicant.middle_name + "_" + d.get('last_name') + "@example.com" - if count <5: + student_applicant.student_email_id = ( + d.get("first_name") + + "_" + + student_applicant.middle_name + + "_" + + d.get("last_name") + + "@example.com" + ) + if count < 5: student_applicant.insert() frappe.db.commit() else: student_applicant.submit() frappe.db.commit() - count+=1 + count += 1 + def make_student_group(): for term in frappe.db.get_list("Academic Term"): @@ -109,17 +122,25 @@ def make_student_group(): student_group.save() frappe.db.commit() -def make_fees_category(): - fee_type = ["Tuition Fee", "Hostel Fee", "Logistics Fee", - "Medical Fee", "Mess Fee", "Security Deposit"] - fee_desc = {"Tuition Fee" : "Curricular activities which includes books, notebooks and faculty charges" , - "Hostel Fee" : "Stay of students in institute premises", - "Logistics Fee" : "Lodging boarding of the students" , - "Medical Fee" : "Medical welfare of the students", - "Mess Fee" : "Food and beverages for your ward", - "Security Deposit" : "In case your child is found to have damaged institutes property" - } +def make_fees_category(): + fee_type = [ + "Tuition Fee", + "Hostel Fee", + "Logistics Fee", + "Medical Fee", + "Mess Fee", + "Security Deposit", + ] + + fee_desc = { + "Tuition Fee": "Curricular activities which includes books, notebooks and faculty charges", + "Hostel Fee": "Stay of students in institute premises", + "Logistics Fee": "Lodging boarding of the students", + "Medical Fee": "Medical welfare of the students", + "Mess Fee": "Food and beverages for your ward", + "Security Deposit": "In case your child is found to have damaged institutes property", + } for i in fee_type: fee_category = frappe.new_doc("Fee Category") @@ -128,6 +149,7 @@ def make_fees_category(): fee_category.insert() frappe.db.commit() + def make_fees_structure(): for d in frappe.db.get_list("Program"): program = frappe.get_doc("Program", d.name) @@ -135,29 +157,40 @@ def make_fees_structure(): fee_structure = frappe.new_doc("Fee Structure") fee_structure.program = d.name fee_structure.academic_term = random.choice(frappe.db.get_list("Academic Term")).name - for j in range(1,4): - temp = {"fees_category": random.choice(frappe.db.get_list("Fee Category")).name , "amount" : random.randint(500,1000)} + for j in range(1, 4): + temp = { + "fees_category": random.choice(frappe.db.get_list("Fee Category")).name, + "amount": random.randint(500, 1000), + } fee_structure.append("components", temp) fee_structure.insert() - program.append("fees", {"academic_term": academic_term, "fee_structure": fee_structure.name, "amount": fee_structure.total_amount}) + program.append( + "fees", + { + "academic_term": academic_term, + "fee_structure": fee_structure.name, + "amount": fee_structure.total_amount, + }, + ) program.save() frappe.db.commit() + def make_assessment_groups(): for year in frappe.db.get_list("Academic Year"): - ag = frappe.new_doc('Assessment Group') + ag = frappe.new_doc("Assessment Group") ag.assessment_group_name = year.name ag.parent_assessment_group = "All Assessment Groups" ag.is_group = 1 ag.insert() - for term in frappe.db.get_list("Academic Term", filters = {"academic_year": year.name}): - ag1 = frappe.new_doc('Assessment Group') + for term in frappe.db.get_list("Academic Term", filters={"academic_year": year.name}): + ag1 = frappe.new_doc("Assessment Group") ag1.assessment_group_name = term.name ag1.parent_assessment_group = ag.name ag1.is_group = 1 ag1.insert() - for assessment_group in ['Term I', 'Term II']: - ag2 = frappe.new_doc('Assessment Group') + for assessment_group in ["Term I", "Term II"]: + ag2 = frappe.new_doc("Assessment Group") ag2.assessment_group_name = ag1.name + " " + assessment_group ag2.parent_assessment_group = ag1.name ag2.insert() @@ -165,7 +198,8 @@ def make_assessment_groups(): def get_json_path(doctype): - return frappe.get_app_path('erpnext', 'demo', 'data', frappe.scrub(doctype) + '.json') + return frappe.get_app_path("erpnext", "demo", "data", frappe.scrub(doctype) + ".json") + def weighted_choice(weights): totals = [] diff --git a/erpnext/demo/setup/healthcare.py b/erpnext/demo/setup/healthcare.py index 56209d9545d..fac421358c0 100644 --- a/erpnext/demo/setup/healthcare.py +++ b/erpnext/demo/setup/healthcare.py @@ -24,11 +24,13 @@ def setup_data(): frappe.db.commit() frappe.clear_cache() + def make_masters(): import_json("Healthcare Practitioner") import_drug() frappe.db.commit() + def make_patient(): file_path = get_json_path("Patient") with open(file_path, "r") as open_file: @@ -37,15 +39,18 @@ def make_patient(): for d in enumerate(patient_data): patient = frappe.new_doc("Patient") - patient.patient_name = d[1]['patient_name'].title() - patient.sex = d[1]['gender'] + patient.patient_name = d[1]["patient_name"].title() + patient.sex = d[1]["gender"] patient.blood_group = "A Positive" patient.date_of_birth = datetime.datetime(1990, 3, 25) - patient.email_id = d[1]['patient_name'] + "_" + patient.date_of_birth.strftime('%m/%d/%Y') + "@example.com" - if count <5: + patient.email_id = ( + d[1]["patient_name"] + "_" + patient.date_of_birth.strftime("%m/%d/%Y") + "@example.com" + ) + if count < 5: patient.insert() frappe.db.commit() - count+=1 + count += 1 + def make_appointment(): i = 1 @@ -56,7 +61,7 @@ def make_appointment(): patient_sex = frappe.get_value("Patient", patient, "sex") appointment = frappe.new_doc("Patient Appointment") startDate = datetime.datetime.now() - for x in random_date(startDate,0): + for x in random_date(startDate, 0): appointment_datetime = x appointment.appointment_datetime = appointment_datetime appointment.appointment_time = appointment_datetime @@ -65,9 +70,10 @@ def make_appointment(): appointment.patient_sex = patient_sex appointment.practitioner = practitioner appointment.department = department - appointment.save(ignore_permissions = True) + appointment.save(ignore_permissions=True) i += 1 + def make_consulation(): for i in range(3): practitioner = get_random("Healthcare Practitioner") @@ -77,14 +83,23 @@ def make_consulation(): encounter = set_encounter(patient, patient_sex, practitioner, department, getdate(), i) encounter.save(ignore_permissions=True) + def consulation_on_appointment(): for i in range(3): appointment = get_random("Patient Appointment") - appointment = frappe.get_doc("Patient Appointment",appointment) - encounter = set_encounter(appointment.patient, appointment.patient_sex, appointment.practitioner, appointment.department, appointment.appointment_date, i) + appointment = frappe.get_doc("Patient Appointment", appointment) + encounter = set_encounter( + appointment.patient, + appointment.patient_sex, + appointment.practitioner, + appointment.department, + appointment.appointment_date, + i, + ) encounter.appointment = appointment.name encounter.save(ignore_permissions=True) + def set_encounter(patient, patient_sex, practitioner, department, encounter_date, i): encounter = frappe.new_doc("Patient Encounter") encounter.patient = patient @@ -92,7 +107,7 @@ def set_encounter(patient, patient_sex, practitioner, department, encounter_date encounter.practitioner = practitioner encounter.visit_department = department encounter.encounter_date = encounter_date - if i > 2 and patient_sex=='Female': + if i > 2 and patient_sex == "Female": encounter.symptoms = "Having chest pains for the last week." encounter.diagnosis = """This patient's description of dull, aching, exertion related substernal chest pain is suggestive of ischemic @@ -104,6 +119,7 @@ def set_encounter(patient, patient_sex, practitioner, department, encounter_date encounter = append_test_rx(encounter) return encounter + def make_lab_test(): practitioner = get_random("Healthcare Practitioner") patient = get_random("Patient") @@ -111,15 +127,23 @@ def make_lab_test(): template = get_random("Lab Test Template") set_lab_test(patient, patient_sex, practitioner, template) + def lab_test_on_encounter(): i = 1 while i <= 2: - test_rx = get_random("Lab Prescription", filters={'test_created': 0}) + test_rx = get_random("Lab Prescription", filters={"test_created": 0}) test_rx = frappe.get_doc("Lab Prescription", test_rx) encounter = frappe.get_doc("Patient Encounter", test_rx.parent) - set_lab_test(encounter.patient, encounter.patient_sex, encounter.practitioner, test_rx.test_code, test_rx.name) + set_lab_test( + encounter.patient, + encounter.patient_sex, + encounter.practitioner, + test_rx.test_code, + test_rx.name, + ) i += 1 + def set_lab_test(patient, patient_sex, practitioner, template, rx=None): lab_test = frappe.new_doc("Lab Test") lab_test.practitioner = practitioner @@ -129,6 +153,7 @@ def set_lab_test(patient, patient_sex, practitioner, template, rx=None): lab_test.prescription = rx create_test_from_template(lab_test) + def append_test_rx(encounter): i = 1 while i <= 2: @@ -137,10 +162,11 @@ def append_test_rx(encounter): i += 1 return encounter + def append_drug_rx(encounter): i = 1 while i <= 3: - drug = get_random("Item", filters={"item_group":"Drug"}) + drug = get_random("Item", filters={"item_group": "Drug"}) drug = frappe.get_doc("Item", drug) drug_rx = encounter.append("drug_prescription") drug_rx.drug_code = drug.item_code @@ -150,21 +176,24 @@ def append_drug_rx(encounter): i += 1 return encounter -def random_date(start,l): - current = start - while l >= 0: - curr = current + datetime.timedelta(minutes=60) - yield curr - l-=1 + +def random_date(start, l): + current = start + while l >= 0: + curr = current + datetime.timedelta(minutes=60) + yield curr + l -= 1 + def import_drug(): frappe.flags.in_import = True - data = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'drug_list.json')).read()) + data = json.loads(open(frappe.get_app_path("erpnext", "demo", "data", "drug_list.json")).read()) for d in data: doc = frappe.new_doc("Item") doc.update(d) doc.insert() frappe.flags.in_import = False + def get_json_path(doctype): - return frappe.get_app_path('erpnext', 'demo', 'data', frappe.scrub(doctype) + '.json') + return frappe.get_app_path("erpnext", "demo", "data", frappe.scrub(doctype) + ".json") diff --git a/erpnext/demo/setup/manufacture.py b/erpnext/demo/setup/manufacture.py index ec6d2810b74..0b482034a0d 100644 --- a/erpnext/demo/setup/manufacture.py +++ b/erpnext/demo/setup/manufacture.py @@ -1,4 +1,3 @@ - import json import random @@ -16,44 +15,49 @@ def setup_data(): setup_item() setup_workstation() setup_asset() - import_json('Operation') + import_json("Operation") setup_item_price() show_item_groups_in_website() - import_json('BOM', submit=True) + import_json("BOM", submit=True) frappe.db.commit() frappe.clear_cache() + def setup_workstation(): - workstations = [u'Drilling Machine 1', u'Lathe 1', u'Assembly Station 1', u'Assembly Station 2', u'Packing and Testing Station'] + workstations = [ + "Drilling Machine 1", + "Lathe 1", + "Assembly Station 1", + "Assembly Station 2", + "Packing and Testing Station", + ] for w in workstations: - frappe.get_doc({ - "doctype": "Workstation", - "workstation_name": w, - "holiday_list": frappe.get_all("Holiday List")[0].name, - "hour_rate_consumable": int(random.random() * 20), - "hour_rate_electricity": int(random.random() * 10), - "hour_rate_labour": int(random.random() * 40), - "hour_rate_rent": int(random.random() * 10), - "working_hours": [ - { - "enabled": 1, - "start_time": "8:00:00", - "end_time": "15:00:00" - } - ] - }).insert() + frappe.get_doc( + { + "doctype": "Workstation", + "workstation_name": w, + "holiday_list": frappe.get_all("Holiday List")[0].name, + "hour_rate_consumable": int(random.random() * 20), + "hour_rate_electricity": int(random.random() * 10), + "hour_rate_labour": int(random.random() * 40), + "hour_rate_rent": int(random.random() * 10), + "working_hours": [{"enabled": 1, "start_time": "8:00:00", "end_time": "15:00:00"}], + } + ).insert() + def show_item_groups_in_website(): """set show_in_website=1 for Item Groups""" products = frappe.get_doc("Item Group", "Products") products.show_in_website = 1 - products.route = 'products' + products.route = "products" products.save() + def setup_asset(): - assets = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'asset.json')).read()) + assets = json.loads(open(frappe.get_app_path("erpnext", "demo", "data", "asset.json")).read()) for d in assets: - asset = frappe.new_doc('Asset') + asset = frappe.new_doc("Asset") asset.update(d) asset.purchase_date = add_days(nowdate(), -random.randint(20, 1500)) asset.next_depreciation_date = add_days(asset.purchase_date, 30) @@ -65,28 +69,35 @@ def setup_asset(): asset.save() asset.submit() + def setup_item(): - items = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'item.json')).read()) + items = json.loads(open(frappe.get_app_path("erpnext", "demo", "data", "item.json")).read()) for i in items: - item = frappe.new_doc('Item') + item = frappe.new_doc("Item") item.update(i) - if hasattr(item, 'item_defaults') and item.item_defaults[0].default_warehouse: - item.item_defaults[0].company = data.get("Manufacturing").get('company_name') - warehouse = frappe.get_all('Warehouse', filters={'warehouse_name': item.item_defaults[0].default_warehouse}, limit=1) + if hasattr(item, "item_defaults") and item.item_defaults[0].default_warehouse: + item.item_defaults[0].company = data.get("Manufacturing").get("company_name") + warehouse = frappe.get_all( + "Warehouse", filters={"warehouse_name": item.item_defaults[0].default_warehouse}, limit=1 + ) if warehouse: item.item_defaults[0].default_warehouse = warehouse[0].name item.insert() + def setup_product_bundle(): - frappe.get_doc({ - 'doctype': 'Product Bundle', - 'new_item_code': 'Wind Mill A Series with Spare Bearing', - 'items': [ - {'item_code': 'Wind Mill A Series', 'qty': 1}, - {'item_code': 'Bearing Collar', 'qty': 1}, - {'item_code': 'Bearing Assembly', 'qty': 1}, - ] - }).insert() + frappe.get_doc( + { + "doctype": "Product Bundle", + "new_item_code": "Wind Mill A Series with Spare Bearing", + "items": [ + {"item_code": "Wind Mill A Series", "qty": 1}, + {"item_code": "Bearing Collar", "qty": 1}, + {"item_code": "Bearing Assembly", "qty": 1}, + ], + } + ).insert() + def setup_item_price(): frappe.db.sql("delete from `tabItem Price`") @@ -109,7 +120,7 @@ def setup_item_price(): "Wind Mill A Series with Spare Bearing": 750, "Wind MIll C Series": 400, "Wind Turbine": 400, - "Wing Sheet": 30.8 + "Wing Sheet": 30.8, } standard_buying = { @@ -126,17 +137,19 @@ def setup_item_price(): "Shaft": 250, "Stand": 300, "Upper Bearing Plate": 200, - "Wing Sheet": 25 + "Wing Sheet": 25, } for price_list in ("standard_buying", "standard_selling"): for item, rate in iteritems(locals().get(price_list)): - frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list.replace("_", " ").title(), - "item_code": item, - "selling": 1 if price_list=="standard_selling" else 0, - "buying": 1 if price_list=="standard_buying" else 0, - "price_list_rate": rate, - "currency": "USD" - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list.replace("_", " ").title(), + "item_code": item, + "selling": 1 if price_list == "standard_selling" else 0, + "buying": 1 if price_list == "standard_buying" else 0, + "price_list_rate": rate, + "currency": "USD", + } + ).insert() diff --git a/erpnext/demo/setup/retail.py b/erpnext/demo/setup/retail.py index 3d2c8b6e822..892c398d57a 100644 --- a/erpnext/demo/setup/retail.py +++ b/erpnext/demo/setup/retail.py @@ -1,4 +1,3 @@ - import json import frappe @@ -13,19 +12,24 @@ def setup_data(): frappe.db.commit() frappe.clear_cache() + def setup_item(): - items = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'item.json')).read()) + items = json.loads(open(frappe.get_app_path("erpnext", "demo", "data", "item.json")).read()) for i in items: - if not i.get("domain") == "Retail": continue - item = frappe.new_doc('Item') + if not i.get("domain") == "Retail": + continue + item = frappe.new_doc("Item") item.update(i) - if hasattr(item, 'item_defaults') and item.item_defaults[0].default_warehouse: - item.item_defaults[0].company = data.get("Retail").get('company_name') - warehouse = frappe.get_all('Warehouse', filters={'warehouse_name': item.item_defaults[0].default_warehouse}, limit=1) + if hasattr(item, "item_defaults") and item.item_defaults[0].default_warehouse: + item.item_defaults[0].company = data.get("Retail").get("company_name") + warehouse = frappe.get_all( + "Warehouse", filters={"warehouse_name": item.item_defaults[0].default_warehouse}, limit=1 + ) if warehouse: item.item_defaults[0].default_warehouse = warehouse[0].name item.insert() + def setup_item_price(): frappe.db.sql("delete from `tabItem Price`") @@ -48,17 +52,19 @@ def setup_item_price(): "Xiaomi Poco F1": 200, "Iphone XS": 600, "Samsung Galaxy S9": 500, - "Sony Bluetooth Headphone": 69 + "Sony Bluetooth Headphone": 69, } for price_list in ("standard_buying", "standard_selling"): for item, rate in iteritems(locals().get(price_list)): - frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list.replace("_", " ").title(), - "item_code": item, - "selling": 1 if price_list=="standard_selling" else 0, - "buying": 1 if price_list=="standard_buying" else 0, - "price_list_rate": rate, - "currency": "USD" - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list.replace("_", " ").title(), + "item_code": item, + "selling": 1 if price_list == "standard_selling" else 0, + "buying": 1 if price_list == "standard_buying" else 0, + "price_list_rate": rate, + "currency": "USD", + } + ).insert() diff --git a/erpnext/demo/setup/setup_data.py b/erpnext/demo/setup/setup_data.py index af4969cf808..de051b68c08 100644 --- a/erpnext/demo/setup/setup_data.py +++ b/erpnext/demo/setup/setup_data.py @@ -1,4 +1,3 @@ - import json import random @@ -25,7 +24,7 @@ def setup(domain): setup_role_permissions() setup_custom_field_for_domain() - employees = frappe.get_all('Employee', fields=['name', 'date_of_joining']) + employees = frappe.get_all("Employee", fields=["name", "date_of_joining"]) # monthly salary setup_salary_structure(employees[:5], 0) @@ -37,11 +36,11 @@ def setup(domain): setup_customer() setup_supplier() setup_warehouse() - import_json('Address') - import_json('Contact') - import_json('Lead') + import_json("Address") + import_json("Contact") + import_json("Lead") setup_currency_exchange() - #setup_mode_of_payment() + # setup_mode_of_payment() setup_account_to_expense_type() setup_budget() setup_pos_profile() @@ -49,35 +48,41 @@ def setup(domain): frappe.db.commit() frappe.clear_cache() -def complete_setup(domain='Manufacturing'): + +def complete_setup(domain="Manufacturing"): print("Complete Setup...") from frappe.desk.page.setup_wizard.setup_wizard import setup_complete - if not frappe.get_all('Company', limit=1): - setup_complete({ - "full_name": "Test User", - "email": "test_demo@erpnext.com", - "company_tagline": 'Awesome Products and Services', - "password": "demo", - "fy_start_date": "2015-01-01", - "fy_end_date": "2015-12-31", - "bank_account": "National Bank", - "domains": [domain], - "company_name": data.get(domain).get('company_name'), - "chart_of_accounts": "Standard", - "company_abbr": ''.join([d[0] for d in data.get(domain).get('company_name').split()]).upper(), - "currency": 'USD', - "timezone": 'America/New_York', - "country": 'United States', - "language": "english" - }) + if not frappe.get_all("Company", limit=1): + setup_complete( + { + "full_name": "Test User", + "email": "test_demo@erpnext.com", + "company_tagline": "Awesome Products and Services", + "password": "demo", + "fy_start_date": "2015-01-01", + "fy_end_date": "2015-12-31", + "bank_account": "National Bank", + "domains": [domain], + "company_name": data.get(domain).get("company_name"), + "chart_of_accounts": "Standard", + "company_abbr": "".join([d[0] for d in data.get(domain).get("company_name").split()]).upper(), + "currency": "USD", + "timezone": "America/New_York", + "country": "United States", + "language": "english", + } + ) company = erpnext.get_default_company() if company: company_doc = frappe.get_doc("Company", company) - company_doc.db_set('default_payroll_payable_account', - frappe.db.get_value('Account', dict(account_name='Payroll Payable'))) + company_doc.db_set( + "default_payroll_payable_account", + frappe.db.get_value("Account", dict(account_name="Payroll Payable")), + ) + def setup_demo_page(): # home page should always be "start" @@ -85,16 +90,19 @@ def setup_demo_page(): website_settings.home_page = "demo" website_settings.save() + def setup_fiscal_year(): fiscal_year = None for year in range(2010, now_datetime().year + 1, 1): try: - fiscal_year = frappe.get_doc({ - "doctype": "Fiscal Year", - "year": cstr(year), - "year_start_date": "{0}-01-01".format(year), - "year_end_date": "{0}-12-31".format(year) - }).insert() + fiscal_year = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": cstr(year), + "year_start_date": "{0}-01-01".format(year), + "year_end_date": "{0}-12-31".format(year), + } + ).insert() except frappe.DuplicateEntryError: pass @@ -102,15 +110,18 @@ def setup_fiscal_year(): if fiscal_year: fiscal_year.set_as_default() + def setup_holiday_list(): """Setup Holiday List for the current year""" year = now_datetime().year - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": str(year), - "from_date": "{0}-01-01".format(year), - "to_date": "{0}-12-31".format(year), - }) + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": str(year), + "from_date": "{0}-01-01".format(year), + "to_date": "{0}-12-31".format(year), + } + ) holiday_list.insert() holiday_list.weekly_off = "Saturday" holiday_list.get_weekly_off_dates() @@ -118,67 +129,85 @@ def setup_holiday_list(): holiday_list.get_weekly_off_dates() holiday_list.save() - frappe.set_value("Company", erpnext.get_default_company(), "default_holiday_list", holiday_list.name) + frappe.set_value( + "Company", erpnext.get_default_company(), "default_holiday_list", holiday_list.name + ) def setup_user(): frappe.db.sql('delete from tabUser where name not in ("Guest", "Administrator")') - for u in json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', 'user.json')).read()): + for u in json.loads(open(frappe.get_app_path("erpnext", "demo", "data", "user.json")).read()): user = frappe.new_doc("User") user.update(u) user.flags.no_welcome_mail = True - user.new_password = 'Demo1234567!!!' + user.new_password = "Demo1234567!!!" user.insert() + def setup_employee(): frappe.db.set_value("HR Settings", None, "emp_created_by", "Naming Series") frappe.db.commit() - for d in frappe.get_all('Salary Component'): - salary_component = frappe.get_doc('Salary Component', d.name) - salary_component.append('accounts', dict( - company=erpnext.get_default_company(), - account=frappe.get_value('Account', dict(account_name=('like', 'Salary%'))) - )) + for d in frappe.get_all("Salary Component"): + salary_component = frappe.get_doc("Salary Component", d.name) + salary_component.append( + "accounts", + dict( + company=erpnext.get_default_company(), + account=frappe.get_value("Account", dict(account_name=("like", "Salary%"))), + ), + ) salary_component.save() - import_json('Employee') - holiday_list = frappe.db.get_value("Holiday List", {"holiday_list_name": str(now_datetime().year)}, 'name') - frappe.db.sql('''update tabEmployee set holiday_list={0}'''.format(holiday_list)) + import_json("Employee") + holiday_list = frappe.db.get_value( + "Holiday List", {"holiday_list_name": str(now_datetime().year)}, "name" + ) + frappe.db.sql("""update tabEmployee set holiday_list={0}""".format(holiday_list)) + def setup_salary_structure(employees, salary_slip_based_on_timesheet=0): - ss = frappe.new_doc('Salary Structure') + ss = frappe.new_doc("Salary Structure") ss.name = "Sample Salary Structure - " + random_string(5) ss.salary_slip_based_on_timesheet = salary_slip_based_on_timesheet if salary_slip_based_on_timesheet: - ss.salary_component = 'Basic' + ss.salary_component = "Basic" ss.hour_rate = flt(random.random() * 10, 2) else: - ss.payroll_frequency = 'Monthly' + ss.payroll_frequency = "Monthly" - ss.payment_account = frappe.get_value('Account', - {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name") + ss.payment_account = frappe.get_value( + "Account", + {"account_type": "Cash", "company": erpnext.get_default_company(), "is_group": 0}, + "name", + ) - ss.append('earnings', { - 'salary_component': 'Basic', - "abbr":'B', - 'formula': 'base*.2', - 'amount_based_on_formula': 1, - "idx": 1 - }) - ss.append('deductions', { - 'salary_component': 'Income Tax', - "abbr":'IT', - 'condition': 'base > 10000', - 'formula': 'base*.1', - "idx": 1 - }) + ss.append( + "earnings", + { + "salary_component": "Basic", + "abbr": "B", + "formula": "base*.2", + "amount_based_on_formula": 1, + "idx": 1, + }, + ) + ss.append( + "deductions", + { + "salary_component": "Income Tax", + "abbr": "IT", + "condition": "base > 10000", + "formula": "base*.1", + "idx": 1, + }, + ) ss.insert() ss.submit() for e in employees: - sa = frappe.new_doc("Salary Structure Assignment") + sa = frappe.new_doc("Salary Structure Assignment") sa.employee = e.name sa.salary_structure = ss.name sa.from_date = "2015-01-01" @@ -188,181 +217,253 @@ def setup_salary_structure(employees, salary_slip_based_on_timesheet=0): return ss + def setup_user_roles(domain): - user = frappe.get_doc('User', 'demo@erpnext.com') - user.add_roles('HR User', 'HR Manager', 'Accounts User', 'Accounts Manager', - 'Stock User', 'Stock Manager', 'Sales User', 'Sales Manager', 'Purchase User', - 'Purchase Manager', 'Projects User', 'Manufacturing User', 'Manufacturing Manager', - 'Support Team') + user = frappe.get_doc("User", "demo@erpnext.com") + user.add_roles( + "HR User", + "HR Manager", + "Accounts User", + "Accounts Manager", + "Stock User", + "Stock Manager", + "Sales User", + "Sales Manager", + "Purchase User", + "Purchase Manager", + "Projects User", + "Manufacturing User", + "Manufacturing Manager", + "Support Team", + ) if domain == "Healthcare": - user.add_roles('Physician', 'Healthcare Administrator', 'Laboratory User', - 'Nursing User', 'Patient') + user.add_roles( + "Physician", "Healthcare Administrator", "Laboratory User", "Nursing User", "Patient" + ) if domain == "Education": - user.add_roles('Academics User') + user.add_roles("Academics User") - if not frappe.db.get_global('demo_hr_user'): - user = frappe.get_doc('User', 'CaitlinSnow@example.com') - user.add_roles('HR User', 'HR Manager', 'Accounts User') - frappe.db.set_global('demo_hr_user', user.name) - update_employee_department(user.name, 'Human Resources') - for d in frappe.get_all('User Permission', filters={"user": "CaitlinSnow@example.com"}): - frappe.delete_doc('User Permission', d.name) + if not frappe.db.get_global("demo_hr_user"): + user = frappe.get_doc("User", "CaitlinSnow@example.com") + user.add_roles("HR User", "HR Manager", "Accounts User") + frappe.db.set_global("demo_hr_user", user.name) + update_employee_department(user.name, "Human Resources") + for d in frappe.get_all("User Permission", filters={"user": "CaitlinSnow@example.com"}): + frappe.delete_doc("User Permission", d.name) - if not frappe.db.get_global('demo_sales_user_1'): - user = frappe.get_doc('User', 'VandalSavage@example.com') - user.add_roles('Sales User') - update_employee_department(user.name, 'Sales') - frappe.db.set_global('demo_sales_user_1', user.name) + if not frappe.db.get_global("demo_sales_user_1"): + user = frappe.get_doc("User", "VandalSavage@example.com") + user.add_roles("Sales User") + update_employee_department(user.name, "Sales") + frappe.db.set_global("demo_sales_user_1", user.name) - if not frappe.db.get_global('demo_sales_user_2'): - user = frappe.get_doc('User', 'GraceChoi@example.com') - user.add_roles('Sales User', 'Sales Manager', 'Accounts User') - update_employee_department(user.name, 'Sales') - frappe.db.set_global('demo_sales_user_2', user.name) + if not frappe.db.get_global("demo_sales_user_2"): + user = frappe.get_doc("User", "GraceChoi@example.com") + user.add_roles("Sales User", "Sales Manager", "Accounts User") + update_employee_department(user.name, "Sales") + frappe.db.set_global("demo_sales_user_2", user.name) - if not frappe.db.get_global('demo_purchase_user'): - user = frappe.get_doc('User', 'MaxwellLord@example.com') - user.add_roles('Purchase User', 'Purchase Manager', 'Accounts User', 'Stock User') - update_employee_department(user.name, 'Purchase') - frappe.db.set_global('demo_purchase_user', user.name) + if not frappe.db.get_global("demo_purchase_user"): + user = frappe.get_doc("User", "MaxwellLord@example.com") + user.add_roles("Purchase User", "Purchase Manager", "Accounts User", "Stock User") + update_employee_department(user.name, "Purchase") + frappe.db.set_global("demo_purchase_user", user.name) - if not frappe.db.get_global('demo_manufacturing_user'): - user = frappe.get_doc('User', 'NeptuniaAquaria@example.com') - user.add_roles('Manufacturing User', 'Stock Manager', 'Stock User', 'Purchase User', 'Accounts User') - update_employee_department(user.name, 'Production') - frappe.db.set_global('demo_manufacturing_user', user.name) + if not frappe.db.get_global("demo_manufacturing_user"): + user = frappe.get_doc("User", "NeptuniaAquaria@example.com") + user.add_roles( + "Manufacturing User", "Stock Manager", "Stock User", "Purchase User", "Accounts User" + ) + update_employee_department(user.name, "Production") + frappe.db.set_global("demo_manufacturing_user", user.name) - if not frappe.db.get_global('demo_stock_user'): - user = frappe.get_doc('User', 'HollyGranger@example.com') - user.add_roles('Manufacturing User', 'Stock User', 'Purchase User', 'Accounts User') - update_employee_department(user.name, 'Production') - frappe.db.set_global('demo_stock_user', user.name) + if not frappe.db.get_global("demo_stock_user"): + user = frappe.get_doc("User", "HollyGranger@example.com") + user.add_roles("Manufacturing User", "Stock User", "Purchase User", "Accounts User") + update_employee_department(user.name, "Production") + frappe.db.set_global("demo_stock_user", user.name) - if not frappe.db.get_global('demo_accounts_user'): - user = frappe.get_doc('User', 'BarryAllen@example.com') - user.add_roles('Accounts User', 'Accounts Manager', 'Sales User', 'Purchase User') - update_employee_department(user.name, 'Accounts') - frappe.db.set_global('demo_accounts_user', user.name) + if not frappe.db.get_global("demo_accounts_user"): + user = frappe.get_doc("User", "BarryAllen@example.com") + user.add_roles("Accounts User", "Accounts Manager", "Sales User", "Purchase User") + update_employee_department(user.name, "Accounts") + frappe.db.set_global("demo_accounts_user", user.name) - if not frappe.db.get_global('demo_projects_user'): - user = frappe.get_doc('User', 'PeterParker@example.com') - user.add_roles('HR User', 'Projects User') - update_employee_department(user.name, 'Management') - frappe.db.set_global('demo_projects_user', user.name) + if not frappe.db.get_global("demo_projects_user"): + user = frappe.get_doc("User", "PeterParker@example.com") + user.add_roles("HR User", "Projects User") + update_employee_department(user.name, "Management") + frappe.db.set_global("demo_projects_user", user.name) if domain == "Education": - if not frappe.db.get_global('demo_education_user'): - user = frappe.get_doc('User', 'ArthurCurry@example.com') - user.add_roles('Academics User') - update_employee_department(user.name, 'Management') - frappe.db.set_global('demo_education_user', user.name) + if not frappe.db.get_global("demo_education_user"): + user = frappe.get_doc("User", "ArthurCurry@example.com") + user.add_roles("Academics User") + update_employee_department(user.name, "Management") + frappe.db.set_global("demo_education_user", user.name) + + # Add Expense Approver + user = frappe.get_doc("User", "ClarkKent@example.com") + user.add_roles("Expense Approver") - #Add Expense Approver - user = frappe.get_doc('User', 'ClarkKent@example.com') - user.add_roles('Expense Approver') def setup_leave_allocation(): year = now_datetime().year - for employee in frappe.get_all('Employee', fields=['name']): - leave_types = frappe.get_all("Leave Type", fields=['name', 'max_continuous_days_allowed']) + for employee in frappe.get_all("Employee", fields=["name"]): + leave_types = frappe.get_all("Leave Type", fields=["name", "max_continuous_days_allowed"]) for leave_type in leave_types: if not leave_type.max_continuous_days_allowed: leave_type.max_continuous_days_allowed = 10 - leave_allocation = frappe.get_doc({ - "doctype": "Leave Allocation", - "employee": employee.name, - "from_date": "{0}-01-01".format(year), - "to_date": "{0}-12-31".format(year), - "leave_type": leave_type.name, - "new_leaves_allocated": random.randint(1, int(leave_type.max_continuous_days_allowed)) - }) + leave_allocation = frappe.get_doc( + { + "doctype": "Leave Allocation", + "employee": employee.name, + "from_date": "{0}-01-01".format(year), + "to_date": "{0}-12-31".format(year), + "leave_type": leave_type.name, + "new_leaves_allocated": random.randint(1, int(leave_type.max_continuous_days_allowed)), + } + ) leave_allocation.insert() leave_allocation.submit() frappe.db.commit() + def setup_customer(): - customers = [u'Asian Junction', u'Life Plan Counselling', u'Two Pesos', u'Mr Fables', u'Intelacard', u'Big D Supermarkets', u'Adaptas', u'Nelson Brothers', u'Landskip Yard Care', u'Buttrey Food & Drug', u'Fayva', u'Asian Fusion', u'Crafts Canada', u'Consumers and Consumers Express', u'Netobill', u'Choices', u'Chi-Chis', u'Red Food', u'Endicott Shoes', u'Hind Enterprises'] + customers = [ + "Asian Junction", + "Life Plan Counselling", + "Two Pesos", + "Mr Fables", + "Intelacard", + "Big D Supermarkets", + "Adaptas", + "Nelson Brothers", + "Landskip Yard Care", + "Buttrey Food & Drug", + "Fayva", + "Asian Fusion", + "Crafts Canada", + "Consumers and Consumers Express", + "Netobill", + "Choices", + "Chi-Chis", + "Red Food", + "Endicott Shoes", + "Hind Enterprises", + ] for c in customers: - frappe.get_doc({ - "doctype": "Customer", - "customer_name": c, - "customer_group": "Commercial", - "customer_type": random.choice(["Company", "Individual"]), - "territory": "Rest Of The World" - }).insert() + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": c, + "customer_group": "Commercial", + "customer_type": random.choice(["Company", "Individual"]), + "territory": "Rest Of The World", + } + ).insert() + def setup_supplier(): - suppliers = [u'Helios Air', u'Ks Merchandise', u'HomeBase', u'Scott Ties', u'Reliable Investments', u'Nan Duskin', u'Rainbow Records', u'New World Realty', u'Asiatic Solutions', u'Eagle Hardware', u'Modern Electricals'] + suppliers = [ + "Helios Air", + "Ks Merchandise", + "HomeBase", + "Scott Ties", + "Reliable Investments", + "Nan Duskin", + "Rainbow Records", + "New World Realty", + "Asiatic Solutions", + "Eagle Hardware", + "Modern Electricals", + ] for s in suppliers: - frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": s, - "supplier_group": random.choice(["Services", "Raw Material"]), - }).insert() + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": s, + "supplier_group": random.choice(["Services", "Raw Material"]), + } + ).insert() + def setup_warehouse(): - w = frappe.new_doc('Warehouse') - w.warehouse_name = 'Supplier' + w = frappe.new_doc("Warehouse") + w.warehouse_name = "Supplier" w.insert() -def setup_currency_exchange(): - frappe.get_doc({ - 'doctype': 'Currency Exchange', - 'from_currency': 'EUR', - 'to_currency': 'USD', - 'exchange_rate': 1.13 - }).insert() - frappe.get_doc({ - 'doctype': 'Currency Exchange', - 'from_currency': 'CNY', - 'to_currency': 'USD', - 'exchange_rate': 0.16 - }).insert() +def setup_currency_exchange(): + frappe.get_doc( + { + "doctype": "Currency Exchange", + "from_currency": "EUR", + "to_currency": "USD", + "exchange_rate": 1.13, + } + ).insert() + + frappe.get_doc( + { + "doctype": "Currency Exchange", + "from_currency": "CNY", + "to_currency": "USD", + "exchange_rate": 0.16, + } + ).insert() + def setup_mode_of_payment(): - company_abbr = frappe.get_cached_value('Company', erpnext.get_default_company(), "abbr") - account_dict = {'Cash': 'Cash - '+ company_abbr , 'Bank': 'National Bank - '+ company_abbr} - for payment_mode in frappe.get_all('Mode of Payment', fields = ["name", "type"]): + company_abbr = frappe.get_cached_value("Company", erpnext.get_default_company(), "abbr") + account_dict = {"Cash": "Cash - " + company_abbr, "Bank": "National Bank - " + company_abbr} + for payment_mode in frappe.get_all("Mode of Payment", fields=["name", "type"]): if payment_mode.type: - mop = frappe.get_doc('Mode of Payment', payment_mode.name) - mop.append('accounts', { - 'company': erpnext.get_default_company(), - 'default_account': account_dict.get(payment_mode.type) - }) + mop = frappe.get_doc("Mode of Payment", payment_mode.name) + mop.append( + "accounts", + { + "company": erpnext.get_default_company(), + "default_account": account_dict.get(payment_mode.type), + }, + ) mop.save(ignore_permissions=True) + def setup_account(): frappe.flags.in_import = True - data = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', - 'account.json')).read()) + data = json.loads(open(frappe.get_app_path("erpnext", "demo", "data", "account.json")).read()) for d in data: - doc = frappe.new_doc('Account') + doc = frappe.new_doc("Account") doc.update(d) - doc.parent_account = frappe.db.get_value('Account', {'account_name': doc.parent_account}) + doc.parent_account = frappe.db.get_value("Account", {"account_name": doc.parent_account}) doc.insert() frappe.flags.in_import = False + def setup_account_to_expense_type(): - company_abbr = frappe.get_cached_value('Company', erpnext.get_default_company(), "abbr") - expense_types = [{'name': _('Calls'), "account": "Sales Expenses - "+ company_abbr}, - {'name': _('Food'), "account": "Entertainment Expenses - "+ company_abbr}, - {'name': _('Medical'), "account": "Utility Expenses - "+ company_abbr}, - {'name': _('Others'), "account": "Miscellaneous Expenses - "+ company_abbr}, - {'name': _('Travel'), "account": "Travel Expenses - "+ company_abbr}] + company_abbr = frappe.get_cached_value("Company", erpnext.get_default_company(), "abbr") + expense_types = [ + {"name": _("Calls"), "account": "Sales Expenses - " + company_abbr}, + {"name": _("Food"), "account": "Entertainment Expenses - " + company_abbr}, + {"name": _("Medical"), "account": "Utility Expenses - " + company_abbr}, + {"name": _("Others"), "account": "Miscellaneous Expenses - " + company_abbr}, + {"name": _("Travel"), "account": "Travel Expenses - " + company_abbr}, + ] for expense_type in expense_types: doc = frappe.get_doc("Expense Claim Type", expense_type["name"]) - doc.append("accounts", { - "company" : erpnext.get_default_company(), - "default_account" : expense_type["account"] - }) + doc.append( + "accounts", + {"company": erpnext.get_default_company(), "default_account": expense_type["account"]}, + ) doc.save(ignore_permissions=True) + def setup_budget(): fiscal_years = frappe.get_all("Fiscal Year", order_by="year_start_date")[-2:] @@ -373,10 +474,13 @@ def setup_budget(): budget.action_if_annual_budget_exceeded = "Warn" expense_ledger_count = frappe.db.count("Account", {"is_group": "0", "root_type": "Expense"}) - add_random_children(budget, "accounts", rows=random.randint(10, expense_ledger_count), - randomize = { - "account": ("Account", {"is_group": "0", "root_type": "Expense"}) - }, unique="account") + add_random_children( + budget, + "accounts", + rows=random.randint(10, expense_ledger_count), + randomize={"account": ("Account", {"is_group": "0", "root_type": "Expense"})}, + unique="account", + ) for d in budget.accounts: d.budget_amount = random.randint(5, 100) * 10000 @@ -384,46 +488,54 @@ def setup_budget(): budget.save() budget.submit() -def setup_pos_profile(): - company_abbr = frappe.get_cached_value('Company', erpnext.get_default_company(), "abbr") - pos = frappe.new_doc('POS Profile') - pos.user = frappe.db.get_global('demo_accounts_user') - pos.name = "Demo POS Profile" - pos.naming_series = 'SINV-' - pos.update_stock = 0 - pos.write_off_account = 'Cost of Goods Sold - '+ company_abbr - pos.write_off_cost_center = 'Main - '+ company_abbr - pos.customer_group = get_root_of('Customer Group') - pos.territory = get_root_of('Territory') - pos.append('payments', { - 'mode_of_payment': frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), - 'amount': 0.0, - 'default': 1 - }) +def setup_pos_profile(): + company_abbr = frappe.get_cached_value("Company", erpnext.get_default_company(), "abbr") + pos = frappe.new_doc("POS Profile") + pos.user = frappe.db.get_global("demo_accounts_user") + pos.name = "Demo POS Profile" + pos.naming_series = "SINV-" + pos.update_stock = 0 + pos.write_off_account = "Cost of Goods Sold - " + company_abbr + pos.write_off_cost_center = "Main - " + company_abbr + pos.customer_group = get_root_of("Customer Group") + pos.territory = get_root_of("Territory") + + pos.append( + "payments", + { + "mode_of_payment": frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name"), + "amount": 0.0, + "default": 1, + }, + ) pos.insert() + def setup_role_permissions(): - role_permissions = {'Batch': ['Accounts User', 'Item Manager']} + role_permissions = {"Batch": ["Accounts User", "Item Manager"]} for doctype, roles in role_permissions.items(): for role in roles: - if not frappe.db.get_value('Custom DocPerm', - {'parent': doctype, 'role': role}): - frappe.get_doc({ - 'doctype': 'Custom DocPerm', - 'role': role, - 'read': 1, - 'write': 1, - 'create': 1, - 'delete': 1, - 'parent': doctype - }).insert(ignore_permissions=True) + if not frappe.db.get_value("Custom DocPerm", {"parent": doctype, "role": role}): + frappe.get_doc( + { + "doctype": "Custom DocPerm", + "role": role, + "read": 1, + "write": 1, + "create": 1, + "delete": 1, + "parent": doctype, + } + ).insert(ignore_permissions=True) + def import_json(doctype, submit=False, values=None): frappe.flags.in_import = True - data = json.loads(open(frappe.get_app_path('erpnext', 'demo', 'data', - frappe.scrub(doctype) + '.json')).read()) + data = json.loads( + open(frappe.get_app_path("erpnext", "demo", "data", frappe.scrub(doctype) + ".json")).read() + ) for d in data: doc = frappe.new_doc(doctype) doc.update(d) @@ -435,17 +547,23 @@ def import_json(doctype, submit=False, values=None): frappe.flags.in_import = False + def update_employee_department(user_id, department): - employee = frappe.db.get_value('Employee', {"user_id": user_id}, 'name') - department = frappe.db.get_value('Department', {'department_name': department}, 'name') - frappe.db.set_value('Employee', employee, 'department', department) + employee = frappe.db.get_value("Employee", {"user_id": user_id}, "name") + department = frappe.db.get_value("Department", {"department_name": department}, "name") + frappe.db.set_value("Employee", employee, "department", department) + def setup_custom_field_for_domain(): field = { "Item": [ - dict(fieldname='domain', label='Domain', - fieldtype='Select', hidden=1, default="Manufacturing", - options="Manufacturing\nService\nDistribution\nRetail" + dict( + fieldname="domain", + label="Domain", + fieldtype="Select", + hidden=1, + default="Manufacturing", + options="Manufacturing\nService\nDistribution\nRetail", ) ] } diff --git a/erpnext/demo/user/accounts.py b/erpnext/demo/user/accounts.py index f0ac173a4a9..d209993da58 100644 --- a/erpnext/demo/user/accounts.py +++ b/erpnext/demo/user/accounts.py @@ -1,4 +1,3 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt @@ -23,18 +22,21 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas def work(): - frappe.set_user(frappe.db.get_global('demo_accounts_user')) + frappe.set_user(frappe.db.get_global("demo_accounts_user")) if random.random() <= 0.6: report = "Ordered Items to be Billed" - for so in list(set([r[0] for r in query_report.run(report)["result"] - if r[0]!="Total"]))[:random.randint(1, 5)]: + for so in list(set([r[0] for r in query_report.run(report)["result"] if r[0] != "Total"]))[ + : random.randint(1, 5) + ]: try: si = frappe.get_doc(make_sales_invoice(so)) si.posting_date = frappe.flags.current_date for d in si.get("items"): if not d.income_account: - d.income_account = "Sales - {}".format(frappe.get_cached_value('Company', si.company, 'abbr')) + d.income_account = "Sales - {}".format( + frappe.get_cached_value("Company", si.company, "abbr") + ) si.insert() si.submit() frappe.db.commit() @@ -43,8 +45,9 @@ def work(): if random.random() <= 0.6: report = "Received Items to be Billed" - for pr in list(set([r[0] for r in query_report.run(report)["result"] - if r[0]!="Total"]))[:random.randint(1, 5)]: + for pr in list(set([r[0] for r in query_report.run(report)["result"] if r[0] != "Total"]))[ + : random.randint(1, 5) + ]: try: pi = frappe.get_doc(make_purchase_invoice(pr)) pi.posting_date = frappe.flags.current_date @@ -55,7 +58,6 @@ def work(): except frappe.ValidationError: pass - if random.random() < 0.5: make_payment_entries("Sales Invoice", "Accounts Receivable") @@ -63,13 +65,19 @@ def work(): make_payment_entries("Purchase Invoice", "Accounts Payable") if random.random() < 0.4: - #make payment request against sales invoice + # make payment request against sales invoice sales_invoice_name = get_random("Sales Invoice", filters={"docstatus": 1}) if sales_invoice_name: si = frappe.get_doc("Sales Invoice", sales_invoice_name) if si.outstanding_amount > 0: - payment_request = make_payment_request(dt="Sales Invoice", dn=si.name, recipient_id=si.contact_email, - submit_doc=True, mute_email=True, use_dummy_message=True) + payment_request = make_payment_request( + dt="Sales Invoice", + dn=si.name, + recipient_id=si.contact_email, + submit_doc=True, + mute_email=True, + use_dummy_message=True, + ) payment_entry = frappe.get_doc(make_payment_entry(payment_request.name)) payment_entry.posting_date = frappe.flags.current_date @@ -77,16 +85,17 @@ def work(): make_pos_invoice() + def make_payment_entries(ref_doctype, report): - outstanding_invoices = frappe.get_all(ref_doctype, fields=["name"], - filters={ - "company": erpnext.get_default_company(), - "outstanding_amount": (">", 0.0) - }) + outstanding_invoices = frappe.get_all( + ref_doctype, + fields=["name"], + filters={"company": erpnext.get_default_company(), "outstanding_amount": (">", 0.0)}, + ) # make Payment Entry - for inv in outstanding_invoices[:random.randint(1, 2)]: + for inv in outstanding_invoices[: random.randint(1, 2)]: pe = get_payment_entry(ref_doctype, inv.name) pe.posting_date = frappe.flags.current_date pe.reference_no = random_string(6) @@ -106,22 +115,23 @@ def make_payment_entries(ref_doctype, report): jv.submit() frappe.db.commit() + def make_pos_invoice(): make_sales_order() - for data in frappe.get_all('Sales Order', fields=["name"], - filters = [["per_billed", "<", "100"]]): + for data in frappe.get_all("Sales Order", fields=["name"], filters=[["per_billed", "<", "100"]]): si = frappe.get_doc(make_sales_invoice(data.name)) - si.is_pos =1 + si.is_pos = 1 si.posting_date = frappe.flags.current_date for d in si.get("items"): if not d.income_account: - d.income_account = "Sales - {}".format(frappe.get_cached_value('Company', si.company, 'abbr')) + d.income_account = "Sales - {}".format(frappe.get_cached_value("Company", si.company, "abbr")) si.set_missing_values() make_payment_entries_for_pos_invoice(si) si.insert() si.submit() + def make_payment_entries_for_pos_invoice(si): for data in si.payments: data.amount = si.outstanding_amount diff --git a/erpnext/demo/user/education.py b/erpnext/demo/user/education.py index 270333c1d2c..22b89e92264 100644 --- a/erpnext/demo/user/education.py +++ b/erpnext/demo/user/education.py @@ -1,4 +1,3 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt @@ -21,7 +20,7 @@ from erpnext.education.api import ( def work(): - frappe.set_user(frappe.db.get_global('demo_education_user')) + frappe.set_user(frappe.db.get_global("demo_education_user")) for d in range(20): approve_random_student_applicant() enroll_random_student(frappe.flags.current_date) @@ -31,11 +30,15 @@ def work(): # make_assessment_plan() make_fees() + def approve_random_student_applicant(): random_student = get_random("Student Applicant", {"application_status": "Applied"}) if random_student: status = ["Approved", "Rejected"] - frappe.db.set_value("Student Applicant", random_student, "application_status", status[weighted_choice([9,3])]) + frappe.db.set_value( + "Student Applicant", random_student, "application_status", status[weighted_choice([9, 3])] + ) + def enroll_random_student(current_date): batch = ["Section-A", "Section-B"] @@ -44,7 +47,7 @@ def enroll_random_student(current_date): enrollment = enroll_student(random_student) enrollment.academic_year = get_random("Academic Year") enrollment.enrollment_date = current_date - enrollment.student_batch_name = batch[weighted_choice([9,3])] + enrollment.student_batch_name = batch[weighted_choice([9, 3])] fee_schedule = get_fee_schedule(enrollment.program) for fee in fee_schedule: enrollment.append("fees", fee) @@ -53,45 +56,81 @@ def enroll_random_student(current_date): enrollment.append("courses", course) enrollment.submit() frappe.db.commit() - assign_student_group(enrollment.student, enrollment.student_name, enrollment.program, - enrolled_courses, enrollment.student_batch_name) + assign_student_group( + enrollment.student, + enrollment.student_name, + enrollment.program, + enrolled_courses, + enrollment.student_batch_name, + ) + def assign_student_group(student, student_name, program, courses, batch): course_list = [d["course"] for d in courses] - for d in frappe.get_list("Student Group", fields=("name"), filters={"program": program, "course":("in", course_list), "disabled": 0}): + for d in frappe.get_list( + "Student Group", + fields=("name"), + filters={"program": program, "course": ("in", course_list), "disabled": 0}, + ): student_group = frappe.get_doc("Student Group", d.name) - student_group.append("students", {"student": student, "student_name": student_name, - "group_roll_number":len(student_group.students)+1, "active":1}) + student_group.append( + "students", + { + "student": student, + "student_name": student_name, + "group_roll_number": len(student_group.students) + 1, + "active": 1, + }, + ) student_group.save() - student_batch = frappe.get_list("Student Group", fields=("name"), filters={"program": program, "group_based_on":"Batch", "batch":batch, "disabled": 0})[0] + student_batch = frappe.get_list( + "Student Group", + fields=("name"), + filters={"program": program, "group_based_on": "Batch", "batch": batch, "disabled": 0}, + )[0] student_batch_doc = frappe.get_doc("Student Group", student_batch.name) - student_batch_doc.append("students", {"student": student, "student_name": student_name, - "group_roll_number":len(student_batch_doc.students)+1, "active":1}) + student_batch_doc.append( + "students", + { + "student": student, + "student_name": student_name, + "group_roll_number": len(student_batch_doc.students) + 1, + "active": 1, + }, + ) student_batch_doc.save() frappe.db.commit() + def mark_student_attendance(current_date): status = ["Present", "Absent"] for d in frappe.db.get_list("Student Group", filters={"group_based_on": "Batch", "disabled": 0}): students = get_student_group_students(d.name) for stud in students: - make_attendance_records(stud.student, stud.student_name, status[weighted_choice([9,4])], None, d.name, current_date) + make_attendance_records( + stud.student, stud.student_name, status[weighted_choice([9, 4])], None, d.name, current_date + ) + def make_fees(): - for d in range(1,10): + for d in range(1, 10): random_fee = get_random("Fees", {"paid_amount": 0}) collect_fees(random_fee, frappe.db.get_value("Fees", random_fee, "outstanding_amount")) + def make_assessment_plan(date): - for d in range(1,4): + for d in range(1, 4): random_group = get_random("Student Group", {"group_based_on": "Course", "disabled": 0}, True) doc = frappe.new_doc("Assessment Plan") doc.student_group = random_group.name doc.course = random_group.course - doc.assessment_group = get_random("Assessment Group", {"is_group": 0, "parent": "2017-18 (Semester 2)"}) + doc.assessment_group = get_random( + "Assessment Group", {"is_group": 0, "parent": "2017-18 (Semester 2)"} + ) doc.grading_scale = get_random("Grading Scale") doc.maximum_assessment_score = 100 + def make_course_schedule(start_date, end_date): for d in frappe.db.get_list("Student Group"): cs = frappe.new_doc("Scheduling Tool") @@ -104,7 +143,7 @@ def make_course_schedule(start_date, end_date): for x in range(3): random_day = random.choice(day) cs.day = random_day - cs.from_time = timedelta(hours=(random.randrange(7, 17,1))) + cs.from_time = timedelta(hours=(random.randrange(7, 17, 1))) cs.to_time = cs.from_time + timedelta(hours=1) cs.schedule_course() day.remove(random_day) diff --git a/erpnext/demo/user/fixed_asset.py b/erpnext/demo/user/fixed_asset.py index 0e66ec04051..ddd32efeeb9 100644 --- a/erpnext/demo/user/fixed_asset.py +++ b/erpnext/demo/user/fixed_asset.py @@ -1,4 +1,3 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt @@ -11,7 +10,7 @@ from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, def work(): - frappe.set_user(frappe.db.get_global('demo_accounts_user')) + frappe.set_user(frappe.db.get_global("demo_accounts_user")) # Enable booking asset depreciation entry automatically frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) @@ -20,7 +19,9 @@ def work(): post_depreciation_entries() # scrap a random asset - frappe.db.set_value("Company", "Wind Power LLC", "disposal_account", "Gain/Loss on Asset Disposal - WPL") + frappe.db.set_value( + "Company", "Wind Power LLC", "disposal_account", "Gain/Loss on Asset Disposal - WPL" + ) asset = get_random_asset() scrap_asset(asset.name) @@ -33,13 +34,19 @@ def sell_an_asset(): asset = get_random_asset() si = make_sales_invoice(asset.name, asset.item_code, "Wind Power LLC") si.customer = get_random("Customer") - si.get("items")[0].rate = asset.value_after_depreciation * 0.8 \ - if asset.value_after_depreciation else asset.gross_purchase_amount * 0.9 + si.get("items")[0].rate = ( + asset.value_after_depreciation * 0.8 + if asset.value_after_depreciation + else asset.gross_purchase_amount * 0.9 + ) si.save() si.submit() def get_random_asset(): - return frappe.db.sql(""" select name, item_code, value_after_depreciation, gross_purchase_amount + return frappe.db.sql( + """ select name, item_code, value_after_depreciation, gross_purchase_amount from `tabAsset` - where docstatus=1 and status not in ("Scrapped", "Sold") order by rand() limit 1""", as_dict=1)[0] + where docstatus=1 and status not in ("Scrapped", "Sold") order by rand() limit 1""", + as_dict=1, + )[0] diff --git a/erpnext/demo/user/hr.py b/erpnext/demo/user/hr.py index 3d1a013956a..217d49a8064 100644 --- a/erpnext/demo/user/hr.py +++ b/erpnext/demo/user/hr.py @@ -1,4 +1,3 @@ - import datetime import random @@ -19,14 +18,16 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_ def work(): - frappe.set_user(frappe.db.get_global('demo_hr_user')) + frappe.set_user(frappe.db.get_global("demo_hr_user")) year, month = frappe.flags.current_date.strftime("%Y-%m").split("-") setup_department_approvers() mark_attendance() make_leave_application() # payroll entry - if not frappe.db.sql('select name from `tabSalary Slip` where month(adddate(start_date, interval 1 month))=month(curdate())'): + if not frappe.db.sql( + "select name from `tabSalary Slip` where month(adddate(start_date, interval 1 month))=month(curdate())" + ): # based on frequency payroll_entry = get_payroll_entry() payroll_entry.salary_slip_based_on_timesheet = 0 @@ -49,28 +50,28 @@ def work(): # payroll_entry.make_journal_entry(reference_date=frappe.flags.current_date, # reference_number=random_string(10)) - if frappe.db.get_global('demo_hr_user'): + if frappe.db.get_global("demo_hr_user"): make_timesheet_records() - #expense claim + # expense claim expense_claim = frappe.new_doc("Expense Claim") - expense_claim.extend('expenses', get_expenses()) + expense_claim.extend("expenses", get_expenses()) expense_claim.employee = get_random("Employee") expense_claim.company = frappe.flags.company expense_claim.payable_account = get_payable_account(expense_claim.company) expense_claim.posting_date = frappe.flags.current_date - expense_claim.expense_approver = frappe.db.get_global('demo_hr_user') + expense_claim.expense_approver = frappe.db.get_global("demo_hr_user") expense_claim.save() rand = random.random() if rand < 0.4: update_sanctioned_amount(expense_claim) - expense_claim.approval_status = 'Approved' + expense_claim.approval_status = "Approved" expense_claim.submit() if random.randint(0, 1): - #make journal entry against expense claim + # make journal entry against expense claim je = frappe.get_doc(make_bank_entry("Expense Claim", expense_claim.name)) je.posting_date = frappe.flags.current_date je.cheque_no = random_string(10) @@ -78,62 +79,91 @@ def work(): je.flags.ignore_permissions = 1 je.submit() + def get_payroll_entry(): # process payroll for previous month payroll_entry = frappe.new_doc("Payroll Entry") payroll_entry.company = frappe.flags.company - payroll_entry.payroll_frequency = 'Monthly' + payroll_entry.payroll_frequency = "Monthly" # select a posting date from the previous month - payroll_entry.posting_date = get_last_day(getdate(frappe.flags.current_date) - datetime.timedelta(days=10)) - payroll_entry.payment_account = frappe.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name") + payroll_entry.posting_date = get_last_day( + getdate(frappe.flags.current_date) - datetime.timedelta(days=10) + ) + payroll_entry.payment_account = frappe.get_value( + "Account", + {"account_type": "Cash", "company": erpnext.get_default_company(), "is_group": 0}, + "name", + ) payroll_entry.set_start_end_dates() return payroll_entry + def get_expenses(): expenses = [] - expese_types = frappe.db.sql("""select ect.name, eca.default_account from `tabExpense Claim Type` ect, + expese_types = frappe.db.sql( + """select ect.name, eca.default_account from `tabExpense Claim Type` ect, `tabExpense Claim Account` eca where eca.parent=ect.name - and eca.company=%s """, frappe.flags.company,as_dict=1) + and eca.company=%s """, + frappe.flags.company, + as_dict=1, + ) - for expense_type in expese_types[:random.randint(1,4)]: - claim_amount = random.randint(1,20)*10 + for expense_type in expese_types[: random.randint(1, 4)]: + claim_amount = random.randint(1, 20) * 10 - expenses.append({ - "expense_date": frappe.flags.current_date, - "expense_type": expense_type.name, - "default_account": expense_type.default_account or "Miscellaneous Expenses - WPL", - "amount": claim_amount, - "sanctioned_amount": claim_amount - }) + expenses.append( + { + "expense_date": frappe.flags.current_date, + "expense_type": expense_type.name, + "default_account": expense_type.default_account or "Miscellaneous Expenses - WPL", + "amount": claim_amount, + "sanctioned_amount": claim_amount, + } + ) return expenses + def update_sanctioned_amount(expense_claim): for expense in expense_claim.expenses: - sanctioned_amount = random.randint(1,20)*10 + sanctioned_amount = random.randint(1, 20) * 10 if sanctioned_amount < expense.amount: expense.sanctioned_amount = sanctioned_amount + def get_timesheet_based_salary_slip_employee(): - sal_struct = frappe.db.sql(""" + sal_struct = frappe.db.sql( + """ select name from `tabSalary Structure` where salary_slip_based_on_timesheet = 1 - and docstatus != 2""") + and docstatus != 2""" + ) if sal_struct: - employees = frappe.db.sql(""" + employees = frappe.db.sql( + """ select employee from `tabSalary Structure Assignment` - where salary_structure IN %(sal_struct)s""", {"sal_struct": sal_struct}, as_dict=True) + where salary_structure IN %(sal_struct)s""", + {"sal_struct": sal_struct}, + as_dict=True, + ) return employees else: return [] + def make_timesheet_records(): employees = get_timesheet_based_salary_slip_employee() for e in employees: - ts = make_timesheet(e.employee, simulate = True, billable = 1, activity_type=get_random("Activity Type"), company=frappe.flags.company) + ts = make_timesheet( + e.employee, + simulate=True, + billable=1, + activity_type=get_random("Activity Type"), + company=frappe.flags.company, + ) frappe.db.commit() rand = random.random() @@ -144,21 +174,25 @@ def make_timesheet_records(): if rand >= 0.2: make_sales_invoice_for_timesheet(ts.name) + def make_salary_slip_for_timesheet(name): salary_slip = make_salary_slip(name) salary_slip.insert() salary_slip.submit() frappe.db.commit() + def make_sales_invoice_for_timesheet(name): sales_invoice = make_sales_invoice(name) sales_invoice.customer = get_random("Customer") - sales_invoice.append('items', { - 'item_code': get_random("Item", {"has_variants": 0, "is_stock_item": 0, - "is_fixed_asset": 0}), - 'qty': 1, - 'rate': 1000 - }) + sales_invoice.append( + "items", + { + "item_code": get_random("Item", {"has_variants": 0, "is_stock_item": 0, "is_fixed_asset": 0}), + "qty": 1, + "rate": 1000, + }, + ) sales_invoice.flags.ignore_permissions = 1 sales_invoice.set_missing_values() sales_invoice.calculate_taxes_and_totals() @@ -166,25 +200,32 @@ def make_sales_invoice_for_timesheet(name): sales_invoice.submit() frappe.db.commit() + def make_leave_application(): - allocated_leaves = frappe.get_all("Leave Allocation", fields=['employee', 'leave_type']) + allocated_leaves = frappe.get_all("Leave Allocation", fields=["employee", "leave_type"]) for allocated_leave in allocated_leaves: - leave_balance = get_leave_balance_on(allocated_leave.employee, allocated_leave.leave_type, frappe.flags.current_date, - consider_all_leaves_in_the_allocation_period=True) + leave_balance = get_leave_balance_on( + allocated_leave.employee, + allocated_leave.leave_type, + frappe.flags.current_date, + consider_all_leaves_in_the_allocation_period=True, + ) if leave_balance != 0: if leave_balance == 1: to_date = frappe.flags.current_date else: - to_date = add_days(frappe.flags.current_date, random.randint(0, leave_balance-1)) + to_date = add_days(frappe.flags.current_date, random.randint(0, leave_balance - 1)) - leave_application = frappe.get_doc({ - "doctype": "Leave Application", - "employee": allocated_leave.employee, - "from_date": frappe.flags.current_date, - "to_date": to_date, - "leave_type": allocated_leave.leave_type, - }) + leave_application = frappe.get_doc( + { + "doctype": "Leave Application", + "employee": allocated_leave.employee, + "from_date": frappe.flags.current_date, + "to_date": to_date, + "leave_type": allocated_leave.leave_type, + } + ) try: leave_application.insert() leave_application.submit() @@ -192,20 +233,24 @@ def make_leave_application(): except (OverlapError, AttendanceAlreadyMarkedError): frappe.db.rollback() + def mark_attendance(): attendance_date = frappe.flags.current_date - for employee in frappe.get_all('Employee', fields=['name'], filters = {'status': 'Active'}): + for employee in frappe.get_all("Employee", fields=["name"], filters={"status": "Active"}): - if not frappe.db.get_value("Attendance", {"employee": employee.name, "attendance_date": attendance_date}): - attendance = frappe.get_doc({ - "doctype": "Attendance", - "employee": employee.name, - "attendance_date": attendance_date - }) + if not frappe.db.get_value( + "Attendance", {"employee": employee.name, "attendance_date": attendance_date} + ): + attendance = frappe.get_doc( + {"doctype": "Attendance", "employee": employee.name, "attendance_date": attendance_date} + ) - leave = frappe.db.sql("""select name from `tabLeave Application` + leave = frappe.db.sql( + """select name from `tabLeave Application` where employee = %s and %s between from_date and to_date - and docstatus = 1""", (employee.name, attendance_date)) + and docstatus = 1""", + (employee.name, attendance_date), + ) if leave: attendance.status = "Absent" @@ -215,10 +260,11 @@ def mark_attendance(): attendance.submit() frappe.db.commit() + def setup_department_approvers(): - for d in frappe.get_all('Department', filters={'department_name': ['!=', 'All Departments']}): - doc = frappe.get_doc('Department', d.name) - doc.append("leave_approvers", {'approver': frappe.session.user}) - doc.append("expense_approvers", {'approver': frappe.session.user}) + for d in frappe.get_all("Department", filters={"department_name": ["!=", "All Departments"]}): + doc = frappe.get_doc("Department", d.name) + doc.append("leave_approvers", {"approver": frappe.session.user}) + doc.append("expense_approvers", {"approver": frappe.session.user}) doc.flags.ignore_mandatory = True doc.save() diff --git a/erpnext/demo/user/manufacturing.py b/erpnext/demo/user/manufacturing.py index 6b617761719..3f126b88ad4 100644 --- a/erpnext/demo/user/manufacturing.py +++ b/erpnext/demo/user/manufacturing.py @@ -14,10 +14,12 @@ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_ord def work(): - if random.random() < 0.3: return + if random.random() < 0.3: + return - frappe.set_user(frappe.db.get_global('demo_manufacturing_user')) - if not frappe.get_all('Sales Order'): return + frappe.set_user(frappe.db.get_global("demo_manufacturing_user")) + if not frappe.get_all("Sales Order"): + return ppt = frappe.new_doc("Production Plan") ppt.company = erpnext.get_default_company() @@ -25,7 +27,8 @@ def work(): ppt.get_items_from = "Sales Order" # ppt.purchase_request_for_warehouse = "Stores - WPL" # refactored ppt.run_method("get_open_sales_orders") - if not ppt.get("sales_orders"): return + if not ppt.get("sales_orders"): + return ppt.run_method("get_items") ppt.run_method("raise_material_requests") ppt.save() @@ -48,25 +51,33 @@ def work(): # stores -> wip if random.random() < 0.4: - for pro in query_report.run("Open Work Orders")["result"][:how_many("Stock Entry for WIP")]: + for pro in query_report.run("Open Work Orders")["result"][: how_many("Stock Entry for WIP")]: make_stock_entry_from_pro(pro[0], "Material Transfer for Manufacture") # wip -> fg if random.random() < 0.4: - for pro in query_report.run("Work Orders in Progress")["result"][:how_many("Stock Entry for FG")]: + for pro in query_report.run("Work Orders in Progress")["result"][ + : how_many("Stock Entry for FG") + ]: make_stock_entry_from_pro(pro[0], "Manufacture") - for bom in frappe.get_all('BOM', fields=['item'], filters = {'with_operations': 1}): - pro_order = make_wo_order_test_record(item=bom.item, qty=2, - source_warehouse="Stores - WPL", wip_warehouse = "Work in Progress - WPL", - fg_warehouse = "Stores - WPL", company = erpnext.get_default_company(), - stock_uom = frappe.db.get_value('Item', bom.item, 'stock_uom'), - planned_start_date = frappe.flags.current_date) + for bom in frappe.get_all("BOM", fields=["item"], filters={"with_operations": 1}): + pro_order = make_wo_order_test_record( + item=bom.item, + qty=2, + source_warehouse="Stores - WPL", + wip_warehouse="Work in Progress - WPL", + fg_warehouse="Stores - WPL", + company=erpnext.get_default_company(), + stock_uom=frappe.db.get_value("Item", bom.item, "stock_uom"), + planned_start_date=frappe.flags.current_date, + ) # submit job card if random.random() < 0.4: submit_job_cards() + def make_stock_entry_from_pro(pro_id, purpose): from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry import ( @@ -81,25 +92,34 @@ def make_stock_entry_from_pro(pro_id, purpose): st.posting_date = frappe.flags.current_date st.fiscal_year = str(frappe.flags.current_date.year) for d in st.get("items"): - d.cost_center = "Main - " + frappe.get_cached_value('Company', st.company, 'abbr') + d.cost_center = "Main - " + frappe.get_cached_value("Company", st.company, "abbr") st.insert() frappe.db.commit() st.submit() frappe.db.commit() - except (NegativeStockError, IncorrectValuationRateError, DuplicateEntryForWorkOrderError, - OperationsNotCompleteError): + except ( + NegativeStockError, + IncorrectValuationRateError, + DuplicateEntryForWorkOrderError, + OperationsNotCompleteError, + ): frappe.db.rollback() + def submit_job_cards(): - work_orders = frappe.get_all("Work Order", ["name", "creation"], {"docstatus": 1, "status": "Not Started"}) + work_orders = frappe.get_all( + "Work Order", ["name", "creation"], {"docstatus": 1, "status": "Not Started"} + ) work_order = random.choice(work_orders) # for work_order in work_orders: start_date = work_order.creation work_order = frappe.get_doc("Work Order", work_order.name) - job = frappe.get_all("Job Card", ["name", "operation", "work_order"], - {"docstatus": 0, "work_order": work_order.name}) + job = frappe.get_all( + "Job Card", ["name", "operation", "work_order"], {"docstatus": 0, "work_order": work_order.name} + ) - if not job: return + if not job: + return job_map = {} for d in job: job_map[d.operation] = frappe.get_doc("Job Card", d.name) @@ -109,12 +129,11 @@ def submit_job_cards(): job_time_log = frappe.new_doc("Job Card Time Log") job_time_log.from_time = start_date minutes = operation.get("time_in_mins") - job_time_log.time_in_mins = random.randint(int(minutes/2), minutes) - job_time_log.to_time = job_time_log.from_time + \ - timedelta(minutes=job_time_log.time_in_mins) + job_time_log.time_in_mins = random.randint(int(minutes / 2), minutes) + job_time_log.to_time = job_time_log.from_time + timedelta(minutes=job_time_log.time_in_mins) job_time_log.parent = job.name - job_time_log.parenttype = 'Job Card' - job_time_log.parentfield = 'time_logs' + job_time_log.parenttype = "Job Card" + job_time_log.parentfield = "time_logs" job_time_log.completed_qty = work_order.qty job_time_log.save(ignore_permissions=True) job.time_logs.append(job_time_log) diff --git a/erpnext/demo/user/projects.py b/erpnext/demo/user/projects.py index 1203be44084..0dc9229bcd0 100644 --- a/erpnext/demo/user/projects.py +++ b/erpnext/demo/user/projects.py @@ -12,33 +12,50 @@ from erpnext.projects.doctype.timesheet.test_timesheet import make_timesheet def run_projects(current_date): - frappe.set_user(frappe.db.get_global('demo_projects_user')) - if frappe.db.get_global('demo_projects_user'): + frappe.set_user(frappe.db.get_global("demo_projects_user")) + if frappe.db.get_global("demo_projects_user"): make_project(current_date) make_timesheet_for_projects(current_date) close_tasks(current_date) -def make_timesheet_for_projects(current_date ): - for data in frappe.get_all("Task", ["name", "project"], {"status": "Open", "exp_end_date": ("<", current_date)}): + +def make_timesheet_for_projects(current_date): + for data in frappe.get_all( + "Task", ["name", "project"], {"status": "Open", "exp_end_date": ("<", current_date)} + ): employee = get_random("Employee") - ts = make_timesheet(employee, simulate = True, billable = 1, company = erpnext.get_default_company(), - activity_type=get_random("Activity Type"), project=data.project, task =data.name) + ts = make_timesheet( + employee, + simulate=True, + billable=1, + company=erpnext.get_default_company(), + activity_type=get_random("Activity Type"), + project=data.project, + task=data.name, + ) if flt(ts.total_billable_amount) > 0.0: make_sales_invoice_for_timesheet(ts.name) frappe.db.commit() + def close_tasks(current_date): - for task in frappe.get_all("Task", ["name"], {"status": "Open", "exp_end_date": ("<", current_date)}): + for task in frappe.get_all( + "Task", ["name"], {"status": "Open", "exp_end_date": ("<", current_date)} + ): task = frappe.get_doc("Task", task.name) task.status = "Completed" task.save() + def make_project(current_date): - if not frappe.db.exists('Project', - "New Product Development " + current_date.strftime("%Y-%m-%d")): - project = frappe.get_doc({ - "doctype": "Project", - "project_name": "New Product Development " + current_date.strftime("%Y-%m-%d"), - }) + if not frappe.db.exists( + "Project", "New Product Development " + current_date.strftime("%Y-%m-%d") + ): + project = frappe.get_doc( + { + "doctype": "Project", + "project_name": "New Product Development " + current_date.strftime("%Y-%m-%d"), + } + ) project.insert() diff --git a/erpnext/demo/user/purchase.py b/erpnext/demo/user/purchase.py index 61f081c26f9..934926ad4e7 100644 --- a/erpnext/demo/user/purchase.py +++ b/erpnext/demo/user/purchase.py @@ -20,21 +20,22 @@ from erpnext.stock.doctype.material_request.material_request import make_request def work(): - frappe.set_user(frappe.db.get_global('demo_purchase_user')) + frappe.set_user(frappe.db.get_global("demo_purchase_user")) if random.random() < 0.6: report = "Items To Be Requested" - for row in query_report.run(report)["result"][:random.randint(1, 5)]: + for row in query_report.run(report)["result"][: random.randint(1, 5)]: item_code, qty = row[0], abs(row[-1]) mr = make_material_request(item_code, qty) if random.random() < 0.6: - for mr in frappe.get_all('Material Request', - filters={'material_request_type': 'Purchase', 'status': 'Open'}, - limit=random.randint(1,6)): - if not frappe.get_all('Request for Quotation', - filters={'material_request': mr.name}, limit=1): + for mr in frappe.get_all( + "Material Request", + filters={"material_request_type": "Purchase", "status": "Open"}, + limit=random.randint(1, 6), + ): + if not frappe.get_all("Request for Quotation", filters={"material_request": mr.name}, limit=1): rfq = make_request_for_quotation(mr.name) rfq.transaction_date = frappe.flags.current_date add_suppliers(rfq) @@ -43,22 +44,30 @@ def work(): # Make suppier quotation from RFQ against each supplier. if random.random() < 0.6: - for rfq in frappe.get_all('Request for Quotation', - filters={'status': 'Open'}, limit=random.randint(1, 6)): - if not frappe.get_all('Supplier Quotation', - filters={'request_for_quotation': rfq.name}, limit=1): - rfq = frappe.get_doc('Request for Quotation', rfq.name) + for rfq in frappe.get_all( + "Request for Quotation", filters={"status": "Open"}, limit=random.randint(1, 6) + ): + if not frappe.get_all( + "Supplier Quotation", filters={"request_for_quotation": rfq.name}, limit=1 + ): + rfq = frappe.get_doc("Request for Quotation", rfq.name) for supplier in rfq.suppliers: - supplier_quotation = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier.supplier) + supplier_quotation = make_supplier_quotation_from_rfq( + rfq.name, for_supplier=supplier.supplier + ) supplier_quotation.save() supplier_quotation.submit() # get supplier details supplier = get_random("Supplier") - company_currency = frappe.get_cached_value('Company', erpnext.get_default_company(), "default_currency") - party_account_currency = get_party_account_currency("Supplier", supplier, erpnext.get_default_company()) + company_currency = frappe.get_cached_value( + "Company", erpnext.get_default_company(), "default_currency" + ) + party_account_currency = get_party_account_currency( + "Supplier", supplier, erpnext.get_default_company() + ) if company_currency == party_account_currency: exchange_rate = 1 else: @@ -69,7 +78,7 @@ def work(): from erpnext.stock.doctype.material_request.material_request import make_supplier_quotation report = "Material Requests for which Supplier Quotations are not created" - for row in query_report.run(report)["result"][:random.randint(1, 3)]: + for row in query_report.run(report)["result"][: random.randint(1, 3)]: if row[0] != "Total": sq = frappe.get_doc(make_supplier_quotation(row[0])) sq.transaction_date = frappe.flags.current_date @@ -83,8 +92,9 @@ def work(): # make purchase orders if random.random() < 0.5: from erpnext.stock.doctype.material_request.material_request import make_purchase_order + report = "Requested Items To Be Ordered" - for row in query_report.run(report)["result"][:how_many("Purchase Order")]: + for row in query_report.run(report)["result"][: how_many("Purchase Order")]: if row[0] != "Total": try: po = frappe.get_doc(make_purchase_order(row[0])) @@ -102,53 +112,63 @@ def work(): if random.random() < 0.5: make_subcontract() + def make_material_request(item_code, qty): mr = frappe.new_doc("Material Request") - variant_of = frappe.db.get_value('Item', item_code, 'variant_of') or item_code + variant_of = frappe.db.get_value("Item", item_code, "variant_of") or item_code - if frappe.db.get_value('BOM', {'item': variant_of, 'is_default': 1, 'is_active': 1}): - mr.material_request_type = 'Manufacture' + if frappe.db.get_value("BOM", {"item": variant_of, "is_default": 1, "is_active": 1}): + mr.material_request_type = "Manufacture" else: mr.material_request_type = "Purchase" mr.transaction_date = frappe.flags.current_date mr.schedule_date = frappe.utils.add_days(mr.transaction_date, 7) - mr.append("items", { - "doctype": "Material Request Item", - "schedule_date": frappe.utils.add_days(mr.transaction_date, 7), - "item_code": item_code, - "qty": qty - }) + mr.append( + "items", + { + "doctype": "Material Request Item", + "schedule_date": frappe.utils.add_days(mr.transaction_date, 7), + "item_code": item_code, + "qty": qty, + }, + ) mr.insert() mr.submit() return mr + def add_suppliers(rfq): for i in range(2): supplier = get_random("Supplier") - if supplier not in [d.supplier for d in rfq.get('suppliers')]: - rfq.append("suppliers", { "supplier": supplier }) + if supplier not in [d.supplier for d in rfq.get("suppliers")]: + rfq.append("suppliers", {"supplier": supplier}) + def make_subcontract(): from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry + item_code = get_random("Item", {"is_sub_contracted_item": 1}) if item_code: # make sub-contract PO po = frappe.new_doc("Purchase Order") po.is_subcontracted = "Yes" po.supplier = get_random("Supplier") - po.transaction_date = frappe.flags.current_date # added + po.transaction_date = frappe.flags.current_date # added po.schedule_date = frappe.utils.add_days(frappe.flags.current_date, 7) item_code = get_random("Item", {"is_sub_contracted_item": 1}) - po.append("items", { - "item_code": item_code, - "schedule_date": frappe.utils.add_days(frappe.flags.current_date, 7), - "qty": random.randint(10, 30) - }) + po.append( + "items", + { + "item_code": item_code, + "schedule_date": frappe.utils.add_days(frappe.flags.current_date, 7), + "qty": random.randint(10, 30), + }, + ) po.set_missing_values() try: po.insert() @@ -167,14 +187,15 @@ def make_subcontract(): stock_entry.to_warehouse = "Supplier - WPL" stock_entry.insert() + def get_rm_item(items, supplied_items): return { "item_code": items.get("item_code"), "rm_item_code": supplied_items.get("rm_item_code"), "item_name": supplied_items.get("rm_item_code"), - "qty": supplied_items.get("required_qty") + random.randint(3,10), + "qty": supplied_items.get("required_qty") + random.randint(3, 10), "amount": supplied_items.get("amount"), "warehouse": supplied_items.get("reserve_warehouse"), "rate": supplied_items.get("rate"), - "stock_uom": supplied_items.get("stock_uom") + "stock_uom": supplied_items.get("stock_uom"), } diff --git a/erpnext/demo/user/sales.py b/erpnext/demo/user/sales.py index ef6e4c42cd8..085fb508e0f 100644 --- a/erpnext/demo/user/sales.py +++ b/erpnext/demo/user/sales.py @@ -18,50 +18,55 @@ from erpnext.setup.utils import get_exchange_rate def work(domain="Manufacturing"): - frappe.set_user(frappe.db.get_global('demo_sales_user_2')) + frappe.set_user(frappe.db.get_global("demo_sales_user_2")) - for i in range(random.randint(1,7)): + for i in range(random.randint(1, 7)): if random.random() < 0.5: make_opportunity(domain) - for i in range(random.randint(1,3)): + for i in range(random.randint(1, 3)): if random.random() < 0.5: make_quotation(domain) try: - lost_reason = frappe.get_doc({ - "doctype": "Opportunity Lost Reason", - "lost_reason": "Did not ask" - }) + lost_reason = frappe.get_doc( + {"doctype": "Opportunity Lost Reason", "lost_reason": "Did not ask"} + ) lost_reason.save(ignore_permissions=True) except frappe.exceptions.DuplicateEntryError: pass # lost quotations / inquiries if random.random() < 0.3: - for i in range(random.randint(1,3)): - quotation = get_random('Quotation', doc=True) - if quotation and quotation.status == 'Submitted': - quotation.declare_order_lost([{'lost_reason': 'Did not ask'}]) + for i in range(random.randint(1, 3)): + quotation = get_random("Quotation", doc=True) + if quotation and quotation.status == "Submitted": + quotation.declare_order_lost([{"lost_reason": "Did not ask"}]) - for i in range(random.randint(1,3)): - opportunity = get_random('Opportunity', doc=True) - if opportunity and opportunity.status in ('Open', 'Replied'): - opportunity.declare_enquiry_lost([{'lost_reason': 'Did not ask'}]) + for i in range(random.randint(1, 3)): + opportunity = get_random("Opportunity", doc=True) + if opportunity and opportunity.status in ("Open", "Replied"): + opportunity.declare_enquiry_lost([{"lost_reason": "Did not ask"}]) - for i in range(random.randint(1,3)): + for i in range(random.randint(1, 3)): if random.random() < 0.6: make_sales_order() if random.random() < 0.5: - #make payment request against Sales Order + # make payment request against Sales Order sales_order_name = get_random("Sales Order", filters={"docstatus": 1}) try: if sales_order_name: so = frappe.get_doc("Sales Order", sales_order_name) if flt(so.per_billed) != 100: - payment_request = make_payment_request(dt="Sales Order", dn=so.name, recipient_id=so.contact_email, - submit_doc=True, mute_email=True, use_dummy_message=True) + payment_request = make_payment_request( + dt="Sales Order", + dn=so.name, + recipient_id=so.contact_email, + submit_doc=True, + mute_email=True, + use_dummy_message=True, + ) payment_entry = frappe.get_doc(make_payment_entry(payment_request.name)) payment_entry.posting_date = frappe.flags.current_date @@ -69,30 +74,41 @@ def work(domain="Manufacturing"): except Exception: pass -def make_opportunity(domain): - b = frappe.get_doc({ - "doctype": "Opportunity", - "opportunity_from": "Customer", - "party_name": frappe.get_value("Customer", get_random("Customer"), 'name'), - "opportunity_type": "Sales", - "with_items": 1, - "transaction_date": frappe.flags.current_date, - }) - add_random_children(b, "items", rows=4, randomize = { - "qty": (1, 5), - "item_code": ("Item", {"has_variants": 0, "is_fixed_asset": 0, "domain": domain}) - }, unique="item_code") +def make_opportunity(domain): + b = frappe.get_doc( + { + "doctype": "Opportunity", + "opportunity_from": "Customer", + "party_name": frappe.get_value("Customer", get_random("Customer"), "name"), + "opportunity_type": "Sales", + "with_items": 1, + "transaction_date": frappe.flags.current_date, + } + ) + + add_random_children( + b, + "items", + rows=4, + randomize={ + "qty": (1, 5), + "item_code": ("Item", {"has_variants": 0, "is_fixed_asset": 0, "domain": domain}), + }, + unique="item_code", + ) b.insert() frappe.db.commit() + def make_quotation(domain): # get open opportunites opportunity = get_random("Opportunity", {"status": "Open", "with_items": 1}) if opportunity: from erpnext.crm.doctype.opportunity.opportunity import make_quotation + qtn = frappe.get_doc(make_quotation(opportunity)) qtn.insert() frappe.db.commit() @@ -104,38 +120,52 @@ def make_quotation(domain): # get customer, currency and exchange_rate customer = get_random("Customer") - company_currency = frappe.get_cached_value('Company', erpnext.get_default_company(), "default_currency") - party_account_currency = get_party_account_currency("Customer", customer, erpnext.get_default_company()) + company_currency = frappe.get_cached_value( + "Company", erpnext.get_default_company(), "default_currency" + ) + party_account_currency = get_party_account_currency( + "Customer", customer, erpnext.get_default_company() + ) if company_currency == party_account_currency: exchange_rate = 1 else: exchange_rate = get_exchange_rate(party_account_currency, company_currency, args="for_selling") - qtn = frappe.get_doc({ - "creation": frappe.flags.current_date, - "doctype": "Quotation", - "quotation_to": "Customer", - "party_name": customer, - "currency": party_account_currency or company_currency, - "conversion_rate": exchange_rate, - "order_type": "Sales", - "transaction_date": frappe.flags.current_date, - }) + qtn = frappe.get_doc( + { + "creation": frappe.flags.current_date, + "doctype": "Quotation", + "quotation_to": "Customer", + "party_name": customer, + "currency": party_account_currency or company_currency, + "conversion_rate": exchange_rate, + "order_type": "Sales", + "transaction_date": frappe.flags.current_date, + } + ) - add_random_children(qtn, "items", rows=3, randomize = { - "qty": (1, 5), - "item_code": ("Item", {"has_variants": "0", "is_fixed_asset": 0, "domain": domain}) - }, unique="item_code") + add_random_children( + qtn, + "items", + rows=3, + randomize={ + "qty": (1, 5), + "item_code": ("Item", {"has_variants": "0", "is_fixed_asset": 0, "domain": domain}), + }, + unique="item_code", + ) qtn.insert() frappe.db.commit() qtn.submit() frappe.db.commit() + def make_sales_order(): q = get_random("Quotation", {"status": "Submitted"}) if q: from erpnext.selling.doctype.quotation.quotation import make_sales_order as mso + so = frappe.get_doc(mso(q)) so.transaction_date = frappe.flags.current_date so.delivery_date = frappe.utils.add_days(frappe.flags.current_date, 10) diff --git a/erpnext/demo/user/stock.py b/erpnext/demo/user/stock.py index de379753b3d..6e4584c3e1e 100644 --- a/erpnext/demo/user/stock.py +++ b/erpnext/demo/user/stock.py @@ -16,7 +16,7 @@ from erpnext.stock.stock_ledger import NegativeStockError def work(): - frappe.set_user(frappe.db.get_global('demo_manufacturing_user')) + frappe.set_user(frappe.db.get_global("demo_manufacturing_user")) make_purchase_receipt() make_delivery_note() @@ -25,15 +25,19 @@ def work(): make_sales_return_records() make_purchase_return_records() + def make_purchase_receipt(): if random.random() < 0.6: from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + report = "Purchase Order Items To Be Received" - po_list =list(set([r[0] for r in query_report.run(report)["result"] if r[0]!="Total"]))[:random.randint(1, 10)] + po_list = list(set([r[0] for r in query_report.run(report)["result"] if r[0] != "Total"]))[ + : random.randint(1, 10) + ] for po in po_list: pr = frappe.get_doc(make_purchase_receipt(po)) - if pr.is_subcontracted=="Yes": + if pr.is_subcontracted == "Yes": pr.supplier_warehouse = "Supplier - WPL" pr.posting_date = frappe.flags.current_date @@ -41,25 +45,29 @@ def make_purchase_receipt(): try: pr.submit() except NegativeStockError: - print('Negative stock for {0}'.format(po)) + print("Negative stock for {0}".format(po)) pass frappe.db.commit() + def make_delivery_note(): # make purchase requests # make delivery notes (if possible) if random.random() < 0.6: from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + report = "Ordered Items To Be Delivered" - for so in list(set([r[0] for r in query_report.run(report)["result"] - if r[0]!="Total"]))[:random.randint(1, 3)]: + for so in list(set([r[0] for r in query_report.run(report)["result"] if r[0] != "Total"]))[ + : random.randint(1, 3) + ]: dn = frappe.get_doc(make_delivery_note(so)) dn.posting_date = frappe.flags.current_date for d in dn.get("items"): if not d.expense_account: - d.expense_account = ("Cost of Goods Sold - {0}".format( - frappe.get_cached_value('Company', dn.company, 'abbr'))) + d.expense_account = "Cost of Goods Sold - {0}".format( + frappe.get_cached_value("Company", dn.company, "abbr") + ) try: dn.insert() @@ -68,6 +76,7 @@ def make_delivery_note(): except (NegativeStockError, SerialNoRequiredError, SerialNoQtyError, UnableToSelectBatchError): frappe.db.rollback() + def make_stock_reconciliation(): # random set some items as damaged from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( @@ -93,6 +102,7 @@ def make_stock_reconciliation(): except EmptyStockReconciliationItemsError: frappe.db.rollback() + def submit_draft_stock_entries(): from erpnext.stock.doctype.stock_entry.stock_entry import ( DuplicateEntryForWorkOrderError, @@ -102,20 +112,25 @@ def submit_draft_stock_entries(): # try posting older drafts (if exists) frappe.db.commit() - for st in frappe.db.get_values("Stock Entry", {"docstatus":0}, "name"): + for st in frappe.db.get_values("Stock Entry", {"docstatus": 0}, "name"): try: ste = frappe.get_doc("Stock Entry", st[0]) ste.posting_date = frappe.flags.current_date ste.save() ste.submit() frappe.db.commit() - except (NegativeStockError, IncorrectValuationRateError, DuplicateEntryForWorkOrderError, - OperationsNotCompleteError): + except ( + NegativeStockError, + IncorrectValuationRateError, + DuplicateEntryForWorkOrderError, + OperationsNotCompleteError, + ): frappe.db.rollback() + def make_sales_return_records(): if random.random() < 0.1: - for data in frappe.get_all('Delivery Note', fields=["name"], filters={"docstatus": 1}): + for data in frappe.get_all("Delivery Note", fields=["name"], filters={"docstatus": 1}): if random.random() < 0.1: try: dn = make_sales_return(data.name) @@ -125,9 +140,10 @@ def make_sales_return_records(): except Exception: frappe.db.rollback() + def make_purchase_return_records(): if random.random() < 0.1: - for data in frappe.get_all('Purchase Receipt', fields=["name"], filters={"docstatus": 1}): + for data in frappe.get_all("Purchase Receipt", fields=["name"], filters={"docstatus": 1}): if random.random() < 0.1: try: pr = make_purchase_return(data.name) diff --git a/erpnext/domains/agriculture.py b/erpnext/domains/agriculture.py index de27a7a2c24..a8110db6918 100644 --- a/erpnext/domains/agriculture.py +++ b/erpnext/domains/agriculture.py @@ -1,27 +1,21 @@ - data = { - 'desktop_icons': [ - 'Agriculture Task', - 'Crop', - 'Crop Cycle', - 'Fertilizer', - 'Item', - 'Location', - 'Disease', - 'Plant Analysis', - 'Soil Analysis', - 'Soil Texture', - 'Task', - 'Water Analysis', - 'Weather' + "desktop_icons": [ + "Agriculture Task", + "Crop", + "Crop Cycle", + "Fertilizer", + "Item", + "Location", + "Disease", + "Plant Analysis", + "Soil Analysis", + "Soil Texture", + "Task", + "Water Analysis", + "Weather", ], - 'restricted_roles': [ - 'Agriculture Manager', - 'Agriculture User' - ], - 'modules': [ - 'Agriculture' - ], - 'default_portal_role': 'System Manager', - 'on_setup': 'erpnext.agriculture.setup.setup_agriculture' + "restricted_roles": ["Agriculture Manager", "Agriculture User"], + "modules": ["Agriculture"], + "default_portal_role": "System Manager", + "on_setup": "erpnext.agriculture.setup.setup_agriculture", } diff --git a/erpnext/domains/distribution.py b/erpnext/domains/distribution.py index 68ac0c3ec5e..5953c4e2a42 100644 --- a/erpnext/domains/distribution.py +++ b/erpnext/domains/distribution.py @@ -1,19 +1,16 @@ - data = { - 'desktop_icons': [ - 'Item', - 'Customer', - 'Supplier', - 'Lead', - 'Sales Order', - 'Purchase Order', - 'Task', - 'Sales Invoice', - 'CRM', - 'ToDo' + "desktop_icons": [ + "Item", + "Customer", + "Supplier", + "Lead", + "Sales Order", + "Purchase Order", + "Task", + "Sales Invoice", + "CRM", + "ToDo", ], - 'set_value': [ - ['Stock Settings', None, 'show_barcode_field', 1] - ], - 'default_portal_role': 'Customer' + "set_value": [["Stock Settings", None, "show_barcode_field", 1]], + "default_portal_role": "Customer", } diff --git a/erpnext/domains/education.py b/erpnext/domains/education.py index d0e597e7b7f..23b8258baf6 100644 --- a/erpnext/domains/education.py +++ b/erpnext/domains/education.py @@ -1,28 +1,19 @@ - data = { - 'desktop_icons': [ - 'Student', - 'Program', - 'Course', - 'Student Group', - 'Instructor', - 'Fees', - 'Task', - 'ToDo', - 'Education', - 'Student Attendance Tool', - 'Student Applicant' + "desktop_icons": [ + "Student", + "Program", + "Course", + "Student Group", + "Instructor", + "Fees", + "Task", + "ToDo", + "Education", + "Student Attendance Tool", + "Student Applicant", ], - 'default_portal_role': 'Student', - 'restricted_roles': [ - 'Student', - 'Instructor', - 'Academics User', - 'Education Manager' - ], - 'modules': [ - 'Education' - ], - 'on_setup': 'erpnext.education.setup.setup_education' - + "default_portal_role": "Student", + "restricted_roles": ["Student", "Instructor", "Academics User", "Education Manager"], + "modules": ["Education"], + "on_setup": "erpnext.education.setup.setup_education", } diff --git a/erpnext/domains/healthcare.py b/erpnext/domains/healthcare.py index 301c133351e..97b9602a5df 100644 --- a/erpnext/domains/healthcare.py +++ b/erpnext/domains/healthcare.py @@ -1,70 +1,96 @@ - data = { - 'desktop_icons': [ - 'Patient', - 'Patient Appointment', - 'Patient Encounter', - 'Lab Test', - 'Healthcare', - 'Vital Signs', - 'Clinical Procedure', - 'Inpatient Record', - 'Accounts', - 'Buying', - 'Stock', - 'HR', - 'ToDo' + "desktop_icons": [ + "Patient", + "Patient Appointment", + "Patient Encounter", + "Lab Test", + "Healthcare", + "Vital Signs", + "Clinical Procedure", + "Inpatient Record", + "Accounts", + "Buying", + "Stock", + "HR", + "ToDo", ], - 'default_portal_role': 'Patient', - 'restricted_roles': [ - 'Healthcare Administrator', - 'LabTest Approver', - 'Laboratory User', - 'Nursing User', - 'Physician', - 'Patient' + "default_portal_role": "Patient", + "restricted_roles": [ + "Healthcare Administrator", + "LabTest Approver", + "Laboratory User", + "Nursing User", + "Physician", + "Patient", ], - 'custom_fields': { - 'Sales Invoice': [ + "custom_fields": { + "Sales Invoice": [ { - 'fieldname': 'patient', 'label': 'Patient', 'fieldtype': 'Link', 'options': 'Patient', - 'insert_after': 'naming_series' + "fieldname": "patient", + "label": "Patient", + "fieldtype": "Link", + "options": "Patient", + "insert_after": "naming_series", }, { - 'fieldname': 'patient_name', 'label': 'Patient Name', 'fieldtype': 'Data', 'fetch_from': 'patient.patient_name', - 'insert_after': 'patient', 'read_only': True + "fieldname": "patient_name", + "label": "Patient Name", + "fieldtype": "Data", + "fetch_from": "patient.patient_name", + "insert_after": "patient", + "read_only": True, }, { - 'fieldname': 'ref_practitioner', 'label': 'Referring Practitioner', 'fieldtype': 'Link', 'options': 'Healthcare Practitioner', - 'insert_after': 'customer' + "fieldname": "ref_practitioner", + "label": "Referring Practitioner", + "fieldtype": "Link", + "options": "Healthcare Practitioner", + "insert_after": "customer", + }, + ], + "Sales Invoice Item": [ + { + "fieldname": "reference_dt", + "label": "Reference DocType", + "fieldtype": "Link", + "options": "DocType", + "insert_after": "edit_references", + }, + { + "fieldname": "reference_dn", + "label": "Reference Name", + "fieldtype": "Dynamic Link", + "options": "reference_dt", + "insert_after": "reference_dt", + }, + ], + "Stock Entry": [ + { + "fieldname": "inpatient_medication_entry", + "label": "Inpatient Medication Entry", + "fieldtype": "Link", + "options": "Inpatient Medication Entry", + "insert_after": "credit_note", + "read_only": True, } ], - 'Sales Invoice Item': [ + "Stock Entry Detail": [ { - 'fieldname': 'reference_dt', 'label': 'Reference DocType', 'fieldtype': 'Link', 'options': 'DocType', - 'insert_after': 'edit_references' + "fieldname": "patient", + "label": "Patient", + "fieldtype": "Link", + "options": "Patient", + "insert_after": "po_detail", + "read_only": True, }, { - 'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': 'reference_dt', - 'insert_after': 'reference_dt' - } - ], - 'Stock Entry': [ - { - 'fieldname': 'inpatient_medication_entry', 'label': 'Inpatient Medication Entry', 'fieldtype': 'Link', 'options': 'Inpatient Medication Entry', - 'insert_after': 'credit_note', 'read_only': True - } - ], - 'Stock Entry Detail': [ - { - 'fieldname': 'patient', 'label': 'Patient', 'fieldtype': 'Link', 'options': 'Patient', - 'insert_after': 'po_detail', 'read_only': True + "fieldname": "inpatient_medication_entry_child", + "label": "Inpatient Medication Entry Child", + "fieldtype": "Data", + "insert_after": "patient", + "read_only": True, }, - { - 'fieldname': 'inpatient_medication_entry_child', 'label': 'Inpatient Medication Entry Child', 'fieldtype': 'Data', - 'insert_after': 'patient', 'read_only': True - } - ] + ], }, - 'on_setup': 'erpnext.healthcare.setup.setup_healthcare' + "on_setup": "erpnext.healthcare.setup.setup_healthcare", } diff --git a/erpnext/domains/hospitality.py b/erpnext/domains/hospitality.py index 5d2a22597e3..1ec3eed304e 100644 --- a/erpnext/domains/hospitality.py +++ b/erpnext/domains/hospitality.py @@ -1,36 +1,31 @@ - data = { - 'desktop_icons': [ - 'Restaurant', - 'Hotels', - 'Accounts', - 'Buying', - 'Stock', - 'HR', - 'Project', - 'ToDo' - ], - 'restricted_roles': [ - 'Restaurant Manager', - 'Hotel Manager', - 'Hotel Reservation User' - ], - 'custom_fields': { - 'Sales Invoice': [ + "desktop_icons": ["Restaurant", "Hotels", "Accounts", "Buying", "Stock", "HR", "Project", "ToDo"], + "restricted_roles": ["Restaurant Manager", "Hotel Manager", "Hotel Reservation User"], + "custom_fields": { + "Sales Invoice": [ { - 'fieldname': 'restaurant', 'fieldtype': 'Link', 'options': 'Restaurant', - 'insert_after': 'customer_name', 'label': 'Restaurant', + "fieldname": "restaurant", + "fieldtype": "Link", + "options": "Restaurant", + "insert_after": "customer_name", + "label": "Restaurant", }, { - 'fieldname': 'restaurant_table', 'fieldtype': 'Link', 'options': 'Restaurant Table', - 'insert_after': 'restaurant', 'label': 'Restaurant Table', + "fieldname": "restaurant_table", + "fieldtype": "Link", + "options": "Restaurant Table", + "insert_after": "restaurant", + "label": "Restaurant Table", + }, + ], + "Price List": [ + { + "fieldname": "restaurant_menu", + "fieldtype": "Link", + "options": "Restaurant Menu", + "label": "Restaurant Menu", + "insert_after": "currency", } ], - 'Price List': [ - { - 'fieldname':'restaurant_menu', 'fieldtype':'Link', 'options':'Restaurant Menu', 'label':'Restaurant Menu', - 'insert_after':'currency' - } - ] - } + }, } diff --git a/erpnext/domains/manufacturing.py b/erpnext/domains/manufacturing.py index 0cd51cf7926..08ed3cf92b5 100644 --- a/erpnext/domains/manufacturing.py +++ b/erpnext/domains/manufacturing.py @@ -1,23 +1,25 @@ - data = { - 'desktop_icons': [ - 'Item', - 'BOM', - 'Customer', - 'Supplier', - 'Sales Order', - 'Purchase Order', - 'Work Order', - 'Task', - 'Accounts', - 'HR', - 'ToDo' - ], - 'properties': [ - {'doctype': 'Item', 'fieldname': 'manufacturing', 'property': 'collapsible_depends_on', 'value': 'is_stock_item'}, + "desktop_icons": [ + "Item", + "BOM", + "Customer", + "Supplier", + "Sales Order", + "Purchase Order", + "Work Order", + "Task", + "Accounts", + "HR", + "ToDo", ], - 'set_value': [ - ['Stock Settings', None, 'show_barcode_field', 1] + "properties": [ + { + "doctype": "Item", + "fieldname": "manufacturing", + "property": "collapsible_depends_on", + "value": "is_stock_item", + }, ], - 'default_portal_role': 'Customer' + "set_value": [["Stock Settings", None, "show_barcode_field", 1]], + "default_portal_role": "Customer", } diff --git a/erpnext/domains/non_profit.py b/erpnext/domains/non_profit.py index 22f05c9e7df..22ab5d6e9f8 100644 --- a/erpnext/domains/non_profit.py +++ b/erpnext/domains/non_profit.py @@ -1,23 +1,16 @@ - data = { - 'desktop_icons': [ - 'Non Profit', - 'Member', - 'Donor', - 'Volunteer', - 'Grant Application', - 'Accounts', - 'Buying', - 'HR', - 'ToDo' + "desktop_icons": [ + "Non Profit", + "Member", + "Donor", + "Volunteer", + "Grant Application", + "Accounts", + "Buying", + "HR", + "ToDo", ], - 'restricted_roles': [ - 'Non Profit Manager', - 'Non Profit Member', - 'Non Profit Portal User' - ], - 'modules': [ - 'Non Profit' - ], - 'default_portal_role': 'Non Profit Manager' + "restricted_roles": ["Non Profit Manager", "Non Profit Member", "Non Profit Portal User"], + "modules": ["Non Profit"], + "default_portal_role": "Non Profit Manager", } diff --git a/erpnext/domains/retail.py b/erpnext/domains/retail.py index 17578d7ddcd..8ea85f10407 100644 --- a/erpnext/domains/retail.py +++ b/erpnext/domains/retail.py @@ -1,17 +1,14 @@ - data = { - 'desktop_icons': [ - 'POS', - 'Item', - 'Customer', - 'Sales Invoice', - 'Purchase Order', - 'Accounts', - 'Task', - 'ToDo' + "desktop_icons": [ + "POS", + "Item", + "Customer", + "Sales Invoice", + "Purchase Order", + "Accounts", + "Task", + "ToDo", ], - 'set_value': [ - ['Stock Settings', None, 'show_barcode_field', 1] - ], - 'default_portal_role': 'Customer' + "set_value": [["Stock Settings", None, "show_barcode_field", 1]], + "default_portal_role": "Customer", } diff --git a/erpnext/domains/services.py b/erpnext/domains/services.py index 39a554f29df..8595c69aff4 100644 --- a/erpnext/domains/services.py +++ b/erpnext/domains/services.py @@ -1,20 +1,17 @@ - data = { - 'desktop_icons': [ - 'Project', - 'Timesheet', - 'Customer', - 'Sales Order', - 'Sales Invoice', - 'CRM', - 'Task', - 'Expense Claim', - 'Employee', - 'HR', - 'ToDo' + "desktop_icons": [ + "Project", + "Timesheet", + "Customer", + "Sales Order", + "Sales Invoice", + "CRM", + "Task", + "Expense Claim", + "Employee", + "HR", + "ToDo", ], - 'set_value': [ - ['Stock Settings', None, 'show_barcode_field', 0] - ], - 'default_portal_role': 'Customer' + "set_value": [["Stock Settings", None, "show_barcode_field", 0]], + "default_portal_role": "Customer", } diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py index 3ad1da4a92f..6e95ba8fcbe 100644 --- a/erpnext/e_commerce/api.py +++ b/erpnext/e_commerce/api.py @@ -14,16 +14,16 @@ from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_web @frappe.whitelist(allow_guest=True) def get_product_filter_data(query_args=None): """ - Returns filtered products and discount filters. - :param query_args (dict): contains filters to get products list + Returns filtered products and discount filters. + :param query_args (dict): contains filters to get products list - Query Args filters: - search (str): Search Term. - field_filters (dict): Keys include item_group, brand, etc. - attribute_filters(dict): Keys include Color, Size, etc. - start (int): Offset items by - item_group (str): Valid Item Group - from_filters (bool): Set as True to jump to page 1 + Query Args filters: + search (str): Search Term. + field_filters (dict): Keys include item_group, brand, etc. + attribute_filters(dict): Keys include Color, Size, etc. + start (int): Offset items by + item_group (str): Valid Item Group + from_filters (bool): Set as True to jump to page 1 """ if isinstance(query_args, str): query_args = json.loads(query_args) @@ -47,17 +47,12 @@ def get_product_filter_data(query_args=None): sub_categories = [] if item_group: - field_filters['item_group'] = item_group sub_categories = get_child_groups_for_website(item_group, immediate=True) engine = ProductQuery() try: result = engine.query( - attribute_filters, - field_filters, - search_term=search, - start=start, - item_group=item_group + attribute_filters, field_filters, search_term=search, start=start, item_group=item_group ) except Exception: traceback = frappe.get_traceback() @@ -77,9 +72,10 @@ def get_product_filter_data(query_args=None): "filters": filters, "settings": engine.settings, "sub_categories": sub_categories, - "items_count": result["items_count"] + "items_count": result["items_count"], } + @frappe.whitelist(allow_guest=True) def get_guest_redirect_on_action(): - return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action") \ No newline at end of file + return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action") diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js index 6302d260e0a..a8966b07a79 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js @@ -24,17 +24,17 @@ frappe.ui.form.on("E Commerce Settings", { ); } - frappe.model.with_doctype("Item", () => { + frappe.model.with_doctype("Website Item", () => { const web_item_meta = frappe.get_meta('Website Item'); const valid_fields = web_item_meta.fields.filter( df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden ).map(df => ({ label: df.label, value: df.fieldname })); - frm.fields_dict.filter_fields.grid.update_docfield_property( + frm.get_field("filter_fields").grid.update_docfield_property( 'fieldname', 'fieldtype', 'Select' ); - frm.fields_dict.filter_fields.grid.update_docfield_property( + frm.get_field("filter_fields").grid.update_docfield_property( 'fieldname', 'options', valid_fields ); }); diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index d5fb9697f89..e6f08f708a8 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -47,7 +47,7 @@ "item_search_settings_section", "redisearch_warning", "search_index_fields", - "show_categories_in_search_autocomplete", + "is_redisearch_enabled", "is_redisearch_loaded", "shop_by_category_section", "slideshow", @@ -293,6 +293,7 @@ "fieldname": "search_index_fields", "fieldtype": "Small Text", "label": "Search Index Fields", + "mandatory_depends_on": "is_redisearch_enabled", "read_only_depends_on": "eval:!doc.is_redisearch_loaded" }, { @@ -301,13 +302,6 @@ "fieldtype": "Section Break", "label": "Item Search Settings" }, - { - "default": "1", - "fieldname": "show_categories_in_search_autocomplete", - "fieldtype": "Check", - "label": "Show Categories in Search Autocomplete", - "read_only_depends_on": "eval:!doc.is_redisearch_loaded" - }, { "default": "0", "fieldname": "is_redisearch_loaded", @@ -365,12 +359,19 @@ "fieldname": "show_price_in_quotation", "fieldtype": "Check", "label": "Show Price in Quotation" + }, + { + "default": "0", + "fieldname": "is_redisearch_enabled", + "fieldtype": "Check", + "label": "Enable Redisearch", + "read_only_depends_on": "eval:!doc.is_redisearch_loaded" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-02 14:02:44.785824", + "modified": "2022-04-01 18:35:56.106756", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", @@ -389,5 +390,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index 1110eb1accd..bd7ac9cdb7a 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -8,20 +8,25 @@ from frappe.utils import comma_and, flt, unique from erpnext.e_commerce.redisearch_utils import ( create_website_items_index, + define_autocomplete_dictionary, get_indexable_web_fields, is_search_module_loaded, ) -class ShoppingCartSetupError(frappe.ValidationError): pass +class ShoppingCartSetupError(frappe.ValidationError): + pass + class ECommerceSettings(Document): def onload(self): self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series") + + # flag >> if redisearch is installed and loaded self.is_redisearch_loaded = is_search_module_loaded() def validate(self): - self.validate_field_filters() + self.validate_field_filters(self.filter_fields, self.enable_field_filters) self.validate_attribute_filters() self.validate_checkout() self.validate_search_index_fields() @@ -31,16 +36,37 @@ class ECommerceSettings(Document): frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings") - def validate_field_filters(self): - if not (self.enable_field_filters and self.filter_fields): + self.is_redisearch_enabled_pre_save = frappe.db.get_single_value( + "E Commerce Settings", "is_redisearch_enabled" + ) + + def after_save(self): + self.create_redisearch_indexes() + + def create_redisearch_indexes(self): + # if redisearch is enabled (value changed) create indexes and dictionary + value_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save + if self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed: + define_autocomplete_dictionary() + create_website_items_index() + + @staticmethod + def validate_field_filters(filter_fields, enable_field_filters): + if not (enable_field_filters and filter_fields): return - item_meta = frappe.get_meta("Item") - valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]] + web_item_meta = frappe.get_meta("Website Item") + valid_fields = [ + df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] + ] - for f in self.filter_fields: - if f.fieldname not in valid_fields: - frappe.throw(_("Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'").format(f.idx, f.fieldname)) + for row in filter_fields: + if row.fieldname not in valid_fields: + frappe.throw( + _( + "Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'" + ).format(row.idx, frappe.bold(row.fieldname)) + ) def validate_attribute_filters(self): if not (self.enable_attribute_filters and self.filter_attributes): @@ -57,8 +83,8 @@ class ECommerceSettings(Document): if not self.search_index_fields: return - fields = self.search_index_fields.replace(' ', '') - fields = unique(fields.strip(',').split(',')) # Remove extra ',' and remove duplicates + fields = self.search_index_fields.replace(" ", "") + fields = unique(fields.strip(",").split(",")) # Remove extra ',' and remove duplicates # All fields should be indexable allowed_indexable_fields = get_indexable_web_fields() @@ -69,18 +95,22 @@ class ECommerceSettings(Document): invalid_fields = comma_and(invalid_fields) if num_invalid_fields > 1: - frappe.throw(_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields))) + frappe.throw( + _("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields)) + ) else: - frappe.throw(_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields))) + frappe.throw( + _("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields)) + ) - self.search_index_fields = ','.join(fields) + self.search_index_fields = ",".join(fields) def validate_price_list_exchange_rate(self): "Check if exchange rate exists for Price List currency (to Company's currency)." from erpnext.setup.utils import get_exchange_rate if not self.enabled or not self.company or not self.price_list: - return # this function is also called from hooks, check values again + return # this function is also called from hooks, check values again company_currency = frappe.get_cached_value("Company", self.company, "default_currency") price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency") @@ -104,12 +134,13 @@ class ECommerceSettings(Document): frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError) def validate_tax_rule(self): - if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"): + if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError) def get_tax_master(self, billing_territory): - tax_master = self.get_name_from_territory(billing_territory, "sales_taxes_and_charges_masters", - "sales_taxes_and_charges_master") + tax_master = self.get_name_from_territory( + billing_territory, "sales_taxes_and_charges_masters", "sales_taxes_and_charges_master" + ) return tax_master and tax_master[0] or None def get_shipping_rules(self, shipping_territory): @@ -126,25 +157,33 @@ class ECommerceSettings(Document): if not (new_fields == old_fields): create_website_items_index() + def validate_cart_settings(doc=None, method=None): frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate") + def get_shopping_cart_settings(): if not getattr(frappe.local, "shopping_cart_settings", None): - frappe.local.shopping_cart_settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings") + frappe.local.shopping_cart_settings = frappe.get_doc( + "E Commerce Settings", "E Commerce Settings" + ) return frappe.local.shopping_cart_settings + @frappe.whitelist(allow_guest=True) def is_cart_enabled(): return get_shopping_cart_settings().enabled + def show_quantity_in_website(): return get_shopping_cart_settings().show_quantity_in_website + def check_shopping_cart_enabled(): if not get_shopping_cart_settings().enabled: frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError) + def show_attachments(): return get_shopping_cart_settings().show_attachments diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py index 32f9800215f..9372f80252f 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import unittest @@ -11,43 +11,35 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( class TestECommerceSettings(unittest.TestCase): - def setUp(self): - frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """) - - def get_cart_settings(self): - return frappe.get_doc({"doctype": "E Commerce Settings", - "company": "_Test Company"}) - - # NOTE: Exchangrate API has all enabled currencies that ERPNext supports. - # We aren't checking just currency exchange record anymore - # while validating price list currency exchange rate to that of company. - # The API is being used to fetch the rate which again almost always - # gives back a valid value (for valid currencies). - # This makes the test obsolete. - # Commenting because im not sure if there's a better test we can write - - # def test_exchange_rate_exists(self): - # frappe.db.sql("""delete from `tabCurrency Exchange`""") - - # cart_settings = self.get_cart_settings() - # cart_settings.price_list = "_Test Price List Rest of the World" - # self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate) - - # from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ - # currency_exchange_records - # frappe.get_doc(currency_exchange_records[0]).insert() - # cart_settings.validate_price_list_exchange_rate() + def tearDown(self): + frappe.db.rollback() def test_tax_rule_validation(self): frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") - cart_settings = self.get_cart_settings() + cart_settings = frappe.get_doc("E Commerce Settings") cart_settings.enabled = 1 if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule) frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1") + def test_invalid_filter_fields(self): + "Check if Item fields are blocked in E Commerce Settings filter fields." + from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + setup_e_commerce_settings({"enable_field_filters": 1}) + + create_custom_field( + "Item", + dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"), + ) + settings = frappe.get_doc("E Commerce Settings") + settings.append("filter_fields", {"fieldname": "test_data"}) + + self.assertRaises(frappe.ValidationError, settings.save) + + def setup_e_commerce_settings(values_dict): "Accepts a dict of values that updates E Commerce Settings." if not values_dict: @@ -57,4 +49,5 @@ def setup_e_commerce_settings(values_dict): doc.update(values_dict) doc.save() + test_dependencies = ["Tax Rule"] diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py index 680fb9427a5..8ca1abfa3db 100644 --- a/erpnext/e_commerce/doctype/item_review/item_review.py +++ b/erpnext/e_commerce/doctype/item_review/item_review.py @@ -18,6 +18,7 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( class UnverifiedReviewer(frappe.ValidationError): pass + class ItemReview(Document): def after_insert(self): # regenerate cache on review creation @@ -54,12 +55,13 @@ def get_item_reviews(web_item, start=0, end=10, data=None): return data + def get_queried_reviews(web_item, start=0, end=10, data=None): """ - Query Website Item wise reviews and cache if needed. - Cache stores only first page of reviews i.e. 10 reviews maximum. - Returns: - dict: Containing reviews, average ratings, % of reviews per rating and total reviews. + Query Website Item wise reviews and cache if needed. + Cache stores only first page of reviews i.e. 10 reviews maximum. + Returns: + dict: Containing reviews, average ratings, % of reviews per rating and total reviews. """ if not data: data = frappe._dict() @@ -69,13 +71,13 @@ def get_queried_reviews(web_item, start=0, end=10, data=None): filters={"website_item": web_item}, fields=["*"], limit_start=start, - limit_page_length=end + limit_page_length=end, ) rating_data = frappe.db.get_all( "Item Review", filters={"website_item": web_item}, - fields=["avg(rating) as average, count(*) as total"] + fields=["avg(rating) as average, count(*) as total"], )[0] data.average_rating = flt(rating_data.average, 1) @@ -83,11 +85,9 @@ def get_queried_reviews(web_item, start=0, end=10, data=None): # get % of reviews per rating reviews_per_rating = [] - for i in range(1,6): + for i in range(1, 6): count = frappe.db.get_all( - "Item Review", - filters={"website_item": web_item, "rating": i}, - fields=["count(*) as count"] + "Item Review", filters={"website_item": web_item, "rating": i}, fields=["count(*) as count"] )[0].count percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0 @@ -98,40 +98,45 @@ def get_queried_reviews(web_item, start=0, end=10, data=None): return data + def set_reviews_in_cache(web_item, reviews_dict): frappe.cache().hset("item_reviews", web_item, reviews_dict) + @frappe.whitelist() def add_item_review(web_item, title, rating, comment=None): - """ Add an Item Review by a user if non-existent. """ + """Add an Item Review by a user if non-existent.""" if frappe.session.user == "Guest": # guest user should not reach here ideally in the case they do via an API, throw error frappe.throw(_("You are not verified to write a review yet."), exc=UnverifiedReviewer) if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}): - doc = frappe.get_doc({ - "doctype": "Item Review", - "user": frappe.session.user, - "customer": get_customer(), - "website_item": web_item, - "item": frappe.db.get_value("Website Item", web_item, "item_code"), - "review_title": title, - "rating": rating, - "comment": comment - }) + doc = frappe.get_doc( + { + "doctype": "Item Review", + "user": frappe.session.user, + "customer": get_customer(), + "website_item": web_item, + "item": frappe.db.get_value("Website Item", web_item, "item_code"), + "review_title": title, + "rating": rating, + "comment": comment, + } + ) doc.published_on = datetime.today().strftime("%d %B %Y") doc.insert() + def get_customer(silent=False): """ - silent: Return customer if exists else return nothing. Dont throw error. + silent: Return customer if exists else return nothing. Dont throw error. """ user = frappe.session.user contact_name = get_contact_name(user) customer = None if contact_name: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) for link in contact.links: if link.link_doctype == "Customer": customer = link.link_name @@ -143,5 +148,6 @@ def get_customer(silent=False): return None else: # should not reach here unless via an API - frappe.throw(_("You are not a verified customer yet. Please contact us to proceed."), - exc=UnverifiedReviewer) + frappe.throw( + _("You are not a verified customer yet. Please contact us to proceed."), exc=UnverifiedReviewer + ) diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index a4c85ec3469..18e18dd31a9 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -18,17 +18,23 @@ from erpnext.stock.doctype.item.item import DataValidationError from erpnext.stock.doctype.item.test_item import make_item WEBITEM_DESK_TESTS = ("test_website_item_desk_item_sync", "test_publish_variant_and_template") -WEBITEM_PRICE_TESTS = ('test_website_item_price_for_logged_in_user', 'test_website_item_price_for_guest_user') +WEBITEM_PRICE_TESTS = ( + "test_website_item_price_for_logged_in_user", + "test_website_item_price_for_guest_user", +) + class TestWebsiteItem(unittest.TestCase): @classmethod def setUpClass(cls): - setup_e_commerce_settings({ - "company": "_Test Company", - "enabled": 1, - "default_customer_group": "_Test Customer Group", - "price_list": "_Test Price List India" - }) + setup_e_commerce_settings( + { + "company": "_Test Company", + "enabled": 1, + "default_customer_group": "_Test Customer Group", + "price_list": "_Test Price List India", + } + ) @classmethod def tearDownClass(cls): @@ -36,40 +42,42 @@ class TestWebsiteItem(unittest.TestCase): def setUp(self): if self._testMethodName in WEBITEM_DESK_TESTS: - make_item("Test Web Item", { - "has_variant": 1, - "variant_based_on": "Item Attribute", - "attributes": [ - { - "attribute": "Test Size" - } - ] - }) + make_item( + "Test Web Item", + { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [{"attribute": "Test Size"}], + }, + ) elif self._testMethodName in WEBITEM_PRICE_TESTS: - create_user_and_customer_if_not_exists("test_contact_customer@example.com", "_Test Contact For _Test Customer") + create_user_and_customer_if_not_exists( + "test_contact_customer@example.com", "_Test Contact For _Test Customer" + ) create_regular_web_item() make_web_item_price(item_code="Test Mobile Phone") # Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass. - # This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor, - # when testing for logged-in user the test will get the previous pricing rule because "selling" is still true. + # This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor, + # when testing for logged-in user the test will get the previous pricing rule because "selling" is still true. # # I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test. make_web_pricing_rule( - title="Test Pricing Rule for Test Mobile Phone", - item_code="Test Mobile Phone", - selling=1) + title="Test Pricing Rule for Test Mobile Phone", item_code="Test Mobile Phone", selling=1 + ) make_web_pricing_rule( title="Test Pricing Rule for Test Mobile Phone (Customer)", item_code="Test Mobile Phone", selling=1, discount_percentage="25", applicable_for="Customer", - customer="_Test Customer") + customer="_Test Customer", + ) def test_index_creation(self): "Check if index is getting created in db." from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update + on_doctype_update() indices = frappe.db.sql("show index from `tabWebsite Item`", as_dict=1) @@ -83,7 +91,7 @@ class TestWebsiteItem(unittest.TestCase): def test_website_item_desk_item_sync(self): "Check creation/updation/deletion of Website Item and its impact on Item master." web_item = None - item = make_item("Test Web Item") # will return item if exists + item = make_item("Test Web Item") # will return item if exists try: web_item = make_website_item(item, save=False) web_item.save() @@ -96,7 +104,7 @@ class TestWebsiteItem(unittest.TestCase): item.reload() self.assertEqual(web_item.published, 1) - self.assertEqual(item.published_in_website, 1) # check if item was back updated + self.assertEqual(item.published_in_website, 1) # check if item was back updated self.assertEqual(web_item.item_group, item.item_group) # check if changing item data changes it in website item @@ -169,9 +177,12 @@ class TestWebsiteItem(unittest.TestCase): from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups item_code = "Test Breadcrumb Item" - item = make_item(item_code, { - "item_group": "_Test Item Group B - 1", - }) + item = make_item( + item_code, + { + "item_group": "_Test Item Group B - 1", + }, + ) if not frappe.db.exists("Website Item", {"item_code": item_code}): web_item = make_website_item(item, save=False) @@ -186,7 +197,7 @@ class TestWebsiteItem(unittest.TestCase): self.assertEqual(breadcrumbs[0]["name"], "Home") self.assertEqual(breadcrumbs[1]["name"], "Shop by Category") - self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group + self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1") # tear down @@ -235,10 +246,7 @@ class TestWebsiteItem(unittest.TestCase): item_code = "Test Mobile Phone" # show price for guest user in e commerce settings - setup_e_commerce_settings({ - "show_price": 1, - "hide_price_for_guest": 0 - }) + setup_e_commerce_settings({"show_price": 1, "hide_price_for_guest": 0}) # price and pricing rule added via setUp @@ -269,11 +277,11 @@ class TestWebsiteItem(unittest.TestCase): def test_website_item_stock_when_out_of_stock(self): """ - Check if stock details are fetched correctly for empty inventory when: - 1) Showing stock availability enabled: - - Warehouse unset - - Warehouse set - 2) Showing stock availability disabled + Check if stock details are fetched correctly for empty inventory when: + 1) Showing stock availability enabled: + - Warehouse unset + - Warehouse set + 2) Showing stock availability disabled """ item_code = "Test Mobile Phone" create_regular_web_item() @@ -287,7 +295,9 @@ class TestWebsiteItem(unittest.TestCase): self.assertFalse(bool(data.product_info["stock_qty"])) # set warehouse - frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC") + frappe.db.set_value( + "Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC" + ) # check if stock details are fetched and item not in stock with warehouse set data = get_product_info_for_website(item_code, skip_quotation_creation=True) @@ -309,11 +319,11 @@ class TestWebsiteItem(unittest.TestCase): def test_website_item_stock_when_in_stock(self): """ - Check if stock details are fetched correctly for available inventory when: - 1) Showing stock availability enabled: - - Warehouse set - - Warehouse unset - 2) Showing stock availability disabled + Check if stock details are fetched correctly for available inventory when: + 1) Showing stock availability enabled: + - Warehouse set + - Warehouse unset + 2) Showing stock availability disabled """ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -323,10 +333,14 @@ class TestWebsiteItem(unittest.TestCase): frappe.local.shopping_cart_settings = None # set warehouse - frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC") + frappe.db.set_value( + "Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC" + ) # stock up item - stock_entry = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100) + stock_entry = make_stock_entry( + item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100 + ) # check if stock details are fetched and item is in stock with warehouse set data = get_product_info_for_website(item_code, skip_quotation_creation=True) @@ -361,10 +375,7 @@ class TestWebsiteItem(unittest.TestCase): item_code = "Test Mobile Phone" web_item = create_regular_web_item(item_code) - setup_e_commerce_settings({ - "enable_recommendations": 1, - "show_price": 1 - }) + setup_e_commerce_settings({"enable_recommendations": 1, "show_price": 1}) # create recommended web item and price for it recommended_web_item = create_regular_web_item("Test Mobile Phone 1") @@ -382,7 +393,7 @@ class TestWebsiteItem(unittest.TestCase): self.assertEqual(len(recommended_items), 1) recomm_item = recommended_items[0] self.assertEqual(recomm_item.get("website_item_name"), "Test Mobile Phone 1") - self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched + self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched price_info = recomm_item.get("price_info") self.assertEqual(price_info.get("price_list_rate"), 1000) @@ -396,7 +407,7 @@ class TestWebsiteItem(unittest.TestCase): recommended_items = web_item.get_recommended_items(e_commerce_settings) self.assertEqual(len(recommended_items), 1) - self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched + self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched # tear down web_item.delete() @@ -409,11 +420,9 @@ class TestWebsiteItem(unittest.TestCase): web_item = create_regular_web_item(item_code) # price visible to guests - setup_e_commerce_settings({ - "enable_recommendations": 1, - "show_price": 1, - "hide_price_for_guest": 0 - }) + setup_e_commerce_settings( + {"enable_recommendations": 1, "show_price": 1, "hide_price_for_guest": 0} + ) # create recommended web item and price for it recommended_web_item = create_regular_web_item("Test Mobile Phone 1") @@ -431,7 +440,7 @@ class TestWebsiteItem(unittest.TestCase): # test results if show price is enabled self.assertEqual(len(recommended_items), 1) - self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched + self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched # price hidden from guests frappe.set_user("Administrator") @@ -444,7 +453,7 @@ class TestWebsiteItem(unittest.TestCase): # test results if show price is enabled self.assertEqual(len(recommended_items), 1) - self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched + self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched # tear down frappe.set_user("Administrator") @@ -452,6 +461,7 @@ class TestWebsiteItem(unittest.TestCase): recommended_web_item.delete() frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete() + def create_regular_web_item(item_code=None, item_args=None, web_args=None): "Create Regular Item and Website Item." item_code = item_code or "Test Mobile Phone" @@ -467,47 +477,51 @@ def create_regular_web_item(item_code=None, item_args=None, web_args=None): return web_item + def make_web_item_price(**kwargs): item_code = kwargs.get("item_code") if not item_code: return if not frappe.db.exists("Item Price", {"item_code": item_code}): - item_price = frappe.get_doc({ - "doctype": "Item Price", - "item_code": item_code, - "price_list": kwargs.get("price_list") or "_Test Price List India", - "price_list_rate": kwargs.get("price_list_rate") or 1000 - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "item_code": item_code, + "price_list": kwargs.get("price_list") or "_Test Price List India", + "price_list_rate": kwargs.get("price_list_rate") or 1000, + } + ) item_price.insert() else: item_price = frappe.get_cached_doc("Item Price", {"item_code": item_code}) return item_price + def make_web_pricing_rule(**kwargs): title = kwargs.get("title") if not title: return if not frappe.db.exists("Pricing Rule", title): - pricing_rule = frappe.get_doc({ - "doctype": "Pricing Rule", - "title": title, - "apply_on": kwargs.get("apply_on") or "Item Code", - "items": [{ - "item_code": kwargs.get("item_code") - }], - "selling": kwargs.get("selling") or 0, - "buying": kwargs.get("buying") or 0, - "rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage", - "discount_percentage": kwargs.get("discount_percentage") or 10, - "company": kwargs.get("company") or "_Test Company", - "currency": kwargs.get("currency") or "INR", - "for_price_list": kwargs.get("price_list") or "_Test Price List India", - "applicable_for": kwargs.get("applicable_for") or "", - "customer": kwargs.get("customer") or "", - }) + pricing_rule = frappe.get_doc( + { + "doctype": "Pricing Rule", + "title": title, + "apply_on": kwargs.get("apply_on") or "Item Code", + "items": [{"item_code": kwargs.get("item_code")}], + "selling": kwargs.get("selling") or 0, + "buying": kwargs.get("buying") or 0, + "rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage", + "discount_percentage": kwargs.get("discount_percentage") or 10, + "company": kwargs.get("company") or "_Test Company", + "currency": kwargs.get("currency") or "INR", + "for_price_list": kwargs.get("price_list") or "_Test Price List India", + "applicable_for": kwargs.get("applicable_for") or "", + "customer": kwargs.get("customer") or "", + } + ) pricing_rule.insert() else: pricing_rule = frappe.get_doc("Pricing Rule", {"title": title}) @@ -515,23 +529,26 @@ def make_web_pricing_rule(**kwargs): return pricing_rule -def create_user_and_customer_if_not_exists(email, first_name = None): +def create_user_and_customer_if_not_exists(email, first_name=None): if frappe.db.exists("User", email): return - frappe.get_doc({ - "doctype": "User", - "user_type": "Website User", - "email": email, - "send_welcome_email": 0, - "first_name": first_name or email.split("@")[0] - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "User", + "user_type": "Website User", + "email": email, + "send_welcome_email": 0, + "first_name": first_name or email.split("@")[0], + } + ).insert(ignore_permissions=True) contact = frappe.get_last_doc("Contact", filters={"email_id": email}) - link = contact.append('links', {}) + link = contact.append("links", {}) link.link_doctype = "Customer" link.link_name = "_Test Customer" link.link_title = "_Test Customer" contact.save() + test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"] diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js index 741e78f4a55..7295e4b56a0 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.js +++ b/erpnext/e_commerce/doctype/website_item/website_item.js @@ -2,23 +2,46 @@ // For license information, please see license.txt frappe.ui.form.on('Website Item', { - onload: function(frm) { + onload: (frm) => { // should never check Private frm.fields_dict["website_image"].df.is_private = 0; + + frm.set_query("website_warehouse", () => { + return { + filters: {"is_group": 0} + }; + }); }, - image: function() { + refresh: (frm) => { + frm.add_custom_button(__("Prices"), function() { + frappe.set_route("List", "Item Price", {"item_code": frm.doc.item_code}); + }, __("View")); + + frm.add_custom_button(__("Stock"), function() { + frappe.route_options = { + "item_code": frm.doc.item_code + }; + frappe.set_route("query-report", "Stock Balance"); + }, __("View")); + + frm.add_custom_button(__("E Commerce Settings"), function() { + frappe.set_route("Form", "E Commerce Settings"); + }, __("View")); + }, + + image: () => { refresh_field("image_view"); }, - copy_from_item_group: function(frm) { + copy_from_item_group: (frm) => { return frm.call({ doc: frm.doc, method: "copy_specification_from_item_group" }); }, - set_meta_tags(frm) { + set_meta_tags: (frm) => { frappe.utils.set_meta_tag(frm.doc.route); } }); diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 181aaa1292e..399958b7c62 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -28,7 +28,7 @@ class WebsiteItem(WebsiteGenerator): page_title_field="web_item_name", condition_field="published", template="templates/generators/item/item.html", - no_cache=1 + no_cache=1, ) def autoname(self): @@ -56,7 +56,8 @@ class WebsiteItem(WebsiteGenerator): self.publish_unpublish_desk_item(publish=True) if not self.get("__islocal"): - self.old_website_item_groups = frappe.db.sql_list(""" + self.old_website_item_groups = frappe.db.sql_list( + """ select item_group from @@ -65,7 +66,9 @@ class WebsiteItem(WebsiteGenerator): parentfield='website_item_groups' and parenttype='Website Item' and parent=%s - """, self.name) + """, + self.name, + ) def on_update(self): invalidate_cache_for_web_item(self) @@ -84,14 +87,17 @@ class WebsiteItem(WebsiteGenerator): def publish_unpublish_desk_item(self, publish=True): if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish: - return # if already published don't publish again + return # if already published don't publish again frappe.db.set_value("Item", self.item_code, "published_in_website", publish) def make_route(self): """Called from set_route in WebsiteGenerator.""" 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)) + 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)) + ) def update_template_item(self): """Publish Template Item if Variant is published.""" @@ -121,12 +127,10 @@ class WebsiteItem(WebsiteGenerator): # find if website image url exists as public file_doc = frappe.get_all( "File", - filters={ - "file_url": self.website_image - }, + filters={"file_url": self.website_image}, fields=["name", "is_private"], order_by="is_private asc", - limit_page_length=1 + limit_page_length=1, ) if file_doc: @@ -134,7 +138,11 @@ class WebsiteItem(WebsiteGenerator): if not file_doc: if not auto_set_website_image: - frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name)) + frappe.msgprint( + _("Website Image {0} attached to Item {1} cannot be found").format( + self.website_image, self.name + ) + ) self.website_image = None @@ -151,18 +159,23 @@ class WebsiteItem(WebsiteGenerator): import requests.exceptions - if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): + if not self.is_new() and self.website_image != frappe.db.get_value( + self.doctype, self.name, "website_image" + ): self.thumbnail = None if self.website_image and not self.thumbnail: file_doc = None try: - file_doc = frappe.get_doc("File", { - "file_url": self.website_image, - "attached_to_doctype": "Website Item", - "attached_to_name": self.name - }) + file_doc = frappe.get_doc( + "File", + { + "file_url": self.website_image, + "attached_to_doctype": "Website Item", + "attached_to_name": self.name, + }, + ) except frappe.DoesNotExistError: pass # cleanup @@ -174,18 +187,21 @@ class WebsiteItem(WebsiteGenerator): except requests.exceptions.SSLError: frappe.msgprint( - _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)) + _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image) + ) self.website_image = None # for CSV import if self.website_image and not file_doc: try: - file_doc = frappe.get_doc({ - "doctype": "File", - "file_url": self.website_image, - "attached_to_doctype": "Website Item", - "attached_to_name": self.name - }).save() + file_doc = frappe.get_doc( + { + "doctype": "File", + "file_url": self.website_image, + "attached_to_doctype": "Website Item", + "attached_to_name": self.name, + } + ).save() except IOError: self.website_image = None @@ -201,11 +217,11 @@ class WebsiteItem(WebsiteGenerator): context.search_link = "/search" context.body_class = "product-page" - context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs + context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs self.attributes = frappe.get_all( "Item Variant Attribute", fields=["attribute", "attribute_value"], - filters={"parent": self.item_code} + filters={"parent": self.item_code}, ) if self.slideshow: @@ -224,7 +240,9 @@ class WebsiteItem(WebsiteGenerator): context.reviews = context.reviews[:4] context.wished = False - if frappe.db.exists("Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}): + if frappe.db.exists( + "Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user} + ): context.wished = True context.user_is_customer = check_if_user_is_customer() @@ -240,11 +258,12 @@ class WebsiteItem(WebsiteGenerator): variant.attributes = frappe.get_all( "Item Variant Attribute", filters={"parent": variant.name}, - fields=["attribute", "attribute_value as value"]) + fields=["attribute", "attribute_value as value"], + ) # make an attribute-value map for easier access in templates variant.attribute_map = frappe._dict( - {attr.attribute : attr.value for attr in variant.attributes} + {attr.attribute: attr.value for attr in variant.attributes} ) for attr in variant.attributes: @@ -264,9 +283,12 @@ class WebsiteItem(WebsiteGenerator): values.append(val) else: # get list of values defined (for sequence) - for attr_value in frappe.db.get_all("Item Attribute Value", + for attr_value in frappe.db.get_all( + "Item Attribute Value", fields=["attribute_value"], - filters={"parent": attr.attribute}, order_by="idx asc"): + filters={"parent": attr.attribute}, + order_by="idx asc", + ): if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): values.append(attr_value.attribute_value) @@ -276,10 +298,10 @@ class WebsiteItem(WebsiteGenerator): safe_description = frappe.utils.to_markdown(self.description) - context.metatags.url = frappe.utils.get_url() + '/' + context.route + context.metatags.url = frappe.utils.get_url() + "/" + context.route if context.website_image: - if context.website_image.startswith('http'): + if context.website_image.startswith("http"): url = context.website_image else: url = frappe.utils.get_url() + context.website_image @@ -289,25 +311,29 @@ class WebsiteItem(WebsiteGenerator): context.metatags.title = self.web_item_name or self.item_name or self.item_code - context.metatags['og:type'] = 'product' - context.metatags['og:site_name'] = 'ERPNext' + context.metatags["og:type"] = "product" + context.metatags["og:site_name"] = "ERPNext" def set_shopping_cart_data(self, context): from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website - context.shopping_cart = get_product_info_for_website(self.item_code, skip_quotation_creation=True) + + context.shopping_cart = get_product_info_for_website( + self.item_code, skip_quotation_creation=True + ) @frappe.whitelist() def copy_specification_from_item_group(self): self.set("website_specifications", []) if self.item_group: - for label, desc in frappe.db.get_values("Item Website Specification", - {"parent": self.item_group}, ["label", "description"]): + for label, desc in frappe.db.get_values( + "Item Website Specification", {"parent": self.item_group}, ["label", "description"] + ): row = self.append("website_specifications") row.label = label row.description = desc def get_product_details_section(self, context): - """ Get section with tabs or website specifications. """ + """Get section with tabs or website specifications.""" context.show_tabs = self.show_tabbed_section if self.show_tabbed_section and (self.tabs or self.website_specifications): context.tabs = self.get_tabs() @@ -319,10 +345,8 @@ class WebsiteItem(WebsiteGenerator): tab_values["tab_1_title"] = "Product Details" tab_values["tab_1_content"] = frappe.render_template( "templates/generators/item/item_specifications.html", - { - "website_specifications": self.website_specifications, - "show_tabs": self.show_tabbed_section - }) + {"website_specifications": self.website_specifications, "show_tabs": self.show_tabbed_section}, + ) for row in self.tabs: tab_values[f"tab_{row.idx + 1}_title"] = _(row.label) @@ -331,7 +355,8 @@ class WebsiteItem(WebsiteGenerator): return tab_values def get_recommended_items(self, settings): - items = frappe.db.sql(f""" + items = frappe.db.sql( + f""" select ri.website_item_thumbnail, ri.website_item_name, ri.route, ri.item_code @@ -342,7 +367,9 @@ class WebsiteItem(WebsiteGenerator): and ri.parent = '{self.name}' and wi.published = 1 order by ri.idx - """, as_dict=1) + """, + as_dict=1, + ) if settings.show_price: is_guest = frappe.session.user == "Guest" @@ -354,22 +381,24 @@ class WebsiteItem(WebsiteGenerator): selling_price_list = _set_price_list(settings, None) for item in items: item.price_info = get_price( - item.item_code, - selling_price_list, - settings.default_customer_group, - settings.company + item.item_code, selling_price_list, settings.default_customer_group, settings.company ) return items + def invalidate_cache_for_web_item(doc): """Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager.""" from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website invalidate_cache_for(doc, doc.item_group) - website_item_groups = list(set((doc.get("old_website_item_groups") or []) - + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group])) + website_item_groups = list( + set( + (doc.get("old_website_item_groups") or []) + + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group] + ) + ) for item_group in website_item_groups: invalidate_cache_for(doc, item_group) @@ -379,6 +408,7 @@ def invalidate_cache_for_web_item(doc): invalidate_item_variants_cache_for_website(doc) + def on_doctype_update(): # since route is a Text column, it needs a length for indexing frappe.db.add_index("Website Item", ["route(500)"]) @@ -386,6 +416,7 @@ def on_doctype_update(): frappe.db.add_index("Website Item", ["item_group"]) frappe.db.add_index("Website Item", ["brand"]) + def check_if_user_is_customer(user=None): from frappe.contacts.doctype.contact.contact import get_contact_name @@ -396,7 +427,7 @@ def check_if_user_is_customer(user=None): customer = None if contact_name: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) for link in contact.links: if link.link_doctype == "Customer": customer = link.link_name @@ -404,6 +435,7 @@ def check_if_user_is_customer(user=None): return True if customer else False + @frappe.whitelist() def make_website_item(doc, save=True): if not doc: @@ -419,8 +451,17 @@ def make_website_item(doc, save=True): website_item = frappe.new_doc("Website Item") website_item.web_item_name = doc.get("item_name") - fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image", - "has_variants", "variant_of", "description"] + fields_to_map = [ + "item_code", + "item_name", + "item_group", + "stock_uom", + "brand", + "image", + "has_variants", + "variant_of", + "description", + ] for field in fields_to_map: website_item.update({field: doc.get(field)}) diff --git a/erpnext/e_commerce/doctype/website_offer/website_offer.py b/erpnext/e_commerce/doctype/website_offer/website_offer.py index c8cac73a0bb..2773962b2b4 100644 --- a/erpnext/e_commerce/doctype/website_offer/website_offer.py +++ b/erpnext/e_commerce/doctype/website_offer/website_offer.py @@ -9,6 +9,7 @@ from frappe.model.document import Document class WebsiteOffer(Document): pass + @frappe.whitelist(allow_guest=True) def get_offer_details(offer_id): - return frappe.db.get_value('Website Offer', {'name': offer_id}, ['offer_details']) + return frappe.db.get_value("Website Offer", {"name": offer_id}, ["offer_details"]) diff --git a/erpnext/e_commerce/doctype/wishlist/test_wishlist.py b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py index 2348ce164ed..9a85aeb83ba 100644 --- a/erpnext/e_commerce/doctype/wishlist/test_wishlist.py +++ b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py @@ -33,14 +33,16 @@ class TestWishlist(unittest.TestCase): # check if wishlist was created and item was added self.assertTrue(frappe.db.exists("Wishlist", {"user": frappe.session.user})) - self.assertTrue(frappe.db.exists("Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user})) + self.assertTrue( + frappe.db.exists( + "Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user} + ) + ) # add second item to wishlist add_to_wishlist("Test Phone Series Y") wishlist_length = frappe.db.get_value( - "Wishlist Item", - {"parent": frappe.session.user}, - "count(*)" + "Wishlist Item", {"parent": frappe.session.user}, "count(*)" ) self.assertEqual(wishlist_length, 2) @@ -48,9 +50,7 @@ class TestWishlist(unittest.TestCase): remove_from_wishlist("Test Phone Series Y") wishlist_length = frappe.db.get_value( - "Wishlist Item", - {"parent": frappe.session.user}, - "count(*)" + "Wishlist Item", {"parent": frappe.session.user}, "count(*)" ) self.assertIsNone(frappe.db.exists("Wishlist Item", {"parent": frappe.session.user})) self.assertEqual(wishlist_length, 0) @@ -73,29 +73,44 @@ class TestWishlist(unittest.TestCase): # check wishlist and its content for users self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user.name})) - self.assertTrue(frappe.db.exists("Wishlist Item", - {"item_code": "Test Phone Series X", "parent": test_user.name})) + self.assertTrue( + frappe.db.exists( + "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} + ) + ) self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user_1.name})) - self.assertTrue(frappe.db.exists("Wishlist Item", - {"item_code": "Test Phone Series X", "parent": test_user_1.name})) + self.assertTrue( + frappe.db.exists( + "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user_1.name} + ) + ) # remove item for second user remove_from_wishlist("Test Phone Series X") # make sure item was removed for second user and not first - self.assertFalse(frappe.db.exists("Wishlist Item", - {"item_code": "Test Phone Series X", "parent": test_user_1.name})) - self.assertTrue(frappe.db.exists("Wishlist Item", - {"item_code": "Test Phone Series X", "parent": test_user.name})) + self.assertFalse( + frappe.db.exists( + "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user_1.name} + ) + ) + self.assertTrue( + frappe.db.exists( + "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} + ) + ) # remove item for first user frappe.set_user(test_user.name) remove_from_wishlist("Test Phone Series X") - self.assertFalse(frappe.db.exists("Wishlist Item", - {"item_code": "Test Phone Series X", "parent": test_user.name})) + self.assertFalse( + frappe.db.exists( + "Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name} + ) + ) # tear down frappe.set_user("Administrator") frappe.get_doc("Wishlist", {"user": test_user.name}).delete() - frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete() \ No newline at end of file + frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete() diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py index ba99a14bc1f..ec3174c25b1 100644 --- a/erpnext/e_commerce/doctype/wishlist/wishlist.py +++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py @@ -9,6 +9,7 @@ from frappe.model.document import Document class Wishlist(Document): pass + @frappe.whitelist() def add_to_wishlist(item_code): """Insert Item into wishlist.""" @@ -20,7 +21,8 @@ def add_to_wishlist(item_code): "Website Item", {"item_code": item_code}, ["image", "website_warehouse", "name", "web_item_name", "item_name", "item_group", "route"], - as_dict=1) + as_dict=1, + ) wished_item_dict = { "item_code": item_code, @@ -30,7 +32,7 @@ def add_to_wishlist(item_code): "web_item_name": web_item_data.get("web_item_name"), "image": web_item_data.get("image"), "warehouse": web_item_data.get("website_warehouse"), - "route": web_item_data.get("route") + "route": web_item_data.get("route"), } if not frappe.db.exists("Wishlist", frappe.session.user): @@ -41,28 +43,20 @@ def add_to_wishlist(item_code): wishlist.save(ignore_permissions=True) else: wishlist = frappe.get_doc("Wishlist", frappe.session.user) - item = wishlist.append('items', wished_item_dict) + item = wishlist.append("items", wished_item_dict) item.db_insert() if hasattr(frappe.local, "cookie_manager"): frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items))) + @frappe.whitelist() def remove_from_wishlist(item_code): if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}): - frappe.db.delete( - "Wishlist Item", - { - "item_code": item_code, - "parent": frappe.session.user - } - ) + frappe.db.delete("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}) frappe.db.commit() - wishlist_items = frappe.db.get_values( - "Wishlist Item", - filters={"parent": frappe.session.user} - ) + wishlist_items = frappe.db.get_values("Wishlist Item", filters={"parent": frappe.session.user}) if hasattr(frappe.local, "cookie_manager"): - frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist_items))) \ No newline at end of file + frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist_items))) diff --git a/erpnext/e_commerce/legacy_search.py b/erpnext/e_commerce/legacy_search.py index 752c33e92ee..ef8e86d4428 100644 --- a/erpnext/e_commerce/legacy_search.py +++ b/erpnext/e_commerce/legacy_search.py @@ -9,8 +9,9 @@ from whoosh.query import Prefix # TODO: Make obsolete INDEX_NAME = "products" + class ProductSearch(FullTextSearch): - """ Wrapper for WebsiteSearch """ + """Wrapper for WebsiteSearch""" def get_schema(self): return Schema( @@ -29,7 +30,7 @@ class ProductSearch(FullTextSearch): in www/ and routes from published documents Returns: - self (object): FullTextSearch Instance + self (object): FullTextSearch Instance """ items = get_all_published_items() documents = [self.get_document_to_index(item) for item in items] @@ -69,12 +70,12 @@ class ProductSearch(FullTextSearch): """Search from the current index Args: - text (str): String to search for - scope (str, optional): Scope to limit the search. Defaults to None. - limit (int, optional): Limit number of search results. Defaults to 20. + text (str): String to search for + scope (str, optional): Scope to limit the search. Defaults to None. + limit (int, optional): Limit number of search results. Defaults to 20. Returns: - [List(_dict)]: Search results + [List(_dict)]: Search results """ ix = self.get_index() @@ -111,17 +112,23 @@ class ProductSearch(FullTextSearch): keyword_highlights=keyword_highlights, ) + def get_all_published_items(): - return frappe.get_all("Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code") + return frappe.get_all( + "Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code" + ) + def update_index_for_path(path): search = ProductSearch(INDEX_NAME) return search.update_index_by_name(path) + def remove_document_from_index(path): search = ProductSearch(INDEX_NAME) return search.remove_document_from_index(path) + def build_index_for_all_routes(): search = ProductSearch(INDEX_NAME) return search.build() diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py index 6d44b2cb977..73d51f6281c 100644 --- a/erpnext/e_commerce/product_data_engine/filters.py +++ b/erpnext/e_commerce/product_data_engine/filters.py @@ -14,39 +14,59 @@ class ProductFiltersBuilder: self.item_group = item_group def get_field_filters(self): + from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website + if not self.item_group and not self.doc.enable_field_filters: return fields, filter_data = [], [] - filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings + filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings - # filter valid field filters i.e. those that exist in Item - item_meta = frappe.get_meta('Item', cached=True) - fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] + # filter valid field filters i.e. those that exist in Website Item + web_item_meta = frappe.get_meta("Website Item", cached=True) + fields = [ + web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field) + ] for df in fields: - item_filters, item_or_filters = {}, [] + item_filters, item_or_filters = {"published": 1}, [] link_doctype_values = self.get_filtered_link_doctype_records(df) if df.fieldtype == "Link": if self.item_group: - item_or_filters.extend([ - ["item_group", "=", self.item_group], - ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups - ]) + include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants") + if include_child: + include_groups = get_child_groups_for_website(self.item_group, include_self=True) + include_groups = [x.name for x in include_groups] + item_or_filters.extend( + [ + ["item_group", "in", include_groups], + ["Website Item Group", "item_group", "=", self.item_group], # consider website item groups + ] + ) + else: + item_or_filters.extend( + [ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group], # consider website item groups + ] + ) + + # exclude variants if mentioned in settings + if frappe.db.get_single_value("E Commerce Settings", "hide_variants"): + item_filters["variant_of"] = ["is", "not set"] # Get link field values attached to published items - item_filters['published_in_website'] = 1 item_values = frappe.get_all( - "Item", + "Website Item", fields=[df.fieldname], filters=item_filters, or_filters=item_or_filters, distinct="True", - pluck=df.fieldname + pluck=df.fieldname, ) - values = list(set(item_values) & link_doctype_values) # intersection of both + values = list(set(item_values) & link_doctype_values) # intersection of both else: # table multiselect values = list(link_doctype_values) @@ -62,10 +82,10 @@ class ProductFiltersBuilder: def get_filtered_link_doctype_records(self, field): """ - Get valid link doctype records depending on filters. - Apply enable/disable/show_in_website filter. - Returns: - set: A set containing valid record names + Get valid link doctype records depending on filters. + Apply enable/disable/show_in_website filter. + Returns: + set: A set containing valid record names """ link_doctype = field.get_link_doctype() meta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None @@ -81,12 +101,12 @@ class ProductFiltersBuilder: if not meta: return filters - if meta.has_field('enabled'): - filters['enabled'] = 1 - if meta.has_field('disabled'): - filters['disabled'] = 0 - if meta.has_field('show_in_website'): - filters['show_in_website'] = 1 + if meta.has_field("enabled"): + filters["enabled"] = 1 + if meta.has_field("disabled"): + filters["disabled"] = 0 + if meta.has_field("show_in_website"): + filters["show_in_website"] = 1 return filters @@ -130,11 +150,13 @@ class ProductFiltersBuilder: # [25, 60] rounded min max min_range_absolute, max_range_absolute = floor(min_discount), floor(max_discount) - min_range = int(min_discount - (min_range_absolute % 10)) # 20 - max_range = int(max_discount - (max_range_absolute % 10)) # 60 + min_range = int(min_discount - (min_range_absolute % 10)) # 20 + max_range = int(max_discount - (max_range_absolute % 10)) # 60 - min_range = (min_range + 10) if min_range != min_range_absolute else min_range # 30 (upper limit of 25.89 in range of 10) - max_range = (max_range + 10) if max_range != max_range_absolute else max_range # 60 + min_range = ( + (min_range + 10) if min_range != min_range_absolute else min_range + ) # 30 (upper limit of 25.89 in range of 10) + max_range = (max_range + 10) if max_range != max_range_absolute else max_range # 60 for discount in range(min_range, (max_range + 1), 10): label = f"{discount}% and below" diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index 1a2ddeb0251..9e0bbf4635c 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -13,12 +13,13 @@ class ProductQuery: """Query engine for product listing Attributes: - fields (list): Fields to fetch in query - conditions (string): Conditions for query building - or_conditions (string): Search conditions - page_length (Int): Length of page for the query - settings (Document): E Commerce Settings DocType + fields (list): Fields to fetch in query + conditions (string): Conditions for query building + or_conditions (string): Search conditions + page_length (Int): Length of page for the query + settings (Document): E Commerce Settings DocType """ + def __init__(self): self.settings = frappe.get_doc("E Commerce Settings") self.page_length = self.settings.products_per_page or 20 @@ -26,30 +27,42 @@ class ProductQuery: self.or_filters = [] self.filters = [["published", "=", 1]] self.fields = [ - "web_item_name", "name", "item_name", "item_code", "website_image", - "variant_of", "has_variants", "item_group", "image", "web_long_description", - "short_description", "route", "website_warehouse", "ranking", "on_backorder" + "web_item_name", + "name", + "item_name", + "item_code", + "website_image", + "variant_of", + "has_variants", + "item_group", + "image", + "web_long_description", + "short_description", + "route", + "website_warehouse", + "ranking", + "on_backorder", ] def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """ Args: - attributes (dict, optional): Item Attribute filters - fields (dict, optional): Field level filters - search_term (str, optional): Search term to lookup - start (int, optional): Page start + attributes (dict, optional): Item Attribute filters + fields (dict, optional): Field level filters + search_term (str, optional): Search term to lookup + start (int, optional): Page start Returns: - dict: Dict containing items, item count & discount range + dict: Dict containing items, item count & discount range """ # track if discounts included in field filters self.filter_with_discount = bool(fields.get("discount")) result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 - website_item_groups = self.get_website_item_group_results(item_group, website_item_groups) - if fields: self.build_fields_filters(fields) + if item_group: + self.build_item_group_filters(item_group) if search_term: self.build_search_filters(search_term) if self.settings.hide_variants: @@ -61,8 +74,6 @@ class ProductQuery: else: result, count = self.query_items(start=start) - result = self.combine_web_item_group_results(item_group, result, website_item_groups) - # sort combined results by ranking result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) @@ -77,11 +88,7 @@ class ProductQuery: result = self.filter_results_by_discount(fields, result) - return { - "items": result, - "items_count": count, - "discounts": discounts - } + return {"items": result, "items_count": count, "discounts": discounts} def query_items(self, start=0): """Build a query to fetch Website Items based on field filters.""" @@ -93,8 +100,9 @@ class ProductQuery: filters=self.filters, or_filters=self.or_filters, limit_page_length=184467440737095516, - limit_start=start, # get all items from this offset for total count ahead - order_by="ranking desc") + limit_start=start, # get all items from this offset for total count ahead + order_by="ranking desc", + ) count = len(count_items) # If discounts included, return all rows. @@ -110,7 +118,8 @@ class ProductQuery: or_filters=self.or_filters, limit_page_length=page_length, limit_start=start, - order_by="ranking desc") + order_by="ranking desc", + ) return items, count @@ -129,8 +138,9 @@ class ProductQuery: filters=[ ["published_in_website", "=", 1], ["Item Variant Attribute", "attribute", "=", attribute], - ["Item Variant Attribute", "attribute_value", "in", values] - ]) + ["Item Variant Attribute", "attribute_value", "in", values], + ], + ) item_codes.append({x.item_code for x in item_code_list}) if item_codes: @@ -145,22 +155,22 @@ class ProductQuery: """Build filters for field values Args: - filters (dict): Filters + filters (dict): Filters """ for field, values in filters.items(): if not values or field == "discount": continue # handle multiselect fields in filter addition - meta = frappe.get_meta('Website Item', cached=True) + meta = frappe.get_meta("Website Item", cached=True) df = meta.get_field(field) - if df.fieldtype == 'Table MultiSelect': + if df.fieldtype == "Table MultiSelect": child_doctype = df.options child_meta = frappe.get_meta(child_doctype, cached=True) fields = child_meta.get("fields") if fields: - self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) + self.filters.append([child_doctype, fields[0].fieldname, "IN", values]) elif isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, "in", values]) @@ -168,14 +178,34 @@ class ProductQuery: # `=` will be faster than `IN` for most cases self.filters.append([field, "=", values]) + def build_item_group_filters(self, item_group): + "Add filters for Item group page and include Website Item Groups." + from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website + + item_group_filters = [] + + item_group_filters.append(["Website Item", "item_group", "=", item_group]) + # Consider Website Item Groups + item_group_filters.append(["Website Item Group", "item_group", "=", item_group]) + + if frappe.db.get_value("Item Group", item_group, "include_descendants"): + # include child item group's items as well + # eg. Group Node A, will show items of child 1 and child 2 as well + # on it's web page + include_groups = get_child_groups_for_website(item_group, include_self=True) + include_groups = [x.name for x in include_groups] + item_group_filters.append(["Website Item", "item_group", "in", include_groups]) + + self.or_filters.extend(item_group_filters) + def build_search_filters(self, search_term): """Query search term in specified fields Args: - search_term (str): Search candidate + search_term (str): Search candidate """ # Default fields to search from - default_fields = {'item_code', 'item_name', 'web_long_description', 'item_group'} + default_fields = {"item_code", "item_name", "web_long_description", "item_group"} # Get meta search fields meta = frappe.get_meta("Website Item") @@ -183,35 +213,24 @@ class ProductQuery: # Join the meta fields and default fields set search_fields = default_fields.union(meta_fields) - if frappe.db.count('Website Item', cache=True) > 50000: - search_fields.discard('web_long_description') + if frappe.db.count("Website Item", cache=True) > 50000: + search_fields.discard("web_long_description") # Build or filters for query - search = '%{}%'.format(search_term) + search = "%{}%".format(search_term) for field in search_fields: self.or_filters.append([field, "like", search]) - def get_website_item_group_results(self, item_group, website_item_groups): - """Get Web Items for Item Group Page via Website Item Groups.""" - if item_group: - website_item_groups = frappe.db.get_all( - "Website Item", - fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], - filters=[ - ["Website Item Group", "item_group", "=", item_group], - ["published", "=", 1] - ] - ) - return website_item_groups - def add_display_details(self, result, discount_list, cart_items): """Add price and availability details in result.""" for item in result: - product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') + product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get( + "product_info" + ) - if product_info and product_info['price']: + if product_info and product_info["price"]: # update/mutate item and discount_list objects - self.get_price_discount_info(item, product_info['price'], discount_list) + self.get_price_discount_info(item, product_info["price"], discount_list) if self.settings.show_stock_availability: self.get_stock_availability(item) @@ -219,7 +238,9 @@ class ProductQuery: item.in_cart = item.item_code in cart_items item.wished = False - if frappe.db.exists("Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}): + if frappe.db.exists( + "Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user} + ): item.wished = True return result, discount_list @@ -230,13 +251,14 @@ class ProductQuery: for field in fields: item[field] = price_object.get(field) - if price_object.get('discount_percent'): + if price_object.get("discount_percent"): item.discount_percent = flt(price_object.discount_percent) discount_list.append(price_object.discount_percent) if item.formatted_mrp: - item.discount = price_object.get('formatted_discount_percent') or \ - price_object.get('formatted_discount_rate') + item.discount = price_object.get("formatted_discount_percent") or price_object.get( + "formatted_discount_rate" + ) def get_stock_availability(self, item): """Modify item object and add stock details.""" @@ -256,47 +278,46 @@ class ProductQuery: elif warehouse: # stock item and has warehouse actual_qty = frappe.db.get_value( - "Bin", - {"item_code": item.item_code,"warehouse": item.get("website_warehouse")}, - "actual_qty") + "Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")}, "actual_qty" + ) item.in_stock = bool(flt(actual_qty)) def get_cart_items(self): customer = get_customer(silent=True) if customer: - quotation = frappe.get_all("Quotation", fields=["name"], filters= - {"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0}, - order_by="modified desc", limit_page_length=1) + quotation = frappe.get_all( + "Quotation", + fields=["name"], + filters={ + "party_name": customer, + "contact_email": frappe.session.user, + "order_type": "Shopping Cart", + "docstatus": 0, + }, + order_by="modified desc", + limit_page_length=1, + ) if quotation: items = frappe.get_all( - "Quotation Item", - fields=["item_code"], - filters={ - "parent": quotation[0].get("name") - }) + "Quotation Item", fields=["item_code"], filters={"parent": quotation[0].get("name")} + ) items = [row.item_code for row in items] return items return [] - def combine_web_item_group_results(self, item_group, result, website_item_groups): - """Combine results with context of website item groups into item results.""" - if item_group and website_item_groups: - items_list = {row.name for row in result} - for row in website_item_groups: - if row.wig_parent not in items_list: - result.append(row) - - return result - def filter_results_by_discount(self, fields, result): if fields and fields.get("discount"): discount_percent = frappe.utils.flt(fields["discount"][0]) - result = [row for row in result if row.get("discount_percent") and row.discount_percent <= discount_percent] + result = [ + row + for row in result + if row.get("discount_percent") and row.discount_percent <= discount_percent + ] if self.filter_with_discount: # no limit was added to results while querying # slice results manually - result[:self.page_length] + result[: self.page_length] return result diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py index f0f7918d00e..45bc20ece6e 100644 --- a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py +++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py @@ -10,17 +10,17 @@ from erpnext.e_commerce.doctype.website_item.test_website_item import create_reg test_dependencies = ["Item", "Item Group"] + class TestItemGroupProductDataEngine(unittest.TestCase): "Test Products & Sub-Category Querying for Product Listing on Item Group Page." - @classmethod - def setUpClass(cls): + def setUp(self): item_codes = [ ("Test Mobile A", "_Test Item Group B"), ("Test Mobile B", "_Test Item Group B"), ("Test Mobile C", "_Test Item Group B - 1"), ("Test Mobile D", "_Test Item Group B - 1"), - ("Test Mobile E", "_Test Item Group B - 2") + ("Test Mobile E", "_Test Item Group B - 2"), ] for item in item_codes: item_code = item[0] @@ -28,18 +28,22 @@ class TestItemGroupProductDataEngine(unittest.TestCase): if not frappe.db.exists("Website Item", {"item_code": item_code}): create_regular_web_item(item_code, item_args=item_args) - @classmethod - def tearDownClass(cls): + frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) + frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1) + + def tearDown(self): frappe.db.rollback() def test_product_listing_in_item_group(self): "Test if only products belonging to the Item Group are fetched." - result = get_product_filter_data(query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B" - }) + result = get_product_filter_data( + query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B", + } + ) items = result.get("items") item_codes = [item.get("item_code") for item in items] @@ -53,49 +57,52 @@ class TestItemGroupProductDataEngine(unittest.TestCase): website_item = frappe.get_doc("Website Item", {"item_code": "Test Mobile E"}) # show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well - website_item.append("website_item_groups", { - "item_group": "_Test Item Group B - 1" - }) + website_item.append("website_item_groups", {"item_group": "_Test Item Group B - 1"}) website_item.save() - result = get_product_filter_data(query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B - 1" - }) + result = get_product_filter_data( + query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B - 1", + } + ) items = result.get("items") item_codes = [item.get("item_code") for item in items] self.assertEqual(len(items), 3) - self.assertIn("Test Mobile E", item_codes) # visible in other item groups + self.assertIn("Test Mobile E", item_codes) # visible in other item groups self.assertIn("Test Mobile C", item_codes) self.assertIn("Test Mobile D", item_codes) - result = get_product_filter_data(query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B - 2" - }) + result = get_product_filter_data( + query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B - 2", + } + ) items = result.get("items") self.assertEqual(len(items), 1) - self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group + self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group def test_item_group_with_sub_groups(self): "Test Valid Sub Item Groups in Item Group Page." - frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0) - result = get_product_filter_data(query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B" - }) + result = get_product_filter_data( + query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B", + } + ) self.assertTrue(bool(result.get("sub_categories"))) @@ -104,14 +111,60 @@ class TestItemGroupProductDataEngine(unittest.TestCase): self.assertIn("_Test Item Group B - 1", child_groups) frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1) - result = get_product_filter_data(query_args={ - "field_filters": {}, - "attribute_filters": {}, - "start": 0, - "item_group": "_Test Item Group B" - }) + result = get_product_filter_data( + query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B", + } + ) child_groups = [d.name for d in result.get("sub_categories")] # check if child group is fetched if shown in website self.assertIn("_Test Item Group B - 1", child_groups) - self.assertIn("_Test Item Group B - 2", child_groups) \ No newline at end of file + self.assertIn("_Test Item Group B - 2", child_groups) + + def test_item_group_page_with_descendants_included(self): + """ + Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3). + > _Test Item Group B [Level 1] + > _Test Item Group B - 1 [Level 2] + > _Test Item Group B - 1 - 1 [Level 3] + """ + frappe.get_doc( + { # create Level 3 nested child group + "doctype": "Item Group", + "is_group": 1, + "item_group_name": "_Test Item Group B - 1 - 1", + "parent_item_group": "_Test Item Group B - 1", + } + ).insert() + + create_regular_web_item( # create an item belonging to level 3 item group + "Test Mobile F", item_args={"item_group": "_Test Item Group B - 1 - 1"} + ) + + frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1) + + # enable 'include descendants' in Level 1 + frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1) + + result = get_product_filter_data( + query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B", + } + ) + + items = result.get("items") + item_codes = [item.get("item_code") for item in items] + + # check if all sub groups' items are pulled + self.assertEqual(len(items), 6) + self.assertIn("Test Mobile A", item_codes) + self.assertIn("Test Mobile C", item_codes) + self.assertIn("Test Mobile E", item_codes) + self.assertIn("Test Mobile F", item_codes) diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py index b52e140fcc4..c3b6ed5da25 100644 --- a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py +++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py @@ -14,19 +14,20 @@ from erpnext.e_commerce.product_data_engine.query import ProductQuery test_dependencies = ["Item", "Item Group"] + class TestProductDataEngine(unittest.TestCase): "Test Products Querying and Filters for Product Listing." @classmethod def setUpClass(cls): item_codes = [ - ("Test 11I Laptop", "Products"), # rank 1 - ("Test 12I Laptop", "Products"), # rank 2 - ("Test 13I Laptop", "Products"), # rank 3 - ("Test 14I Laptop", "Raw Material"), # rank 4 - ("Test 15I Laptop", "Raw Material"), # rank 5 - ("Test 16I Laptop", "Raw Material"), # rank 6 - ("Test 17I Laptop", "Products") # rank 7 + ("Test 11I Laptop", "Products"), # rank 1 + ("Test 12I Laptop", "Products"), # rank 2 + ("Test 13I Laptop", "Products"), # rank 3 + ("Test 14I Laptop", "Raw Material"), # rank 4 + ("Test 15I Laptop", "Raw Material"), # rank 5 + ("Test 16I Laptop", "Raw Material"), # rank 6 + ("Test 17I Laptop", "Products"), # rank 7 ] for index, item in enumerate(item_codes, start=1): item_code = item[0] @@ -35,17 +36,19 @@ class TestProductDataEngine(unittest.TestCase): if not frappe.db.exists("Website Item", {"item_code": item_code}): create_regular_web_item(item_code, item_args=item_args, web_args=web_args) - setup_e_commerce_settings({ - "products_per_page": 4, - "enable_field_filters": 1, - "filter_fields": [{"fieldname": "item_group"}], - "enable_attribute_filters": 1, - "filter_attributes": [{"attribute": "Test Size"}], - "company": "_Test Company", - "enabled": 1, - "default_customer_group": "_Test Customer Group", - "price_list": "_Test Price List India" - }) + setup_e_commerce_settings( + { + "products_per_page": 4, + "enable_field_filters": 1, + "filter_fields": [{"fieldname": "item_group"}], + "enable_attribute_filters": 1, + "filter_attributes": [{"attribute": "Test Size"}], + "company": "_Test Company", + "enabled": 1, + "default_customer_group": "_Test Customer Group", + "price_list": "_Test Price List India", + } + ) frappe.local.shopping_cart_settings = None @classmethod @@ -55,13 +58,7 @@ class TestProductDataEngine(unittest.TestCase): def test_product_list_ordering_and_paging(self): "Test if website items appear by ranking on different pages." engine = ProductQuery() - result = engine.query( - attributes={}, - fields={}, - search_term=None, - start=0, - item_group=None - ) + result = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None) items = result.get("items") self.assertIsNotNone(items) @@ -75,13 +72,7 @@ class TestProductDataEngine(unittest.TestCase): self.assertEqual(items[3].get("item_code"), "Test 14I Laptop") # check next page - result = engine.query( - attributes={}, - fields={}, - search_term=None, - start=4, - item_group=None - ) + result = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None) items = result.get("items") # check if items appear as per ranking set in setUpClass on next page @@ -101,13 +92,7 @@ class TestProductDataEngine(unittest.TestCase): frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", 10) engine = ProductQuery() - result = engine.query( - attributes={}, - fields={}, - search_term=None, - start=0, - item_group=None - ) + result = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None) items = result.get("items") # check if item is the first item on the first page @@ -152,11 +137,7 @@ class TestProductDataEngine(unittest.TestCase): engine = ProductQuery() result = engine.query( - attributes={}, - fields=field_filters, - search_term=None, - start=0, - item_group=None + attributes={}, fields=field_filters, search_term=None, start=0, item_group=None ) items = result.get("items") @@ -188,11 +169,7 @@ class TestProductDataEngine(unittest.TestCase): attribute_filters = {"Test Size": ["Large"]} engine = ProductQuery() result = engine.query( - attributes=attribute_filters, - fields={}, - search_term=None, - start=0, - item_group=None + attributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None ) items = result.get("items") @@ -209,24 +186,13 @@ class TestProductDataEngine(unittest.TestCase): item_code = "Test 12I Laptop" make_web_item_price(item_code=item_code) - make_web_pricing_rule( - title=f"Test Pricing Rule for {item_code}", - item_code=item_code, - selling=1 - ) + make_web_pricing_rule(title=f"Test Pricing Rule for {item_code}", item_code=item_code, selling=1) setup_e_commerce_settings({"show_price": 1}) frappe.local.shopping_cart_settings = None - engine = ProductQuery() - result = engine.query( - attributes={}, - fields={}, - search_term=None, - start=4, - item_group=None - ) + result = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None) self.assertTrue(bool(result.get("discounts"))) filter_engine = ProductFiltersBuilder() @@ -247,16 +213,16 @@ class TestProductDataEngine(unittest.TestCase): make_web_item_price(item_code="Test 12I Laptop") make_web_pricing_rule( - title="Test Pricing Rule for Test 12I Laptop", # 10% discount + title="Test Pricing Rule for Test 12I Laptop", # 10% discount item_code="Test 12I Laptop", - selling=1 + selling=1, ) make_web_item_price(item_code="Test 13I Laptop") make_web_pricing_rule( - title="Test Pricing Rule for Test 13I Laptop", # 15% discount + title="Test Pricing Rule for Test 13I Laptop", # 15% discount item_code="Test 13I Laptop", discount_percentage=15, - selling=1 + selling=1, ) setup_e_commerce_settings({"show_price": 1}) @@ -264,11 +230,7 @@ class TestProductDataEngine(unittest.TestCase): engine = ProductQuery() result = engine.query( - attributes={}, - fields=field_filters, - search_term=None, - start=0, - item_group=None + attributes={}, fields=field_filters, search_term=None, start=0, item_group=None ) items = result.get("items") @@ -282,15 +244,13 @@ class TestProductDataEngine(unittest.TestCase): create_variant_web_item() - result = get_product_filter_data(query_args={ - "field_filters": { - "item_group": "Products" - }, - "attribute_filters": { - "Test Size": ["Large"] - }, - "start": 0 - }) + result = get_product_filter_data( + query_args={ + "field_filters": {"item_group": "Products"}, + "attribute_filters": {"Test Size": ["Large"]}, + "start": 0, + } + ) items = result.get("items") @@ -301,20 +261,13 @@ class TestProductDataEngine(unittest.TestCase): "Test if variants are hideen on hiding variants in settings." create_variant_web_item() - setup_e_commerce_settings({ - "enable_attribute_filters": 0, - "hide_variants": 1 - }) + setup_e_commerce_settings({"enable_attribute_filters": 0, "hide_variants": 1}) frappe.local.shopping_cart_settings = None attribute_filters = {"Test Size": ["Large"]} engine = ProductQuery() result = engine.query( - attributes=attribute_filters, - fields={}, - search_term=None, - start=0, - item_group=None + attributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None ) items = result.get("items") @@ -322,10 +275,56 @@ class TestProductDataEngine(unittest.TestCase): self.assertEqual(len(items), 0) # tear down - setup_e_commerce_settings({ - "enable_attribute_filters": 1, - "hide_variants": 0 - }) + setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0}) + + def test_custom_field_as_filter(self): + "Test if custom field functions as filter correctly." + from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + create_custom_field( + "Website Item", + dict( + owner="Administrator", + fieldname="supplier", + label="Supplier", + fieldtype="Link", + options="Supplier", + insert_after="on_backorder", + ), + ) + + frappe.db.set_value( + "Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier" + ) + frappe.db.set_value( + "Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1" + ) + + settings = frappe.get_doc("E Commerce Settings") + settings.append("filter_fields", {"fieldname": "supplier"}) + settings.save() + + filter_engine = ProductFiltersBuilder() + field_filters = filter_engine.get_field_filters() + custom_filter = field_filters[1] + filter_values = custom_filter[1] + + self.assertEqual(custom_filter[0].options, "Supplier") + self.assertEqual(len(filter_values), 2) + self.assertIn("_Test Supplier", filter_values) + + # test if custom filter works in query + field_filters = {"supplier": "_Test Supplier 1"} + engine = ProductQuery() + result = engine.query( + attributes={}, fields=field_filters, search_term=None, start=0, item_group=None + ) + items = result.get("items") + + # check if only 'Raw Material' are fetched in the right order + self.assertEqual(len(items), 1) + self.assertEqual(items[0].get("item_code"), "Test 12I Laptop") + def create_variant_web_item(): "Create Variant and Template Website Items." @@ -333,15 +332,14 @@ def create_variant_web_item(): from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.stock.doctype.item.test_item import make_item - make_item("Test Web Item", { - "has_variant": 1, - "variant_based_on": "Item Attribute", - "attributes": [ - { - "attribute": "Test Size" - } - ] - }) + make_item( + "Test Web Item", + { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [{"attribute": "Test Size"}], + }, + ) if not frappe.db.exists("Item", "Test Web Item-L"): variant = create_variant("Test Web Item", {"Test Size": "Large"}) variant.save() diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js index 99b91afac17..eab1699538e 100644 --- a/erpnext/e_commerce/product_ui/views.js +++ b/erpnext/e_commerce/product_ui/views.js @@ -424,6 +424,22 @@ erpnext.ProductView = class { me.change_route_with_filters(); }); + + // bind filter lookup input box + $('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => { + const $input = $(e.target); + const keyword = ($input.val() || '').toLowerCase(); + const $filter_options = $input.next('.filter-options'); + + $filter_options.find('.filter-lookup-wrapper').show(); + $filter_options.find('.filter-lookup-wrapper').each((i, el) => { + const $el = $(el); + const value = $el.data('value').toLowerCase(); + if (!value.includes(keyword)) { + $el.hide(); + } + }); + }, 300)); } change_route_with_filters() { @@ -501,7 +517,7 @@ erpnext.ProductView = class { categories.forEach(category => { sub_group_html += ` - +
${ category.name }
diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py index 59c7f32fd46..f2dd796f2c5 100644 --- a/erpnext/e_commerce/redisearch_utils.py +++ b/erpnext/e_commerce/redisearch_utils.py @@ -1,73 +1,90 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +import json + import frappe +from frappe import _ from frappe.utils.redis_wrapper import RedisWrapper +from redis import ResponseError from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField -WEBSITE_ITEM_INDEX = 'website_items_index' -WEBSITE_ITEM_KEY_PREFIX = 'website_item:' -WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict' -WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict' +WEBSITE_ITEM_INDEX = "website_items_index" +WEBSITE_ITEM_KEY_PREFIX = "website_item:" +WEBSITE_ITEM_NAME_AUTOCOMPLETE = "website_items_name_dict" +WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = "website_items_category_dict" + def get_indexable_web_fields(): "Return valid fields from Website Item that can be searched for." web_item_meta = frappe.get_meta("Website Item", cached=True) valid_fields = filter( lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"), - web_item_meta.fields) + web_item_meta.fields, + ) return [df.fieldname for df in valid_fields] + +def is_redisearch_enabled(): + "Return True only if redisearch is loaded and enabled." + is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled") + return is_search_module_loaded() and is_redisearch_enabled + + def is_search_module_loaded(): try: cache = frappe.cache() - out = cache.execute_command('MODULE LIST') + out = cache.execute_command("MODULE LIST") parsed_output = " ".join( (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out) ) return "search" in parsed_output except Exception: - return False + return False # handling older redis versions + + +def if_redisearch_enabled(function): + "Decorator to check if Redisearch is enabled." -def if_redisearch_loaded(function): - "Decorator to check if Redisearch is loaded." def wrapper(*args, **kwargs): - if is_search_module_loaded(): + if is_redisearch_enabled(): func = function(*args, **kwargs) return func return return wrapper -def make_key(key): - return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8') -@if_redisearch_loaded +def make_key(key): + return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8") + + +@if_redisearch_enabled def create_website_items_index(): "Creates Index Definition." # CREATE index client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache()) - # DROP if already exists try: - client.drop_index() - except Exception: + client.drop_index() # drop if already exists + except ResponseError: + # will most likely raise a ResponseError if index does not exist + # ignore and create index pass + except Exception: + raise_redisearch_error() idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)]) - # Based on e-commerce settings - idx_fields = frappe.db.get_single_value( - 'E Commerce Settings', - 'search_index_fields' - ) - idx_fields = idx_fields.split(',') if idx_fields else [] + # Index fields mentioned in e-commerce settings + idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields") + idx_fields = idx_fields.split(",") if idx_fields else [] - if 'web_item_name' in idx_fields: - idx_fields.remove('web_item_name') + if "web_item_name" in idx_fields: + idx_fields.remove("web_item_name") idx_fields = list(map(to_search_field, idx_fields)) @@ -79,45 +96,51 @@ def create_website_items_index(): reindex_all_web_items() define_autocomplete_dictionary() + def to_search_field(field): if field == "tags": return TagField("tags", separator=",") return TextField(field) -@if_redisearch_loaded + +@if_redisearch_enabled def insert_item_to_index(website_item_doc): # Insert item to index key = get_cache_key(website_item_doc.name) cache = frappe.cache() web_item = create_web_item_map(website_item_doc) - for k, v in web_item.items(): - super(RedisWrapper, cache).hset(make_key(key), k, v) + for field, value in web_item.items(): + super(RedisWrapper, cache).hset(make_key(key), field, value) insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) -@if_redisearch_loaded + +@if_redisearch_enabled def insert_to_name_ac(web_name, doc_name): ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache()) ac.add_suggestions(Suggestion(web_name, payload=doc_name)) + def create_web_item_map(website_item_doc): fields_to_index = get_fields_indexed() web_item = {} - for f in fields_to_index: - web_item[f] = website_item_doc.get(f) or '' + for field in fields_to_index: + web_item[field] = website_item_doc.get(field) or "" return web_item -@if_redisearch_loaded + +@if_redisearch_enabled def update_index_for_item(website_item_doc): # Reinsert to Cache insert_item_to_index(website_item_doc) define_autocomplete_dictionary() -@if_redisearch_loaded + +@if_redisearch_enabled def delete_item_from_index(website_item_doc): cache = frappe.cache() key = get_cache_key(website_item_doc.name) @@ -125,86 +148,107 @@ def delete_item_from_index(website_item_doc): try: cache.delete(key) except Exception: - return False + raise_redisearch_error() delete_from_ac_dict(website_item_doc) return True -@if_redisearch_loaded + +@if_redisearch_enabled def delete_from_ac_dict(website_item_doc): - '''Removes this items's name from autocomplete dictionary''' + """Removes this items's name from autocomplete dictionary""" cache = frappe.cache() name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) name_ac.delete(website_item_doc.web_item_name) -@if_redisearch_loaded + +@if_redisearch_enabled def define_autocomplete_dictionary(): - """Creates an autocomplete search dictionary for `name`. - Also creats autocomplete dictionary for `categories` if - checked in E Commerce Settings""" + """ + Defines/Redefines an autocomplete search dictionary for Website Item Name. + Also creats autocomplete dictionary for Published Item Groups. + """ cache = frappe.cache() - name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) - cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache) - - ac_categories = frappe.db.get_single_value( - 'E Commerce Settings', - 'show_categories_in_search_autocomplete' - ) + item_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) + item_group_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache) # Delete both autocomplete dicts try: cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE)) cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE)) except Exception: - return False + raise_redisearch_error() + create_items_autocomplete_dict(autocompleter=item_ac) + create_item_groups_autocomplete_dict(autocompleter=item_group_ac) + + +@if_redisearch_enabled +def create_items_autocomplete_dict(autocompleter): + "Add items as suggestions in Autocompleter." items = frappe.get_all( - 'Website Item', - fields=['web_item_name', 'item_group'], - filters={"published": 1} + "Website Item", fields=["web_item_name", "item_group"], filters={"published": 1} ) for item in items: - name_ac.add_suggestions(Suggestion(item.web_item_name)) - if ac_categories and item.item_group: - cat_ac.add_suggestions(Suggestion(item.item_group)) + autocompleter.add_suggestions(Suggestion(item.web_item_name)) - return True -@if_redisearch_loaded -def reindex_all_web_items(): - items = frappe.get_all( - 'Website Item', - fields=get_fields_indexed(), - filters={"published": True} +@if_redisearch_enabled +def create_item_groups_autocomplete_dict(autocompleter): + "Add item groups with weightage as suggestions in Autocompleter." + published_item_groups = frappe.get_all( + "Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1} ) + if not published_item_groups: + return + + for item_group in published_item_groups: + payload = json.dumps({"name": item_group.name, "route": item_group.route}) + autocompleter.add_suggestions( + Suggestion( + string=item_group.name, + score=frappe.utils.flt(item_group.weightage) or 1.0, + payload=payload, # additional info that can be retrieved later + ) + ) + + +@if_redisearch_enabled +def reindex_all_web_items(): + items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True}) cache = frappe.cache() for item in items: web_item = create_web_item_map(item) key = make_key(get_cache_key(item.name)) - for k, v in web_item.items(): - super(RedisWrapper, cache).hset(key, k, v) + for field, value in web_item.items(): + super(RedisWrapper, cache).hset(key, field, value) + def get_cache_key(name): name = frappe.scrub(name) return f"{WEBSITE_ITEM_KEY_PREFIX}{name}" -def get_fields_indexed(): - fields_to_index = frappe.db.get_single_value( - 'E Commerce Settings', - 'search_index_fields' - ) - fields_to_index = fields_to_index.split(',') if fields_to_index else [] - mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail', 'ranking'] +def get_fields_indexed(): + fields_to_index = frappe.db.get_single_value("E Commerce Settings", "search_index_fields") + fields_to_index = fields_to_index.split(",") if fields_to_index else [] + + mandatory_fields = ["name", "web_item_name", "route", "thumbnail", "ranking"] fields_to_index = fields_to_index + mandatory_fields return fields_to_index -# TODO: Remove later -# # Figure out a way to run this at startup -define_autocomplete_dictionary() -create_website_items_index() + +def raise_redisearch_error(): + "Create an Error Log and raise error." + traceback = frappe.get_traceback() + log = frappe.log_error(traceback, frappe._("Redisearch Error")) + log_link = frappe.utils.get_link_to_form("Error Log", log.name) + + frappe.throw( + msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error") + ) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index fff9f079744..134933bd369 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -20,6 +20,7 @@ from erpnext.utilities.product import get_web_item_qty_in_stock class WebsitePriceListMissingError(frappe.ValidationError): pass + def set_cart_count(quotation=None): if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")): if not quotation: @@ -29,6 +30,7 @@ def set_cart_count(quotation=None): if hasattr(frappe.local, "cookie_manager"): frappe.local.cookie_manager.set_cookie("cart_count", cart_count) + @frappe.whitelist() def get_cart_quotation(doc=None): party = get_party() @@ -48,38 +50,46 @@ def get_cart_quotation(doc=None): "shipping_addresses": get_shipping_addresses(party), "billing_addresses": get_billing_addresses(party), "shipping_rules": get_applicable_shipping_rules(party), - "cart_settings": frappe.get_cached_doc("E Commerce Settings") + "cart_settings": frappe.get_cached_doc("E Commerce Settings"), } + @frappe.whitelist() def get_shipping_addresses(party=None): if not party: party = get_party() addresses = get_address_docs(party=party) - return [{"name": address.name, "title": address.address_title, "display": address.display} - for address in addresses if address.address_type == "Shipping" + return [ + {"name": address.name, "title": address.address_title, "display": address.display} + for address in addresses + if address.address_type == "Shipping" ] + @frappe.whitelist() def get_billing_addresses(party=None): if not party: party = get_party() addresses = get_address_docs(party=party) - return [{"name": address.name, "title": address.address_title, "display": address.display} - for address in addresses if address.address_type == "Billing" + return [ + {"name": address.name, "title": address.address_title, "display": address.display} + for address in addresses + if address.address_type == "Billing" ] + @frappe.whitelist() def place_order(): quotation = _get_cart_quotation() - cart_settings = frappe.db.get_value("E Commerce Settings", None, - ["company", "allow_items_not_in_stock"], as_dict=1) + cart_settings = frappe.db.get_value( + "E Commerce Settings", None, ["company", "allow_items_not_in_stock"], as_dict=1 + ) quotation.company = cart_settings.company quotation.flags.ignore_permissions = True quotation.submit() - if quotation.quotation_to == 'Lead' and quotation.party_name: + if quotation.quotation_to == "Lead" and quotation.party_name: # company used to create customer accounts frappe.defaults.set_user_default("company", quotation.company) @@ -87,17 +97,14 @@ def place_order(): frappe.throw(_("Set Shipping Address or Billing Address")) from erpnext.selling.doctype.quotation.quotation import _make_sales_order + sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True)) sales_order.payment_schedule = [] if not cint(cart_settings.allow_items_not_in_stock): for item in sales_order.get("items"): item.warehouse = frappe.db.get_value( - "Website Item", - { - "item_code": item.item_code - }, - "website_warehouse" + "Website Item", {"item_code": item.item_code}, "website_warehouse" ) is_stock_item = frappe.db.get_value("Item", item.item_code, "is_stock_item") @@ -117,13 +124,19 @@ def place_order(): return sales_order.name + @frappe.whitelist() def request_for_quotation(): quotation = _get_cart_quotation() quotation.flags.ignore_permissions = True - quotation.submit() + + if get_shopping_cart_settings().save_quotations_as_draft: + quotation.save() + else: + quotation.submit() return quotation.name + @frappe.whitelist() def update_cart(item_code, qty, additional_notes=None, with_items=False): quotation = _get_cart_quotation() @@ -140,12 +153,15 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): else: quotation_items = quotation.get("items", {"item_code": item_code}) if not quotation_items: - quotation.append("items", { - "doctype": "Quotation Item", - "item_code": item_code, - "qty": qty, - "additional_notes": additional_notes - }) + quotation.append( + "items", + { + "doctype": "Quotation Item", + "item_code": item_code, + "qty": qty, + "additional_notes": additional_notes, + }, + ) else: quotation_items[0].qty = qty quotation_items[0].additional_notes = additional_notes @@ -165,73 +181,75 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): if cint(with_items): context = get_cart_quotation(quotation) return { - "items": frappe.render_template("templates/includes/cart/cart_items.html", - context), - "total": frappe.render_template("templates/includes/cart/cart_items_total.html", - context), - "taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html", - context) + "items": frappe.render_template("templates/includes/cart/cart_items.html", context), + "total": frappe.render_template("templates/includes/cart/cart_items_total.html", context), + "taxes_and_totals": frappe.render_template( + "templates/includes/cart/cart_payment_summary.html", context + ), } else: - return { - 'name': quotation.name - } + return {"name": quotation.name} + @frappe.whitelist() def get_shopping_cart_menu(context=None): if not context: context = get_cart_quotation() - return frappe.render_template('templates/includes/cart/cart_dropdown.html', context) + return frappe.render_template("templates/includes/cart/cart_dropdown.html", context) @frappe.whitelist() def add_new_address(doc): doc = frappe.parse_json(doc) - doc.update({ - 'doctype': 'Address' - }) + doc.update({"doctype": "Address"}) address = frappe.get_doc(doc) address.save(ignore_permissions=True) return address + @frappe.whitelist(allow_guest=True) def create_lead_for_item_inquiry(lead, subject, message): lead = frappe.parse_json(lead) - lead_doc = frappe.new_doc('Lead') + lead_doc = frappe.new_doc("Lead") for fieldname in ("lead_name", "company_name", "email_id", "phone"): lead_doc.set(fieldname, lead.get(fieldname)) - lead_doc.set('lead_owner', '') + lead_doc.set("lead_owner", "") - if not frappe.db.exists('Lead Source', 'Product Inquiry'): - frappe.get_doc({ - 'doctype': 'Lead Source', - 'source_name' : 'Product Inquiry' - }).insert(ignore_permissions=True) + if not frappe.db.exists("Lead Source", "Product Inquiry"): + frappe.get_doc({"doctype": "Lead Source", "source_name": "Product Inquiry"}).insert( + ignore_permissions=True + ) - lead_doc.set('source', 'Product Inquiry') + lead_doc.set("source", "Product Inquiry") try: lead_doc.save(ignore_permissions=True) except frappe.exceptions.DuplicateEntryError: frappe.clear_messages() - lead_doc = frappe.get_doc('Lead', {'email_id': lead['email_id']}) + lead_doc = frappe.get_doc("Lead", {"email_id": lead["email_id"]}) - lead_doc.add_comment('Comment', text=''' + lead_doc.add_comment( + "Comment", + text="""
{subject}

{message}

- '''.format(subject=subject, message=message)) + """.format( + subject=subject, message=message + ), + ) return lead_doc @frappe.whitelist() def get_terms_and_conditions(terms_name): - return frappe.db.get_value('Terms and Conditions', terms_name, 'terms') + return frappe.db.get_value("Terms and Conditions", terms_name, "terms") + @frappe.whitelist() def update_cart_address(address_type, address_name): @@ -248,31 +266,35 @@ def update_cart_address(address_type, address_name): quotation.shipping_address_name = address_name quotation.shipping_address = address_display quotation.customer_address = quotation.customer_address or address_name - address_doc = next((doc for doc in get_shipping_addresses() if doc["name"] == address_name), None) + address_doc = next( + (doc for doc in get_shipping_addresses() if doc["name"] == address_name), None + ) apply_cart_settings(quotation=quotation) quotation.flags.ignore_permissions = True quotation.save() context = get_cart_quotation(quotation) - context['address'] = address_doc + context["address"] = address_doc return { - "taxes": frappe.render_template("templates/includes/order/order_taxes.html", - context), - "address": frappe.render_template("templates/includes/cart/address_card.html", - context) + "taxes": frappe.render_template("templates/includes/order/order_taxes.html", context), + "address": frappe.render_template("templates/includes/cart/address_card.html", context), } + def guess_territory(): territory = None geoip_country = frappe.session.get("session_country") if geoip_country: territory = frappe.db.get_value("Territory", geoip_country) - return territory or \ - frappe.db.get_value("E Commerce Settings", None, "territory") or \ - get_root_of("Territory") + return ( + territory + or frappe.db.get_value("E Commerce Settings", None, "territory") + or get_root_of("Territory") + ) + def decorate_quotation_doc(doc): for d in doc.get("items", []): @@ -285,50 +307,56 @@ def decorate_quotation_doc(doc): "Item", filters={"item_code": item_code}, fieldname=["variant_of", "item_name", "image"], - as_dict=True + as_dict=True, )[0] item_code = variant_data.variant_of fields = fields[1:] d.web_item_name = variant_data.item_name - if variant_data.image: # get image from variant or template web item + if variant_data.image: # get image from variant or template web item d.thumbnail = variant_data.image fields = fields[2:] - d.update(frappe.db.get_value( - "Website Item", - {"item_code": item_code}, - fields, - as_dict=True) - ) + d.update(frappe.db.get_value("Website Item", {"item_code": item_code}, fields, as_dict=True)) return doc def _get_cart_quotation(party=None): - '''Return the open Quotation of type "Shopping Cart" or make a new one''' + """Return the open Quotation of type "Shopping Cart" or make a new one""" if not party: party = get_party() - quotation = frappe.get_all("Quotation", fields=["name"], filters= - {"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0}, - order_by="modified desc", limit_page_length=1) + quotation = frappe.get_all( + "Quotation", + fields=["name"], + filters={ + "party_name": party.name, + "contact_email": frappe.session.user, + "order_type": "Shopping Cart", + "docstatus": 0, + }, + order_by="modified desc", + limit_page_length=1, + ) if quotation: qdoc = frappe.get_doc("Quotation", quotation[0].name) else: company = frappe.db.get_value("E Commerce Settings", None, ["company"]) - qdoc = frappe.get_doc({ - "doctype": "Quotation", - "naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-", - "quotation_to": party.doctype, - "company": company, - "order_type": "Shopping Cart", - "status": "Draft", - "docstatus": 0, - "__islocal": 1, - "party_name": party.name - }) + qdoc = frappe.get_doc( + { + "doctype": "Quotation", + "naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-", + "quotation_to": party.doctype, + "company": company, + "order_type": "Shopping Cart", + "status": "Draft", + "docstatus": 0, + "__islocal": 1, + "party_name": party.name, + } + ) qdoc.contact_person = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) qdoc.contact_email = frappe.session.user @@ -339,6 +367,7 @@ def _get_cart_quotation(party=None): return qdoc + def update_party(fullname, company_name=None, mobile_no=None, phone=None): party = get_party() @@ -366,6 +395,7 @@ def update_party(fullname, company_name=None, mobile_no=None, phone=None): qdoc.flags.ignore_permissions = True qdoc.save() + def apply_cart_settings(party=None, quotation=None): if not party: party = get_party() @@ -382,14 +412,16 @@ def apply_cart_settings(party=None, quotation=None): _apply_shipping_rule(party, quotation, cart_settings) + def set_price_list_and_rate(quotation, cart_settings): """set price list based on billing territory""" _set_price_list(cart_settings, quotation) # reset values - quotation.price_list_currency = quotation.currency = \ - quotation.plc_conversion_rate = quotation.conversion_rate = None + quotation.price_list_currency = ( + quotation.currency + ) = quotation.plc_conversion_rate = quotation.conversion_rate = None for item in quotation.get("items"): item.price_list_rate = item.discount_percentage = item.rate = item.amount = None @@ -400,9 +432,11 @@ def set_price_list_and_rate(quotation, cart_settings): # set it in cookies for using in product page frappe.local.cookie_manager.set_cookie("selling_price_list", quotation.selling_price_list) + def _set_price_list(cart_settings, quotation=None): """Set price list based on customer or shopping cart default""" from erpnext.accounts.party import get_default_price_list + party_name = quotation.get("party_name") if quotation else get_party().get("name") selling_price_list = None @@ -419,23 +453,33 @@ def _set_price_list(cart_settings, quotation=None): return selling_price_list + def set_taxes(quotation, cart_settings): """set taxes based on billing territory""" from erpnext.accounts.party import set_taxes customer_group = frappe.db.get_value("Customer", quotation.party_name, "customer_group") - quotation.taxes_and_charges = set_taxes(quotation.party_name, "Customer", - quotation.transaction_date, quotation.company, customer_group=customer_group, supplier_group=None, - tax_category=quotation.tax_category, billing_address=quotation.customer_address, - shipping_address=quotation.shipping_address_name, use_for_shopping_cart=1) -# -# # clear table + quotation.taxes_and_charges = set_taxes( + quotation.party_name, + "Customer", + quotation.transaction_date, + quotation.company, + customer_group=customer_group, + supplier_group=None, + tax_category=quotation.tax_category, + billing_address=quotation.customer_address, + shipping_address=quotation.shipping_address_name, + use_for_shopping_cart=1, + ) + # + # # clear table quotation.set("taxes", []) -# -# # append taxes + # + # # append taxes quotation.append_taxes_from_master() + def get_party(user=None): if not user: user = frappe.session.user @@ -444,14 +488,14 @@ def get_party(user=None): party = None if contact_name: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) if contact.links: party_doctype = contact.links[0].link_doctype party = contact.links[0].link_name cart_settings = frappe.get_doc("E Commerce Settings") - debtors_account = '' + debtors_account = "" if cart_settings.enable_checkout: debtors_account = get_debtors_account(cart_settings) @@ -465,57 +509,62 @@ def get_party(user=None): raise frappe.Redirect customer = frappe.new_doc("Customer") fullname = get_fullname(user) - customer.update({ - "customer_name": fullname, - "customer_type": "Individual", - "customer_group": get_shopping_cart_settings().default_customer_group, - "territory": get_root_of("Territory") - }) + customer.update( + { + "customer_name": fullname, + "customer_type": "Individual", + "customer_group": get_shopping_cart_settings().default_customer_group, + "territory": get_root_of("Territory"), + } + ) if debtors_account: - customer.update({ - "accounts": [{ - "company": cart_settings.company, - "account": debtors_account - }] - }) + customer.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]}) customer.flags.ignore_mandatory = True customer.insert(ignore_permissions=True) contact = frappe.new_doc("Contact") - contact.update({ - "first_name": fullname, - "email_ids": [{"email_id": user, "is_primary": 1}] - }) - contact.append('links', dict(link_doctype='Customer', link_name=customer.name)) + contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]}) + contact.append("links", dict(link_doctype="Customer", link_name=customer.name)) contact.flags.ignore_mandatory = True contact.insert(ignore_permissions=True) return customer + def get_debtors_account(cart_settings): if not cart_settings.payment_gateway_account: frappe.throw(_("Payment Gateway Account not set"), _("Mandatory")) - payment_gateway_account_currency = \ - frappe.get_doc("Payment Gateway Account", cart_settings.payment_gateway_account).currency + payment_gateway_account_currency = frappe.get_doc( + "Payment Gateway Account", cart_settings.payment_gateway_account + ).currency account_name = _("Debtors ({0})").format(payment_gateway_account_currency) - debtors_account_name = get_account_name("Receivable", "Asset", is_group=0,\ - account_currency=payment_gateway_account_currency, company=cart_settings.company) + debtors_account_name = get_account_name( + "Receivable", + "Asset", + is_group=0, + account_currency=payment_gateway_account_currency, + company=cart_settings.company, + ) if not debtors_account_name: - debtors_account = frappe.get_doc({ - "doctype": "Account", - "account_type": "Receivable", - "root_type": "Asset", - "is_group": 0, - "parent_account": get_account_name(root_type="Asset", is_group=1, company=cart_settings.company), - "account_name": account_name, - "currency": payment_gateway_account_currency - }).insert(ignore_permissions=True) + debtors_account = frappe.get_doc( + { + "doctype": "Account", + "account_type": "Receivable", + "root_type": "Asset", + "is_group": 0, + "parent_account": get_account_name( + root_type="Asset", is_group=1, company=cart_settings.company + ), + "account_name": account_name, + "currency": payment_gateway_account_currency, + } + ).insert(ignore_permissions=True) return debtors_account.name @@ -523,26 +572,31 @@ def get_debtors_account(cart_settings): return debtors_account_name -def get_address_docs(doctype=None, txt=None, filters=None, limit_start=0, limit_page_length=20, - party=None): +def get_address_docs( + doctype=None, txt=None, filters=None, limit_start=0, limit_page_length=20, party=None +): if not party: party = get_party() if not party: return [] - address_names = frappe.db.get_all('Dynamic Link', fields=('parent'), - filters=dict(parenttype='Address', link_doctype=party.doctype, link_name=party.name)) + address_names = frappe.db.get_all( + "Dynamic Link", + fields=("parent"), + filters=dict(parenttype="Address", link_doctype=party.doctype, link_name=party.name), + ) out = [] for a in address_names: - address = frappe.get_doc('Address', a.parent) + address = frappe.get_doc("Address", a.parent) address.display = get_address_display(address.as_dict()) out.append(address) return out + @frappe.whitelist() def apply_shipping_rule(shipping_rule): quotation = _get_cart_quotation() @@ -556,6 +610,7 @@ def apply_shipping_rule(shipping_rule): return get_cart_quotation(quotation) + def _apply_shipping_rule(party=None, quotation=None, cart_settings=None): if not quotation.shipping_rule: shipping_rules = get_shipping_rules(quotation, cart_settings) @@ -570,6 +625,7 @@ def _apply_shipping_rule(party=None, quotation=None, cart_settings=None): quotation.run_method("apply_shipping_rule") quotation.run_method("calculate_taxes_and_totals") + def get_applicable_shipping_rules(party=None, quotation=None): shipping_rules = get_shipping_rules(quotation) @@ -578,6 +634,7 @@ def get_applicable_shipping_rules(party=None, quotation=None): # we need this in sorted order as per the position of the rule in the settings page return [[rule, rule] for rule in shipping_rules] + def get_shipping_rules(quotation=None, cart_settings=None): if not quotation: quotation = _get_cart_quotation() @@ -586,20 +643,23 @@ def get_shipping_rules(quotation=None, cart_settings=None): if quotation.shipping_address_name: country = frappe.db.get_value("Address", quotation.shipping_address_name, "country") if country: - shipping_rules = frappe.db.sql_list("""select distinct sr.name + shipping_rules = frappe.db.sql_list( + """select distinct sr.name from `tabShipping Rule Country` src, `tabShipping Rule` sr where src.country = %s and - sr.disabled != 1 and sr.name = src.parent""", country) + sr.disabled != 1 and sr.name = src.parent""", + country, + ) return shipping_rules + def get_address_territory(address_name): """Tries to match city, state and country of address to existing territory""" territory = None if address_name: - address_fields = frappe.db.get_value("Address", address_name, - ["city", "state", "country"]) + address_fields = frappe.db.get_value("Address", address_name, ["city", "state", "country"]) for value in address_fields: territory = frappe.db.get_value("Territory", value) if territory: @@ -607,9 +667,11 @@ def get_address_territory(address_name): return territory + def show_terms(doc): return doc.tc_name + @frappe.whitelist(allow_guest=True) def apply_coupon_code(applied_code, applied_referral_sales_partner): quotation = True @@ -617,13 +679,14 @@ def apply_coupon_code(applied_code, applied_referral_sales_partner): if not applied_code: frappe.throw(_("Please enter a coupon code")) - coupon_list = frappe.get_all('Coupon Code', filters={'coupon_code': applied_code}) + coupon_list = frappe.get_all("Coupon Code", filters={"coupon_code": applied_code}) if not coupon_list: frappe.throw(_("Please enter a valid coupon code")) coupon_name = coupon_list[0].name from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(coupon_name) quotation = _get_cart_quotation() quotation.coupon_code = coupon_name @@ -631,7 +694,9 @@ def apply_coupon_code(applied_code, applied_referral_sales_partner): quotation.save() if applied_referral_sales_partner: - sales_partner_list = frappe.get_all('Sales Partner', filters={'referral_code': applied_referral_sales_partner}) + sales_partner_list = frappe.get_all( + "Sales Partner", filters={"referral_code": applied_referral_sales_partner} + ) if sales_partner_list: sales_partner_name = sales_partner_list[0].name quotation.referral_sales_partner = sales_partner_name diff --git a/erpnext/e_commerce/shopping_cart/product_info.py b/erpnext/e_commerce/shopping_cart/product_info.py index bde8ca3e145..cdda7f108f1 100644 --- a/erpnext/e_commerce/shopping_cart/product_info.py +++ b/erpnext/e_commerce/shopping_cart/product_info.py @@ -23,16 +23,17 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): cart_settings = get_shopping_cart_settings() if not cart_settings.enabled: # return settings even if cart is disabled - return frappe._dict({ - "product_info": {}, - "cart_settings": cart_settings - }) + return frappe._dict({"product_info": {}, "cart_settings": cart_settings}) cart_quotation = frappe._dict() if not skip_quotation_creation: cart_quotation = _get_cart_quotation() - selling_price_list = cart_quotation.get("selling_price_list") if cart_quotation else _set_price_list(cart_settings, None) + selling_price_list = ( + cart_quotation.get("selling_price_list") + if cart_quotation + else _set_price_list(cart_settings, None) + ) price = {} if cart_settings.show_price: @@ -41,10 +42,7 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): # If not logged in, check if price is hidden for guest. if not is_guest or not cart_settings.hide_price_for_guest: price = get_price( - item_code, - selling_price_list, - cart_settings.default_customer_group, - cart_settings.company + item_code, selling_price_list, cart_settings.default_customer_group, cart_settings.company ) stock_status = None @@ -60,7 +58,7 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): "price": price, "qty": 0, "uom": frappe.db.get_value("Item", item_code, "stock_uom"), - "sales_uom": frappe.db.get_value("Item", item_code, "sales_uom") + "sales_uom": frappe.db.get_value("Item", item_code, "sales_uom"), } if stock_status: @@ -68,7 +66,11 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): product_info["on_backorder"] = True else: product_info["stock_qty"] = stock_status.stock_qty - product_info["in_stock"] = stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse") + product_info["in_stock"] = ( + stock_status.in_stock + if stock_status.is_stock_item + else get_non_stock_item_status(item_code, "website_warehouse") + ) product_info["show_stock_qty"] = show_quantity_in_website() if product_info["price"]: @@ -77,14 +79,14 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): if item: product_info["qty"] = item[0].qty - return frappe._dict({ - "product_info": product_info, - "cart_settings": cart_settings - }) + return frappe._dict({"product_info": product_info, "cart_settings": cart_settings}) + def set_product_info_for_website(item): """set product price uom for website""" - product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get("product_info") + product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get( + "product_info" + ) if product_info: item.update(product_info) diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index ba3a36685df..f4b0d14f8c5 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -5,7 +5,8 @@ import unittest import frappe -from frappe.utils import add_months, nowdate +from frappe.tests.utils import change_settings +from frappe.utils import add_months, cint, nowdate from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule from erpnext.e_commerce.doctype.website_item.website_item import make_website_item @@ -13,44 +14,47 @@ from erpnext.e_commerce.shopping_cart.cart import ( _get_cart_quotation, get_cart_quotation, get_party, + request_for_quotation, update_cart, ) -from erpnext.tests.utils import change_settings, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address class TestShoppingCart(unittest.TestCase): """ - Note: - Shopping Cart == Quotation + Note: + Shopping Cart == Quotation """ - @classmethod - def tearDownClass(cls): - frappe.db.sql("delete from `tabTax Rule`") - def setUp(self): frappe.set_user("Administrator") create_test_contact_and_address() self.enable_shopping_cart() if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}): - make_website_item(frappe.get_cached_doc("Item", "_Test Item")) + make_website_item(frappe.get_cached_doc("Item", "_Test Item")) if not frappe.db.exists("Website Item", {"item_code": "_Test Item 2"}): - make_website_item(frappe.get_cached_doc("Item", "_Test Item 2")) + make_website_item(frappe.get_cached_doc("Item", "_Test Item 2")) def tearDown(self): frappe.db.rollback() frappe.set_user("Administrator") self.disable_shopping_cart() + @classmethod + def tearDownClass(cls): + frappe.db.sql("delete from `tabTax Rule`") + def test_get_cart_new_user(self): self.login_as_new_user() # test if lead is created and quotation with new lead is fetched quotation = _get_cart_quotation() self.assertEqual(quotation.quotation_to, "Customer") - self.assertEqual(quotation.contact_person, - frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com"))) + self.assertEqual( + quotation.contact_person, + frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")), + ) self.assertEqual(quotation.contact_email, frappe.session.user) return quotation @@ -64,7 +68,9 @@ class TestShoppingCart(unittest.TestCase): self.assertEqual(quotation.contact_email, frappe.session.user) return quotation - self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer") + self.login_as_customer( + "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" + ) validate_quotation() self.login_as_customer() @@ -130,46 +136,55 @@ class TestShoppingCart(unittest.TestCase): from erpnext.accounts.party import set_taxes - tax_rule_master = set_taxes(quotation.party_name, "Customer", - quotation.transaction_date, quotation.company, customer_group=None, supplier_group=None, - tax_category=quotation.tax_category, billing_address=quotation.customer_address, - shipping_address=quotation.shipping_address_name, use_for_shopping_cart=1) + tax_rule_master = set_taxes( + quotation.party_name, + "Customer", + quotation.transaction_date, + quotation.company, + customer_group=None, + supplier_group=None, + tax_category=quotation.tax_category, + billing_address=quotation.customer_address, + shipping_address=quotation.shipping_address_name, + use_for_shopping_cart=1, + ) self.assertEqual(quotation.taxes_and_charges, tax_rule_master) self.assertEqual(quotation.total_taxes_and_charges, 1000.0) self.remove_test_quotation(quotation) - @change_settings("E Commerce Settings",{ - "company": "_Test Company", - "enabled": 1, - "default_customer_group": "_Test Customer Group", - "price_list": "_Test Price List India", - "show_price": 1 - }) + @change_settings( + "E Commerce Settings", + { + "company": "_Test Company", + "enabled": 1, + "default_customer_group": "_Test Customer Group", + "price_list": "_Test Price List India", + "show_price": 1, + }, + ) def test_add_item_variant_without_web_item_to_cart(self): "Test adding Variants having no Website Items in cart via Template Web Item." from erpnext.controllers.item_variant import create_variant from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.stock.doctype.item.test_item import make_item - template_item = make_item("Test-Tshirt-Temp", { - "has_variant": 1, - "variant_based_on": "Item Attribute", - "attributes": [ - {"attribute": "Test Size"}, - {"attribute": "Test Colour"} - ] - }) - variant = create_variant("Test-Tshirt-Temp", { - "Test Size": "Small", "Test Colour": "Red" - }) + template_item = make_item( + "Test-Tshirt-Temp", + { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [{"attribute": "Test Size"}, {"attribute": "Test Colour"}], + }, + ) + variant = create_variant("Test-Tshirt-Temp", {"Test Size": "Small", "Test Colour": "Red"}) variant.save() - make_website_item(template_item) # publish template not variant + make_website_item(template_item) # publish template not variant update_cart("Test-Tshirt-Temp-S-R", 1) - cart = get_cart_quotation() # test if cart page gets data without errors + cart = get_cart_quotation() # test if cart page gets data without errors doc = cart.get("doc") self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R") @@ -177,6 +192,26 @@ class TestShoppingCart(unittest.TestCase): # test if items are rendered without error frappe.render_template("templates/includes/cart/cart_items.html", cart) + @change_settings("E Commerce Settings", {"save_quotations_as_draft": 1}) + def test_cart_without_checkout_and_draft_quotation(self): + "Test impact of 'save_quotations_as_draft' checkbox." + frappe.local.shopping_cart_settings = None + + # add item to cart + update_cart("_Test Item", 1) + quote_name = request_for_quotation() # Request for Quote + quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus")) + + self.assertEqual(quote_doctstatus, 0) + + frappe.db.set_value("E Commerce Settings", None, "save_quotations_as_draft", 0) + frappe.local.shopping_cart_settings = None + update_cart("_Test Item", 1) + quote_name = request_for_quotation() # Request for Quote + quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus")) + + self.assertEqual(quote_doctstatus, 1) + def create_tax_rule(self): tax_rule = frappe.get_test_records("Tax Rule")[0] try: @@ -196,16 +231,13 @@ class TestShoppingCart(unittest.TestCase): "contact_email": frappe.session.user, "selling_price_list": "_Test Price List Rest of the World", "currency": "USD", - "taxes_and_charges" : "_Test Tax 1 - _TC", - "conversion_rate":1, - "transaction_date" : nowdate(), - "valid_till" : add_months(nowdate(), 1), - "items": [{ - "item_code": "_Test Item", - "qty": 1 - }], + "taxes_and_charges": "_Test Tax 1 - _TC", + "conversion_rate": 1, + "transaction_date": nowdate(), + "valid_till": add_months(nowdate(), 1), + "items": [{"item_code": "_Test Item", "qty": 1}], "taxes": frappe.get_doc("Sales Taxes and Charges Template", "_Test Tax 1 - _TC").taxes, - "company": "_Test Company" + "company": "_Test Company", } quotation.update(values) @@ -222,29 +254,36 @@ class TestShoppingCart(unittest.TestCase): def enable_shopping_cart(self): settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings") - settings.update({ - "enabled": 1, - "company": "_Test Company", - "default_customer_group": "_Test Customer Group", - "quotation_series": "_T-Quotation-", - "price_list": "_Test Price List India" - }) + settings.update( + { + "enabled": 1, + "company": "_Test Company", + "default_customer_group": "_Test Customer Group", + "quotation_series": "_T-Quotation-", + "price_list": "_Test Price List India", + } + ) # insert item price - if not frappe.db.get_value("Item Price", {"price_list": "_Test Price List India", - "item_code": "_Test Item"}): - frappe.get_doc({ - "doctype": "Item Price", - "price_list": "_Test Price List India", - "item_code": "_Test Item", - "price_list_rate": 10 - }).insert() - frappe.get_doc({ - "doctype": "Item Price", - "price_list": "_Test Price List India", - "item_code": "_Test Item 2", - "price_list_rate": 20 - }).insert() + if not frappe.db.get_value( + "Item Price", {"price_list": "_Test Price List India", "item_code": "_Test Item"} + ): + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "_Test Price List India", + "item_code": "_Test Item", + "price_list_rate": 10, + } + ).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "_Test Price List India", + "item_code": "_Test Item 2", + "price_list_rate": 20, + } + ).insert() settings.save() frappe.local.shopping_cart_settings = None @@ -259,31 +298,49 @@ class TestShoppingCart(unittest.TestCase): self.create_user_if_not_exists("test_cart_user@example.com") frappe.set_user("test_cart_user@example.com") - def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"): + def login_as_customer( + self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer" + ): self.create_user_if_not_exists(email, name) frappe.set_user(email) def clear_existing_quotations(self): - quotations = frappe.get_all("Quotation", filters={ - "party_name": get_party().name, - "order_type": "Shopping Cart", - "docstatus": 0 - }, order_by="modified desc", pluck="name") + quotations = frappe.get_all( + "Quotation", + filters={"party_name": get_party().name, "order_type": "Shopping Cart", "docstatus": 0}, + order_by="modified desc", + pluck="name", + ) for quotation in quotations: frappe.delete_doc("Quotation", quotation, ignore_permissions=True, force=True) - def create_user_if_not_exists(self, email, first_name = None): + def create_user_if_not_exists(self, email, first_name=None): if frappe.db.exists("User", email): return - frappe.get_doc({ - "doctype": "User", - "user_type": "Website User", - "email": email, - "send_welcome_email": 0, - "first_name": first_name or email.split("@")[0] - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "User", + "user_type": "Website User", + "email": email, + "send_welcome_email": 0, + "first_name": first_name or email.split("@")[0], + } + ).insert(ignore_permissions=True) -test_dependencies = ["Sales Taxes and Charges Template", "Price List", "Item Price", "Shipping Rule", "Currency Exchange", - "Customer Group", "Lead", "Customer", "Contact", "Address", "Item", "Tax Rule"] + +test_dependencies = [ + "Sales Taxes and Charges Template", + "Price List", + "Item Price", + "Shipping Rule", + "Currency Exchange", + "Customer Group", + "Lead", + "Customer", + "Contact", + "Address", + "Item", + "Tax Rule", +] diff --git a/erpnext/e_commerce/shopping_cart/utils.py b/erpnext/e_commerce/shopping_cart/utils.py index 0cc0ab7c002..d6bc4a3682d 100644 --- a/erpnext/e_commerce/shopping_cart/utils.py +++ b/erpnext/e_commerce/shopping_cart/utils.py @@ -7,12 +7,15 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import i def show_cart_count(): - if (is_cart_enabled() and - frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User"): + if ( + is_cart_enabled() + and frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User" + ): return True return False + def set_cart_count(login_manager): # since this is run only on hooks login event # make sure user is already a customer @@ -29,21 +32,24 @@ def set_cart_count(login_manager): # cart count is calculated from this quotation's items set_cart_count() + def clear_cart_count(login_manager): if show_cart_count(): frappe.local.cookie_manager.delete_cookie("cart_count") + def update_website_context(context): cart_enabled = is_cart_enabled() context["shopping_cart_enabled"] = cart_enabled + def is_customer(): if frappe.session.user and frappe.session.user != "Guest": contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user}) if contact_name: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) for link in contact.links: - if link.link_doctype == 'Customer': + if link.link_doctype == "Customer": return True return False diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 3107c019e62..f8439d5d43d 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -6,63 +6,60 @@ class ItemVariantsCacheManager: self.item_code = item_code def get_item_variants_data(self): - val = frappe.cache().hget('item_variants_data', self.item_code) + val = frappe.cache().hget("item_variants_data", self.item_code) if not val: self.build_cache() - return frappe.cache().hget('item_variants_data', self.item_code) - + return frappe.cache().hget("item_variants_data", self.item_code) def get_attribute_value_item_map(self): - val = frappe.cache().hget('attribute_value_item_map', self.item_code) + val = frappe.cache().hget("attribute_value_item_map", self.item_code) if not val: self.build_cache() - return frappe.cache().hget('attribute_value_item_map', self.item_code) - + return frappe.cache().hget("attribute_value_item_map", self.item_code) def get_item_attribute_value_map(self): - val = frappe.cache().hget('item_attribute_value_map', self.item_code) + val = frappe.cache().hget("item_attribute_value_map", self.item_code) if not val: self.build_cache() - return frappe.cache().hget('item_attribute_value_map', self.item_code) - + return frappe.cache().hget("item_attribute_value_map", self.item_code) def get_optional_attributes(self): - val = frappe.cache().hget('optional_attributes', self.item_code) + val = frappe.cache().hget("optional_attributes", self.item_code) if not val: self.build_cache() - return frappe.cache().hget('optional_attributes', self.item_code) + return frappe.cache().hget("optional_attributes", self.item_code) def get_ordered_attribute_values(self): - val = frappe.cache().get_value('ordered_attribute_values_map') - if val: return val + val = frappe.cache().get_value("ordered_attribute_values_map") + if val: + return val - all_attribute_values = frappe.get_all('Item Attribute Value', - ['attribute_value', 'idx', 'parent'], order_by='idx asc') + all_attribute_values = frappe.get_all( + "Item Attribute Value", ["attribute_value", "idx", "parent"], order_by="idx asc" + ) ordered_attribute_values_map = frappe._dict({}) for d in all_attribute_values: ordered_attribute_values_map.setdefault(d.parent, []).append(d.attribute_value) - frappe.cache().set_value('ordered_attribute_values_map', ordered_attribute_values_map) + frappe.cache().set_value("ordered_attribute_values_map", ordered_attribute_values_map) return ordered_attribute_values_map def build_cache(self): parent_item_code = self.item_code attributes = [ - a.attribute for a in frappe.get_all( - 'Item Variant Attribute', - {'parent': parent_item_code}, - ['attribute'], - order_by='idx asc' + a.attribute + for a in frappe.get_all( + "Item Variant Attribute", {"parent": parent_item_code}, ["attribute"], order_by="idx asc" ) ] @@ -71,13 +68,11 @@ class ItemVariantsCacheManager: item = frappe.qb.DocType("Item") query = ( frappe.qb.from_(iva) - .join(item).on(item.name == iva.parent) - .select( - iva.parent, iva.attribute, iva.attribute_value - ).where( - (iva.variant_of == parent_item_code) - & (item.disabled == 0) - ).orderby(iva.name) + .join(item) + .on(item.name == iva.parent) + .select(iva.parent, iva.attribute, iva.attribute_value) + .where((iva.variant_of == parent_item_code) & (item.disabled == 0)) + .orderby(iva.name) ) item_variants_data = query.run() @@ -97,13 +92,18 @@ class ItemVariantsCacheManager: if attribute not in attr_dict: optional_attributes.add(attribute) - frappe.cache().hset('attribute_value_item_map', parent_item_code, attribute_value_item_map) - frappe.cache().hset('item_attribute_value_map', parent_item_code, item_attribute_value_map) - frappe.cache().hset('item_variants_data', parent_item_code, item_variants_data) - frappe.cache().hset('optional_attributes', parent_item_code, optional_attributes) + frappe.cache().hset("attribute_value_item_map", parent_item_code, attribute_value_item_map) + frappe.cache().hset("item_attribute_value_map", parent_item_code, item_attribute_value_map) + frappe.cache().hset("item_variants_data", parent_item_code, item_variants_data) + frappe.cache().hset("optional_attributes", parent_item_code, optional_attributes) def clear_cache(self): - keys = ['attribute_value_item_map', 'item_attribute_value_map', 'item_variants_data', 'optional_attributes'] + keys = [ + "attribute_value_item_map", + "item_attribute_value_map", + "item_variants_data", + "optional_attributes", + ] for key in keys: frappe.cache().hdel(key, self.item_code) @@ -114,15 +114,17 @@ class ItemVariantsCacheManager: def build_cache(item_code): - frappe.cache().hset('item_cache_build_in_progress', item_code, 1) + frappe.cache().hset("item_cache_build_in_progress", item_code, 1) i = ItemVariantsCacheManager(item_code) i.build_cache() - frappe.cache().hset('item_cache_build_in_progress', item_code, 0) + frappe.cache().hset("item_cache_build_in_progress", item_code, 0) + def enqueue_build_cache(item_code): - if frappe.cache().hget('item_cache_build_in_progress', item_code): + if frappe.cache().hget("item_cache_build_in_progress", item_code): return frappe.enqueue( "erpnext.e_commerce.variant_selector.item_variants_cache.build_cache", - item_code=item_code, queue='long' + item_code=item_code, + queue="long", ) diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index 967be838e67..b74c7470c08 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -1,6 +1,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.controllers.item_variant import create_variant from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( @@ -9,44 +10,45 @@ from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings imp from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values from erpnext.stock.doctype.item.test_item import make_item -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Item"] -class TestVariantSelector(ERPNextTestCase): +class TestVariantSelector(FrappeTestCase): @classmethod def setUpClass(cls): - template_item = make_item("Test-Tshirt-Temp", { - "has_variant": 1, - "variant_based_on": "Item Attribute", - "attributes": [ - {"attribute": "Test Size"}, - {"attribute": "Test Colour"} - ] - }) + template_item = make_item( + "Test-Tshirt-Temp", + { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [{"attribute": "Test Size"}, {"attribute": "Test Colour"}], + }, + ) # create L-R, L-G, M-R, M-G and S-R - for size in ("Large", "Medium",): - for colour in ("Red", "Green",): - variant = create_variant("Test-Tshirt-Temp", { - "Test Size": size, "Test Colour": colour - }) + for size in ( + "Large", + "Medium", + ): + for colour in ( + "Red", + "Green", + ): + variant = create_variant("Test-Tshirt-Temp", {"Test Size": size, "Test Colour": colour}) variant.save() - variant = create_variant("Test-Tshirt-Temp", { - "Test Size": "Small", "Test Colour": "Red" - }) + variant = create_variant("Test-Tshirt-Temp", {"Test Size": "Small", "Test Colour": "Red"}) variant.save() - make_website_item(template_item) # publish template not variants + make_website_item(template_item) # publish template not variants def test_item_attributes(self): """ - Test if the right attributes are fetched in the popup. - (Attributes must only come from active items) + Test if the right attributes are fetched in the popup. + (Attributes must only come from active items) - Attribute selection must not be linked to Website Items. + Attribute selection must not be linked to Website Items. """ from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values @@ -54,14 +56,14 @@ class TestVariantSelector(ERPNextTestCase): self.assertEqual(attr_data[0]["attribute"], "Test Size") self.assertEqual(attr_data[1]["attribute"], "Test Colour") - self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large'] - self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green'] + self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large'] + self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green'] # disable small red tshirt, now there are no small tshirts. # but there are some red tshirts small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R") small_variant.disabled = 1 - small_variant.save() # trigger cache rebuild + small_variant.save() # trigger cache rebuild attr_data = get_attributes_and_values("Test-Tshirt-Temp") @@ -74,14 +76,16 @@ class TestVariantSelector(ERPNextTestCase): def test_next_item_variant_values(self): """ - Test if on selecting an attribute value, the next possible values - are filtered accordingly. - Values that dont apply should not be fetched. - E.g. - There is a ** Small-Red ** Tshirt. No other colour in this size. - On selecting ** Small **, only ** Red ** should be selectable next. + Test if on selecting an attribute value, the next possible values + are filtered accordingly. + Values that dont apply should not be fetched. + E.g. + There is a ** Small-Red ** Tshirt. No other colour in this size. + On selecting ** Small **, only ** Red ** should be selectable next. """ - next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"}) + next_values = get_next_attribute_and_values( + "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"} + ) next_colours = next_values["valid_options_for_attributes"]["Test Colour"] filtered_items = next_values["filtered_items"] @@ -92,30 +96,31 @@ class TestVariantSelector(ERPNextTestCase): def test_exact_match_with_price(self): """ - Test price fetching and matching of variant without Website Item + Test price fetching and matching of variant without Website Item """ from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price frappe.set_user("Administrator") - setup_e_commerce_settings({ - "company": "_Test Company", - "enabled": 1, - "default_customer_group": "_Test Customer Group", - "price_list": "_Test Price List India", - "show_price": 1 - }) + setup_e_commerce_settings( + { + "company": "_Test Company", + "enabled": 1, + "default_customer_group": "_Test Customer Group", + "price_list": "_Test Price List India", + "show_price": 1, + } + ) make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100) - frappe.local.shopping_cart_settings = None # clear cached settings values + frappe.local.shopping_cart_settings = None # clear cached settings values next_values = get_next_attribute_and_values( - "Test-Tshirt-Temp", - selected_attributes={"Test Size": "Small", "Test Colour": "Red"} + "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small", "Test Colour": "Red"} ) print(">>>>", next_values) price_info = next_values["product_info"]["price"] - self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R") - self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R") + self.assertEqual(next_values["exact_match"][0], "Test-Tshirt-Temp-S-R") + self.assertEqual(next_values["exact_match"][0], "Test-Tshirt-Temp-S-R") self.assertEqual(price_info["price_list_rate"], 100.0) - self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00") \ No newline at end of file + self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00") diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py index 5caa4d0819f..323920478c4 100644 --- a/erpnext/e_commerce/variant_selector/utils.py +++ b/erpnext/e_commerce/variant_selector/utils.py @@ -24,18 +24,18 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None): wheres = [] query_values = [] for attribute_value in attribute_values: - wheres.append('( attribute = %s and attribute_value = %s )') + wheres.append("( attribute = %s and attribute_value = %s )") query_values += [attribute, attribute_value] - attribute_query = ' or '.join(wheres) + attribute_query = " or ".join(wheres) if template_item_code: - variant_of_query = 'AND t2.variant_of = %s' + variant_of_query = "AND t2.variant_of = %s" query_values.append(template_item_code) else: - variant_of_query = '' + variant_of_query = "" - query = ''' + query = """ SELECT t1.parent FROM @@ -58,7 +58,9 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None): t1.parent ORDER BY NULL - '''.format(attribute_query=attribute_query, variant_of_query=variant_of_query) + """.format( + attribute_query=attribute_query, variant_of_query=variant_of_query + ) item_codes = set([r[0] for r in frappe.db.sql(query, query_values)]) items.append(item_codes) @@ -67,11 +69,12 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None): return res + @frappe.whitelist(allow_guest=True) def get_attributes_and_values(item_code): - '''Build a list of attributes and their possible values. + """Build a list of attributes and their possible values. This will ignore the values upon selection of which there cannot exist one item. - ''' + """ item_cache = ItemVariantsCacheManager(item_code) item_variants_data = item_cache.get_item_variants_data() @@ -83,8 +86,9 @@ def get_attributes_and_values(item_code): if attribute in attribute_list: valid_options.setdefault(attribute, set()).add(attribute_value) - item_attribute_values = frappe.db.get_all('Item Attribute Value', - ['parent', 'attribute_value', 'idx'], order_by='parent asc, idx asc') + item_attribute_values = frappe.db.get_all( + "Item Attribute Value", ["parent", "attribute_value", "idx"], order_by="parent asc, idx asc" + ) ordered_attribute_value_map = frappe._dict() for iv in item_attribute_values: ordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value) @@ -93,18 +97,18 @@ def get_attributes_and_values(item_code): for attr in attributes: valid_attribute_values = valid_options.get(attr.attribute, []) ordered_values = ordered_attribute_value_map.get(attr.attribute, []) - attr['values'] = [v for v in ordered_values if v in valid_attribute_values] + attr["values"] = [v for v in ordered_values if v in valid_attribute_values] return attributes @frappe.whitelist(allow_guest=True) def get_next_attribute_and_values(item_code, selected_attributes): - '''Find the count of Items that match the selected attributes. + """Find the count of Items that match the selected attributes. Also, find the attribute values that are not applicable for further searching. If less than equal to 10 items are found, return item_codes of those items. If one item is matched exactly, return item_code of that item. - ''' + """ selected_attributes = frappe.parse_json(selected_attributes) item_cache = ItemVariantsCacheManager(item_code) @@ -133,7 +137,11 @@ def get_next_attribute_and_values(item_code, selected_attributes): for row in item_variants_data: item_code, attribute, attribute_value = row - if item_code in filtered_items and attribute not in selected_attributes and attribute in attribute_list: + if ( + item_code in filtered_items + and attribute not in selected_attributes + and attribute in attribute_list + ): valid_options_for_attributes[attribute].add(attribute_value) optional_attributes = item_cache.get_optional_attributes() @@ -159,12 +167,12 @@ def get_next_attribute_and_values(item_code, selected_attributes): product_info = None return { - 'next_attribute': next_attribute, - 'valid_options_for_attributes': valid_options_for_attributes, - 'filtered_items_count': filtered_items_count, - 'filtered_items': filtered_items if filtered_items_count < 10 else [], - 'exact_match': exact_match, - 'product_info': product_info + "next_attribute": next_attribute, + "valid_options_for_attributes": valid_options_for_attributes, + "filtered_items_count": filtered_items_count, + "filtered_items": filtered_items if filtered_items_count < 10 else [], + "exact_match": exact_match, + "product_info": product_info, } @@ -179,16 +187,16 @@ def get_items_with_selected_attributes(item_code, selected_attributes): return set.intersection(*items) + # utilities + def get_item_attributes(item_code): - attributes = frappe.db.get_all('Item Variant Attribute', - fields=['attribute'], - filters={ - 'parenttype': 'Item', - 'parent': item_code - }, - order_by='idx asc' + attributes = frappe.db.get_all( + "Item Variant Attribute", + fields=["attribute"], + filters={"parenttype": "Item", "parent": item_code}, + order_by="idx asc", ) optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes() @@ -199,6 +207,7 @@ def get_item_attributes(item_code): return attributes + def get_item_variant_price_dict(item_code, cart_settings): if cart_settings.enabled and cart_settings.show_price: is_guest = frappe.session.user == "Guest" @@ -207,12 +216,8 @@ def get_item_variant_price_dict(item_code, cart_settings): if not is_guest or not cart_settings.hide_price_for_guest: price_list = _set_price_list(cart_settings, None) price = get_price( - item_code, - price_list, - cart_settings.default_customer_group, - cart_settings.company + item_code, price_list, cart_settings.default_customer_group, cart_settings.company ) return {"price": price} return None - diff --git a/erpnext/education/__init__.py b/erpnext/education/__init__.py index cf8efde7df3..f6f84b37302 100644 --- a/erpnext/education/__init__.py +++ b/erpnext/education/__init__.py @@ -1,12 +1,17 @@ - import frappe from frappe import _ -class StudentNotInGroupError(frappe.ValidationError): pass +class StudentNotInGroupError(frappe.ValidationError): + pass + def validate_student_belongs_to_group(student, student_group): - groups = frappe.db.get_all('Student Group Student', ['parent'], dict(student = student, active=1)) + groups = frappe.db.get_all("Student Group Student", ["parent"], dict(student=student, active=1)) if not student_group in [d.parent for d in groups]: - frappe.throw(_('Student {0} does not belong to group {1}').format(frappe.bold(student), frappe.bold(student_group)), - StudentNotInGroupError) + frappe.throw( + _("Student {0} does not belong to group {1}").format( + frappe.bold(student), frappe.bold(student_group) + ), + StudentNotInGroupError, + ) diff --git a/erpnext/education/api.py b/erpnext/education/api.py index 636b948a1cc..8fd1725b2de 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -12,11 +12,12 @@ from frappe.utils import cstr, flt, getdate def get_course(program): - '''Return list of courses for a particular program + """Return list of courses for a particular program :param program: Program - ''' - courses = frappe.db.sql('''select course, course_name from `tabProgram Course` where parent=%s''', - (program), as_dict=1) + """ + courses = frappe.db.sql( + """select course, course_name from `tabProgram Course` where parent=%s""", (program), as_dict=1 + ) return courses @@ -26,24 +27,24 @@ def enroll_student(source_name): :param source_name: Student Applicant. """ - frappe.publish_realtime('enroll_student_progress', {"progress": [1, 4]}, user=frappe.session.user) - student = get_mapped_doc("Student Applicant", source_name, - {"Student Applicant": { - "doctype": "Student", - "field_map": { - "name": "student_applicant" - } - }}, ignore_permissions=True) + frappe.publish_realtime("enroll_student_progress", {"progress": [1, 4]}, user=frappe.session.user) + student = get_mapped_doc( + "Student Applicant", + source_name, + {"Student Applicant": {"doctype": "Student", "field_map": {"name": "student_applicant"}}}, + ignore_permissions=True, + ) student.save() - student_applicant = frappe.db.get_value("Student Applicant", source_name, - ["student_category", "program"], as_dict=True) + student_applicant = frappe.db.get_value( + "Student Applicant", source_name, ["student_category", "program"], as_dict=True + ) program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment.student = student.name program_enrollment.student_category = student_applicant.student_category program_enrollment.student_name = student.title program_enrollment.program = student_applicant.program - frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) + frappe.publish_realtime("enroll_student_progress", {"progress": [2, 4]}, user=frappe.session.user) return program_enrollment @@ -58,11 +59,15 @@ def check_attendance_records_exist(course_schedule=None, student_group=None, dat if course_schedule: return frappe.get_list("Student Attendance", filters={"course_schedule": course_schedule}) else: - return frappe.get_list("Student Attendance", filters={"student_group": student_group, "date": date}) + return frappe.get_list( + "Student Attendance", filters={"student_group": student_group, "date": date} + ) @frappe.whitelist() -def mark_attendance(students_present, students_absent, course_schedule=None, student_group=None, date=None): +def mark_attendance( + students_present, students_absent, course_schedule=None, student_group=None, date=None +): """Creates Multiple Attendance Records. :param students_present: Students Present JSON. @@ -73,26 +78,36 @@ def mark_attendance(students_present, students_absent, course_schedule=None, stu """ if student_group: - academic_year = frappe.db.get_value('Student Group', student_group, 'academic_year') + academic_year = frappe.db.get_value("Student Group", student_group, "academic_year") if academic_year: - year_start_date, year_end_date = frappe.db.get_value('Academic Year', academic_year, ['year_start_date', 'year_end_date']) + year_start_date, year_end_date = frappe.db.get_value( + "Academic Year", academic_year, ["year_start_date", "year_end_date"] + ) if getdate(date) < getdate(year_start_date) or getdate(date) > getdate(year_end_date): - frappe.throw(_('Attendance cannot be marked outside of Academic Year {0}').format(academic_year)) + frappe.throw( + _("Attendance cannot be marked outside of Academic Year {0}").format(academic_year) + ) present = json.loads(students_present) absent = json.loads(students_absent) for d in present: - make_attendance_records(d["student"], d["student_name"], "Present", course_schedule, student_group, date) + make_attendance_records( + d["student"], d["student_name"], "Present", course_schedule, student_group, date + ) for d in absent: - make_attendance_records(d["student"], d["student_name"], "Absent", course_schedule, student_group, date) + make_attendance_records( + d["student"], d["student_name"], "Absent", course_schedule, student_group, date + ) frappe.db.commit() frappe.msgprint(_("Attendance has been marked successfully.")) -def make_attendance_records(student, student_name, status, course_schedule=None, student_group=None, date=None): +def make_attendance_records( + student, student_name, status, course_schedule=None, student_group=None, date=None +): """Creates/Update Attendance Record. :param student: Student. @@ -100,13 +115,15 @@ def make_attendance_records(student, student_name, status, course_schedule=None, :param course_schedule: Course Schedule. :param status: Status (Present/Absent) """ - student_attendance = frappe.get_doc({ - "doctype": "Student Attendance", - "student": student, - "course_schedule": course_schedule, - "student_group": student_group, - "date": date - }) + student_attendance = frappe.get_doc( + { + "doctype": "Student Attendance", + "student": student, + "course_schedule": course_schedule, + "student_group": student_group, + "date": date, + } + ) if not student_attendance: student_attendance = frappe.new_doc("Student Attendance") student_attendance.student = student @@ -125,8 +142,7 @@ def get_student_guardians(student): :param student: Student. """ - guardians = frappe.get_all("Student Guardian", fields=["guardian"] , - filters={"parent": student}) + guardians = frappe.get_all("Student Guardian", fields=["guardian"], filters={"parent": student}) return guardians @@ -137,11 +153,19 @@ def get_student_group_students(student_group, include_inactive=0): :param student_group: Student Group. """ if include_inactive: - students = frappe.get_all("Student Group Student", fields=["student", "student_name"] , - filters={"parent": student_group}, order_by= "group_roll_number") + students = frappe.get_all( + "Student Group Student", + fields=["student", "student_name"], + filters={"parent": student_group}, + order_by="group_roll_number", + ) else: - students = frappe.get_all("Student Group Student", fields=["student", "student_name"] , - filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") + students = frappe.get_all( + "Student Group Student", + fields=["student", "student_name"], + filters={"parent": student_group, "active": 1}, + order_by="group_roll_number", + ) return students @@ -152,8 +176,9 @@ def get_fee_structure(program, academic_term=None): :param program: Program. :param academic_term: Academic Term. """ - fee_structure = frappe.db.get_values("Fee Structure", {"program": program, - "academic_term": academic_term}, 'name', as_dict=True) + fee_structure = frappe.db.get_values( + "Fee Structure", {"program": program, "academic_term": academic_term}, "name", as_dict=True + ) return fee_structure[0].name if fee_structure else None @@ -164,7 +189,12 @@ def get_fee_components(fee_structure): :param fee_structure: Fee Structure. """ if fee_structure: - fs = frappe.get_all("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx") + fs = frappe.get_all( + "Fee Component", + fields=["fees_category", "description", "amount"], + filters={"parent": fee_structure}, + order_by="idx", + ) return fs @@ -175,8 +205,12 @@ def get_fee_schedule(program, student_category=None): :param program: Program. :param student_category: Student Category """ - fs = frappe.get_all("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] , - filters={"parent": program, "student_category": student_category }, order_by= "idx") + fs = frappe.get_all( + "Program Fee", + fields=["academic_term", "fee_structure", "due_date", "amount"], + filters={"parent": program, "student_category": student_category}, + order_by="idx", + ) return fs @@ -198,18 +232,23 @@ def get_course_schedule_events(start, end, filters=None): :param filters: Filters (JSON). """ from frappe.desk.calendar import get_event_conditions + conditions = get_event_conditions("Course Schedule", filters) - data = frappe.db.sql("""select name, course, color, + data = frappe.db.sql( + """select name, course, color, timestamp(schedule_date, from_time) as from_time, timestamp(schedule_date, to_time) as to_time, room, student_group, 0 as 'allDay' from `tabCourse Schedule` where ( schedule_date between %(start)s and %(end)s ) - {conditions}""".format(conditions=conditions), { - "start": start, - "end": end - }, as_dict=True, update={"allDay": 0}) + {conditions}""".format( + conditions=conditions + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) return data @@ -220,8 +259,12 @@ def get_assessment_criteria(course): :param Course: Course """ - return frappe.get_all("Course Assessment Criteria", - fields=["assessment_criteria", "weightage"], filters={"parent": course}, order_by= "idx") + return frappe.get_all( + "Course Assessment Criteria", + fields=["assessment_criteria", "weightage"], + filters={"parent": course}, + order_by="idx", + ) @frappe.whitelist() @@ -233,17 +276,14 @@ def get_assessment_students(assessment_plan, student_group): student_result = {} for d in result.details: student_result.update({d.assessment_criteria: [cstr(d.score), d.grade]}) - student_result.update({ - "total_score": [cstr(result.total_score), result.grade], - "comment": result.comment - }) - student.update({ - "assessment_details": student_result, - "docstatus": result.docstatus, - "name": result.name - }) + student_result.update( + {"total_score": [cstr(result.total_score), result.grade], "comment": result.comment} + ) + student.update( + {"assessment_details": student_result, "docstatus": result.docstatus, "name": result.name} + ) else: - student.update({'assessment_details': None}) + student.update({"assessment_details": None}) return student_list @@ -253,8 +293,12 @@ def get_assessment_details(assessment_plan): :param Assessment Plan: Assessment Plan """ - return frappe.get_all("Assessment Plan Criteria", - fields=["assessment_criteria", "maximum_score", "docstatus"], filters={"parent": assessment_plan}, order_by= "idx") + return frappe.get_all( + "Assessment Plan Criteria", + fields=["assessment_criteria", "maximum_score", "docstatus"], + filters={"parent": assessment_plan}, + order_by="idx", + ) @frappe.whitelist() @@ -264,8 +308,10 @@ def get_result(student, assessment_plan): :param Student: Student :param Assessment Plan: Assessment Plan """ - results = frappe.get_all("Assessment Result", filters={"student": student, - "assessment_plan": assessment_plan, "docstatus": ("!=", 2)}) + results = frappe.get_all( + "Assessment Result", + filters={"student": student, "assessment_plan": assessment_plan, "docstatus": ("!=", 2)}, + ) if results: return frappe.get_doc("Assessment Result", results[0]) else: @@ -280,11 +326,13 @@ def get_grade(grading_scale, percentage): :param Percentage: Score Percentage Percentage """ grading_scale_intervals = {} - if not hasattr(frappe.local, 'grading_scale'): - grading_scale = frappe.get_all("Grading Scale Interval", fields=["grade_code", "threshold"], filters={"parent": grading_scale}) + if not hasattr(frappe.local, "grading_scale"): + grading_scale = frappe.get_all( + "Grading Scale Interval", fields=["grade_code", "threshold"], filters={"parent": grading_scale} + ) frappe.local.grading_scale = grading_scale for d in frappe.local.grading_scale: - grading_scale_intervals.update({d.threshold:d.grade_code}) + grading_scale_intervals.update({d.threshold: d.grade_code}) intervals = sorted(grading_scale_intervals.keys(), key=float, reverse=True) for interval in intervals: if flt(percentage) >= interval: @@ -297,21 +345,22 @@ def get_grade(grading_scale, percentage): @frappe.whitelist() def mark_assessment_result(assessment_plan, scores): - student_score = json.loads(scores); + student_score = json.loads(scores) assessment_details = [] for criteria in student_score.get("assessment_details"): - assessment_details.append({ - "assessment_criteria": criteria, - "score": flt(student_score["assessment_details"][criteria]) - }) + assessment_details.append( + {"assessment_criteria": criteria, "score": flt(student_score["assessment_details"][criteria])} + ) assessment_result = get_assessment_result_doc(student_score["student"], assessment_plan) - assessment_result.update({ - "student": student_score.get("student"), - "assessment_plan": assessment_plan, - "comment": student_score.get("comment"), - "total_score":student_score.get("total_score"), - "details": assessment_details - }) + assessment_result.update( + { + "student": student_score.get("student"), + "assessment_plan": assessment_plan, + "comment": student_score.get("comment"), + "total_score": student_score.get("total_score"), + "details": assessment_details, + } + ) assessment_result.save() details = {} for d in assessment_result.details: @@ -321,7 +370,7 @@ def mark_assessment_result(assessment_plan, scores): "student": assessment_result.student, "total_score": assessment_result.total_score, "grade": assessment_result.grade, - "details": details + "details": details, } return assessment_result_dict @@ -332,15 +381,17 @@ def submit_assessment_results(assessment_plan, student_group): student_list = get_student_group_students(student_group) for i, student in enumerate(student_list): doc = get_result(student.student, assessment_plan) - if doc and doc.docstatus==0: + if doc and doc.docstatus == 0: total_result += 1 doc.submit() return total_result def get_assessment_result_doc(student, assessment_plan): - assessment_result = frappe.get_all("Assessment Result", filters={"student": student, - "assessment_plan": assessment_plan, "docstatus": ("!=", 2)}) + assessment_result = frappe.get_all( + "Assessment Result", + filters={"student": student, "assessment_plan": assessment_plan, "docstatus": ("!=", 2)}, + ) if assessment_result: doc = frappe.get_doc("Assessment Result", assessment_result[0]) if doc.docstatus == 0: @@ -369,10 +420,12 @@ def update_email_group(doctype, name): email_list.append(email) add_subscribers(name, email_list) + @frappe.whitelist() def get_current_enrollment(student, academic_year=None): current_academic_year = academic_year or frappe.defaults.get_defaults().academic_year - program_enrollment_list = frappe.db.sql(''' + program_enrollment_list = frappe.db.sql( + """ select name as program_enrollment, student_name, program, student_batch_name as student_batch, student_category, academic_term, academic_year @@ -380,7 +433,10 @@ def get_current_enrollment(student, academic_year=None): `tabProgram Enrollment` where student = %s and academic_year = %s - order by creation''', (student, current_academic_year), as_dict=1) + order by creation""", + (student, current_academic_year), + as_dict=1, + ) if program_enrollment_list: return program_enrollment_list[0] diff --git a/erpnext/education/doctype/academic_term/academic_term.py b/erpnext/education/doctype/academic_term/academic_term.py index 93861ca78af..b44fe99a30a 100644 --- a/erpnext/education/doctype/academic_term/academic_term.py +++ b/erpnext/education/doctype/academic_term/academic_term.py @@ -9,32 +9,62 @@ from frappe.utils import getdate class AcademicTerm(Document): - def autoname(self): - self.name = self.academic_year + " ({})".format(self.term_name) if self.term_name else "" + def autoname(self): + self.name = self.academic_year + " ({})".format(self.term_name) if self.term_name else "" - def validate(self): - #Check if entry with same academic_year and the term_name already exists - validate_duplication(self) - self.title = self.academic_year + " ({})".format(self.term_name) if self.term_name else "" + def validate(self): + # Check if entry with same academic_year and the term_name already exists + validate_duplication(self) + self.title = self.academic_year + " ({})".format(self.term_name) if self.term_name else "" - #Check that start of academic year is earlier than end of academic year - if self.term_start_date and self.term_end_date \ - and getdate(self.term_start_date) > getdate(self.term_end_date): - frappe.throw(_("The Term End Date cannot be earlier than the Term Start Date. Please correct the dates and try again.")) + # Check that start of academic year is earlier than end of academic year + if ( + self.term_start_date + and self.term_end_date + and getdate(self.term_start_date) > getdate(self.term_end_date) + ): + frappe.throw( + _( + "The Term End Date cannot be earlier than the Term Start Date. Please correct the dates and try again." + ) + ) - # Check that the start of the term is not before the start of the academic year + # Check that the start of the term is not before the start of the academic year # and end of term is not after the end of the academic year""" - year = frappe.get_doc("Academic Year",self.academic_year) - if self.term_start_date and getdate(year.year_start_date) and (getdate(self.term_start_date) < getdate(year.year_start_date)): - frappe.throw(_("The Term Start Date cannot be earlier than the Year Start Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again.").format(self.academic_year)) + year = frappe.get_doc("Academic Year", self.academic_year) + if ( + self.term_start_date + and getdate(year.year_start_date) + and (getdate(self.term_start_date) < getdate(year.year_start_date)) + ): + frappe.throw( + _( + "The Term Start Date cannot be earlier than the Year Start Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again." + ).format(self.academic_year) + ) - if self.term_end_date and getdate(year.year_end_date) and (getdate(self.term_end_date) > getdate(year.year_end_date)): - frappe.throw(_("The Term End Date cannot be later than the Year End Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again.").format(self.academic_year)) + if ( + self.term_end_date + and getdate(year.year_end_date) + and (getdate(self.term_end_date) > getdate(year.year_end_date)) + ): + frappe.throw( + _( + "The Term End Date cannot be later than the Year End Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again." + ).format(self.academic_year) + ) def validate_duplication(self): - term = frappe.db.sql("""select name from `tabAcademic Term` where academic_year= %s and term_name= %s - and docstatus<2 and name != %s""", (self.academic_year, self.term_name, self.name)) - if term: - frappe.throw(_("An academic term with this 'Academic Year' {0} and 'Term Name' {1} already exists. Please modify these entries and try again.").format(self.academic_year,self.term_name)) + term = frappe.db.sql( + """select name from `tabAcademic Term` where academic_year= %s and term_name= %s + and docstatus<2 and name != %s""", + (self.academic_year, self.term_name, self.name), + ) + if term: + frappe.throw( + _( + "An academic term with this 'Academic Year' {0} and 'Term Name' {1} already exists. Please modify these entries and try again." + ).format(self.academic_year, self.term_name) + ) diff --git a/erpnext/education/doctype/academic_term/academic_term_dashboard.py b/erpnext/education/doctype/academic_term/academic_term_dashboard.py index 97f581a1e02..348c04d3f6e 100644 --- a/erpnext/education/doctype/academic_term/academic_term_dashboard.py +++ b/erpnext/education/doctype/academic_term/academic_term_dashboard.py @@ -1,26 +1,13 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'academic_term', - 'transactions': [ - { - 'label': _('Student'), - 'items': ['Student Applicant', 'Student Group', 'Student Log'] - }, - { - 'label': _('Fee'), - 'items': ['Fees', 'Fee Schedule', 'Fee Structure'] - }, - { - 'label': _('Program'), - 'items': ['Program Enrollment'] - }, - { - 'label': _('Assessment'), - 'items': ['Assessment Plan', 'Assessment Result'] - } - ] + "fieldname": "academic_term", + "transactions": [ + {"label": _("Student"), "items": ["Student Applicant", "Student Group", "Student Log"]}, + {"label": _("Fee"), "items": ["Fees", "Fee Schedule", "Fee Structure"]}, + {"label": _("Program"), "items": ["Program Enrollment"]}, + {"label": _("Assessment"), "items": ["Assessment Plan", "Assessment Result"]}, + ], } diff --git a/erpnext/education/doctype/academic_term/test_academic_term.py b/erpnext/education/doctype/academic_term/test_academic_term.py index 0e39fb03d47..b4516b3a425 100644 --- a/erpnext/education/doctype/academic_term/test_academic_term.py +++ b/erpnext/education/doctype/academic_term/test_academic_term.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Academic Term') + class TestAcademicTerm(unittest.TestCase): pass diff --git a/erpnext/education/doctype/academic_year/academic_year.py b/erpnext/education/doctype/academic_year/academic_year.py index e2010fb1b05..2a0438b7756 100644 --- a/erpnext/education/doctype/academic_year/academic_year.py +++ b/erpnext/education/doctype/academic_year/academic_year.py @@ -8,7 +8,11 @@ from frappe.model.document import Document class AcademicYear(Document): - def validate(self): - #Check that start of academic year is earlier than end of academic year - if self.year_start_date and self.year_end_date and self.year_start_date > self.year_end_date: - frappe.throw(_("The Year End Date cannot be earlier than the Year Start Date. Please correct the dates and try again.")) + def validate(self): + # Check that start of academic year is earlier than end of academic year + if self.year_start_date and self.year_end_date and self.year_start_date > self.year_end_date: + frappe.throw( + _( + "The Year End Date cannot be earlier than the Year Start Date. Please correct the dates and try again." + ) + ) diff --git a/erpnext/education/doctype/academic_year/academic_year_dashboard.py b/erpnext/education/doctype/academic_year/academic_year_dashboard.py index 3615fd1c374..c69c97017a8 100644 --- a/erpnext/education/doctype/academic_year/academic_year_dashboard.py +++ b/erpnext/education/doctype/academic_year/academic_year_dashboard.py @@ -1,26 +1,16 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'academic_year', - 'transactions': [ + "fieldname": "academic_year", + "transactions": [ { - 'label': _('Student'), - 'items': ['Student Admission', 'Student Applicant', 'Student Group', 'Student Log'] + "label": _("Student"), + "items": ["Student Admission", "Student Applicant", "Student Group", "Student Log"], }, - { - 'label': _('Fee'), - 'items': ['Fees', 'Fee Schedule', 'Fee Structure'] - }, - { - 'label': _('Academic Term and Program'), - 'items': ['Academic Term', 'Program Enrollment'] - }, - { - 'label': _('Assessment'), - 'items': ['Assessment Plan', 'Assessment Result'] - } - ] + {"label": _("Fee"), "items": ["Fees", "Fee Schedule", "Fee Structure"]}, + {"label": _("Academic Term and Program"), "items": ["Academic Term", "Program Enrollment"]}, + {"label": _("Assessment"), "items": ["Assessment Plan", "Assessment Result"]}, + ], } diff --git a/erpnext/education/doctype/academic_year/test_academic_year.py b/erpnext/education/doctype/academic_year/test_academic_year.py index 6d33fe6d7d5..e51cd0e2355 100644 --- a/erpnext/education/doctype/academic_year/test_academic_year.py +++ b/erpnext/education/doctype/academic_year/test_academic_year.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Academic Year') + class TestAcademicYear(unittest.TestCase): pass diff --git a/erpnext/education/doctype/article/article.py b/erpnext/education/doctype/article/article.py index 8f1a2e33b34..12b6618e732 100644 --- a/erpnext/education/doctype/article/article.py +++ b/erpnext/education/doctype/article/article.py @@ -10,11 +10,12 @@ class Article(Document): def get_article(self): pass + @frappe.whitelist() def get_topics_without_article(article): data = [] - for entry in frappe.db.get_all('Topic'): - topic = frappe.get_doc('Topic', entry.name) + for entry in frappe.db.get_all("Topic"): + topic = frappe.get_doc("Topic", entry.name) topic_contents = [tc.content for tc in topic.topic_content] if not topic_contents or article not in topic_contents: data.append(topic.name) diff --git a/erpnext/education/doctype/assessment_criteria/assessment_criteria.py b/erpnext/education/doctype/assessment_criteria/assessment_criteria.py index 58448ea9418..ef9692a8eb0 100644 --- a/erpnext/education/doctype/assessment_criteria/assessment_criteria.py +++ b/erpnext/education/doctype/assessment_criteria/assessment_criteria.py @@ -8,6 +8,7 @@ from frappe.model.document import Document STD_CRITERIA = ["total", "total score", "total grade", "maximum score", "score", "grade"] + class AssessmentCriteria(Document): def validate(self): if self.assessment_criteria.lower() in STD_CRITERIA: diff --git a/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.py b/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.py index 40ba0e7816a..90ff5754451 100644 --- a/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.py +++ b/erpnext/education/doctype/assessment_criteria/test_assessment_criteria.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Assessment Criteria') + class TestAssessmentCriteria(unittest.TestCase): pass diff --git a/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.py b/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.py index ccf82bad04a..b52aef83819 100644 --- a/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.py +++ b/erpnext/education/doctype/assessment_criteria_group/test_assessment_criteria_group.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Assessment Criteria Group') + class TestAssessmentCriteriaGroup(unittest.TestCase): pass diff --git a/erpnext/education/doctype/assessment_group/assessment_group_dashboard.py b/erpnext/education/doctype/assessment_group/assessment_group_dashboard.py index 956809179bc..7c75100b708 100644 --- a/erpnext/education/doctype/assessment_group/assessment_group_dashboard.py +++ b/erpnext/education/doctype/assessment_group/assessment_group_dashboard.py @@ -6,11 +6,6 @@ from frappe import _ def get_data(): return { - 'fieldname': 'assessment_group', - 'transactions': [ - { - 'label': _('Assessment'), - 'items': ['Assessment Plan', 'Assessment Result'] - } - ] + "fieldname": "assessment_group", + "transactions": [{"label": _("Assessment"), "items": ["Assessment Plan", "Assessment Result"]}], } diff --git a/erpnext/education/doctype/assessment_group/test_assessment_group.py b/erpnext/education/doctype/assessment_group/test_assessment_group.py index 6e840aa2510..6f05caae946 100644 --- a/erpnext/education/doctype/assessment_group/test_assessment_group.py +++ b/erpnext/education/doctype/assessment_group/test_assessment_group.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Assessment Group') + class TestAssessmentGroup(unittest.TestCase): pass diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan.py b/erpnext/education/doctype/assessment_plan/assessment_plan.py index 82a28de1cb9..e1265b42087 100644 --- a/erpnext/education/doctype/assessment_plan/assessment_plan.py +++ b/erpnext/education/doctype/assessment_plan/assessment_plan.py @@ -18,14 +18,14 @@ class AssessmentPlan(Document): from erpnext.education.utils import validate_overlap_for - #Validate overlapping course schedules. + # Validate overlapping course schedules. if self.student_group: validate_overlap_for(self, "Course Schedule", "student_group") validate_overlap_for(self, "Course Schedule", "instructor") validate_overlap_for(self, "Course Schedule", "room") - #validate overlapping assessment schedules. + # validate overlapping assessment schedules. if self.student_group: validate_overlap_for(self, "Assessment Plan", "student_group") @@ -37,14 +37,24 @@ class AssessmentPlan(Document): for d in self.assessment_criteria: max_score += d.maximum_score if self.maximum_assessment_score != max_score: - frappe.throw(_("Sum of Scores of Assessment Criteria needs to be {0}.").format(self.maximum_assessment_score)) + frappe.throw( + _("Sum of Scores of Assessment Criteria needs to be {0}.").format( + self.maximum_assessment_score + ) + ) def validate_assessment_criteria(self): - assessment_criteria_list = frappe.db.sql_list(''' select apc.assessment_criteria + assessment_criteria_list = frappe.db.sql_list( + """ select apc.assessment_criteria from `tabAssessment Plan` ap , `tabAssessment Plan Criteria` apc where ap.name = apc.parent and ap.course=%s and ap.student_group=%s and ap.assessment_group=%s - and ap.name != %s and ap.docstatus=1''', (self.course, self.student_group, self.assessment_group, self.name)) + and ap.name != %s and ap.docstatus=1""", + (self.course, self.student_group, self.assessment_group, self.name), + ) for d in self.assessment_criteria: if d.assessment_criteria in assessment_criteria_list: - frappe.throw(_("You have already assessed for the assessment criteria {}.") - .format(frappe.bold(d.assessment_criteria))) + frappe.throw( + _("You have already assessed for the assessment criteria {}.").format( + frappe.bold(d.assessment_criteria) + ) + ) diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py b/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py index 31b9509f191..f9c583a80fc 100644 --- a/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py +++ b/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py @@ -6,17 +6,7 @@ from frappe import _ def get_data(): return { - 'fieldname': 'assessment_plan', - 'transactions': [ - { - 'label': _('Assessment'), - 'items': ['Assessment Result'] - } - ], - 'reports': [ - { - 'label': _('Report'), - 'items': ['Assessment Plan Status'] - } - ] + "fieldname": "assessment_plan", + "transactions": [{"label": _("Assessment"), "items": ["Assessment Result"]}], + "reports": [{"label": _("Report"), "items": ["Assessment Plan Status"]}], } diff --git a/erpnext/education/doctype/assessment_plan/test_assessment_plan.py b/erpnext/education/doctype/assessment_plan/test_assessment_plan.py index 9f55a78667b..e294c50ce48 100644 --- a/erpnext/education/doctype/assessment_plan/test_assessment_plan.py +++ b/erpnext/education/doctype/assessment_plan/test_assessment_plan.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Assessment Plan') + class TestAssessmentPlan(unittest.TestCase): pass diff --git a/erpnext/education/doctype/assessment_result/assessment_result.py b/erpnext/education/doctype/assessment_result/assessment_result.py index 8278b9eebab..1d485c139e7 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result.py +++ b/erpnext/education/doctype/assessment_result/assessment_result.py @@ -33,12 +33,23 @@ class AssessmentResult(Document): def validate_grade(self): self.total_score = 0.0 for d in self.details: - d.grade = get_grade(self.grading_scale, (flt(d.score)/d.maximum_score)*100) + d.grade = get_grade(self.grading_scale, (flt(d.score) / d.maximum_score) * 100) self.total_score += d.score - self.grade = get_grade(self.grading_scale, (self.total_score/self.maximum_score)*100) + self.grade = get_grade(self.grading_scale, (self.total_score / self.maximum_score) * 100) def validate_duplicate(self): - assessment_result = frappe.get_list("Assessment Result", filters={"name": ("not in", [self.name]), - "student":self.student, "assessment_plan":self.assessment_plan, "docstatus":("!=", 2)}) + assessment_result = frappe.get_list( + "Assessment Result", + filters={ + "name": ("not in", [self.name]), + "student": self.student, + "assessment_plan": self.assessment_plan, + "docstatus": ("!=", 2), + }, + ) if assessment_result: - frappe.throw(_("Assessment Result record {0} already exists.").format(getlink("Assessment Result",assessment_result[0].name))) + frappe.throw( + _("Assessment Result record {0} already exists.").format( + getlink("Assessment Result", assessment_result[0].name) + ) + ) diff --git a/erpnext/education/doctype/assessment_result/assessment_result_dashboard.py b/erpnext/education/doctype/assessment_result/assessment_result_dashboard.py index 3b07417b88d..5501f9221a2 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result_dashboard.py +++ b/erpnext/education/doctype/assessment_result/assessment_result_dashboard.py @@ -6,10 +6,7 @@ from frappe import _ def get_data(): return { - 'reports': [ - { - 'label': _('Reports'), - 'items': ['Final Assessment Grades', 'Course wise Assessment Report'] - } + "reports": [ + {"label": _("Reports"), "items": ["Final Assessment Grades", "Course wise Assessment Report"]} ] } diff --git a/erpnext/education/doctype/assessment_result/test_assessment_result.py b/erpnext/education/doctype/assessment_result/test_assessment_result.py index c0872dfb06d..4872f48eda0 100644 --- a/erpnext/education/doctype/assessment_result/test_assessment_result.py +++ b/erpnext/education/doctype/assessment_result/test_assessment_result.py @@ -7,6 +7,7 @@ from erpnext.education.api import get_grade # test_records = frappe.get_test_records('Assessment Result') + class TestAssessmentResult(unittest.TestCase): def test_grade(self): grade = get_grade("_Test Grading Scale", 80) diff --git a/erpnext/education/doctype/course/course.py b/erpnext/education/doctype/course/course.py index 2d4f28226a6..baf72e8cb78 100644 --- a/erpnext/education/doctype/course/course.py +++ b/erpnext/education/doctype/course/course.py @@ -19,12 +19,12 @@ class Course(Document): for criteria in self.assessment_criteria: total_weightage += criteria.weightage or 0 if total_weightage != 100: - frappe.throw(_('Total Weightage of all Assessment Criteria must be 100%')) + frappe.throw(_("Total Weightage of all Assessment Criteria must be 100%")) def get_topics(self): - topic_data= [] + topic_data = [] for topic in self.topics: - topic_doc = frappe.get_doc('Topic', topic.topic) + topic_doc = frappe.get_doc("Topic", topic.topic) if topic_doc.topic_content: topic_data.append(topic_doc) return topic_data @@ -34,23 +34,25 @@ class Course(Document): def add_course_to_programs(course, programs, mandatory=False): programs = json.loads(programs) for entry in programs: - program = frappe.get_doc('Program', entry) - program.append('courses', { - 'course': course, - 'course_name': course, - 'mandatory': mandatory - }) + program = frappe.get_doc("Program", entry) + program.append("courses", {"course": course, "course_name": course, "mandatory": mandatory}) program.flags.ignore_mandatory = True program.save() frappe.db.commit() - frappe.msgprint(_('Course {0} has been added to all the selected programs successfully.').format(frappe.bold(course)), - title=_('Programs updated'), indicator='green') + frappe.msgprint( + _("Course {0} has been added to all the selected programs successfully.").format( + frappe.bold(course) + ), + title=_("Programs updated"), + indicator="green", + ) + @frappe.whitelist() def get_programs_without_course(course): data = [] - for entry in frappe.db.get_all('Program'): - program = frappe.get_doc('Program', entry.name) + for entry in frappe.db.get_all("Program"): + program = frappe.get_doc("Program", entry.name) courses = [c.course for c in program.courses] if not courses or course not in courses: data.append(program.name) diff --git a/erpnext/education/doctype/course/course_dashboard.py b/erpnext/education/doctype/course/course_dashboard.py index 276830f38ae..6ba44750797 100644 --- a/erpnext/education/doctype/course/course_dashboard.py +++ b/erpnext/education/doctype/course/course_dashboard.py @@ -6,19 +6,13 @@ from frappe import _ def get_data(): return { - 'fieldname': 'course', - 'transactions': [ + "fieldname": "course", + "transactions": [ { - 'label': _('Program and Course'), - 'items': ['Program', 'Course Enrollment', 'Course Schedule'] + "label": _("Program and Course"), + "items": ["Program", "Course Enrollment", "Course Schedule"], }, - { - 'label': _('Student'), - 'items': ['Student Group'] - }, - { - 'label': _('Assessment'), - 'items': ['Assessment Plan', 'Assessment Result'] - }, - ] + {"label": _("Student"), "items": ["Student Group"]}, + {"label": _("Assessment"), "items": ["Assessment Plan", "Assessment Result"]}, + ], } diff --git a/erpnext/education/doctype/course/test_course.py b/erpnext/education/doctype/course/test_course.py index 6381cdb11b2..caddefef3e5 100644 --- a/erpnext/education/doctype/course/test_course.py +++ b/erpnext/education/doctype/course/test_course.py @@ -9,10 +9,11 @@ from erpnext.education.doctype.topic.test_topic import make_topic, make_topic_an # test_records = frappe.get_test_records('Course') + class TestCourse(unittest.TestCase): def setUp(self): - make_topic_and_linked_content("_Test Topic 1", [{"type":"Article", "name": "_Test Article 1"}]) - make_topic_and_linked_content("_Test Topic 2", [{"type":"Article", "name": "_Test Article 2"}]) + make_topic_and_linked_content("_Test Topic 1", [{"type": "Article", "name": "_Test Article 1"}]) + make_topic_and_linked_content("_Test Topic 2", [{"type": "Article", "name": "_Test Article 2"}]) make_course_and_linked_topic("_Test Course 1", ["_Test Topic 1", "_Test Topic 2"]) def test_get_topics(self): @@ -22,17 +23,15 @@ class TestCourse(unittest.TestCase): self.assertEqual(topics[1].name, "_Test Topic 2") frappe.db.rollback() + def make_course(name): try: course = frappe.get_doc("Course", name) except frappe.DoesNotExistError: - course = frappe.get_doc({ - "doctype": "Course", - "course_name": name, - "course_code": name - }).insert() + course = frappe.get_doc({"doctype": "Course", "course_name": name, "course_code": name}).insert() return course.name + def make_course_and_linked_topic(course_name, topic_name_list): try: course = frappe.get_doc("Course", course_name) diff --git a/erpnext/education/doctype/course_activity/course_activity.py b/erpnext/education/doctype/course_activity/course_activity.py index c1d82427dd5..784260d07c0 100644 --- a/erpnext/education/doctype/course_activity/course_activity.py +++ b/erpnext/education/doctype/course_activity/course_activity.py @@ -11,7 +11,6 @@ class CourseActivity(Document): def validate(self): self.check_if_enrolled() - def check_if_enrolled(self): if frappe.db.exists("Course Enrollment", self.enrollment): return True diff --git a/erpnext/education/doctype/course_activity/test_course_activity.py b/erpnext/education/doctype/course_activity/test_course_activity.py index 9514ff1bda9..677b60a8456 100644 --- a/erpnext/education/doctype/course_activity/test_course_activity.py +++ b/erpnext/education/doctype/course_activity/test_course_activity.py @@ -9,16 +9,22 @@ import frappe class TestCourseActivity(unittest.TestCase): pass + def make_course_activity(enrollment, content_type, content): - activity = frappe.get_all("Course Activity", filters={'enrollment': enrollment, 'content_type': content_type, 'content': content}) + activity = frappe.get_all( + "Course Activity", + filters={"enrollment": enrollment, "content_type": content_type, "content": content}, + ) try: - activity = frappe.get_doc("Course Activity", activity[0]['name']) + activity = frappe.get_doc("Course Activity", activity[0]["name"]) except (IndexError, frappe.DoesNotExistError): - activity = frappe.get_doc({ - "doctype": "Course Activity", - "enrollment": enrollment, - "content_type": content_type, - "content": content, - "activity_date": frappe.utils.datetime.datetime.now() - }).insert() + activity = frappe.get_doc( + { + "doctype": "Course Activity", + "enrollment": enrollment, + "content_type": content_type, + "content": content, + "activity_date": frappe.utils.datetime.datetime.now(), + } + ).insert() return activity diff --git a/erpnext/education/doctype/course_enrollment/course_enrollment.py b/erpnext/education/doctype/course_enrollment/course_enrollment.py index 79212847533..18639b11785 100644 --- a/erpnext/education/doctype/course_enrollment/course_enrollment.py +++ b/erpnext/education/doctype/course_enrollment/course_enrollment.py @@ -18,77 +18,94 @@ class CourseEnrollment(Document): """ Returns Progress of given student for a particular course enrollment - :param self: Course Enrollment Object - :param student: Student Object + :param self: Course Enrollment Object + :param student: Student Object """ - course = frappe.get_doc('Course', self.course) + course = frappe.get_doc("Course", self.course) topics = course.get_topics() progress = [] for topic in topics: progress.append(student.get_topic_progress(self.name, topic)) if progress: - return reduce(lambda x,y: x+y, progress) # Flatten out the List + return reduce(lambda x, y: x + y, progress) # Flatten out the List else: return [] def validate_duplication(self): - enrollment = frappe.db.exists("Course Enrollment", { - "student": self.student, - "course": self.course, - "program_enrollment": self.program_enrollment, - "name": ("!=", self.name) - }) + enrollment = frappe.db.exists( + "Course Enrollment", + { + "student": self.student, + "course": self.course, + "program_enrollment": self.program_enrollment, + "name": ("!=", self.name), + }, + ) if enrollment: - frappe.throw(_("Student is already enrolled via Course Enrollment {0}").format( - get_link_to_form("Course Enrollment", enrollment)), title=_('Duplicate Entry')) + frappe.throw( + _("Student is already enrolled via Course Enrollment {0}").format( + get_link_to_form("Course Enrollment", enrollment) + ), + title=_("Duplicate Entry"), + ) def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status, time_taken): - result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()} + result = {k: ("Correct" if v else "Wrong") for k, v in answers.items()} result_data = [] for key in answers: item = {} - item['question'] = key - item['quiz_result'] = result[key] + item["question"] = key + item["quiz_result"] = result[key] try: if not quiz_response[key]: - item['selected_option'] = "Unattempted" + item["selected_option"] = "Unattempted" elif isinstance(quiz_response[key], list): - item['selected_option'] = ', '.join(frappe.get_value('Options', res, 'option') for res in quiz_response[key]) + item["selected_option"] = ", ".join( + frappe.get_value("Options", res, "option") for res in quiz_response[key] + ) else: - item['selected_option'] = frappe.get_value('Options', quiz_response[key], 'option') + item["selected_option"] = frappe.get_value("Options", quiz_response[key], "option") except KeyError: - item['selected_option'] = "Unattempted" + item["selected_option"] = "Unattempted" result_data.append(item) - quiz_activity = frappe.get_doc({ - "doctype": "Quiz Activity", - "enrollment": self.name, - "quiz": quiz_name, - "activity_date": frappe.utils.datetime.datetime.now(), - "result": result_data, - "score": score, - "status": status, - "time_taken": time_taken - }).insert(ignore_permissions = True) + quiz_activity = frappe.get_doc( + { + "doctype": "Quiz Activity", + "enrollment": self.name, + "quiz": quiz_name, + "activity_date": frappe.utils.datetime.datetime.now(), + "result": result_data, + "score": score, + "status": status, + "time_taken": time_taken, + } + ).insert(ignore_permissions=True) def add_activity(self, content_type, content): activity = check_activity_exists(self.name, content_type, content) if activity: return activity else: - activity = frappe.get_doc({ - "doctype": "Course Activity", - "enrollment": self.name, - "content_type": content_type, - "content": content, - "activity_date": frappe.utils.datetime.datetime.now() - }) + activity = frappe.get_doc( + { + "doctype": "Course Activity", + "enrollment": self.name, + "content_type": content_type, + "content": content, + "activity_date": frappe.utils.datetime.datetime.now(), + } + ) activity.insert(ignore_permissions=True) return activity.name + def check_activity_exists(enrollment, content_type, content): - activity = frappe.get_all("Course Activity", filters={'enrollment': enrollment, 'content_type': content_type, 'content': content}) + activity = frappe.get_all( + "Course Activity", + filters={"enrollment": enrollment, "content_type": content_type, "content": content}, + ) if activity: return activity[0].name else: diff --git a/erpnext/education/doctype/course_enrollment/course_enrollment_dashboard.py b/erpnext/education/doctype/course_enrollment/course_enrollment_dashboard.py index 14a7a8fde12..31a90fd5adc 100644 --- a/erpnext/education/doctype/course_enrollment/course_enrollment_dashboard.py +++ b/erpnext/education/doctype/course_enrollment/course_enrollment_dashboard.py @@ -6,11 +6,6 @@ from frappe import _ def get_data(): return { - 'fieldname': 'enrollment', - 'transactions': [ - { - 'label': _('Activity'), - 'items': ['Course Activity', 'Quiz Activity'] - } - ] + "fieldname": "enrollment", + "transactions": [{"label": _("Activity"), "items": ["Course Activity", "Quiz Activity"]}], } diff --git a/erpnext/education/doctype/course_enrollment/test_course_enrollment.py b/erpnext/education/doctype/course_enrollment/test_course_enrollment.py index e74d510e521..6862e058f0d 100644 --- a/erpnext/education/doctype/course_enrollment/test_course_enrollment.py +++ b/erpnext/education/doctype/course_enrollment/test_course_enrollment.py @@ -13,19 +13,37 @@ from erpnext.education.doctype.student.test_student import create_student, get_s class TestCourseEnrollment(unittest.TestCase): def setUp(self): setup_program() - student = create_student({"first_name": "_Test First", "last_name": "_Test Last", "email": "_test_student_1@example.com"}) + student = create_student( + {"first_name": "_Test First", "last_name": "_Test Last", "email": "_test_student_1@example.com"} + ) program_enrollment = student.enroll_in_program("_Test Program") - course_enrollment = frappe.db.get_value("Course Enrollment", - {"course": "_Test Course 1", "student": student.name, "program_enrollment": program_enrollment.name}, 'name') + course_enrollment = frappe.db.get_value( + "Course Enrollment", + { + "course": "_Test Course 1", + "student": student.name, + "program_enrollment": program_enrollment.name, + }, + "name", + ) make_course_activity(course_enrollment, "Article", "_Test Article 1-1") def test_get_progress(self): student = get_student("_test_student_1@example.com") - program_enrollment_name = frappe.get_list("Program Enrollment", filters={"student": student.name, "Program": "_Test Program"})[0].name - course_enrollment_name = frappe.get_list("Course Enrollment", filters={"student": student.name, "course": "_Test Course 1", "program_enrollment": program_enrollment_name})[0].name + program_enrollment_name = frappe.get_list( + "Program Enrollment", filters={"student": student.name, "Program": "_Test Program"} + )[0].name + course_enrollment_name = frappe.get_list( + "Course Enrollment", + filters={ + "student": student.name, + "course": "_Test Course 1", + "program_enrollment": program_enrollment_name, + }, + )[0].name course_enrollment = frappe.get_doc("Course Enrollment", course_enrollment_name) progress = course_enrollment.get_progress(student) - finished = {'content': '_Test Article 1-1', 'content_type': 'Article', 'is_complete': True} + finished = {"content": "_Test Article 1-1", "content_type": "Article", "is_complete": True} self.assertTrue(finished in progress) frappe.db.rollback() diff --git a/erpnext/education/doctype/course_schedule/course_schedule.py b/erpnext/education/doctype/course_schedule/course_schedule.py index 335cec43527..d2b31f4e054 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule.py +++ b/erpnext/education/doctype/course_schedule/course_schedule.py @@ -1,4 +1,4 @@ - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors # For license information, please see license.txt @@ -20,10 +20,14 @@ class CourseSchedule(Document): def set_title(self): """Set document Title""" - self.title = self.course + " by " + (self.instructor_name if self.instructor_name else self.instructor) + self.title = ( + self.course + " by " + (self.instructor_name if self.instructor_name else self.instructor) + ) def validate_course(self): - group_based_on, course = frappe.db.get_value("Student Group", self.student_group, ["group_based_on", "course"]) + group_based_on, course = frappe.db.get_value( + "Student Group", self.student_group, ["group_based_on", "course"] + ) if group_based_on == "Course": self.course = course @@ -35,7 +39,7 @@ class CourseSchedule(Document): """Handles specicfic case to update schedule date in calendar """ if isinstance(self.from_time, str): try: - datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S') + datetime_obj = datetime.strptime(self.from_time, "%Y-%m-%d %H:%M:%S") self.schedule_date = datetime_obj except ValueError: pass @@ -45,16 +49,16 @@ class CourseSchedule(Document): from erpnext.education.utils import validate_overlap_for - #Validate overlapping course schedules. + # Validate overlapping course schedules. if self.student_group: validate_overlap_for(self, "Course Schedule", "student_group") validate_overlap_for(self, "Course Schedule", "instructor") validate_overlap_for(self, "Course Schedule", "room") - #validate overlapping assessment schedules. + # validate overlapping assessment schedules. if self.student_group: validate_overlap_for(self, "Assessment Plan", "student_group") validate_overlap_for(self, "Assessment Plan", "room") - validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) \ No newline at end of file + validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) diff --git a/erpnext/education/doctype/course_schedule/course_schedule_dashboard.py b/erpnext/education/doctype/course_schedule/course_schedule_dashboard.py index 256e40b3b18..76a3f048058 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule_dashboard.py +++ b/erpnext/education/doctype/course_schedule/course_schedule_dashboard.py @@ -6,11 +6,6 @@ from frappe import _ def get_data(): return { - 'fieldname': 'course_schedule', - 'transactions': [ - { - 'label': _('Attendance'), - 'items': ['Student Attendance'] - } - ] + "fieldname": "course_schedule", + "transactions": [{"label": _("Attendance"), "items": ["Student Attendance"]}], } diff --git a/erpnext/education/doctype/course_schedule/test_course_schedule.py b/erpnext/education/doctype/course_schedule/test_course_schedule.py index 56149affcea..ac094645ba3 100644 --- a/erpnext/education/doctype/course_schedule/test_course_schedule.py +++ b/erpnext/education/doctype/course_schedule/test_course_schedule.py @@ -12,52 +12,76 @@ from erpnext.education.utils import OverlapError # test_records = frappe.get_test_records('Course Schedule') + class TestCourseSchedule(unittest.TestCase): def test_student_group_conflict(self): - cs1 = make_course_schedule_test_record(simulate= True) + cs1 = make_course_schedule_test_record(simulate=True) - cs2 = make_course_schedule_test_record(schedule_date=cs1.schedule_date, from_time= cs1.from_time, - to_time= cs1.to_time, instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name, do_not_save= 1) + cs2 = make_course_schedule_test_record( + schedule_date=cs1.schedule_date, + from_time=cs1.from_time, + to_time=cs1.to_time, + instructor="_Test Instructor 2", + room=frappe.get_all("Room")[1].name, + do_not_save=1, + ) self.assertRaises(OverlapError, cs2.save) def test_instructor_conflict(self): - cs1 = make_course_schedule_test_record(simulate= True) + cs1 = make_course_schedule_test_record(simulate=True) - cs2 = make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, - student_group="Course-TC101-2014-2015 (_Test Academic Term)", room=frappe.get_all("Room")[1].name, do_not_save= 1) + cs2 = make_course_schedule_test_record( + from_time=cs1.from_time, + to_time=cs1.to_time, + student_group="Course-TC101-2014-2015 (_Test Academic Term)", + room=frappe.get_all("Room")[1].name, + do_not_save=1, + ) self.assertRaises(OverlapError, cs2.save) def test_room_conflict(self): - cs1 = make_course_schedule_test_record(simulate= True) + cs1 = make_course_schedule_test_record(simulate=True) - cs2 = make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, - student_group="Course-TC101-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", do_not_save= 1) + cs2 = make_course_schedule_test_record( + from_time=cs1.from_time, + to_time=cs1.to_time, + student_group="Course-TC101-2014-2015 (_Test Academic Term)", + instructor="_Test Instructor 2", + do_not_save=1, + ) self.assertRaises(OverlapError, cs2.save) def test_no_conflict(self): - cs1 = make_course_schedule_test_record(simulate= True) + cs1 = make_course_schedule_test_record(simulate=True) - make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, - student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name) + make_course_schedule_test_record( + from_time=cs1.from_time, + to_time=cs1.to_time, + student_group="Course-TC102-2014-2015 (_Test Academic Term)", + instructor="_Test Instructor 2", + room=frappe.get_all("Room")[1].name, + ) def test_update_schedule_date(self): - doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1)) + doc = make_course_schedule_test_record(schedule_date=add_to_date(today(), days=1)) doc.schedule_date = add_to_date(doc.schedule_date, days=1) doc.save() + def make_course_schedule_test_record(**args): args = frappe._dict(args) course_schedule = frappe.new_doc("Course Schedule") - course_schedule.student_group = args.student_group or "Course-TC101-2014-2015 (_Test Academic Term)" + course_schedule.student_group = ( + args.student_group or "Course-TC101-2014-2015 (_Test Academic Term)" + ) course_schedule.course = args.course or "TC101" course_schedule.instructor = args.instructor or "_Test Instructor" course_schedule.room = args.room or frappe.get_all("Room")[0].name course_schedule.schedule_date = args.schedule_date or today() course_schedule.from_time = args.from_time or to_timedelta("01:00:00") - course_schedule.to_time = args.to_time or course_schedule.from_time + datetime.timedelta(hours= 1) - + course_schedule.to_time = args.to_time or course_schedule.from_time + datetime.timedelta(hours=1) if not args.do_not_save: if args.simulate: @@ -67,7 +91,7 @@ def make_course_schedule_test_record(**args): break except OverlapError: course_schedule.from_time = course_schedule.from_time + datetime.timedelta(minutes=10) - course_schedule.to_time = course_schedule.from_time + datetime.timedelta(hours= 1) + course_schedule.to_time = course_schedule.from_time + datetime.timedelta(hours=1) else: course_schedule.save() diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py index a309e4694c8..4db6f981fca 100644 --- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py +++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py @@ -13,7 +13,6 @@ from erpnext.education.utils import OverlapError class CourseSchedulingTool(Document): - @frappe.whitelist() def schedule_course(self): """Creates course schedules as per specified parameters""" @@ -25,28 +24,27 @@ class CourseSchedulingTool(Document): self.validate_mandatory() self.validate_date() - self.instructor_name = frappe.db.get_value( - "Instructor", self.instructor, "instructor_name") + self.instructor_name = frappe.db.get_value("Instructor", self.instructor, "instructor_name") group_based_on, course = frappe.db.get_value( - "Student Group", self.student_group, ["group_based_on", "course"]) + "Student Group", self.student_group, ["group_based_on", "course"] + ) if group_based_on == "Course": self.course = course if self.reschedule: - rescheduled, reschedule_errors = self.delete_course_schedule( - rescheduled, reschedule_errors) + rescheduled, reschedule_errors = self.delete_course_schedule(rescheduled, reschedule_errors) date = self.course_start_date while date < self.course_end_date: if self.day == calendar.day_name[getdate(date).weekday()]: course_schedule = self.make_course_schedule(date) try: - print('pass') + print("pass") course_schedule.save() except OverlapError: - print('fail') + print("fail") course_schedules_errors.append(date) else: course_schedules.append(course_schedule) @@ -59,36 +57,43 @@ class CourseSchedulingTool(Document): course_schedules=course_schedules, course_schedules_errors=course_schedules_errors, rescheduled=rescheduled, - reschedule_errors=reschedule_errors + reschedule_errors=reschedule_errors, ) def validate_mandatory(self): """Validates all mandatory fields""" - fields = ['course', 'room', 'instructor', 'from_time', - 'to_time', 'course_start_date', 'course_end_date', 'day'] + fields = [ + "course", + "room", + "instructor", + "from_time", + "to_time", + "course_start_date", + "course_end_date", + "day", + ] for d in fields: if not self.get(d): - frappe.throw(_("{0} is mandatory").format( - self.meta.get_label(d))) + frappe.throw(_("{0} is mandatory").format(self.meta.get_label(d))) def validate_date(self): """Validates if Course Start Date is greater than Course End Date""" if self.course_start_date > self.course_end_date: - frappe.throw( - _("Course Start Date cannot be greater than Course End Date.")) + frappe.throw(_("Course Start Date cannot be greater than Course End Date.")) def delete_course_schedule(self, rescheduled, reschedule_errors): """Delete all course schedule within the Date range and specified filters""" - schedules = frappe.get_list("Course Schedule", + schedules = frappe.get_list( + "Course Schedule", fields=["name", "schedule_date"], filters=[ ["student_group", "=", self.student_group], ["course", "=", self.course], ["schedule_date", ">=", self.course_start_date], - ["schedule_date", "<=", self.course_end_date] - ] + ["schedule_date", "<=", self.course_end_date], + ], ) for d in schedules: diff --git a/erpnext/education/doctype/education_settings/education_settings.py b/erpnext/education/doctype/education_settings/education_settings.py index 13123be78a3..cde5089e88d 100644 --- a/erpnext/education/doctype/education_settings/education_settings.py +++ b/erpnext/education/doctype/education_settings/education_settings.py @@ -11,7 +11,7 @@ education_keydict = { "academic_year": "current_academic_year", "academic_term": "current_academic_term", "validate_batch": "validate_batch", - "validate_course": "validate_course" + "validate_course": "validate_course", } @@ -19,7 +19,7 @@ class EducationSettings(Document): def on_update(self): """update defaults""" for key in education_keydict: - frappe.db.set_default(key, self.get(education_keydict[key], '')) + frappe.db.set_default(key, self.get(education_keydict[key], "")) # clear cache frappe.clear_cache() @@ -29,10 +29,16 @@ class EducationSettings(Document): def validate(self): from frappe.custom.doctype.property_setter.property_setter import make_property_setter - if self.get('instructor_created_by')=='Naming Series': - make_property_setter('Instructor', "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False) + + if self.get("instructor_created_by") == "Naming Series": + make_property_setter( + "Instructor", "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False + ) else: - make_property_setter('Instructor', "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False) + make_property_setter( + "Instructor", "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False + ) + def update_website_context(context): context["lms_enabled"] = frappe.get_doc("Education Settings").enable_lms diff --git a/erpnext/education/doctype/fee_category/test_fee_category.py b/erpnext/education/doctype/fee_category/test_fee_category.py index 9e74c7de11e..93565a9f926 100644 --- a/erpnext/education/doctype/fee_category/test_fee_category.py +++ b/erpnext/education/doctype/fee_category/test_fee_category.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Fee Category') + class TestFeeCategory(unittest.TestCase): pass diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.py b/erpnext/education/doctype/fee_schedule/fee_schedule.py index a122fe88564..9ae5582f213 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule.py +++ b/erpnext/education/doctype/fee_schedule/fee_schedule.py @@ -15,17 +15,20 @@ import erpnext class FeeSchedule(Document): def onload(self): info = self.get_dashboard_info() - self.set_onload('dashboard_info', info) + self.set_onload("dashboard_info", info) def get_dashboard_info(self): info = { "total_paid": 0, "total_unpaid": 0, - "currency": erpnext.get_company_currency(self.company) + "currency": erpnext.get_company_currency(self.company), } - fees_amount = frappe.db.sql("""select sum(grand_total), sum(outstanding_amount) from tabFees - where fee_schedule=%s and docstatus=1""", (self.name)) + fees_amount = frappe.db.sql( + """select sum(grand_total), sum(outstanding_amount) from tabFees + where fee_schedule=%s and docstatus=1""", + (self.name), + ) if fees_amount: info["total_paid"] = flt(fees_amount[0][0]) - flt(fees_amount[0][1]) @@ -40,33 +43,42 @@ class FeeSchedule(Document): no_of_students = 0 for d in self.student_groups: # if not d.total_students: - d.total_students = get_total_students(d.student_group, self.academic_year, - self.academic_term, self.student_category) + d.total_students = get_total_students( + d.student_group, self.academic_year, self.academic_term, self.student_category + ) no_of_students += cint(d.total_students) # validate the program of fee structure and student groups student_group_program = frappe.db.get_value("Student Group", d.student_group, "program") if self.program and student_group_program and self.program != student_group_program: - frappe.msgprint(_("Program in the Fee Structure and Student Group {0} are different.") - .format(d.student_group)) - self.grand_total = no_of_students*self.total_amount + frappe.msgprint( + _("Program in the Fee Structure and Student Group {0} are different.").format(d.student_group) + ) + self.grand_total = no_of_students * self.total_amount self.grand_total_in_words = money_in_words(self.grand_total) @frappe.whitelist() def create_fees(self): self.db_set("fee_creation_status", "In Process") - frappe.publish_realtime("fee_schedule_progress", - {"progress": "0", "reload": 1}, user=frappe.session.user) + frappe.publish_realtime( + "fee_schedule_progress", {"progress": "0", "reload": 1}, user=frappe.session.user + ) total_records = sum([int(d.total_students) for d in self.student_groups]) if total_records > 10: - frappe.msgprint(_('''Fee records will be created in the background. - In case of any error the error message will be updated in the Schedule.''')) - enqueue(generate_fee, queue='default', timeout=6000, event='generate_fee', - fee_schedule=self.name) + frappe.msgprint( + _( + """Fee records will be created in the background. + In case of any error the error message will be updated in the Schedule.""" + ) + ) + enqueue( + generate_fee, queue="default", timeout=6000, event="generate_fee", fee_schedule=self.name + ) else: generate_fee(self.name) + def generate_fee(fee_schedule): doc = frappe.get_doc("Fee Schedule", fee_schedule) error = False @@ -77,17 +89,16 @@ def generate_fee(fee_schedule): frappe.throw(_("Please setup Students under Student Groups")) for d in doc.student_groups: - students = get_students(d.student_group, doc.academic_year, doc.academic_term, doc.student_category) + students = get_students( + d.student_group, doc.academic_year, doc.academic_term, doc.student_category + ) for student in students: try: - fees_doc = get_mapped_doc("Fee Schedule", fee_schedule, { - "Fee Schedule": { - "doctype": "Fees", - "field_map": { - "name": "Fee Schedule" - } - } - }) + fees_doc = get_mapped_doc( + "Fee Schedule", + fee_schedule, + {"Fee Schedule": {"doctype": "Fees", "field_map": {"name": "Fee Schedule"}}}, + ) fees_doc.posting_date = doc.posting_date fees_doc.student = student.student fees_doc.student_name = student.student_name @@ -97,7 +108,11 @@ def generate_fee(fee_schedule): fees_doc.save() fees_doc.submit() created_records += 1 - frappe.publish_realtime("fee_schedule_progress", {"progress": str(int(created_records * 100/total_records))}, user=frappe.session.user) + frappe.publish_realtime( + "fee_schedule_progress", + {"progress": str(int(created_records * 100 / total_records))}, + user=frappe.session.user, + ) except Exception as e: error = True @@ -112,8 +127,9 @@ def generate_fee(fee_schedule): frappe.db.set_value("Fee Schedule", fee_schedule, "fee_creation_status", "Successful") frappe.db.set_value("Fee Schedule", fee_schedule, "error_log", None) - frappe.publish_realtime("fee_schedule_progress", - {"progress": "100", "reload": 1}, user=frappe.session.user) + frappe.publish_realtime( + "fee_schedule_progress", {"progress": "100", "reload": 1}, user=frappe.session.user + ) def get_students(student_group, academic_year, academic_term=None, student_category=None): @@ -123,14 +139,20 @@ def get_students(student_group, academic_year, academic_term=None, student_categ if academic_term: conditions = " and pe.academic_term={}".format(frappe.db.escape(academic_term)) - students = frappe.db.sql(""" + students = frappe.db.sql( + """ select pe.student, pe.student_name, pe.program, pe.student_batch_name from `tabStudent Group Student` sgs, `tabProgram Enrollment` pe where pe.student = sgs.student and pe.academic_year = %s and sgs.parent = %s and sgs.active = 1 {conditions} - """.format(conditions=conditions), (academic_year, student_group), as_dict=1) + """.format( + conditions=conditions + ), + (academic_year, student_group), + as_dict=1, + ) return students @@ -141,9 +163,11 @@ def get_total_students(student_group, academic_year, academic_term=None, student @frappe.whitelist() -def get_fee_structure(source_name,target_doc=None): - fee_request = get_mapped_doc("Fee Structure", source_name, - {"Fee Structure": { - "doctype": "Fee Schedule" - }}, ignore_permissions=True) +def get_fee_structure(source_name, target_doc=None): + fee_request = get_mapped_doc( + "Fee Structure", + source_name, + {"Fee Structure": {"doctype": "Fee Schedule"}}, + ignore_permissions=True, + ) return fee_request diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule_dashboard.py b/erpnext/education/doctype/fee_schedule/fee_schedule_dashboard.py index f5d1dee98b4..4b99d27f299 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule_dashboard.py +++ b/erpnext/education/doctype/fee_schedule/fee_schedule_dashboard.py @@ -3,11 +3,4 @@ def get_data(): - return { - 'fieldname': 'fee_schedule', - 'transactions': [ - { - 'items': ['Fees'] - } - ] - } + return {"fieldname": "fee_schedule", "transactions": [{"items": ["Fees"]}]} diff --git a/erpnext/education/doctype/fee_structure/fee_structure.py b/erpnext/education/doctype/fee_structure/fee_structure.py index 9090a6b9d54..f1b7bb726e8 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.py +++ b/erpnext/education/doctype/fee_structure/fee_structure.py @@ -20,14 +20,17 @@ class FeeStructure(Document): @frappe.whitelist() def make_fee_schedule(source_name, target_doc=None): - return get_mapped_doc("Fee Structure", source_name, { - "Fee Structure": { - "doctype": "Fee Schedule", - "validation": { - "docstatus": ["=", 1], - } + return get_mapped_doc( + "Fee Structure", + source_name, + { + "Fee Structure": { + "doctype": "Fee Schedule", + "validation": { + "docstatus": ["=", 1], + }, + }, + "Fee Component": {"doctype": "Fee Component"}, }, - "Fee Component": { - "doctype": "Fee Component" - } - }, target_doc) + target_doc, + ) diff --git a/erpnext/education/doctype/fee_structure/fee_structure_dashboard.py b/erpnext/education/doctype/fee_structure/fee_structure_dashboard.py index 27ce06b29b3..0ae3032b0c2 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure_dashboard.py +++ b/erpnext/education/doctype/fee_structure/fee_structure_dashboard.py @@ -6,11 +6,6 @@ from frappe import _ def get_data(): return { - 'fieldname': 'fee_structure', - 'transactions': [ - { - 'label': _('Fee'), - 'items': ['Fees', 'Fee Schedule'] - } - ] + "fieldname": "fee_structure", + "transactions": [{"label": _("Fee"), "items": ["Fees", "Fee Schedule"]}], } diff --git a/erpnext/education/doctype/fee_structure/test_fee_structure.py b/erpnext/education/doctype/fee_structure/test_fee_structure.py index 61381a6289c..ebe0fea53bd 100644 --- a/erpnext/education/doctype/fee_structure/test_fee_structure.py +++ b/erpnext/education/doctype/fee_structure/test_fee_structure.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Fee Structure') + class TestFeeStructure(unittest.TestCase): pass diff --git a/erpnext/education/doctype/fees/fees.py b/erpnext/education/doctype/fees/fees.py index 41d428d23de..be608bf731e 100644 --- a/erpnext/education/doctype/fees/fees.py +++ b/erpnext/education/doctype/fees/fees.py @@ -33,9 +33,11 @@ class Fees(AccountsController): if not self.currency: self.currency = erpnext.get_company_currency(self.company) if not (self.receivable_account and self.income_account and self.cost_center): - accounts_details = frappe.get_all("Company", + accounts_details = frappe.get_all( + "Company", fields=["default_receivable_account", "default_income_account", "cost_center"], - filters={"name": self.company})[0] + filters={"name": self.company}, + )[0] if not self.receivable_account: self.receivable_account = accounts_details.default_receivable_account if not self.income_account: @@ -46,12 +48,15 @@ class Fees(AccountsController): self.student_email = self.get_student_emails() def get_student_emails(self): - student_emails = frappe.db.sql_list(""" + student_emails = frappe.db.sql_list( + """ select g.email_address from `tabGuardian` g, `tabStudent Guardian` sg where g.name = sg.guardian and sg.parent = %s and sg.parenttype = 'Student' and ifnull(g.email_address, '')!='' - """, self.student) + """, + self.student, + ) student_email_id = frappe.db.get_value("Student", self.student, "student_email_id") if student_email_id: @@ -61,7 +66,6 @@ class Fees(AccountsController): else: return None - def calculate_total(self): """Calculates total amount.""" self.grand_total = 0 @@ -75,61 +79,84 @@ class Fees(AccountsController): self.make_gl_entries() if self.send_payment_request and self.student_email: - pr = make_payment_request(party_type="Student", party=self.student, dt="Fees", - dn=self.name, recipient_id=self.student_email, - submit_doc=True, use_dummy_message=True) + pr = make_payment_request( + party_type="Student", + party=self.student, + dt="Fees", + dn=self.name, + recipient_id=self.student_email, + submit_doc=True, + use_dummy_message=True, + ) frappe.msgprint(_("Payment request {0} created").format(getlink("Payment Request", pr.name))) def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) # frappe.db.set(self, 'status', 'Cancelled') - def make_gl_entries(self): if not self.grand_total: return - student_gl_entries = self.get_gl_dict({ - "account": self.receivable_account, - "party_type": "Student", - "party": self.student, - "against": self.income_account, - "debit": self.grand_total, - "debit_in_account_currency": self.grand_total, - "against_voucher": self.name, - "against_voucher_type": self.doctype - }, item=self) + student_gl_entries = self.get_gl_dict( + { + "account": self.receivable_account, + "party_type": "Student", + "party": self.student, + "against": self.income_account, + "debit": self.grand_total, + "debit_in_account_currency": self.grand_total, + "against_voucher": self.name, + "against_voucher_type": self.doctype, + }, + item=self, + ) - fee_gl_entry = self.get_gl_dict({ - "account": self.income_account, - "against": self.student, - "credit": self.grand_total, - "credit_in_account_currency": self.grand_total, - "cost_center": self.cost_center - }, item=self) + fee_gl_entry = self.get_gl_dict( + { + "account": self.income_account, + "against": self.student, + "credit": self.grand_total, + "credit_in_account_currency": self.grand_total, + "cost_center": self.cost_center, + }, + item=self, + ) from erpnext.accounts.general_ledger import make_gl_entries - make_gl_entries([student_gl_entries, fee_gl_entry], cancel=(self.docstatus == 2), - update_outstanding="Yes", merge_entries=False) + + make_gl_entries( + [student_gl_entries, fee_gl_entry], + cancel=(self.docstatus == 2), + update_outstanding="Yes", + merge_entries=False, + ) + def get_fee_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): user = frappe.session.user student = frappe.db.sql("select name from `tabStudent` where student_email_id= %s", user) if student: - return frappe. db.sql(''' + return frappe.db.sql( + """ select name, program, due_date, grand_total - outstanding_amount as paid_amount, outstanding_amount, grand_total, currency from `tabFees` where student= %s and docstatus=1 - order by due_date asc limit {0} , {1}''' - .format(limit_start, limit_page_length), student, as_dict = True) + order by due_date asc limit {0} , {1}""".format( + limit_start, limit_page_length + ), + student, + as_dict=True, + ) + def get_list_context(context=None): return { "show_sidebar": True, "show_search": True, - 'no_breadcrumbs': True, + "no_breadcrumbs": True, "title": _("Fees"), "get_list": get_fee_list, - "row_template": "templates/includes/fee/fee_row.html" + "row_template": "templates/includes/fee/fee_row.html", } diff --git a/erpnext/education/doctype/fees/test_fees.py b/erpnext/education/doctype/fees/test_fees.py index 72f1d113962..f0f7e67d2bb 100644 --- a/erpnext/education/doctype/fees/test_fees.py +++ b/erpnext/education/doctype/fees/test_fees.py @@ -9,12 +9,15 @@ from frappe.utils.make_random import get_random from erpnext.education.doctype.program.test_program import make_program_and_linked_courses -test_dependencies = ['Company'] -class TestFees(unittest.TestCase): +test_dependencies = ["Company"] + +class TestFees(unittest.TestCase): def test_fees(self): student = get_random("Student") - program = make_program_and_linked_courses("_Test Program 1", ["_Test Course 1", "_Test Course 2"]) + program = make_program_and_linked_courses( + "_Test Program 1", ["_Test Course 1", "_Test Course 2"] + ) fee = frappe.new_doc("Fees") fee.posting_date = nowdate() fee.due_date = nowdate() @@ -25,22 +28,24 @@ class TestFees(unittest.TestCase): fee.company = "_Test Company" fee.program = program.name - fee.extend("components", [ - { - "fees_category": "Tuition Fee", - "amount": 40000 - }, - { - "fees_category": "Transportation Fee", - "amount": 10000 - }]) + fee.extend( + "components", + [ + {"fees_category": "Tuition Fee", "amount": 40000}, + {"fees_category": "Transportation Fee", "amount": 10000}, + ], + ) fee.save() fee.submit() - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select account, posting_date, party_type, party, cost_center, fiscal_year, voucher_type, voucher_no, against_voucher_type, against_voucher, cost_center, company, credit, debit - from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", ("Fees", fee.name), as_dict=True) + from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", + ("Fees", fee.name), + as_dict=True, + ) if gl_entries[0].account == "_Test Receivable - _TC": self.assertEqual(gl_entries[0].debit, 50000) diff --git a/erpnext/education/doctype/grading_scale/grading_scale_dashboard.py b/erpnext/education/doctype/grading_scale/grading_scale_dashboard.py index 44313f2bbca..6995666d52c 100644 --- a/erpnext/education/doctype/grading_scale/grading_scale_dashboard.py +++ b/erpnext/education/doctype/grading_scale/grading_scale_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'grading_scale', - 'non_standard_fieldnames': { - 'Course': 'default_grading_scale' - }, - 'transactions': [ - { - 'label': _('Course'), - 'items': ['Course'] - }, - { - 'label': _('Assessment'), - 'items': ['Assessment Plan', 'Assessment Result'] - } - ] + "fieldname": "grading_scale", + "non_standard_fieldnames": {"Course": "default_grading_scale"}, + "transactions": [ + {"label": _("Course"), "items": ["Course"]}, + {"label": _("Assessment"), "items": ["Assessment Plan", "Assessment Result"]}, + ], } diff --git a/erpnext/education/doctype/grading_scale/test_grading_scale.py b/erpnext/education/doctype/grading_scale/test_grading_scale.py index 3ebefda22fc..09a092cc2a5 100644 --- a/erpnext/education/doctype/grading_scale/test_grading_scale.py +++ b/erpnext/education/doctype/grading_scale/test_grading_scale.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Grading Scale') + class TestGradingScale(unittest.TestCase): pass diff --git a/erpnext/education/doctype/guardian/guardian.py b/erpnext/education/doctype/guardian/guardian.py index aae651b8550..a456e08d2f5 100644 --- a/erpnext/education/doctype/guardian/guardian.py +++ b/erpnext/education/doctype/guardian/guardian.py @@ -19,12 +19,15 @@ class Guardian(Document): def load_students(self): """Load `students` from the database""" self.students = [] - students = frappe.get_all("Student Guardian", filters={"guardian":self.name}, fields=["parent"]) + students = frappe.get_all("Student Guardian", filters={"guardian": self.name}, fields=["parent"]) for student in students: - self.append("students", { - "student":student.parent, - "student_name": frappe.db.get_value("Student", student.parent, "title") - }) + self.append( + "students", + { + "student": student.parent, + "student_name": frappe.db.get_value("Student", student.parent, "title"), + }, + ) def validate(self): self.students = [] @@ -36,17 +39,19 @@ def invite_guardian(guardian): if not guardian_doc.email_address: frappe.throw(_("Please set Email Address")) else: - guardian_as_user = frappe.get_value('User', dict(email=guardian_doc.email_address)) + guardian_as_user = frappe.get_value("User", dict(email=guardian_doc.email_address)) if guardian_as_user: frappe.msgprint(_("User {0} already exists").format(getlink("User", guardian_as_user))) return guardian_as_user else: - user = frappe.get_doc({ - "doctype": "User", - "first_name": guardian_doc.guardian_name, - "email": guardian_doc.email_address, - "user_type": "Website User", - "send_welcome_email": 1 - }).insert(ignore_permissions = True) + user = frappe.get_doc( + { + "doctype": "User", + "first_name": guardian_doc.guardian_name, + "email": guardian_doc.email_address, + "user_type": "Website User", + "send_welcome_email": 1, + } + ).insert(ignore_permissions=True) frappe.msgprint(_("User {0} created").format(getlink("User", user.name))) return user.name diff --git a/erpnext/education/doctype/guardian/test_guardian.py b/erpnext/education/doctype/guardian/test_guardian.py index f474ed5e06e..de3638c3099 100644 --- a/erpnext/education/doctype/guardian/test_guardian.py +++ b/erpnext/education/doctype/guardian/test_guardian.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Guardian') + class TestGuardian(unittest.TestCase): pass diff --git a/erpnext/education/doctype/instructor/instructor.py b/erpnext/education/doctype/instructor/instructor.py index 0076240f86f..24e0607e2d4 100644 --- a/erpnext/education/doctype/instructor/instructor.py +++ b/erpnext/education/doctype/instructor/instructor.py @@ -14,30 +14,37 @@ class Instructor(Document): if not naming_method: frappe.throw(_("Please setup Instructor Naming System in Education > Education Settings")) else: - if naming_method == 'Naming Series': + if naming_method == "Naming Series": set_name_by_naming_series(self) - elif naming_method == 'Employee Number': + elif naming_method == "Employee Number": if not self.employee: frappe.throw(_("Please select Employee")) self.name = self.employee - elif naming_method == 'Full Name': + elif naming_method == "Full Name": self.name = self.instructor_name def validate(self): self.validate_duplicate_employee() def validate_duplicate_employee(self): - if self.employee and frappe.db.get_value("Instructor", {'employee': self.employee, 'name': ['!=', self.name]}, 'name'): + if self.employee and frappe.db.get_value( + "Instructor", {"employee": self.employee, "name": ["!=", self.name]}, "name" + ): frappe.throw(_("Employee ID is linked with another instructor")) + def get_timeline_data(doctype, name): """Return timeline for course schedule""" - return dict(frappe.db.sql( - """ + return dict( + frappe.db.sql( + """ SELECT unix_timestamp(`schedule_date`), count(*) FROM `tabCourse Schedule` WHERE instructor=%s and `schedule_date` > date_sub(curdate(), interval 1 year) GROUP BY schedule_date - """, name)) + """, + name, + ) + ) diff --git a/erpnext/education/doctype/instructor/instructor_dashboard.py b/erpnext/education/doctype/instructor/instructor_dashboard.py index eae67acabfd..ead10ca7f83 100644 --- a/erpnext/education/doctype/instructor/instructor_dashboard.py +++ b/erpnext/education/doctype/instructor/instructor_dashboard.py @@ -6,20 +6,12 @@ from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on the course schedules of this Instructor'), - 'fieldname': 'instructor', - 'non_standard_fieldnames': { - 'Assessment Plan': 'supervisor' - }, - 'transactions': [ - { - 'label': _('Course and Assessment'), - 'items': ['Course Schedule', 'Assessment Plan'] - }, - { - 'label': _('Students'), - 'items': ['Student Group'] - } - ] + "heatmap": True, + "heatmap_message": _("This is based on the course schedules of this Instructor"), + "fieldname": "instructor", + "non_standard_fieldnames": {"Assessment Plan": "supervisor"}, + "transactions": [ + {"label": _("Course and Assessment"), "items": ["Course Schedule", "Assessment Plan"]}, + {"label": _("Students"), "items": ["Student Group"]}, + ], } diff --git a/erpnext/education/doctype/instructor/test_instructor.py b/erpnext/education/doctype/instructor/test_instructor.py index 4eab37ae01f..ee1e81dd306 100644 --- a/erpnext/education/doctype/instructor/test_instructor.py +++ b/erpnext/education/doctype/instructor/test_instructor.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Instructor') + class TestInstructor(unittest.TestCase): pass diff --git a/erpnext/education/doctype/program/program.py b/erpnext/education/doctype/program/program.py index a9ce64409ab..5250f8c05e6 100644 --- a/erpnext/education/doctype/program/program.py +++ b/erpnext/education/doctype/program/program.py @@ -7,8 +7,9 @@ from frappe.model.document import Document class Program(Document): - def get_course_list(self): program_course_list = self.courses - course_list = [frappe.get_doc("Course", program_course.course) for program_course in program_course_list] + course_list = [ + frappe.get_doc("Course", program_course.course) for program_course in program_course_list + ] return course_list diff --git a/erpnext/education/doctype/program/program_dashboard.py b/erpnext/education/doctype/program/program_dashboard.py index 66960767f16..21820618bbe 100644 --- a/erpnext/education/doctype/program/program_dashboard.py +++ b/erpnext/education/doctype/program/program_dashboard.py @@ -3,23 +3,11 @@ from frappe import _ def get_data(): return { - 'fieldname': 'program', - 'transactions': [ - { - 'label': _('Admission and Enrollment'), - 'items': ['Student Applicant', 'Program Enrollment'] - }, - { - 'label': _('Student Activity'), - 'items': ['Student Group', 'Student Log'] - }, - { - 'label': _('Fee'), - 'items': ['Fees','Fee Structure', 'Fee Schedule'] - }, - { - 'label': _('Assessment'), - 'items': ['Assessment Plan', 'Assessment Result'] - } - ] + "fieldname": "program", + "transactions": [ + {"label": _("Admission and Enrollment"), "items": ["Student Applicant", "Program Enrollment"]}, + {"label": _("Student Activity"), "items": ["Student Group", "Student Log"]}, + {"label": _("Fee"), "items": ["Fees", "Fee Structure", "Fee Schedule"]}, + {"label": _("Assessment"), "items": ["Assessment Plan", "Assessment Result"]}, + ], } diff --git a/erpnext/education/doctype/program/test_program.py b/erpnext/education/doctype/program/test_program.py index cb8926bcf7e..c3dbad2d163 100644 --- a/erpnext/education/doctype/program/test_program.py +++ b/erpnext/education/doctype/program/test_program.py @@ -11,32 +11,30 @@ from erpnext.education.doctype.topic.test_topic import make_topic_and_linked_con test_data = { "program_name": "_Test Program", "description": "_Test Program", - "course": [{ - "course_name": "_Test Course 1", - "topic": [{ - "topic_name": "_Test Topic 1-1", - "content": [{ - "type": "Article", - "name": "_Test Article 1-1" - }, { - "type": "Article", - "name": "_Test Article 1-2" - }] - }, - { - "topic_name": "_Test Topic 1-2", - "content": [{ - "type": "Article", - "name": "_Test Article 1-3" - }, { - "type": "Article", - "name": "_Test Article 1-4" - }] - } - ] - }] + "course": [ + { + "course_name": "_Test Course 1", + "topic": [ + { + "topic_name": "_Test Topic 1-1", + "content": [ + {"type": "Article", "name": "_Test Article 1-1"}, + {"type": "Article", "name": "_Test Article 1-2"}, + ], + }, + { + "topic_name": "_Test Topic 1-2", + "content": [ + {"type": "Article", "name": "_Test Article 1-3"}, + {"type": "Article", "name": "_Test Article 1-4"}, + ], + }, + ], + } + ], } + class TestProgram(unittest.TestCase): def setUp(self): make_program_and_linked_courses("_Test Program 1", ["_Test Course 1", "_Test Course 2"]) @@ -53,17 +51,21 @@ class TestProgram(unittest.TestCase): for entry in frappe.get_all(dt): frappe.delete_doc(dt, entry.program) + def make_program(name): - program = frappe.get_doc({ - "doctype": "Program", - "program_name": name, - "program_code": name, - "description": "_test description", - "is_published": True, - "is_featured": True, - }).insert() + program = frappe.get_doc( + { + "doctype": "Program", + "program_name": name, + "program_code": name, + "description": "_test description", + "is_published": True, + "is_featured": True, + } + ).insert() return program.name + def make_program_and_linked_courses(program_name, course_name_list): try: program = frappe.get_doc("Program", program_name) @@ -76,15 +78,19 @@ def make_program_and_linked_courses(program_name, course_name_list): program.save() return program + def setup_program(): - topic_list = [course['topic'] for course in test_data['course']] + topic_list = [course["topic"] for course in test_data["course"]] for topic in topic_list[0]: - make_topic_and_linked_content(topic['topic_name'], topic['content']) + make_topic_and_linked_content(topic["topic_name"], topic["content"]) - all_courses_list = [{'course': course['course_name'], 'topic': [topic['topic_name'] for topic in course['topic']]} for course in test_data['course']] # returns [{'course': 'Applied Math', 'topic': ['Trignometry', 'Geometry']}] + all_courses_list = [ + {"course": course["course_name"], "topic": [topic["topic_name"] for topic in course["topic"]]} + for course in test_data["course"] + ] # returns [{'course': 'Applied Math', 'topic': ['Trignometry', 'Geometry']}] for course in all_courses_list: - make_course_and_linked_topic(course['course'], course['topic']) + make_course_and_linked_topic(course["course"], course["topic"]) - course_list = [course['course_name'] for course in test_data['course']] - program = make_program_and_linked_courses(test_data['program_name'], course_list) + course_list = [course["course_name"] for course in test_data["course"]] + program = make_program_and_linked_courses(test_data["program_name"], course_list) return program diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index 4d0f3a98011..69d281b9386 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -27,45 +27,64 @@ class ProgramEnrollment(Document): self.create_course_enrollments() def validate_academic_year(self): - start_date, end_date = frappe.db.get_value("Academic Year", self.academic_year, ["year_start_date", "year_end_date"]) + start_date, end_date = frappe.db.get_value( + "Academic Year", self.academic_year, ["year_start_date", "year_end_date"] + ) if self.enrollment_date: if start_date and getdate(self.enrollment_date) < getdate(start_date): - frappe.throw(_("Enrollment Date cannot be before the Start Date of the Academic Year {0}").format( - get_link_to_form("Academic Year", self.academic_year))) + frappe.throw( + _("Enrollment Date cannot be before the Start Date of the Academic Year {0}").format( + get_link_to_form("Academic Year", self.academic_year) + ) + ) if end_date and getdate(self.enrollment_date) > getdate(end_date): - frappe.throw(_("Enrollment Date cannot be after the End Date of the Academic Term {0}").format( - get_link_to_form("Academic Year", self.academic_year))) + frappe.throw( + _("Enrollment Date cannot be after the End Date of the Academic Term {0}").format( + get_link_to_form("Academic Year", self.academic_year) + ) + ) def validate_academic_term(self): - start_date, end_date = frappe.db.get_value("Academic Term", self.academic_term, ["term_start_date", "term_end_date"]) + start_date, end_date = frappe.db.get_value( + "Academic Term", self.academic_term, ["term_start_date", "term_end_date"] + ) if self.enrollment_date: if start_date and getdate(self.enrollment_date) < getdate(start_date): - frappe.throw(_("Enrollment Date cannot be before the Start Date of the Academic Term {0}").format( - get_link_to_form("Academic Term", self.academic_term))) + frappe.throw( + _("Enrollment Date cannot be before the Start Date of the Academic Term {0}").format( + get_link_to_form("Academic Term", self.academic_term) + ) + ) if end_date and getdate(self.enrollment_date) > getdate(end_date): - frappe.throw(_("Enrollment Date cannot be after the End Date of the Academic Term {0}").format( - get_link_to_form("Academic Term", self.academic_term))) + frappe.throw( + _("Enrollment Date cannot be after the End Date of the Academic Term {0}").format( + get_link_to_form("Academic Term", self.academic_term) + ) + ) def validate_duplication(self): - enrollment = frappe.get_all("Program Enrollment", filters={ - "student": self.student, - "program": self.program, - "academic_year": self.academic_year, - "academic_term": self.academic_term, - "docstatus": ("<", 2), - "name": ("!=", self.name) - }) + enrollment = frappe.get_all( + "Program Enrollment", + filters={ + "student": self.student, + "program": self.program, + "academic_year": self.academic_year, + "academic_term": self.academic_term, + "docstatus": ("<", 2), + "name": ("!=", self.name), + }, + ) if enrollment: frappe.throw(_("Student is already enrolled.")) def update_student_joining_date(self): - table = frappe.qb.DocType('Program Enrollment') + table = frappe.qb.DocType("Program Enrollment") date = ( frappe.qb.from_(table) - .select(Min(table.enrollment_date).as_('enrollment_date')) - .where(table.student == self.student) + .select(Min(table.enrollment_date).as_("enrollment_date")) + .where(table.student == self.student) ).run(as_dict=True) if date: @@ -73,45 +92,59 @@ class ProgramEnrollment(Document): def make_fee_records(self): from erpnext.education.api import get_fee_components + fee_list = [] for d in self.fees: fee_components = get_fee_components(d.fee_structure) if fee_components: fees = frappe.new_doc("Fees") - fees.update({ - "student": self.student, - "academic_year": self.academic_year, - "academic_term": d.academic_term, - "fee_structure": d.fee_structure, - "program": self.program, - "due_date": d.due_date, - "student_name": self.student_name, - "program_enrollment": self.name, - "components": fee_components - }) + fees.update( + { + "student": self.student, + "academic_year": self.academic_year, + "academic_term": d.academic_term, + "fee_structure": d.fee_structure, + "program": self.program, + "due_date": d.due_date, + "student_name": self.student_name, + "program_enrollment": self.name, + "components": fee_components, + } + ) fees.save() fees.submit() fee_list.append(fees.name) if fee_list: - fee_list = ["""
%s""" % \ - (fee, fee) for fee in fee_list] + fee_list = [ + """%s""" % (fee, fee) for fee in fee_list + ] msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list))) - @frappe.whitelist() def get_courses(self): - return frappe.db.sql('''select course from `tabProgram Course` where parent = %s and required = 1''', (self.program), as_dict=1) + return frappe.db.sql( + """select course from `tabProgram Course` where parent = %s and required = 1""", + (self.program), + as_dict=1, + ) def create_course_enrollments(self): student = frappe.get_doc("Student", self.student) course_list = [course.course for course in self.courses] for course_name in course_list: - student.enroll_in_course(course_name=course_name, program_enrollment=self.name, enrollment_date=self.enrollment_date) + student.enroll_in_course( + course_name=course_name, program_enrollment=self.name, enrollment_date=self.enrollment_date + ) def get_all_course_enrollments(self): - course_enrollment_names = frappe.get_list("Course Enrollment", filters={'program_enrollment': self.name}) - return [frappe.get_doc('Course Enrollment', course_enrollment.name) for course_enrollment in course_enrollment_names] + course_enrollment_names = frappe.get_list( + "Course Enrollment", filters={"program_enrollment": self.name} + ) + return [ + frappe.get_doc("Course Enrollment", course_enrollment.name) + for course_enrollment in course_enrollment_names + ] def get_quiz_progress(self): student = frappe.get_doc("Student", self.student) @@ -120,8 +153,8 @@ class ProgramEnrollment(Document): for course_enrollment in self.get_all_course_enrollments(): course_progress = course_enrollment.get_progress(student) for progress_item in course_progress: - if progress_item['content_type'] == "Quiz": - progress_item['course'] = course_enrollment.course + if progress_item["content_type"] == "Quiz": + progress_item["course"] = course_enrollment.course progress_list.append(progress_item) if not progress_list: return None @@ -130,27 +163,27 @@ class ProgramEnrollment(Document): quiz_progress.program = self.program return quiz_progress + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_program_courses(doctype, txt, searchfield, start, page_len, filters): - if not filters.get('program'): + if not filters.get("program"): frappe.msgprint(_("Please select a Program first.")) return [] - return frappe.db.sql("""select course, course_name from `tabProgram Course` + return frappe.db.sql( + """select course, course_name from `tabProgram Course` where parent = %(program)s and course like %(txt)s {match_cond} order by if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999), idx desc, `tabProgram Course`.course asc limit {start}, {page_len}""".format( - match_cond=get_match_cond(doctype), - start=start, - page_len=page_len), { - "txt": "%{0}%".format(txt), - "_txt": txt.replace('%', ''), - "program": filters['program'] - }) + match_cond=get_match_cond(doctype), start=start, page_len=page_len + ), + {"txt": "%{0}%".format(txt), "_txt": txt.replace("%", ""), "program": filters["program"]}, + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -161,14 +194,19 @@ def get_students(doctype, txt, searchfield, start, page_len, filters): if not filters.get("academic_year"): filters["academic_year"] = frappe.defaults.get_defaults().academic_year - enrolled_students = frappe.get_list("Program Enrollment", filters={ - "academic_term": filters.get('academic_term'), - "academic_year": filters.get('academic_year') - }, fields=["student"]) + enrolled_students = frappe.get_list( + "Program Enrollment", + filters={ + "academic_term": filters.get("academic_term"), + "academic_year": filters.get("academic_year"), + }, + fields=["student"], + ) students = [d.student for d in enrolled_students] if enrolled_students else [""] - return frappe.db.sql("""select + return frappe.db.sql( + """select name, title from tabStudent where name not in (%s) @@ -176,8 +214,7 @@ def get_students(doctype, txt, searchfield, start, page_len, filters): `%s` LIKE %s order by idx desc, name - limit %s, %s"""%( - ", ".join(['%s']*len(students)), searchfield, "%s", "%s", "%s"), - tuple(students + ["%%%s%%" % txt, start, page_len] - ) + limit %s, %s""" + % (", ".join(["%s"] * len(students)), searchfield, "%s", "%s", "%s"), + tuple(students + ["%%%s%%" % txt, start, page_len]), ) diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment_dashboard.py b/erpnext/education/doctype/program_enrollment/program_enrollment_dashboard.py index 81bb30b1da5..c308961d8cd 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment_dashboard.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment_dashboard.py @@ -1,20 +1,9 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'program_enrollment', - 'transactions': [ - { - 'label': _('Course and Fee'), - 'items': ['Course Enrollment', 'Fees'] - } - ], - 'reports': [ - { - 'label': _('Report'), - 'items': ['Student and Guardian Contact Details'] - } - ] + "fieldname": "program_enrollment", + "transactions": [{"label": _("Course and Fee"), "items": ["Course Enrollment", "Fees"]}], + "reports": [{"label": _("Report"), "items": ["Student and Guardian Contact Details"]}], } diff --git a/erpnext/education/doctype/program_enrollment/test_program_enrollment.py b/erpnext/education/doctype/program_enrollment/test_program_enrollment.py index dda2465eafd..4d43175fef0 100644 --- a/erpnext/education/doctype/program_enrollment/test_program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/test_program_enrollment.py @@ -10,9 +10,14 @@ from erpnext.education.doctype.student.test_student import create_student, get_s class TestProgramEnrollment(unittest.TestCase): - def setUp(self): - create_student({"first_name": "_Test Name", "last_name": "_Test Last Name", "email": "_test_student@example.com"}) + create_student( + { + "first_name": "_Test Name", + "last_name": "_Test Last Name", + "email": "_test_student@example.com", + } + ) make_program_and_linked_courses("_Test Program 1", ["_Test Course 1", "_Test Course 2"]) def test_create_course_enrollments(self): diff --git a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py index 7ffa077534a..9e72876f194 100644 --- a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py +++ b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py @@ -12,7 +12,7 @@ from erpnext.education.api import enroll_student class ProgramEnrollmentTool(Document): def onload(self): - academic_term_reqd = cint(frappe.db.get_single_value('Education Settings', 'academic_term_reqd')) + academic_term_reqd = cint(frappe.db.get_single_value("Education Settings", "academic_term_reqd")) self.set_onload("academic_term_reqd", academic_term_reqd) @frappe.whitelist() @@ -25,22 +25,36 @@ class ProgramEnrollmentTool(Document): elif not self.academic_year: frappe.throw(_("Mandatory field - Academic Year")) else: - condition = 'and academic_term=%(academic_term)s' if self.academic_term else " " + condition = "and academic_term=%(academic_term)s" if self.academic_term else " " if self.get_students_from == "Student Applicant": - students = frappe.db.sql('''select name as student_applicant, title as student_name from `tabStudent Applicant` - where application_status="Approved" and program=%(program)s and academic_year=%(academic_year)s {0}''' - .format(condition), self.as_dict(), as_dict=1) + students = frappe.db.sql( + """select name as student_applicant, title as student_name from `tabStudent Applicant` + where application_status="Approved" and program=%(program)s and academic_year=%(academic_year)s {0}""".format( + condition + ), + self.as_dict(), + as_dict=1, + ) elif self.get_students_from == "Program Enrollment": - condition2 = 'and student_batch_name=%(student_batch)s' if self.student_batch else " " - students = frappe.db.sql('''select student, student_name, student_batch_name, student_category from `tabProgram Enrollment` - where program=%(program)s and academic_year=%(academic_year)s {0} {1} and docstatus != 2''' - .format(condition, condition2), self.as_dict(), as_dict=1) + condition2 = "and student_batch_name=%(student_batch)s" if self.student_batch else " " + students = frappe.db.sql( + """select student, student_name, student_batch_name, student_category from `tabProgram Enrollment` + where program=%(program)s and academic_year=%(academic_year)s {0} {1} and docstatus != 2""".format( + condition, condition2 + ), + self.as_dict(), + as_dict=1, + ) student_list = [d.student for d in students] if student_list: - inactive_students = frappe.db.sql(''' - select name as student, title as student_name from `tabStudent` where name in (%s) and enabled = 0''' % - ', '.join(['%s']*len(student_list)), tuple(student_list), as_dict=1) + inactive_students = frappe.db.sql( + """ + select name as student, title as student_name from `tabStudent` where name in (%s) and enabled = 0""" + % ", ".join(["%s"] * len(student_list)), + tuple(student_list), + as_dict=1, + ) for student in students: if student.student in [d.student for d in inactive_students]: @@ -55,7 +69,9 @@ class ProgramEnrollmentTool(Document): def enroll_students(self): total = len(self.students) for i, stud in enumerate(self.students): - frappe.publish_realtime("program_enrollment_tool", dict(progress=[i+1, total]), user=frappe.session.user) + frappe.publish_realtime( + "program_enrollment_tool", dict(progress=[i + 1, total]), user=frappe.session.user + ) if stud.student: prog_enrollment = frappe.new_doc("Program Enrollment") prog_enrollment.student = stud.student @@ -64,12 +80,16 @@ class ProgramEnrollmentTool(Document): prog_enrollment.program = self.new_program prog_enrollment.academic_year = self.new_academic_year prog_enrollment.academic_term = self.new_academic_term - prog_enrollment.student_batch_name = stud.student_batch_name if stud.student_batch_name else self.new_student_batch + prog_enrollment.student_batch_name = ( + stud.student_batch_name if stud.student_batch_name else self.new_student_batch + ) prog_enrollment.save() elif stud.student_applicant: prog_enrollment = enroll_student(stud.student_applicant) prog_enrollment.academic_year = self.academic_year prog_enrollment.academic_term = self.academic_term - prog_enrollment.student_batch_name = stud.student_batch_name if stud.student_batch_name else self.new_student_batch + prog_enrollment.student_batch_name = ( + stud.student_batch_name if stud.student_batch_name else self.new_student_batch + ) prog_enrollment.save() frappe.msgprint(_("{0} Students have been enrolled").format(total)) diff --git a/erpnext/education/doctype/question/question.py b/erpnext/education/doctype/question/question.py index aa6cf9f38d0..99910b3c90f 100644 --- a/erpnext/education/doctype/question/question.py +++ b/erpnext/education/doctype/question/question.py @@ -8,7 +8,6 @@ from frappe.model.document import Document class Question(Document): - def validate(self): self.check_at_least_one_option() self.check_minimum_one_correct_answer() diff --git a/erpnext/education/doctype/quiz/quiz.py b/erpnext/education/doctype/quiz/quiz.py index 9ad7252db62..7a9cdf3d9ae 100644 --- a/erpnext/education/doctype/quiz/quiz.py +++ b/erpnext/education/doctype/quiz/quiz.py @@ -13,11 +13,14 @@ class Quiz(Document): frappe.throw(_("Passing Score value should be between 0 and 100")) def allowed_attempt(self, enrollment, quiz_name): - if self.max_attempts == 0: + if self.max_attempts == 0: return True try: - if len(frappe.get_all("Quiz Activity", {'enrollment': enrollment.name, 'quiz': quiz_name})) >= self.max_attempts: + if ( + len(frappe.get_all("Quiz Activity", {"enrollment": enrollment.name, "quiz": quiz_name})) + >= self.max_attempts + ): frappe.msgprint(_("Maximum attempts for this quiz reached!")) return False else: @@ -25,30 +28,29 @@ class Quiz(Document): except Exception as e: return False - def evaluate(self, response_dict, quiz_name): - questions = [frappe.get_doc('Question', question.question_link) for question in self.question] - answers = {q.name:q.get_answer() for q in questions} + questions = [frappe.get_doc("Question", question.question_link) for question in self.question] + answers = {q.name: q.get_answer() for q in questions} result = {} for key in answers: try: if isinstance(response_dict[key], list): is_correct = compare_list_elementwise(response_dict[key], answers[key]) else: - is_correct = (response_dict[key] == answers[key]) + is_correct = response_dict[key] == answers[key] except Exception as e: is_correct = False result[key] = is_correct - score = (sum(result.values()) * 100 ) / len(answers) + score = (sum(result.values()) * 100) / len(answers) if score >= self.passing_score: status = "Pass" else: status = "Fail" return result, score, status - def get_questions(self): - return [frappe.get_doc('Question', question.question_link) for question in self.question] + return [frappe.get_doc("Question", question.question_link) for question in self.question] + def compare_list_elementwise(*args): try: @@ -59,11 +61,12 @@ def compare_list_elementwise(*args): except TypeError: frappe.throw(_("Compare List function takes on list arguments")) + @frappe.whitelist() def get_topics_without_quiz(quiz): data = [] - for entry in frappe.db.get_all('Topic'): - topic = frappe.get_doc('Topic', entry.name) + for entry in frappe.db.get_all("Topic"): + topic = frappe.get_doc("Topic", entry.name) topic_contents = [tc.content for tc in topic.topic_content] if not topic_contents or quiz not in topic_contents: data.append(topic.name) diff --git a/erpnext/education/doctype/room/room_dashboard.py b/erpnext/education/doctype/room/room_dashboard.py index b710722f55d..dfc295cc44f 100644 --- a/erpnext/education/doctype/room/room_dashboard.py +++ b/erpnext/education/doctype/room/room_dashboard.py @@ -6,15 +6,9 @@ from frappe import _ def get_data(): return { - 'fieldname': 'room', - 'transactions': [ - { - 'label': _('Course'), - 'items': ['Course Schedule'] - }, - { - 'label': _('Assessment'), - 'items': ['Assessment Plan'] - } - ] + "fieldname": "room", + "transactions": [ + {"label": _("Course"), "items": ["Course Schedule"]}, + {"label": _("Assessment"), "items": ["Assessment Plan"]}, + ], } diff --git a/erpnext/education/doctype/room/test_room.py b/erpnext/education/doctype/room/test_room.py index 68c97c7a008..ea0baf2137c 100644 --- a/erpnext/education/doctype/room/test_room.py +++ b/erpnext/education/doctype/room/test_room.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Room') + class TestRoom(unittest.TestCase): pass diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 44a327777bf..712d742dee9 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -26,7 +26,9 @@ class Student(Document): def validate_dates(self): for sibling in self.siblings: if sibling.date_of_birth and getdate(sibling.date_of_birth) > getdate(): - frappe.throw(_("Row {0}:Sibling Date of Birth cannot be greater than today.").format(sibling.idx)) + frappe.throw( + _("Row {0}:Sibling Date of Birth cannot be greater than today.").format(sibling.idx) + ) if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): frappe.throw(_("Date of Birth cannot be greater than today.")) @@ -34,7 +36,11 @@ class Student(Document): if self.date_of_birth and getdate(self.date_of_birth) >= getdate(self.joining_date): frappe.throw(_("Date of Birth cannot be greater than Joining Date.")) - if self.joining_date and self.date_of_leaving and getdate(self.joining_date) > getdate(self.date_of_leaving): + if ( + self.joining_date + and self.date_of_leaving + and getdate(self.joining_date) > getdate(self.date_of_leaving) + ): frappe.throw(_("Joining Date can not be greater than Leaving Date")) def update_student_name_in_linked_doctype(self): @@ -43,36 +49,54 @@ class Student(Document): meta = frappe.get_meta(d) if not meta.issingle: if "student_name" in [f.fieldname for f in meta.fields]: - frappe.db.sql("""UPDATE `tab{0}` set student_name = %s where {1} = %s""" - .format(d, linked_doctypes[d]["fieldname"][0]),(self.title, self.name)) + frappe.db.sql( + """UPDATE `tab{0}` set student_name = %s where {1} = %s""".format( + d, linked_doctypes[d]["fieldname"][0] + ), + (self.title, self.name), + ) - if "child_doctype" in linked_doctypes[d].keys() and "student_name" in \ - [f.fieldname for f in frappe.get_meta(linked_doctypes[d]["child_doctype"]).fields]: - frappe.db.sql("""UPDATE `tab{0}` set student_name = %s where {1} = %s""" - .format(linked_doctypes[d]["child_doctype"], linked_doctypes[d]["fieldname"][0]),(self.title, self.name)) + if "child_doctype" in linked_doctypes[d].keys() and "student_name" in [ + f.fieldname for f in frappe.get_meta(linked_doctypes[d]["child_doctype"]).fields + ]: + frappe.db.sql( + """UPDATE `tab{0}` set student_name = %s where {1} = %s""".format( + linked_doctypes[d]["child_doctype"], linked_doctypes[d]["fieldname"][0] + ), + (self.title, self.name), + ) def check_unique(self): """Validates if the Student Applicant is Unique""" - student = frappe.db.sql("select name from `tabStudent` where student_applicant=%s and name!=%s", (self.student_applicant, self.name)) + student = frappe.db.sql( + "select name from `tabStudent` where student_applicant=%s and name!=%s", + (self.student_applicant, self.name), + ) if student: - frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant)) + frappe.throw( + _("Student {0} exist against student applicant {1}").format( + student[0][0], self.student_applicant + ) + ) def after_insert(self): - if not frappe.get_single('Education Settings').get('user_creation_skip'): + if not frappe.get_single("Education Settings").get("user_creation_skip"): self.create_student_user() def create_student_user(self): """Create a website user for student creation if not already exists""" if not frappe.db.exists("User", self.student_email_id): - student_user = frappe.get_doc({ - 'doctype':'User', - 'first_name': self.first_name, - 'last_name': self.last_name, - 'email': self.student_email_id, - 'gender': self.gender, - 'send_welcome_email': 1, - 'user_type': 'Website User' - }) + student_user = frappe.get_doc( + { + "doctype": "User", + "first_name": self.first_name, + "last_name": self.last_name, + "email": self.student_email_id, + "gender": self.gender, + "send_welcome_email": 1, + "user_type": "Website User", + } + ) student_user.flags.ignore_permissions = True student_user.add_roles("Student") student_user.save() @@ -80,57 +104,77 @@ class Student(Document): def update_applicant_status(self): """Updates Student Applicant status to Admitted""" if self.student_applicant: - frappe.db.set_value("Student Applicant", self.student_applicant, "application_status", "Admitted") + frappe.db.set_value( + "Student Applicant", self.student_applicant, "application_status", "Admitted" + ) def get_all_course_enrollments(self): """Returns a list of course enrollments linked with the current student""" - course_enrollments = frappe.get_all("Course Enrollment", filters={"student": self.name}, fields=['course', 'name']) + course_enrollments = frappe.get_all( + "Course Enrollment", filters={"student": self.name}, fields=["course", "name"] + ) if not course_enrollments: return None else: - enrollments = {item['course']:item['name'] for item in course_enrollments} + enrollments = {item["course"]: item["name"] for item in course_enrollments} return enrollments def get_program_enrollments(self): """Returns a list of course enrollments linked with the current student""" - program_enrollments = frappe.get_all("Program Enrollment", filters={"student": self.name}, fields=['program']) + program_enrollments = frappe.get_all( + "Program Enrollment", filters={"student": self.name}, fields=["program"] + ) if not program_enrollments: return None else: - enrollments = [item['program'] for item in program_enrollments] + enrollments = [item["program"] for item in program_enrollments] return enrollments def get_topic_progress(self, course_enrollment_name, topic): """ Get Progress Dictionary of a student for a particular topic - :param self: Student Object - :param course_enrollment_name: Name of the Course Enrollment - :param topic: Topic DocType Object + :param self: Student Object + :param course_enrollment_name: Name of the Course Enrollment + :param topic: Topic DocType Object """ contents = topic.get_contents() progress = [] if contents: for content in contents: - if content.doctype in ('Article', 'Video'): + if content.doctype in ("Article", "Video"): status = check_content_completion(content.name, content.doctype, course_enrollment_name) - progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status}) - elif content.doctype == 'Quiz': + progress.append( + {"content": content.name, "content_type": content.doctype, "is_complete": status} + ) + elif content.doctype == "Quiz": status, score, result, time_taken = check_quiz_completion(content, course_enrollment_name) - progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status, 'score': score, 'result': result}) + progress.append( + { + "content": content.name, + "content_type": content.doctype, + "is_complete": status, + "score": score, + "result": result, + } + ) return progress def enroll_in_program(self, program_name): try: - enrollment = frappe.get_doc({ + enrollment = frappe.get_doc( + { "doctype": "Program Enrollment", "student": self.name, "academic_year": frappe.get_last_doc("Academic Year").name, "program": program_name, - "enrollment_date": frappe.utils.datetime.datetime.now() - }) + "enrollment_date": frappe.utils.datetime.datetime.now(), + } + ) enrollment.save(ignore_permissions=True) except frappe.exceptions.ValidationError: - enrollment_name = frappe.get_list("Program Enrollment", filters={"student": self.name, "Program": program_name})[0].name + enrollment_name = frappe.get_list( + "Program Enrollment", filters={"student": self.name, "Program": program_name} + )[0].name return frappe.get_doc("Program Enrollment", enrollment_name) else: enrollment.submit() @@ -140,25 +184,40 @@ class Student(Document): if enrollment_date is None: enrollment_date = frappe.utils.datetime.datetime.now() try: - enrollment = frappe.get_doc({ + enrollment = frappe.get_doc( + { "doctype": "Course Enrollment", "student": self.name, "course": course_name, "program_enrollment": program_enrollment, - "enrollment_date": enrollment_date - }) + "enrollment_date": enrollment_date, + } + ) enrollment.save(ignore_permissions=True) except frappe.exceptions.ValidationError: - enrollment_name = frappe.get_list("Course Enrollment", filters={"student": self.name, "course": course_name, "program_enrollment": program_enrollment})[0].name + enrollment_name = frappe.get_list( + "Course Enrollment", + filters={ + "student": self.name, + "course": course_name, + "program_enrollment": program_enrollment, + }, + )[0].name return frappe.get_doc("Course Enrollment", enrollment_name) else: return enrollment + def get_timeline_data(doctype, name): - '''Return timeline for attendance''' - return dict(frappe.db.sql('''select unix_timestamp(`date`), count(*) + """Return timeline for attendance""" + return dict( + frappe.db.sql( + """select unix_timestamp(`date`), count(*) from `tabStudent Attendance` where student=%s and `date` > date_sub(curdate(), interval 1 year) and docstatus = 1 and status = 'Present' - group by date''', name)) + group by date""", + name, + ) + ) diff --git a/erpnext/education/doctype/student/student_dashboard.py b/erpnext/education/doctype/student/student_dashboard.py index efd5dc9104a..bcc85217380 100644 --- a/erpnext/education/doctype/student/student_dashboard.py +++ b/erpnext/education/doctype/student/student_dashboard.py @@ -1,39 +1,24 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on the attendance of this Student'), - 'fieldname': 'student', - 'non_standard_fieldnames': { - 'Bank Account': 'party' - }, - 'transactions': [ + "heatmap": True, + "heatmap_message": _("This is based on the attendance of this Student"), + "fieldname": "student", + "non_standard_fieldnames": {"Bank Account": "party"}, + "transactions": [ + {"label": _("Admission"), "items": ["Program Enrollment", "Course Enrollment"]}, { - 'label': _('Admission'), - 'items': ['Program Enrollment', 'Course Enrollment'] + "label": _("Student Activity"), + "items": [ + "Student Log", + "Student Group", + ], }, - { - 'label': _('Student Activity'), - 'items': ['Student Log', 'Student Group', ] - }, - { - 'label': _('Assessment'), - 'items': ['Assessment Result'] - }, - { - 'label': _('Student LMS Activity'), - 'items': ['Course Activity', 'Quiz Activity' ] - }, - { - 'label': _('Attendance'), - 'items': ['Student Attendance', 'Student Leave Application'] - }, - { - 'label': _('Fee'), - 'items': ['Fees', 'Bank Account'] - } - ] + {"label": _("Assessment"), "items": ["Assessment Result"]}, + {"label": _("Student LMS Activity"), "items": ["Course Activity", "Quiz Activity"]}, + {"label": _("Attendance"), "items": ["Student Attendance", "Student Leave Application"]}, + {"label": _("Fee"), "items": ["Fees", "Bank Account"]}, + ], } diff --git a/erpnext/education/doctype/student/test_student.py b/erpnext/education/doctype/student/test_student.py index 0a85708152c..89449b0299c 100644 --- a/erpnext/education/doctype/student/test_student.py +++ b/erpnext/education/doctype/student/test_student.py @@ -7,10 +7,18 @@ import frappe from erpnext.education.doctype.program.test_program import make_program_and_linked_courses -test_records = frappe.get_test_records('Student') +test_records = frappe.get_test_records("Student") + + class TestStudent(unittest.TestCase): def setUp(self): - create_student({"first_name": "_Test Name", "last_name": "_Test Last Name", "email": "_test_student@example.com"}) + create_student( + { + "first_name": "_Test Name", + "last_name": "_Test Last Name", + "email": "_test_student@example.com", + } + ) make_program_and_linked_courses("_Test Program 1", ["_Test Course 1", "_Test Course 2"]) def test_create_student_user(self): @@ -20,9 +28,11 @@ class TestStudent(unittest.TestCase): def test_enroll_in_program(self): student = get_student("_test_student@example.com") enrollment = student.enroll_in_program("_Test Program 1") - test_enrollment = frappe.get_all("Program Enrollment", filters={"student": student.name, "Program": "_Test Program 1"}) + test_enrollment = frappe.get_all( + "Program Enrollment", filters={"student": student.name, "Program": "_Test Program 1"} + ) self.assertTrue(len(test_enrollment)) - self.assertEqual(test_enrollment[0]['name'], enrollment.name) + self.assertEqual(test_enrollment[0]["name"], enrollment.name) frappe.db.rollback() def test_get_program_enrollments(self): @@ -51,16 +61,19 @@ class TestStudent(unittest.TestCase): def create_student(student_dict): - student = get_student(student_dict['email']) + student = get_student(student_dict["email"]) if not student: - student = frappe.get_doc({ - "doctype": "Student", - "first_name": student_dict['first_name'], - "last_name": student_dict['last_name'], - "student_email_id": student_dict['email'] - }).insert() + student = frappe.get_doc( + { + "doctype": "Student", + "first_name": student_dict["first_name"], + "last_name": student_dict["last_name"], + "student_email_id": student_dict["email"], + } + ).insert() return student + def get_student(email): try: student_id = frappe.get_all("Student", {"student_email_id": email}, ["name"])[0].name diff --git a/erpnext/education/doctype/student_admission/student_admission.py b/erpnext/education/doctype/student_admission/student_admission.py index b1fd780d8cc..6c775f0dffc 100644 --- a/erpnext/education/doctype/student_admission/student_admission.py +++ b/erpnext/education/doctype/student_admission/student_admission.py @@ -15,7 +15,7 @@ class StudentAdmission(WebsiteGenerator): self.name = self.title def validate(self): - if not self.route: #pylint: disable=E0203 + if not self.route: # pylint: disable=E0203 self.route = "admissions/" + "-".join(self.title.split(" ")) if self.enable_admission_application and not self.program_details: @@ -25,22 +25,35 @@ class StudentAdmission(WebsiteGenerator): context.no_cache = 1 context.show_sidebar = True context.title = self.title - context.parents = [{'name': 'admissions', 'title': _('All Student Admissions'), 'route': 'admissions' }] + context.parents = [ + {"name": "admissions", "title": _("All Student Admissions"), "route": "admissions"} + ] def get_title(self): return _("Admissions for {0}").format(self.academic_year) def get_list_context(context=None): - context.update({ - "show_sidebar": True, - "title": _("Student Admissions"), - "get_list": get_admission_list, - "row_template": "education/doctype/student_admission/templates/student_admission_row.html", - }) + context.update( + { + "show_sidebar": True, + "title": _("Student Admissions"), + "get_list": get_admission_list, + "row_template": "education/doctype/student_admission/templates/student_admission_row.html", + } + ) -def get_admission_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): - return frappe.db.sql('''select name, title, academic_year, modified, admission_start_date, route, + +def get_admission_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" +): + return frappe.db.sql( + """select name, title, academic_year, modified, admission_start_date, route, admission_end_date from `tabStudent Admission` where published=1 and admission_end_date >= %s order by admission_end_date asc limit {0}, {1} - '''.format(limit_start, limit_page_length), [nowdate()], as_dict=1) + """.format( + limit_start, limit_page_length + ), + [nowdate()], + as_dict=1, + ) diff --git a/erpnext/education/doctype/student_admission/templates/student_admission_row.html b/erpnext/education/doctype/student_admission/templates/student_admission_row.html index 529d65184a8..dc4587bc940 100644 --- a/erpnext/education/doctype/student_admission/templates/student_admission_row.html +++ b/erpnext/education/doctype/student_admission/templates/student_admission_row.html @@ -1,6 +1,6 @@
{% set today = frappe.utils.getdate(frappe.utils.nowdate()) %} - +
0: - frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth")) + if ( + student_admission + and student_admission.min_age + and date_diff(nowdate(), add_years(getdate(self.date_of_birth), student_admission.min_age)) < 0 + ): + frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth")) + if ( + student_admission + and student_admission.max_age + and date_diff(nowdate(), add_years(getdate(self.date_of_birth), student_admission.max_age)) > 0 + ): + frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth")) def on_payment_authorized(self, *args, **kwargs): - self.db_set('paid', 1) + self.db_set("paid", 1) def get_student_admission_data(student_admission, program): - student_admission = frappe.db.sql("""select sa.admission_start_date, sa.admission_end_date, + student_admission = frappe.db.sql( + """select sa.admission_start_date, sa.admission_end_date, sap.program, sap.min_age, sap.max_age, sap.applicant_naming_series from `tabStudent Admission` sa, `tabStudent Admission Program` sap - where sa.name = sap.parent and sa.name = %s and sap.program = %s""", (student_admission, program), as_dict=1) + where sa.name = sap.parent and sa.name = %s and sap.program = %s""", + (student_admission, program), + as_dict=1, + ) if student_admission: return student_admission[0] diff --git a/erpnext/education/doctype/student_applicant/test_student_applicant.py b/erpnext/education/doctype/student_applicant/test_student_applicant.py index ba2e9c18863..bd0e360db7e 100644 --- a/erpnext/education/doctype/student_applicant/test_student_applicant.py +++ b/erpnext/education/doctype/student_applicant/test_student_applicant.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Student Applicant') + class TestStudentApplicant(unittest.TestCase): pass diff --git a/erpnext/education/doctype/student_attendance/student_attendance.py b/erpnext/education/doctype/student_attendance/student_attendance.py index db0fd3719da..20f8fb7d77a 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.py +++ b/erpnext/education/doctype/student_attendance/student_attendance.py @@ -24,76 +24,109 @@ class StudentAttendance(Document): def set_date(self): if self.course_schedule: - self.date = frappe.db.get_value('Course Schedule', self.course_schedule, 'schedule_date') + self.date = frappe.db.get_value("Course Schedule", self.course_schedule, "schedule_date") def validate_mandatory(self): if not (self.student_group or self.course_schedule): - frappe.throw(_('{0} or {1} is mandatory').format(frappe.bold('Student Group'), - frappe.bold('Course Schedule')), title=_('Mandatory Fields')) + frappe.throw( + _("{0} or {1} is mandatory").format( + frappe.bold("Student Group"), frappe.bold("Course Schedule") + ), + title=_("Mandatory Fields"), + ) def validate_date(self): if not self.leave_application and getdate(self.date) > getdate(): - frappe.throw(_('Attendance cannot be marked for future dates.')) + frappe.throw(_("Attendance cannot be marked for future dates.")) if self.student_group: - academic_year = frappe.db.get_value('Student Group', self.student_group, 'academic_year') + academic_year = frappe.db.get_value("Student Group", self.student_group, "academic_year") if academic_year: - year_start_date, year_end_date = frappe.db.get_value('Academic Year', academic_year, ['year_start_date', 'year_end_date']) + year_start_date, year_end_date = frappe.db.get_value( + "Academic Year", academic_year, ["year_start_date", "year_end_date"] + ) if year_start_date and year_end_date: - if getdate(self.date) < getdate(year_start_date) or getdate(self.date) > getdate(year_end_date): - frappe.throw(_('Attendance cannot be marked outside of Academic Year {0}').format(academic_year)) + if getdate(self.date) < getdate(year_start_date) or getdate(self.date) > getdate( + year_end_date + ): + frappe.throw( + _("Attendance cannot be marked outside of Academic Year {0}").format(academic_year) + ) def set_student_group(self): if self.course_schedule: - self.student_group = frappe.db.get_value('Course Schedule', self.course_schedule, 'student_group') + self.student_group = frappe.db.get_value( + "Course Schedule", self.course_schedule, "student_group" + ) def validate_student(self): if self.course_schedule: - student_group = frappe.db.get_value('Course Schedule', self.course_schedule, 'student_group') + student_group = frappe.db.get_value("Course Schedule", self.course_schedule, "student_group") else: student_group = self.student_group student_group_students = [d.student for d in get_student_group_students(student_group)] if student_group and self.student not in student_group_students: - student_group_doc = get_link_to_form('Student Group', student_group) - frappe.throw(_('Student {0}: {1} does not belong to Student Group {2}').format( - frappe.bold(self.student), self.student_name, frappe.bold(student_group_doc))) + student_group_doc = get_link_to_form("Student Group", student_group) + frappe.throw( + _("Student {0}: {1} does not belong to Student Group {2}").format( + frappe.bold(self.student), self.student_name, frappe.bold(student_group_doc) + ) + ) def validate_duplication(self): """Check if the Attendance Record is Unique""" attendance_record = None if self.course_schedule: - attendance_record = frappe.db.exists('Student Attendance', { - 'student': self.student, - 'course_schedule': self.course_schedule, - 'docstatus': ('!=', 2), - 'name': ('!=', self.name) - }) + attendance_record = frappe.db.exists( + "Student Attendance", + { + "student": self.student, + "course_schedule": self.course_schedule, + "docstatus": ("!=", 2), + "name": ("!=", self.name), + }, + ) else: - attendance_record = frappe.db.exists('Student Attendance', { - 'student': self.student, - 'student_group': self.student_group, - 'date': self.date, - 'docstatus': ('!=', 2), - 'name': ('!=', self.name), - 'course_schedule': '' - }) + attendance_record = frappe.db.exists( + "Student Attendance", + { + "student": self.student, + "student_group": self.student_group, + "date": self.date, + "docstatus": ("!=", 2), + "name": ("!=", self.name), + "course_schedule": "", + }, + ) if attendance_record: - record = get_link_to_form('Student Attendance', attendance_record) - frappe.throw(_('Student Attendance record {0} already exists against the Student {1}') - .format(record, frappe.bold(self.student)), title=_('Duplicate Entry')) + record = get_link_to_form("Student Attendance", attendance_record) + frappe.throw( + _("Student Attendance record {0} already exists against the Student {1}").format( + record, frappe.bold(self.student) + ), + title=_("Duplicate Entry"), + ) def validate_is_holiday(self): holiday_list = get_holiday_list() if is_holiday(holiday_list, self.date): - frappe.throw(_('Attendance cannot be marked for {0} as it is a holiday.').format( - frappe.bold(formatdate(self.date)))) + frappe.throw( + _("Attendance cannot be marked for {0} as it is a holiday.").format( + frappe.bold(formatdate(self.date)) + ) + ) + def get_holiday_list(company=None): if not company: - company = get_default_company() or frappe.get_all('Company')[0].name + company = get_default_company() or frappe.get_all("Company")[0].name - holiday_list = frappe.get_cached_value('Company', company, 'default_holiday_list') + holiday_list = frappe.get_cached_value("Company", company, "default_holiday_list") if not holiday_list: - frappe.throw(_('Please set a default Holiday List for Company {0}').format(frappe.bold(get_default_company()))) + frappe.throw( + _("Please set a default Holiday List for Company {0}").format( + frappe.bold(get_default_company()) + ) + ) return holiday_list diff --git a/erpnext/education/doctype/student_attendance/student_attendance_dashboard.py b/erpnext/education/doctype/student_attendance/student_attendance_dashboard.py index 97547992043..34132af69e6 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance_dashboard.py +++ b/erpnext/education/doctype/student_attendance/student_attendance_dashboard.py @@ -1,13 +1,12 @@ - from frappe import _ def get_data(): return { - 'reports': [ + "reports": [ { - 'label': _('Reports'), - 'items': ['Student Monthly Attendance Sheet', 'Student Batch-Wise Attendance'] + "label": _("Reports"), + "items": ["Student Monthly Attendance Sheet", "Student Batch-Wise Attendance"], } ] } diff --git a/erpnext/education/doctype/student_attendance/test_student_attendance.py b/erpnext/education/doctype/student_attendance/test_student_attendance.py index 6a43e30a33c..e5b1841e1db 100644 --- a/erpnext/education/doctype/student_attendance/test_student_attendance.py +++ b/erpnext/education/doctype/student_attendance/test_student_attendance.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Student Attendance') + class TestStudentAttendance(unittest.TestCase): pass diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py index 7deb6b18da5..92fc25e54e7 100644 --- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py +++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py @@ -9,6 +9,7 @@ from frappe.model.document import Document class StudentAttendanceTool(Document): pass + @frappe.whitelist() def get_student_attendance_records(based_on, date=None, student_group=None, course_schedule=None): student_list = [] @@ -17,33 +18,38 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour if based_on == "Course Schedule": student_group = frappe.db.get_value("Course Schedule", course_schedule, "student_group") if student_group: - student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"], - filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") + student_list = frappe.get_all( + "Student Group Student", + fields=["student", "student_name", "group_roll_number"], + filters={"parent": student_group, "active": 1}, + order_by="group_roll_number", + ) if not student_list: - student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"], - filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") + student_list = frappe.get_all( + "Student Group Student", + fields=["student", "student_name", "group_roll_number"], + filters={"parent": student_group, "active": 1}, + order_by="group_roll_number", + ) table = frappe.qb.DocType("Student Attendance") if course_schedule: student_attendance_list = ( frappe.qb.from_(table) - .select(table.student, table.status) - .where( - (table.course_schedule == course_schedule) - ) - ).run(as_dict=True) + .select(table.student, table.status) + .where((table.course_schedule == course_schedule)) + ).run(as_dict=True) else: student_attendance_list = ( frappe.qb.from_(table) - .select(table.student, table.status) - .where( - (table.student_group == student_group) - & (table.date == date) - & (table.course_schedule == "") | (table.course_schedule.isnull()) - ) - ).run(as_dict=True) + .select(table.student, table.status) + .where( + (table.student_group == student_group) & (table.date == date) & (table.course_schedule == "") + | (table.course_schedule.isnull()) + ) + ).run(as_dict=True) for attendance in student_attendance_list: for student in student_list: diff --git a/erpnext/education/doctype/student_batch_name/test_student_batch_name.py b/erpnext/education/doctype/student_batch_name/test_student_batch_name.py index ad9b545e744..bf9639bf627 100644 --- a/erpnext/education/doctype/student_batch_name/test_student_batch_name.py +++ b/erpnext/education/doctype/student_batch_name/test_student_batch_name.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Student Batch Name') + class TestStudentBatchName(unittest.TestCase): pass diff --git a/erpnext/education/doctype/student_category/student_category_dashboard.py b/erpnext/education/doctype/student_category/student_category_dashboard.py index be1e0054915..d7a332c4d2a 100644 --- a/erpnext/education/doctype/student_category/student_category_dashboard.py +++ b/erpnext/education/doctype/student_category/student_category_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'student_category', - 'transactions': [ - { - 'label': _('Fee'), - 'items': ['Fee Structure', 'Fee Schedule', 'Fees'] - } - ] + "fieldname": "student_category", + "transactions": [{"label": _("Fee"), "items": ["Fee Structure", "Fee Schedule", "Fees"]}], } diff --git a/erpnext/education/doctype/student_category/test_student_category.py b/erpnext/education/doctype/student_category/test_student_category.py index 76469fff250..5671e9fa447 100644 --- a/erpnext/education/doctype/student_category/test_student_category.py +++ b/erpnext/education/doctype/student_category/test_student_category.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Student Category') + class TestStudentCategory(unittest.TestCase): pass diff --git a/erpnext/education/doctype/student_group/student_group.py b/erpnext/education/doctype/student_group/student_group.py index ceae036e3ed..a94489ba53b 100644 --- a/erpnext/education/doctype/student_group/student_group.py +++ b/erpnext/education/doctype/student_group/student_group.py @@ -30,22 +30,45 @@ class StudentGroup(Document): if cint(self.max_strength) < 0: frappe.throw(_("""Max strength cannot be less than zero.""")) if self.max_strength and len(self.students) > self.max_strength: - frappe.throw(_("""Cannot enroll more than {0} students for this student group.""").format(self.max_strength)) + frappe.throw( + _("""Cannot enroll more than {0} students for this student group.""").format(self.max_strength) + ) def validate_students(self): - program_enrollment = get_program_enrollment(self.academic_year, self.academic_term, self.program, self.batch, self.student_category, self.course) + program_enrollment = get_program_enrollment( + self.academic_year, + self.academic_term, + self.program, + self.batch, + self.student_category, + self.course, + ) students = [d.student for d in program_enrollment] if program_enrollment else [] for d in self.students: if not frappe.db.get_value("Student", d.student, "enabled") and d.active and not self.disabled: frappe.throw(_("{0} - {1} is inactive student").format(d.group_roll_number, d.student_name)) - if (self.group_based_on == "Batch") and cint(frappe.defaults.get_defaults().validate_batch)\ - and d.student not in students: - frappe.throw(_("{0} - {1} is not enrolled in the Batch {2}").format(d.group_roll_number, d.student_name, self.batch)) + if ( + (self.group_based_on == "Batch") + and cint(frappe.defaults.get_defaults().validate_batch) + and d.student not in students + ): + frappe.throw( + _("{0} - {1} is not enrolled in the Batch {2}").format( + d.group_roll_number, d.student_name, self.batch + ) + ) - if (self.group_based_on == "Course") and cint(frappe.defaults.get_defaults().validate_course)\ - and (d.student not in students): - frappe.throw(_("{0} - {1} is not enrolled in the Course {2}").format(d.group_roll_number, d.student_name, self.course)) + if ( + (self.group_based_on == "Course") + and cint(frappe.defaults.get_defaults().validate_course) + and (d.student not in students) + ): + frappe.throw( + _("{0} - {1} is not enrolled in the Course {2}").format( + d.group_roll_number, d.student_name, self.course + ) + ) def validate_and_set_child_table_fields(self): roll_numbers = [d.group_roll_number for d in self.students if d.group_roll_number] @@ -62,9 +85,20 @@ class StudentGroup(Document): else: roll_no_list.append(d.group_roll_number) + @frappe.whitelist() -def get_students(academic_year, group_based_on, academic_term=None, program=None, batch=None, student_category=None, course=None): - enrolled_students = get_program_enrollment(academic_year, academic_term, program, batch, student_category, course) +def get_students( + academic_year, + group_based_on, + academic_term=None, + program=None, + batch=None, + student_category=None, + course=None, +): + enrolled_students = get_program_enrollment( + academic_year, academic_term, program, batch, student_category, course + ) if enrolled_students: student_list = [] @@ -79,7 +113,10 @@ def get_students(academic_year, group_based_on, academic_term=None, program=None frappe.msgprint(_("No students found")) return [] -def get_program_enrollment(academic_year, academic_term=None, program=None, batch=None, student_category=None, course=None): + +def get_program_enrollment( + academic_year, academic_term=None, program=None, batch=None, student_category=None, course=None +): condition1 = " " condition2 = " " @@ -95,7 +132,8 @@ def get_program_enrollment(academic_year, academic_term=None, program=None, batc condition1 += " and pe.name = pec.parent and pec.course = %(course)s" condition2 = ", `tabProgram Enrollment Course` pec" - return frappe.db.sql(''' + return frappe.db.sql( + """ select pe.student, pe.student_name from @@ -104,28 +142,59 @@ def get_program_enrollment(academic_year, academic_term=None, program=None, batc pe.academic_year = %(academic_year)s {condition1} order by pe.student_name asc - '''.format(condition1=condition1, condition2=condition2), - ({"academic_year": academic_year, "academic_term":academic_term, "program": program, "batch": batch, "student_category": student_category, "course": course}), as_dict=1) + """.format( + condition1=condition1, condition2=condition2 + ), + ( + { + "academic_year": academic_year, + "academic_term": academic_term, + "program": program, + "batch": batch, + "student_category": student_category, + "course": course, + } + ), + as_dict=1, + ) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def fetch_students(doctype, txt, searchfield, start, page_len, filters): if filters.get("group_based_on") != "Activity": - enrolled_students = get_program_enrollment(filters.get('academic_year'), filters.get('academic_term'), - filters.get('program'), filters.get('batch'), filters.get('student_category')) - student_group_student = frappe.db.sql_list('''select student from `tabStudent Group Student` where parent=%s''', - (filters.get('student_group'))) - students = ([d.student for d in enrolled_students if d.student not in student_group_student] - if enrolled_students else [""]) or [""] - return frappe.db.sql("""select name, title from tabStudent + enrolled_students = get_program_enrollment( + filters.get("academic_year"), + filters.get("academic_term"), + filters.get("program"), + filters.get("batch"), + filters.get("student_category"), + ) + student_group_student = frappe.db.sql_list( + """select student from `tabStudent Group Student` where parent=%s""", + (filters.get("student_group")), + ) + students = ( + [d.student for d in enrolled_students if d.student not in student_group_student] + if enrolled_students + else [""] + ) or [""] + return frappe.db.sql( + """select name, title from tabStudent where name in ({0}) and (`{1}` LIKE %s or title LIKE %s) order by idx desc, name - limit %s, %s""".format(", ".join(['%s']*len(students)), searchfield), - tuple(students + ["%%%s%%" % txt, "%%%s%%" % txt, start, page_len])) + limit %s, %s""".format( + ", ".join(["%s"] * len(students)), searchfield + ), + tuple(students + ["%%%s%%" % txt, "%%%s%%" % txt, start, page_len]), + ) else: - return frappe.db.sql("""select name, title from tabStudent + return frappe.db.sql( + """select name, title from tabStudent where `{0}` LIKE %s or title LIKE %s order by idx desc, name - limit %s, %s""".format(searchfield), - tuple(["%%%s%%" % txt, "%%%s%%" % txt, start, page_len])) + limit %s, %s""".format( + searchfield + ), + tuple(["%%%s%%" % txt, "%%%s%%" % txt, start, page_len]), + ) diff --git a/erpnext/education/doctype/student_group/student_group_dashboard.py b/erpnext/education/doctype/student_group/student_group_dashboard.py index d5b930237f4..094b5a02351 100644 --- a/erpnext/education/doctype/student_group/student_group_dashboard.py +++ b/erpnext/education/doctype/student_group/student_group_dashboard.py @@ -6,15 +6,9 @@ from frappe import _ def get_data(): return { - 'fieldname': 'student_group', - 'transactions': [ - { - 'label': _('Assessment'), - 'items': ['Assessment Plan', 'Assessment Result'] - }, - { - 'label': _('Course'), - 'items': ['Course Schedule'] - } - ] + "fieldname": "student_group", + "transactions": [ + {"label": _("Assessment"), "items": ["Assessment Plan", "Assessment Result"]}, + {"label": _("Course"), "items": ["Course Schedule"]}, + ], } diff --git a/erpnext/education/doctype/student_group/test_student_group.py b/erpnext/education/doctype/student_group/test_student_group.py index 807c63280f2..84b49309acb 100644 --- a/erpnext/education/doctype/student_group/test_student_group.py +++ b/erpnext/education/doctype/student_group/test_student_group.py @@ -9,19 +9,22 @@ import erpnext.education def get_random_group(): - doc = frappe.get_doc({ - "doctype": "Student Group", - "student_group_name": "_Test Student Group-" + frappe.generate_hash(length=5), - "group_based_on": "Activity" - }).insert() + doc = frappe.get_doc( + { + "doctype": "Student Group", + "student_group_name": "_Test Student Group-" + frappe.generate_hash(length=5), + "group_based_on": "Activity", + } + ).insert() - student_list = frappe.get_all('Student', limit=5) + student_list = frappe.get_all("Student", limit=5) - doc.extend("students", [{"student":d.name, "active": 1} for d in student_list]) + doc.extend("students", [{"student": d.name, "active": 1} for d in student_list]) doc.save() return doc + class TestStudentGroup(unittest.TestCase): def test_student_roll_no(self): doc = get_random_group() @@ -36,8 +39,12 @@ class TestStudentGroup(unittest.TestCase): doc.students = doc.students[:-1] doc.save() - self.assertRaises(erpnext.education.StudentNotInGroupError, - erpnext.education.validate_student_belongs_to_group, last_student, doc.name) + self.assertRaises( + erpnext.education.StudentNotInGroupError, + erpnext.education.validate_student_belongs_to_group, + last_student, + doc.name, + ) # safe, don't throw validation erpnext.education.validate_student_belongs_to_group(doc.students[0].student, doc.name) diff --git a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py index 8fbfcec3b57..0fb255077f1 100644 --- a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py +++ b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py @@ -14,32 +14,49 @@ class StudentGroupCreationTool(Document): def get_courses(self): group_list = [] - batches = frappe.db.sql('''select name as batch from `tabStudent Batch Name`''', as_dict=1) + batches = frappe.db.sql("""select name as batch from `tabStudent Batch Name`""", as_dict=1) for batch in batches: - group_list.append({"group_based_on":"Batch", "batch":batch.batch}) + group_list.append({"group_based_on": "Batch", "batch": batch.batch}) - courses = frappe.db.sql('''select course, course_name from `tabProgram Course` where parent=%s''', - (self.program), as_dict=1) + courses = frappe.db.sql( + """select course, course_name from `tabProgram Course` where parent=%s""", + (self.program), + as_dict=1, + ) if self.separate_groups: from itertools import product - course_list = product(courses,batches) + + course_list = product(courses, batches) for course in course_list: temp_dict = {} - temp_dict.update({"group_based_on":"Course"}) + temp_dict.update({"group_based_on": "Course"}) temp_dict.update(course[0]) temp_dict.update(course[1]) group_list.append(temp_dict) else: for course in courses: - course.update({"group_based_on":"Course"}) + course.update({"group_based_on": "Course"}) group_list.append(course) for group in group_list: if group.get("group_based_on") == "Batch": - student_group_name = self.program + "/" + group.get("batch") + "/" + (self.academic_term if self.academic_term else self.academic_year) + student_group_name = ( + self.program + + "/" + + group.get("batch") + + "/" + + (self.academic_term if self.academic_term else self.academic_year) + ) group.update({"student_group_name": student_group_name}) elif group.get("group_based_on") == "Course": - student_group_name = group.get("course") + "/" + self.program + ("/" + group.get("batch") if group.get("batch") else "") + "/" + (self.academic_term if self.academic_term else self.academic_year) + student_group_name = ( + group.get("course") + + "/" + + self.program + + ("/" + group.get("batch") if group.get("batch") else "") + + "/" + + (self.academic_term if self.academic_term else self.academic_year) + ) group.update({"student_group_name": student_group_name}) return group_list @@ -60,7 +77,9 @@ class StudentGroupCreationTool(Document): if d.group_based_on == "Batch" and not d.batch: frappe.throw(_("""Batch is mandatory in row {0}""".format(d.idx))) - frappe.publish_realtime('student_group_creation_progress', {"progress": [d.idx, l]}, user=frappe.session.user) + frappe.publish_realtime( + "student_group_creation_progress", {"progress": [d.idx, l]}, user=frappe.session.user + ) student_group = frappe.new_doc("Student Group") student_group.student_group_name = d.student_group_name @@ -71,10 +90,12 @@ class StudentGroupCreationTool(Document): student_group.max_strength = d.max_strength student_group.academic_term = self.academic_term student_group.academic_year = self.academic_year - student_list = get_students(self.academic_year, d.group_based_on, self.academic_term, self.program, d.batch, d.course) + student_list = get_students( + self.academic_year, d.group_based_on, self.academic_term, self.program, d.batch, d.course + ) for student in student_list: - student_group.append('students', student) + student_group.append("students", student) student_group.save() frappe.msgprint(_("{0} Student Groups created.").format(l)) diff --git a/erpnext/education/doctype/student_language/test_student_language.py b/erpnext/education/doctype/student_language/test_student_language.py index 718896c6c51..d1a8b6d5266 100644 --- a/erpnext/education/doctype/student_language/test_student_language.py +++ b/erpnext/education/doctype/student_language/test_student_language.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Student Language') + class TestStudentLanguage(unittest.TestCase): pass diff --git a/erpnext/education/doctype/student_leave_application/student_leave_application.py b/erpnext/education/doctype/student_leave_application/student_leave_application.py index b1eda9a3c10..c3645cf4852 100644 --- a/erpnext/education/doctype/student_leave_application/student_leave_application.py +++ b/erpnext/education/doctype/student_leave_application/student_leave_application.py @@ -17,7 +17,7 @@ class StudentLeaveApplication(Document): def validate(self): self.validate_holiday_list() self.validate_duplicate() - self.validate_from_to_dates('from_date', 'to_date') + self.validate_from_to_dates("from_date", "to_date") def on_submit(self): self.update_attendance() @@ -26,23 +26,31 @@ class StudentLeaveApplication(Document): self.cancel_attendance() def validate_duplicate(self): - data = frappe.db.sql("""select name from `tabStudent Leave Application` + data = frappe.db.sql( + """select name from `tabStudent Leave Application` where ((%(from_date)s > from_date and %(from_date)s < to_date) or (%(to_date)s > from_date and %(to_date)s < to_date) or (%(from_date)s <= from_date and %(to_date)s >= to_date)) and name != %(name)s and student = %(student)s and docstatus < 2 - """, { - 'from_date': self.from_date, - 'to_date': self.to_date, - 'student': self.student, - 'name': self.name - }, as_dict=1) + """, + { + "from_date": self.from_date, + "to_date": self.to_date, + "student": self.student, + "name": self.name, + }, + as_dict=1, + ) if data: - link = get_link_to_form('Student Leave Application', data[0].name) - frappe.throw(_('Leave application {0} already exists against the student {1}') - .format(link, frappe.bold(self.student)), title=_('Duplicate Entry')) + link = get_link_to_form("Student Leave Application", data[0].name) + frappe.throw( + _("Leave application {0} already exists against the student {1}").format( + link, frappe.bold(self.student) + ), + title=_("Duplicate Entry"), + ) def validate_holiday_list(self): holiday_list = get_holiday_list() @@ -52,33 +60,31 @@ class StudentLeaveApplication(Document): holiday_list = get_holiday_list() for dt in daterange(getdate(self.from_date), getdate(self.to_date)): - date = dt.strftime('%Y-%m-%d') + date = dt.strftime("%Y-%m-%d") if is_holiday(holiday_list, date): continue - attendance = frappe.db.exists('Student Attendance', { - 'student': self.student, - 'date': date, - 'docstatus': ('!=', 2) - }) + attendance = frappe.db.exists( + "Student Attendance", {"student": self.student, "date": date, "docstatus": ("!=", 2)} + ) - status = 'Present' if self.mark_as_present else 'Absent' + status = "Present" if self.mark_as_present else "Absent" if attendance: # update existing attendance record values = dict() - values['status'] = status - values['leave_application'] = self.name - frappe.db.set_value('Student Attendance', attendance, values) + values["status"] = status + values["leave_application"] = self.name + frappe.db.set_value("Student Attendance", attendance, values) else: # make a new attendance record - doc = frappe.new_doc('Student Attendance') + doc = frappe.new_doc("Student Attendance") doc.student = self.student doc.student_name = self.student_name doc.date = date doc.leave_application = self.name doc.status = status - if self.attendance_based_on == 'Student Group': + if self.attendance_based_on == "Student Group": doc.student_group = self.student_group else: doc.course_schedule = self.course_schedule @@ -87,34 +93,42 @@ class StudentLeaveApplication(Document): def cancel_attendance(self): if self.docstatus == 2: - attendance = frappe.db.sql(""" + attendance = frappe.db.sql( + """ SELECT name FROM `tabStudent Attendance` WHERE student = %s and (date between %s and %s) and docstatus < 2 - """, (self.student, self.from_date, self.to_date), as_dict=1) + """, + (self.student, self.from_date, self.to_date), + as_dict=1, + ) for name in attendance: - frappe.db.set_value('Student Attendance', name, 'docstatus', 2) + frappe.db.set_value("Student Attendance", name, "docstatus", 2) def daterange(start_date, end_date): - for n in range(int ((end_date - start_date).days)+1): + for n in range(int((end_date - start_date).days) + 1): yield start_date + timedelta(n) + def get_number_of_leave_days(from_date, to_date, holiday_list): number_of_days = date_diff(to_date, from_date) + 1 - holidays = frappe.db.sql(""" + holidays = frappe.db.sql( + """ SELECT COUNT(DISTINCT holiday_date) FROM `tabHoliday` h1,`tabHoliday List` h2 WHERE h1.parent = h2.name and h1.holiday_date between %s and %s and - h2.name = %s""", (from_date, to_date, holiday_list))[0][0] + h2.name = %s""", + (from_date, to_date, holiday_list), + )[0][0] number_of_days = flt(number_of_days) - flt(holidays) diff --git a/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py b/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py index fca5ad6ed56..8819c3bf657 100644 --- a/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py +++ b/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'leave_application', - 'transactions': [ - { - 'items': ['Student Attendance'] - } - ] - } + return {"fieldname": "leave_application", "transactions": [{"items": ["Student Attendance"]}]} diff --git a/erpnext/education/doctype/student_leave_application/test_student_leave_application.py b/erpnext/education/doctype/student_leave_application/test_student_leave_application.py index 92e82c5b8a1..7146fc25768 100644 --- a/erpnext/education/doctype/student_leave_application/test_student_leave_application.py +++ b/erpnext/education/doctype/student_leave_application/test_student_leave_application.py @@ -18,36 +18,44 @@ class TestStudentLeaveApplication(unittest.TestCase): def test_attendance_record_creation(self): leave_application = create_leave_application() - attendance_record = frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'status': 'Absent'}) + attendance_record = frappe.db.exists( + "Student Attendance", {"leave_application": leave_application.name, "status": "Absent"} + ) self.assertTrue(attendance_record) # mark as present date = add_days(getdate(), -1) leave_application = create_leave_application(date, date, 1) - attendance_record = frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'status': 'Present'}) + attendance_record = frappe.db.exists( + "Student Attendance", {"leave_application": leave_application.name, "status": "Present"} + ) self.assertTrue(attendance_record) def test_attendance_record_updated(self): attendance = create_student_attendance() create_leave_application() - self.assertEqual(frappe.db.get_value('Student Attendance', attendance.name, 'status'), 'Absent') + self.assertEqual(frappe.db.get_value("Student Attendance", attendance.name, "status"), "Absent") def test_attendance_record_cancellation(self): leave_application = create_leave_application() leave_application.cancel() - attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus') + attendance_status = frappe.db.get_value( + "Student Attendance", {"leave_application": leave_application.name}, "docstatus" + ) self.assertTrue(attendance_status, 2) def test_holiday(self): today = getdate() - leave_application = create_leave_application(from_date=today, to_date= add_days(today, 1), submit=0) + leave_application = create_leave_application( + from_date=today, to_date=add_days(today, 1), submit=0 + ) # holiday list validation - company = get_default_company() or frappe.get_all('Company')[0].name - frappe.db.set_value('Company', company, 'default_holiday_list', '') + company = get_default_company() or frappe.get_all("Company")[0].name + frappe.db.set_value("Company", company, "default_holiday_list", "") self.assertRaises(frappe.ValidationError, leave_application.save) - frappe.db.set_value('Company', company, 'default_holiday_list', 'Test Holiday List for Student') + frappe.db.set_value("Company", company, "default_holiday_list", "Test Holiday List for Student") leave_application.save() leave_application.reload() @@ -55,19 +63,23 @@ class TestStudentLeaveApplication(unittest.TestCase): # check no attendance record created for a holiday leave_application.submit() - self.assertIsNone(frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'date': add_days(today, 1)})) + self.assertIsNone( + frappe.db.exists( + "Student Attendance", {"leave_application": leave_application.name, "date": add_days(today, 1)} + ) + ) def tearDown(self): - company = get_default_company() or frappe.get_all('Company')[0].name - frappe.db.set_value('Company', company, 'default_holiday_list', '_Test Holiday List') + company = get_default_company() or frappe.get_all("Company")[0].name + frappe.db.set_value("Company", company, "default_holiday_list", "_Test Holiday List") def create_leave_application(from_date=None, to_date=None, mark_as_present=0, submit=1): student = get_student() - leave_application = frappe.new_doc('Student Leave Application') + leave_application = frappe.new_doc("Student Leave Application") leave_application.student = student.name - leave_application.attendance_based_on = 'Student Group' + leave_application.attendance_based_on = "Student Group" leave_application.student_group = get_random_group().name leave_application.from_date = from_date if from_date else getdate() leave_application.to_date = from_date if from_date else getdate() @@ -79,38 +91,41 @@ def create_leave_application(from_date=None, to_date=None, mark_as_present=0, su return leave_application + def create_student_attendance(date=None, status=None): student = get_student() - attendance = frappe.get_doc({ - 'doctype': 'Student Attendance', - 'student': student.name, - 'status': status if status else 'Present', - 'date': date if date else getdate(), - 'student_group': get_random_group().name - }).insert() + attendance = frappe.get_doc( + { + "doctype": "Student Attendance", + "student": student.name, + "status": status if status else "Present", + "date": date if date else getdate(), + "student_group": get_random_group().name, + } + ).insert() return attendance + def get_student(): - return create_student(dict( - email='test_student@gmail.com', - first_name='Test', - last_name='Student' - )) + return create_student( + dict(email="test_student@gmail.com", first_name="Test", last_name="Student") + ) + def create_holiday_list(): - holiday_list = 'Test Holiday List for Student' + holiday_list = "Test Holiday List for Student" today = getdate() - if not frappe.db.exists('Holiday List', holiday_list): - frappe.get_doc(dict( - doctype = 'Holiday List', - holiday_list_name = holiday_list, - from_date = add_months(today, -6), - to_date = add_months(today, 6), - holidays = [ - dict(holiday_date=add_days(today, 1), description = 'Test') - ] - )).insert() + if not frappe.db.exists("Holiday List", holiday_list): + frappe.get_doc( + dict( + doctype="Holiday List", + holiday_list_name=holiday_list, + from_date=add_months(today, -6), + to_date=add_months(today, 6), + holidays=[dict(holiday_date=add_days(today, 1), description="Test")], + ) + ).insert() - company = get_default_company() or frappe.get_all('Company')[0].name - frappe.db.set_value('Company', company, 'default_holiday_list', holiday_list) + company = get_default_company() or frappe.get_all("Company")[0].name + frappe.db.set_value("Company", company, "default_holiday_list", holiday_list) return holiday_list diff --git a/erpnext/education/doctype/student_log/test_student_log.py b/erpnext/education/doctype/student_log/test_student_log.py index 91fdb3c8167..fef1ab56ac1 100644 --- a/erpnext/education/doctype/student_log/test_student_log.py +++ b/erpnext/education/doctype/student_log/test_student_log.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Student Log') + class TestStudentLog(unittest.TestCase): pass diff --git a/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.py b/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.py index 43802abea65..ae3c140a2ef 100644 --- a/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.py +++ b/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.py @@ -24,14 +24,19 @@ def preview_report_card(doc): doc = frappe._dict(json.loads(doc)) doc.students = [doc.student] if not (doc.student_name and doc.student_batch): - program_enrollment = frappe.get_all("Program Enrollment", fields=["student_batch_name", "student_name"], - filters={"student": doc.student, "docstatus": ('!=', 2), "academic_year": doc.academic_year}) + program_enrollment = frappe.get_all( + "Program Enrollment", + fields=["student_batch_name", "student_name"], + filters={"student": doc.student, "docstatus": ("!=", 2), "academic_year": doc.academic_year}, + ) if program_enrollment: doc.batch = program_enrollment[0].student_batch_name doc.student_name = program_enrollment[0].student_name # get the assessment result of the selected student - values = get_formatted_result(doc, get_course=True, get_all_assessment_groups=doc.include_all_assessment) + values = get_formatted_result( + doc, get_course=True, get_all_assessment_groups=doc.include_all_assessment + ) assessment_result = values.get("assessment_result").get(doc.student) courses = values.get("course_dict") course_criteria = get_courses_criteria(courses) @@ -45,23 +50,32 @@ def preview_report_card(doc): # get the attendance of the student for that peroid of time. doc.attendance = get_attendance_count(doc.students[0], doc.academic_year, doc.academic_term) - template = "erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.html" + template = ( + "erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.html" + ) base_template_path = "frappe/www/printview.html" from frappe.www.printview import get_letter_head - letterhead = get_letter_head(frappe._dict({"letter_head": doc.letterhead}), not doc.add_letterhead) - html = frappe.render_template(template, + letterhead = get_letter_head( + frappe._dict({"letter_head": doc.letterhead}), not doc.add_letterhead + ) + + html = frappe.render_template( + template, { "doc": doc, "assessment_result": assessment_result, "courses": courses, "assessment_groups": assessment_groups, "course_criteria": course_criteria, - "letterhead": letterhead and letterhead.get('content', None), - "add_letterhead": doc.add_letterhead if doc.add_letterhead else 0 - }) - final_template = frappe.render_template(base_template_path, {"body": html, "title": "Report Card"}) + "letterhead": letterhead and letterhead.get("content", None), + "add_letterhead": doc.add_letterhead if doc.add_letterhead else 0, + }, + ) + final_template = frappe.render_template( + base_template_path, {"body": html, "title": "Report Card"} + ) frappe.response.filename = "Report Card " + doc.students[0] + ".pdf" frappe.response.filecontent = get_pdf(final_template) @@ -71,21 +85,33 @@ def preview_report_card(doc): def get_courses_criteria(courses): course_criteria = frappe._dict() for course in courses: - course_criteria[course] = [d.assessment_criteria for d in frappe.get_all("Course Assessment Criteria", - fields=["assessment_criteria"], filters={"parent": course})] + course_criteria[course] = [ + d.assessment_criteria + for d in frappe.get_all( + "Course Assessment Criteria", fields=["assessment_criteria"], filters={"parent": course} + ) + ] return course_criteria def get_attendance_count(student, academic_year, academic_term=None): if academic_year: - from_date, to_date = frappe.db.get_value("Academic Year", academic_year, ["year_start_date", "year_end_date"]) + from_date, to_date = frappe.db.get_value( + "Academic Year", academic_year, ["year_start_date", "year_end_date"] + ) elif academic_term: - from_date, to_date = frappe.db.get_value("Academic Term", academic_term, ["term_start_date", "term_end_date"]) + from_date, to_date = frappe.db.get_value( + "Academic Term", academic_term, ["term_start_date", "term_end_date"] + ) if from_date and to_date: - attendance = dict(frappe.db.sql('''select status, count(student) as no_of_days + attendance = dict( + frappe.db.sql( + """select status, count(student) as no_of_days from `tabStudent Attendance` where student = %s and docstatus = 1 - and date between %s and %s group by status''', - (student, from_date, to_date))) + and date between %s and %s group by status""", + (student, from_date, to_date), + ) + ) if "Absent" not in attendance.keys(): attendance["Absent"] = 0 if "Present" not in attendance.keys(): diff --git a/erpnext/education/doctype/topic/test_topic.py b/erpnext/education/doctype/topic/test_topic.py index d1d664bb21a..c11204a575c 100644 --- a/erpnext/education/doctype/topic/test_topic.py +++ b/erpnext/education/doctype/topic/test_topic.py @@ -8,7 +8,7 @@ import frappe class TestTopic(unittest.TestCase): def setUp(self): - make_topic_and_linked_content("_Test Topic 1", [{"type":"Article", "name": "_Test Article 1"}]) + make_topic_and_linked_content("_Test Topic 1", [{"type": "Article", "name": "_Test Article 1"}]) def test_get_contents(self): topic = frappe.get_doc("Topic", "_Test Topic 1") @@ -17,24 +17,28 @@ class TestTopic(unittest.TestCase): self.assertEqual(contents[0].name, "_Test Article 1") frappe.db.rollback() + def make_topic(name): try: topic = frappe.get_doc("Topic", name) except frappe.DoesNotExistError: - topic = frappe.get_doc({ - "doctype": "Topic", - "topic_name": name, - "topic_code": name, - }).insert() + topic = frappe.get_doc( + { + "doctype": "Topic", + "topic_name": name, + "topic_code": name, + } + ).insert() return topic.name + def make_topic_and_linked_content(topic_name, content_dict_list): try: topic = frappe.get_doc("Topic", topic_name) except frappe.DoesNotExistError: make_topic(topic_name) topic = frappe.get_doc("Topic", topic_name) - content_list = [make_content(content['type'], content['name']) for content in content_dict_list] + content_list = [make_content(content["type"], content["name"]) for content in content_dict_list] for content in content_list: topic.append("topic_content", {"content": content.title, "content_type": content.doctype}) topic.save() diff --git a/erpnext/education/doctype/topic/topic.py b/erpnext/education/doctype/topic/topic.py index 146f57453ad..6dd965552b1 100644 --- a/erpnext/education/doctype/topic/topic.py +++ b/erpnext/education/doctype/topic/topic.py @@ -13,48 +13,64 @@ class Topic(Document): def get_contents(self): try: topic_content_list = self.topic_content - content_data = [frappe.get_doc(topic_content.content_type, topic_content.content) for topic_content in topic_content_list] + content_data = [ + frappe.get_doc(topic_content.content_type, topic_content.content) + for topic_content in topic_content_list + ] except Exception as e: frappe.log_error(frappe.get_traceback()) return None return content_data + @frappe.whitelist() def get_courses_without_topic(topic): data = [] - for entry in frappe.db.get_all('Course'): - course = frappe.get_doc('Course', entry.name) + for entry in frappe.db.get_all("Course"): + course = frappe.get_doc("Course", entry.name) topics = [t.topic for t in course.topics] if not topics or topic not in topics: data.append(course.name) return data + @frappe.whitelist() def add_topic_to_courses(topic, courses, mandatory=False): courses = json.loads(courses) for entry in courses: - course = frappe.get_doc('Course', entry) - course.append('topics', { - 'topic': topic, - 'topic_name': topic - }) + course = frappe.get_doc("Course", entry) + course.append("topics", {"topic": topic, "topic_name": topic}) course.flags.ignore_mandatory = True course.save() frappe.db.commit() - frappe.msgprint(_('Topic {0} has been added to all the selected courses successfully.').format(frappe.bold(topic)), - title=_('Courses updated'), indicator='green') + frappe.msgprint( + _("Topic {0} has been added to all the selected courses successfully.").format( + frappe.bold(topic) + ), + title=_("Courses updated"), + indicator="green", + ) + @frappe.whitelist() def add_content_to_topics(content_type, content, topics): topics = json.loads(topics) for entry in topics: - topic = frappe.get_doc('Topic', entry) - topic.append('topic_content', { - 'content_type': content_type, - 'content': content, - }) + topic = frappe.get_doc("Topic", entry) + topic.append( + "topic_content", + { + "content_type": content_type, + "content": content, + }, + ) topic.flags.ignore_mandatory = True topic.save() frappe.db.commit() - frappe.msgprint(_('{0} {1} has been added to all the selected topics successfully.').format(content_type, frappe.bold(content)), - title=_('Topics updated'), indicator='green') + frappe.msgprint( + _("{0} {1} has been added to all the selected topics successfully.").format( + content_type, frappe.bold(content) + ), + title=_("Topics updated"), + indicator="green", + ) diff --git a/erpnext/education/report/absent_student_report/absent_student_report.py b/erpnext/education/report/absent_student_report/absent_student_report.py index c274d333492..ac27f715ec9 100644 --- a/erpnext/education/report/absent_student_report/absent_student_report.py +++ b/erpnext/education/report/absent_student_report/absent_student_report.py @@ -11,7 +11,8 @@ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if not filters.get("date"): msgprint(_("Please select date"), raise_exception=1) @@ -21,8 +22,11 @@ def execute(filters=None): holiday_list = get_holiday_list() if is_holiday(holiday_list, filters.get("date")): - msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date"))))) - + msgprint( + _("No attendance has been marked for {0} as it is a Holiday").format( + frappe.bold(formatdate(filters.get("date"))) + ) + ) absent_students = get_absent_students(date) leave_applicants = get_leave_applications(date) @@ -34,17 +38,19 @@ def execute(filters=None): for student in absent_students: if not student.student in leave_applicants: row = [student.student, student.student_name, student.student_group] - stud_details = frappe.db.get_value("Student", student.student, ['student_email_id', 'student_mobile_number'], as_dict=True) + stud_details = frappe.db.get_value( + "Student", student.student, ["student_email_id", "student_mobile_number"], as_dict=True + ) if stud_details.student_email_id: - row+=[stud_details.student_email_id] + row += [stud_details.student_email_id] else: - row+= [""] + row += [""] if stud_details.student_mobile_number: - row+=[stud_details.student_mobile_number] + row += [stud_details.student_mobile_number] else: - row+= [""] + row += [""] if transportation_details.get(student.student): row += transportation_details.get(student.student) @@ -52,6 +58,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ _("Student") + ":Link/Student:90", @@ -64,34 +71,45 @@ def get_columns(filters): ] return columns + def get_absent_students(date): - absent_students = frappe.db.sql(""" + absent_students = frappe.db.sql( + """ SELECT student, student_name, student_group FROM `tabStudent Attendance` WHERE status='Absent' and docstatus=1 and date = %s ORDER BY student_group, student_name""", - date, as_dict=1) + date, + as_dict=1, + ) return absent_students + def get_leave_applications(date): leave_applicants = [] - leave_applications = frappe.db.sql(""" + leave_applications = frappe.db.sql( + """ SELECT student FROM `tabStudent Leave Application` WHERE docstatus = 1 and mark_as_present = 1 and from_date <= %s and to_date >= %s - """, (date, date)) + """, + (date, date), + ) for student in leave_applications: leave_applicants.append(student[0]) return leave_applicants + def get_transportation_details(date, student_list): - academic_year = frappe.get_all("Academic Year", filters=[["year_start_date", "<=", date],["year_end_date", ">=", date]]) + academic_year = frappe.get_all( + "Academic Year", filters=[["year_start_date", "<=", date], ["year_end_date", ">=", date]] + ) if academic_year: academic_year = academic_year[0].name elif frappe.defaults.get_defaults().academic_year: @@ -99,8 +117,15 @@ def get_transportation_details(date, student_list): else: return {} - transportation_details = frappe.get_all("Program Enrollment", fields=["student", "mode_of_transportation", "vehicle_no"], - filters={"student": ("in", student_list), "academic_year": academic_year, "docstatus": ("not in", ["2"])}) + transportation_details = frappe.get_all( + "Program Enrollment", + fields=["student", "mode_of_transportation", "vehicle_no"], + filters={ + "student": ("in", student_list), + "academic_year": academic_year, + "docstatus": ("not in", ["2"]), + }, + ) transportation_map = {} for d in transportation_details: transportation_map[d.student] = [d.mode_of_transportation, d.vehicle_no] diff --git a/erpnext/education/report/assessment_plan_status/assessment_plan_status.py b/erpnext/education/report/assessment_plan_status/assessment_plan_status.py index 86f2451cbf0..7cf5b30548a 100644 --- a/erpnext/education/report/assessment_plan_status/assessment_plan_status.py +++ b/erpnext/education/report/assessment_plan_status/assessment_plan_status.py @@ -13,6 +13,7 @@ DOCSTATUS = { 1: "submitted", } + def execute(filters=None): columns, data = [], [] @@ -32,13 +33,14 @@ def get_assessment_data(args=None): # [total, saved, submitted, remaining] chart_data = [0, 0, 0, 0] - condition = '' + condition = "" if args["assessment_group"]: condition += "and assessment_group = %(assessment_group)s" if args["schedule_date"]: condition += "and schedule_date <= %(schedule_date)s" - assessment_plan = frappe.db.sql(''' + assessment_plan = frappe.db.sql( + """ SELECT ap.name as assessment_plan, ap.assessment_name, @@ -52,24 +54,33 @@ def get_assessment_data(args=None): ap.docstatus = 1 {condition} ORDER BY ap.modified desc - '''.format(condition=condition), (args), as_dict=1) + """.format( + condition=condition + ), + (args), + as_dict=1, + ) - assessment_plan_list = [d.assessment_plan for d in assessment_plan] if assessment_plan else [''] + assessment_plan_list = [d.assessment_plan for d in assessment_plan] if assessment_plan else [""] assessment_result = get_assessment_result(assessment_plan_list) for d in assessment_plan: assessment_plan_details = assessment_result.get(d.assessment_plan) - assessment_plan_details = frappe._dict() if not assessment_plan_details else \ - frappe._dict(assessment_plan_details) + assessment_plan_details = ( + frappe._dict() if not assessment_plan_details else frappe._dict(assessment_plan_details) + ) if "saved" not in assessment_plan_details: assessment_plan_details.update({"saved": 0}) if "submitted" not in assessment_plan_details: assessment_plan_details.update({"submitted": 0}) # remaining students whose marks not entered - remaining_students = cint(d.student_group_strength) - cint(assessment_plan_details.saved) -\ - cint(assessment_plan_details.submitted) + remaining_students = ( + cint(d.student_group_strength) + - cint(assessment_plan_details.saved) + - cint(assessment_plan_details.submitted) + ) assessment_plan_details.update({"remaining": remaining_students}) d.update(assessment_plan_details) @@ -86,7 +97,8 @@ def get_assessment_data(args=None): def get_assessment_result(assessment_plan_list): assessment_result_dict = frappe._dict() - assessment_result = frappe.db.sql(''' + assessment_result = frappe.db.sql( + """ SELECT assessment_plan, docstatus, count(*) as count FROM @@ -97,13 +109,17 @@ def get_assessment_result(assessment_plan_list): assessment_plan, docstatus ORDER BY assessment_plan - ''' %', '.join(['%s']*len(assessment_plan_list)), tuple(assessment_plan_list), as_dict=1) + """ + % ", ".join(["%s"] * len(assessment_plan_list)), + tuple(assessment_plan_list), + as_dict=1, + ) for key, group in groupby(assessment_result, lambda ap: ap["assessment_plan"]): tmp = {} for d in group: - if d.docstatus in [0,1]: - tmp.update({DOCSTATUS[d.docstatus]:d.count}) + if d.docstatus in [0, 1]: + tmp.update({DOCSTATUS[d.docstatus]: d.count}) assessment_result_dict[key] = tmp return assessment_result_dict @@ -111,70 +127,61 @@ def get_assessment_result(assessment_plan_list): def get_chart(chart_data): return { - "data": { - "labels": ["Saved", "Submitted", "Remaining"], - "datasets": [{ - "values": chart_data - }] - }, - "type": 'percentage', + "data": {"labels": ["Saved", "Submitted", "Remaining"], "datasets": [{"values": chart_data}]}, + "type": "percentage", } def get_column(): - return [{ - "fieldname": "assessment_plan", - "label": _("Assessment Plan"), - "fieldtype": "Link", - "options": "Assessment Plan", - "width": 120 - }, - { - "fieldname": "assessment_name", - "label": _("Assessment Plan Name"), - "fieldtype": "Data", - "options": "", - "width": 200 - }, - { - "fieldname": "schedule_date", - "label": _("Schedule Date"), - "fieldtype": "Date", - "options": "", - "width": 100 - }, - { - "fieldname": "student_group", - "label": _("Student Group"), - "fieldtype": "Link", - "options": "Student Group", - "width": 200 - }, - { - "fieldname": "student_group_strength", - "label": _("Total Student"), - "fieldtype": "Data", - "options": "", - "width": 100 - }, - { - "fieldname": "submitted", - "label": _("Submitted"), - "fieldtype": "Data", - "options": "", - "width": 100 - }, - { - "fieldname": "saved", - "label": _("Saved"), - "fieldtype": "Data", - "options": "", - "width": 100 - }, - { - "fieldname": "remaining", - "label": _("Remaining"), - "fieldtype": "Data", - "options": "", - "width": 100 - }] + return [ + { + "fieldname": "assessment_plan", + "label": _("Assessment Plan"), + "fieldtype": "Link", + "options": "Assessment Plan", + "width": 120, + }, + { + "fieldname": "assessment_name", + "label": _("Assessment Plan Name"), + "fieldtype": "Data", + "options": "", + "width": 200, + }, + { + "fieldname": "schedule_date", + "label": _("Schedule Date"), + "fieldtype": "Date", + "options": "", + "width": 100, + }, + { + "fieldname": "student_group", + "label": _("Student Group"), + "fieldtype": "Link", + "options": "Student Group", + "width": 200, + }, + { + "fieldname": "student_group_strength", + "label": _("Total Student"), + "fieldtype": "Data", + "options": "", + "width": 100, + }, + { + "fieldname": "submitted", + "label": _("Submitted"), + "fieldtype": "Data", + "options": "", + "width": 100, + }, + {"fieldname": "saved", "label": _("Saved"), "fieldtype": "Data", "options": "", "width": 100}, + { + "fieldname": "remaining", + "label": _("Remaining"), + "fieldtype": "Data", + "options": "", + "width": 100, + }, + ] diff --git a/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.py b/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.py index 38eef68d0e7..0930882e403 100644 --- a/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.py +++ b/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.py @@ -37,8 +37,12 @@ def execute(filters=None): for criteria in assessment_criteria_dict: scrub_criteria = frappe.scrub(criteria) if criteria in result_dict[student][args.course][args.assessment_group]: - student_row[scrub_criteria] = result_dict[student][args.course][args.assessment_group][criteria]["grade"] - student_row[scrub_criteria + "_score"] = result_dict[student][args.course][args.assessment_group][criteria]["score"] + student_row[scrub_criteria] = result_dict[student][args.course][args.assessment_group][ + criteria + ]["grade"] + student_row[scrub_criteria + "_score"] = result_dict[student][args.course][ + args.assessment_group + ][criteria]["score"] # create the list of possible grades if student_row[scrub_criteria] not in grades: @@ -51,7 +55,7 @@ def execute(filters=None): grade_wise_analysis[criteria][student_row[scrub_criteria]] += 1 else: student_row[frappe.scrub(criteria)] = "" - student_row[frappe.scrub(criteria)+ "_score"] = "" + student_row[frappe.scrub(criteria) + "_score"] = "" data.append(student_row) assessment_criteria_list = [d for d in assessment_criteria_dict] @@ -61,7 +65,9 @@ def execute(filters=None): return columns, data, None, chart -def get_formatted_result(args, get_assessment_criteria=False, get_course=False, get_all_assessment_groups=False): +def get_formatted_result( + args, get_assessment_criteria=False, get_course=False, get_all_assessment_groups=False +): cond, cond1, cond2, cond3, cond4 = " ", " ", " ", " ", " " args_list = [args.academic_year] @@ -80,14 +86,15 @@ def get_formatted_result(args, get_assessment_criteria=False, get_course=False, create_total_dict = False assessment_groups = get_child_assessment_groups(args.assessment_group) - cond3 = " and ar.assessment_group in (%s)"%(', '.join(['%s']*len(assessment_groups))) + cond3 = " and ar.assessment_group in (%s)" % (", ".join(["%s"] * len(assessment_groups))) args_list += assessment_groups if args.students: - cond4 = " and ar.student in (%s)"%(', '.join(['%s']*len(args.students))) + cond4 = " and ar.student in (%s)" % (", ".join(["%s"] * len(args.students))) args_list += args.students - assessment_result = frappe.db.sql(''' + assessment_result = frappe.db.sql( + """ SELECT ar.student, ar.student_name, ar.academic_year, ar.academic_term, ar.program, ar.course, ar.assessment_plan, ar.grading_scale, ar.assessment_group, ar.student_group, @@ -97,8 +104,12 @@ def get_formatted_result(args, get_assessment_criteria=False, get_course=False, WHERE ar.name=ard.parent and ar.docstatus=1 and ar.academic_year=%s {0} {1} {2} {3} {4} ORDER BY - ard.assessment_criteria'''.format(cond, cond1, cond2, cond3, cond4), - tuple(args_list), as_dict=1) + ard.assessment_criteria""".format( + cond, cond1, cond2, cond3, cond4 + ), + tuple(args_list), + as_dict=1, + ) # create the nested dictionary structure as given below: # ..... @@ -114,21 +125,48 @@ def get_formatted_result(args, get_assessment_criteria=False, get_course=False, # add the score for a given score and recalculate the grades def add_score_and_recalculate_grade(result, assessment_group, assessment_criteria): - formatted_assessment_result[result.student][result.course][assessment_group]\ - [assessment_criteria]["maximum_score"] += result.maximum_score - formatted_assessment_result[result.student][result.course][assessment_group]\ - [assessment_criteria]["score"] += result.score - tmp_grade = get_grade(result.grading_scale, ((formatted_assessment_result[result.student][result.course] - [assessment_group][assessment_criteria]["score"])/(formatted_assessment_result[result.student] - [result.course][assessment_group][assessment_criteria]["maximum_score"]))*100) - formatted_assessment_result[result.student][result.course][assessment_group]\ - [assessment_criteria]["grade"] = tmp_grade + formatted_assessment_result[result.student][result.course][assessment_group][ + assessment_criteria + ]["maximum_score"] += result.maximum_score + formatted_assessment_result[result.student][result.course][assessment_group][ + assessment_criteria + ]["score"] += result.score + tmp_grade = get_grade( + result.grading_scale, + ( + ( + formatted_assessment_result[result.student][result.course][assessment_group][ + assessment_criteria + ]["score"] + ) + / ( + formatted_assessment_result[result.student][result.course][assessment_group][ + assessment_criteria + ]["maximum_score"] + ) + ) + * 100, + ) + formatted_assessment_result[result.student][result.course][assessment_group][ + assessment_criteria + ]["grade"] = tmp_grade # create the assessment criteria "Final Grade" with the sum of all the scores of the assessment criteria in a given assessment group def add_total_score(result, assessment_group): - if "Final Grade" not in formatted_assessment_result[result.student][result.course][assessment_group]: - formatted_assessment_result[result.student][result.course][assessment_group]["Final Grade"] = frappe._dict({ - "assessment_criteria": "Final Grade", "maximum_score": result.maximum_score, "score": result.score, "grade": result.grade}) + if ( + "Final Grade" + not in formatted_assessment_result[result.student][result.course][assessment_group] + ): + formatted_assessment_result[result.student][result.course][assessment_group][ + "Final Grade" + ] = frappe._dict( + { + "assessment_criteria": "Final Grade", + "maximum_score": result.maximum_score, + "score": result.score, + "grade": result.grade, + } + ) else: add_score_and_recalculate_grade(result, assessment_group, "Final Grade") @@ -136,8 +174,14 @@ def get_formatted_result(args, get_assessment_criteria=False, get_course=False, if result.student not in student_details: student_details[result.student] = result.student_name - assessment_criteria_details = frappe._dict({"assessment_criteria": result.assessment_criteria, - "maximum_score": result.maximum_score, "score": result.score, "grade": result.grade}) + assessment_criteria_details = frappe._dict( + { + "assessment_criteria": result.assessment_criteria, + "maximum_score": result.maximum_score, + "score": result.score, + "grade": result.grade, + } + ) if not formatted_assessment_result[result.student]: formatted_assessment_result[result.student] = defaultdict(dict) @@ -145,32 +189,46 @@ def get_formatted_result(args, get_assessment_criteria=False, get_course=False, formatted_assessment_result[result.student][result.course] = defaultdict(dict) if not create_total_dict: - formatted_assessment_result[result.student][result.course][result.assessment_group]\ - [result.assessment_criteria] = assessment_criteria_details + formatted_assessment_result[result.student][result.course][result.assessment_group][ + result.assessment_criteria + ] = assessment_criteria_details add_total_score(result, result.assessment_group) # create the total of all the assessment groups criteria-wise elif create_total_dict: if get_all_assessment_groups: - formatted_assessment_result[result.student][result.course][result.assessment_group]\ - [result.assessment_criteria] = assessment_criteria_details + formatted_assessment_result[result.student][result.course][result.assessment_group][ + result.assessment_criteria + ] = assessment_criteria_details if not formatted_assessment_result[result.student][result.course][args.assessment_group]: - formatted_assessment_result[result.student][result.course][args.assessment_group] = defaultdict(dict) - formatted_assessment_result[result.student][result.course][args.assessment_group]\ - [result.assessment_criteria] = assessment_criteria_details - elif result.assessment_criteria not in formatted_assessment_result[result.student][result.course][args.assessment_group]: - formatted_assessment_result[result.student][result.course][args.assessment_group]\ - [result.assessment_criteria] = assessment_criteria_details - elif result.assessment_criteria in formatted_assessment_result[result.student][result.course][args.assessment_group]: + formatted_assessment_result[result.student][result.course][ + args.assessment_group + ] = defaultdict(dict) + formatted_assessment_result[result.student][result.course][args.assessment_group][ + result.assessment_criteria + ] = assessment_criteria_details + elif ( + result.assessment_criteria + not in formatted_assessment_result[result.student][result.course][args.assessment_group] + ): + formatted_assessment_result[result.student][result.course][args.assessment_group][ + result.assessment_criteria + ] = assessment_criteria_details + elif ( + result.assessment_criteria + in formatted_assessment_result[result.student][result.course][args.assessment_group] + ): add_score_and_recalculate_grade(result, args.assessment_group, result.assessment_criteria) add_total_score(result, args.assessment_group) - total_maximum_score = formatted_assessment_result[result.student][result.course][args.assessment_group]\ - ["Final Grade"]["maximum_score"] + total_maximum_score = formatted_assessment_result[result.student][result.course][ + args.assessment_group + ]["Final Grade"]["maximum_score"] if get_assessment_criteria: - assessment_criteria_dict[result.assessment_criteria] = formatted_assessment_result[result.student][result.course]\ - [args.assessment_group][result.assessment_criteria]["maximum_score"] + assessment_criteria_dict[result.assessment_criteria] = formatted_assessment_result[ + result.student + ][result.course][args.assessment_group][result.assessment_criteria]["maximum_score"] if get_course: course_dict[result.course] = total_maximum_score @@ -181,37 +239,31 @@ def get_formatted_result(args, get_assessment_criteria=False, get_course=False, "student_details": student_details, "assessment_result": formatted_assessment_result, "assessment_criteria": assessment_criteria_dict, - "course_dict": course_dict + "course_dict": course_dict, } def get_column(assessment_criteria): - columns = [{ - "fieldname": "student", - "label": _("Student ID"), - "fieldtype": "Link", - "options": "Student", - "width": 90 - }, - { - "fieldname": "student_name", - "label": _("Student Name"), - "fieldtype": "Data", - "width": 160 - }] + columns = [ + { + "fieldname": "student", + "label": _("Student ID"), + "fieldtype": "Link", + "options": "Student", + "width": 90, + }, + {"fieldname": "student_name", "label": _("Student Name"), "fieldtype": "Data", "width": 160}, + ] for d in assessment_criteria: - columns.append({ - "fieldname": frappe.scrub(d), - "label": d, - "fieldtype": "Data", - "width": 110 - }) - columns.append({ - "fieldname": frappe.scrub(d) +"_score", - "label": "Score(" + str(int(assessment_criteria[d])) + ")", - "fieldtype": "Float", - "width": 100 - }) + columns.append({"fieldname": frappe.scrub(d), "label": d, "fieldtype": "Data", "width": 110}) + columns.append( + { + "fieldname": frappe.scrub(d) + "_score", + "label": "Score(" + str(int(assessment_criteria[d])) + ")", + "fieldtype": "Float", + "width": 100, + } + ) return columns @@ -221,7 +273,7 @@ def get_chart_data(grades, criteria_list, kounter): datasets = [] for grade in grades: - tmp = frappe._dict({"name": grade, "values":[]}) + tmp = frappe._dict({"name": grade, "values": []}) for criteria in criteria_list: if grade in kounter[criteria]: tmp["values"].append(kounter[criteria][grade]) @@ -230,11 +282,8 @@ def get_chart_data(grades, criteria_list, kounter): datasets.append(tmp) return { - "data": { - "labels": criteria_list, - "datasets": datasets - }, - "type": 'bar', + "data": {"labels": criteria_list, "datasets": datasets}, + "type": "bar", } @@ -243,8 +292,12 @@ def get_child_assessment_groups(assessment_group): group_type = frappe.get_value("Assessment Group", assessment_group, "is_group") if group_type: from frappe.desk.treeview import get_children - assessment_groups = [d.get("value") for d in get_children("Assessment Group", - assessment_group) if d.get("value") and not d.get("expandable")] + + assessment_groups = [ + d.get("value") + for d in get_children("Assessment Group", assessment_group) + if d.get("value") and not d.get("expandable") + ] else: assessment_groups = [assessment_group] return assessment_groups diff --git a/erpnext/education/report/final_assessment_grades/final_assessment_grades.py b/erpnext/education/report/final_assessment_grades/final_assessment_grades.py index b0428678045..8d5acc61f2f 100644 --- a/erpnext/education/report/final_assessment_grades/final_assessment_grades.py +++ b/erpnext/education/report/final_assessment_grades/final_assessment_grades.py @@ -22,7 +22,9 @@ def execute(filters=None): assessment_group = args["assessment_group"] = filters.get("assessment_group") student_group = filters.get("student_group") - args.students = frappe.db.sql_list("select student from `tabStudent Group Student` where parent=%s", (student_group)) + args.students = frappe.db.sql_list( + "select student from `tabStudent Group Student` where parent=%s", (student_group) + ) values = get_formatted_result(args, get_course=True) student_details = values.get("student_details") @@ -37,8 +39,12 @@ def execute(filters=None): for course in course_dict: scrub_course = frappe.scrub(course) if assessment_group in assessment_result[student][course]: - student_row["grade_" + scrub_course] = assessment_result[student][course][assessment_group]["Total Score"]["grade"] - student_row["score_" + scrub_course] = assessment_result[student][course][assessment_group]["Total Score"]["score"] + student_row["grade_" + scrub_course] = assessment_result[student][course][assessment_group][ + "Total Score" + ]["grade"] + student_row["score_" + scrub_course] = assessment_result[student][course][assessment_group][ + "Total Score" + ]["score"] # create the list of possible grades if student_row["grade_" + scrub_course] not in grades: @@ -59,31 +65,32 @@ def execute(filters=None): def get_column(course_dict): - columns = [{ - "fieldname": "student", - "label": _("Student ID"), - "fieldtype": "Link", - "options": "Student", - "width": 90 - }, - { - "fieldname": "student_name", - "label": _("Student Name"), - "fieldtype": "Data", - "width": 160 - }] + columns = [ + { + "fieldname": "student", + "label": _("Student ID"), + "fieldtype": "Link", + "options": "Student", + "width": 90, + }, + {"fieldname": "student_name", "label": _("Student Name"), "fieldtype": "Data", "width": 160}, + ] for course in course_dict: - columns.append({ - "fieldname": "grade_" + frappe.scrub(course), - "label": course, - "fieldtype": "Data", - "width": 110 - }) - columns.append({ - "fieldname": "score_" + frappe.scrub(course), - "label": "Score(" + str(course_dict[course]) + ")", - "fieldtype": "Float", - "width": 100 - }) + columns.append( + { + "fieldname": "grade_" + frappe.scrub(course), + "label": course, + "fieldtype": "Data", + "width": 110, + } + ) + columns.append( + { + "fieldname": "score_" + frappe.scrub(course), + "label": "Score(" + str(course_dict[course]) + ")", + "fieldtype": "Float", + "width": 100, + } + ) return columns diff --git a/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py index 0599dadf934..e5591f9b6be 100644 --- a/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py +++ b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py @@ -16,33 +16,29 @@ def execute(filters=None): return columns, data, None, chart + def get_columns(filters=None): return [ { - 'label': _('Program'), - 'fieldname': 'program', - 'fieldtype': 'Link', - 'options': 'Program', - 'width': 300 + "label": _("Program"), + "fieldname": "program", + "fieldtype": "Link", + "options": "Program", + "width": 300, }, { - 'label': _('Fees Collected'), - 'fieldname': 'fees_collected', - 'fieldtype': 'Currency', - 'width': 200 + "label": _("Fees Collected"), + "fieldname": "fees_collected", + "fieldtype": "Currency", + "width": 200, }, { - 'label': _('Outstanding Amount'), - 'fieldname': 'outstanding_amount', - 'fieldtype': 'Currency', - 'width': 200 + "label": _("Outstanding Amount"), + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "width": 200, }, - { - 'label': _('Grand Total'), - 'fieldname': 'grand_total', - 'fieldtype': 'Currency', - 'width': 200 - } + {"label": _("Grand Total"), "fieldname": "grand_total", "fieldtype": "Currency", "width": 200}, ] @@ -71,24 +67,32 @@ def get_data(filters=None): GROUP BY program ) AS FeesCollected ORDER BY FeesCollected.paid_amount DESC - """ % (conditions) - , as_dict=1) + """ + % (conditions), + as_dict=1, + ) for entry in fee_details: - data.append({ - 'program': entry.program, - 'fees_collected': entry.paid_amount, - 'outstanding_amount': entry.outstanding_amount, - 'grand_total': entry.grand_total - }) + data.append( + { + "program": entry.program, + "fees_collected": entry.paid_amount, + "outstanding_amount": entry.outstanding_amount, + "grand_total": entry.grand_total, + } + ) return data -def get_filter_conditions(filters): - conditions = '' - if filters.get('from_date') and filters.get('to_date'): - conditions += " and posting_date BETWEEN '%s' and '%s'" % (filters.get('from_date'), filters.get('to_date')) +def get_filter_conditions(filters): + conditions = "" + + if filters.get("from_date") and filters.get("to_date"): + conditions += " and posting_date BETWEEN '%s' and '%s'" % ( + filters.get("from_date"), + filters.get("to_date"), + ) return conditions @@ -102,23 +106,17 @@ def get_chart_data(data): outstanding_amount = [] for entry in data: - labels.append(entry.get('program')) - fees_collected.append(entry.get('fees_collected')) - outstanding_amount.append(entry.get('outstanding_amount')) + labels.append(entry.get("program")) + fees_collected.append(entry.get("fees_collected")) + outstanding_amount.append(entry.get("outstanding_amount")) return { - 'data': { - 'labels': labels, - 'datasets': [ - { - 'name': _('Fees Collected'), - 'values': fees_collected - }, - { - 'name': _('Outstanding Amt'), - 'values': outstanding_amount - } - ] + "data": { + "labels": labels, + "datasets": [ + {"name": _("Fees Collected"), "values": fees_collected}, + {"name": _("Outstanding Amt"), "values": outstanding_amount}, + ], }, - 'type': 'bar' + "type": "bar", } diff --git a/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py b/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py index 7097b8072b2..6f871d7ad7a 100644 --- a/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py +++ b/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.py @@ -15,12 +15,19 @@ def execute(filters=None): columns = get_columns() - program_enrollments = frappe.get_list("Program Enrollment", fields=["student", "student_name"], - filters={"academic_year": academic_year, "program": program, "student_batch_name": student_batch_name}) + program_enrollments = frappe.get_list( + "Program Enrollment", + fields=["student", "student_name"], + filters={ + "academic_year": academic_year, + "program": program, + "student_batch_name": student_batch_name, + }, + ) student_list = [d.student for d in program_enrollments] if not student_list: - return columns, [] + return columns, [] group_roll_no_map = get_student_roll_no(academic_year, program, student_batch_name) student_map = get_student_details(student_list) @@ -28,8 +35,14 @@ def execute(filters=None): for d in program_enrollments: student_details = student_map.get(d.student) - row = [group_roll_no_map.get(d.student), d.student, d.student_name, student_details.get("student_mobile_number"),\ - student_details.get("student_email_id"), student_details.get("address")] + row = [ + group_roll_no_map.get(d.student), + d.student, + d.student_name, + student_details.get("student_mobile_number"), + student_details.get("student_email_id"), + student_details.get("address"), + ] student_guardians = guardian_map.get(d.student) @@ -63,32 +76,56 @@ def get_columns(): ] return columns + def get_student_details(student_list): student_map = frappe._dict() - student_details = frappe.db.sql(''' - select name, student_mobile_number, student_email_id, address_line_1, address_line_2, city, state from `tabStudent` where name in (%s)''' % - ', '.join(['%s']*len(student_list)), tuple(student_list), as_dict=1) + student_details = frappe.db.sql( + """ + select name, student_mobile_number, student_email_id, address_line_1, address_line_2, city, state from `tabStudent` where name in (%s)""" + % ", ".join(["%s"] * len(student_list)), + tuple(student_list), + as_dict=1, + ) for s in student_details: student = frappe._dict() student["student_mobile_number"] = s.student_mobile_number student["student_email_id"] = s.student_email_id - student["address"] = ', '.join([d for d in [s.address_line_1, s.address_line_2, s.city, s.state] if d]) + student["address"] = ", ".join( + [d for d in [s.address_line_1, s.address_line_2, s.city, s.state] if d] + ) student_map[s.name] = student return student_map + def get_guardian_map(student_list): guardian_map = frappe._dict() - guardian_details = frappe.db.sql(''' - select parent, guardian, guardian_name, relation from `tabStudent Guardian` where parent in (%s)''' % - ', '.join(['%s']*len(student_list)), tuple(student_list), as_dict=1) + guardian_details = frappe.db.sql( + """ + select parent, guardian, guardian_name, relation from `tabStudent Guardian` where parent in (%s)""" + % ", ".join(["%s"] * len(student_list)), + tuple(student_list), + as_dict=1, + ) - guardian_list = list(set([g.guardian for g in guardian_details])) or [''] + guardian_list = list(set([g.guardian for g in guardian_details])) or [""] - guardian_mobile_no = dict(frappe.db.sql("""select name, mobile_number from `tabGuardian` - where name in (%s)""" % ", ".join(['%s']*len(guardian_list)), tuple(guardian_list))) + guardian_mobile_no = dict( + frappe.db.sql( + """select name, mobile_number from `tabGuardian` + where name in (%s)""" + % ", ".join(["%s"] * len(guardian_list)), + tuple(guardian_list), + ) + ) - guardian_email_id = dict(frappe.db.sql("""select name, email_address from `tabGuardian` - where name in (%s)""" % ", ".join(['%s']*len(guardian_list)), tuple(guardian_list))) + guardian_email_id = dict( + frappe.db.sql( + """select name, email_address from `tabGuardian` + where name in (%s)""" + % ", ".join(["%s"] * len(guardian_list)), + tuple(guardian_list), + ) + ) for guardian in guardian_details: guardian["mobile_number"] = guardian_mobile_no.get(guardian.guardian) @@ -97,11 +134,18 @@ def get_guardian_map(student_list): return guardian_map + def get_student_roll_no(academic_year, program, batch): - student_group = frappe.get_all("Student Group", - filters={"academic_year":academic_year, "program":program, "batch":batch, "disabled": 0}) + student_group = frappe.get_all( + "Student Group", + filters={"academic_year": academic_year, "program": program, "batch": batch, "disabled": 0}, + ) if student_group: - roll_no_dict = dict(frappe.db.sql('''select student, group_roll_number from `tabStudent Group Student` where parent=%s''', - (student_group[0].name))) + roll_no_dict = dict( + frappe.db.sql( + """select student, group_roll_number from `tabStudent Group Student` where parent=%s""", + (student_group[0].name), + ) + ) return roll_no_dict return {} diff --git a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py index 52055dceb8c..282f5c9aec4 100644 --- a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py +++ b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py @@ -11,14 +11,19 @@ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if not filters.get("date"): msgprint(_("Please select date"), raise_exception=1) holiday_list = get_holiday_list() if is_holiday(holiday_list, filters.get("date")): - msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date"))))) + msgprint( + _("No attendance has been marked for {0} as it is a Holiday").format( + frappe.bold(formatdate(filters.get("date"))) + ) + ) columns = get_columns(filters) @@ -33,40 +38,54 @@ def execute(filters=None): student_attendance = get_student_attendance(student_group.name, filters.get("date")) if student_attendance: for attendance in student_attendance: - if attendance.status== "Present": + if attendance.status == "Present": present_students = attendance.count - elif attendance.status== "Absent": + elif attendance.status == "Absent": absent_students = attendance.count unmarked_students = student_group_strength - (present_students + absent_students) - row+= [student_group_strength, present_students, absent_students, unmarked_students] + row += [student_group_strength, present_students, absent_students, unmarked_students] data.append(row) return columns, data + def get_columns(filters): columns = [ _("Student Group") + ":Link/Student Group:250", _("Student Group Strength") + "::170", _("Present") + "::90", _("Absent") + "::90", - _("Not Marked") + "::90" + _("Not Marked") + "::90", ] return columns + def get_active_student_group(): - active_student_groups = frappe.db.sql("""select name from `tabStudent Group` where group_based_on = "Batch" - and academic_year=%s order by name""", (frappe.defaults.get_defaults().academic_year), as_dict=1) + active_student_groups = frappe.db.sql( + """select name from `tabStudent Group` where group_based_on = "Batch" + and academic_year=%s order by name""", + (frappe.defaults.get_defaults().academic_year), + as_dict=1, + ) return active_student_groups + def get_student_group_strength(student_group): - student_group_strength = frappe.db.sql("""select count(*) from `tabStudent Group Student` - where parent = %s and active=1""", student_group)[0][0] + student_group_strength = frappe.db.sql( + """select count(*) from `tabStudent Group Student` + where parent = %s and active=1""", + student_group, + )[0][0] return student_group_strength + def get_student_attendance(student_group, date): - student_attendance = frappe.db.sql("""select count(*) as count, status from `tabStudent Attendance` where + student_attendance = frappe.db.sql( + """select count(*) as count, status from `tabStudent Attendance` where student_group= %s and date= %s and docstatus = 1 and (course_schedule is Null or course_schedule='') group by status""", - (student_group, date), as_dict=1) + (student_group, date), + as_dict=1, + ) return student_attendance diff --git a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py index 1166a75b2cc..9312e2b0181 100644 --- a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py +++ b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py @@ -12,13 +12,14 @@ from erpnext.support.doctype.issue.issue import get_holidays def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} - from_date = get_first_day(filters["month"] + '-' + filters["year"]) - to_date = get_last_day(filters["month"] + '-' + filters["year"]) - total_days_in_month = date_diff(to_date, from_date) +1 + from_date = get_first_day(filters["month"] + "-" + filters["year"]) + to_date = get_last_day(filters["month"] + "-" + filters["year"]) + total_days_in_month = date_diff(to_date, from_date) + 1 columns = get_columns(total_days_in_month) - students = get_student_group_students(filters.get("student_group"),1) + students = get_student_group_students(filters.get("student_group"), 1) students_list = get_students_list(students) att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list) data = [] @@ -30,7 +31,7 @@ def execute(filters=None): total_p = total_a = 0.0 for day in range(total_days_in_month): - status="None" + status = "None" if att_map.get(stud.student): status = att_map.get(stud.student).get(date, "None") @@ -39,7 +40,7 @@ def execute(filters=None): else: status = "None" - status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-", "Holiday":"H"} + status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive": "-", "Holiday": "H"} row.append(status_map[status]) if status == "Present": @@ -52,33 +53,43 @@ def execute(filters=None): data.append(row) return columns, data + def get_columns(days_in_month): - columns = [ _("Student") + ":Link/Student:90", _("Student Name") + "::150"] + columns = [_("Student") + ":Link/Student:90", _("Student Name") + "::150"] for day in range(days_in_month): - columns.append(cstr(day+1) +"::20") + columns.append(cstr(day + 1) + "::20") columns += [_("Total Present") + ":Int:95", _("Total Absent") + ":Int:90"] return columns + def get_students_list(students): student_list = [] for stud in students: student_list.append(stud.student) return student_list + def get_attendance_list(from_date, to_date, student_group, students_list): - attendance_list = frappe.db.sql('''select student, date, status + attendance_list = frappe.db.sql( + """select student, date, status from `tabStudent Attendance` where student_group = %s and docstatus = 1 and date between %s and %s - order by student, date''', - (student_group, from_date, to_date), as_dict=1) + order by student, date""", + (student_group, from_date, to_date), + as_dict=1, + ) att_map = {} - students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list) + students_with_leave_application = get_students_with_leave_application( + from_date, to_date, students_list + ) for d in attendance_list: att_map.setdefault(d.student, frappe._dict()).setdefault(d.date, "") - if students_with_leave_application.get(d.date) and d.student in students_with_leave_application.get(d.date): + if students_with_leave_application.get( + d.date + ) and d.student in students_with_leave_application.get(d.date): att_map[d.student][d.date] = "Present" else: att_map[d.student][d.date] = d.status @@ -87,9 +98,12 @@ def get_attendance_list(from_date, to_date, student_group, students_list): return att_map + def get_students_with_leave_application(from_date, to_date, students_list): - if not students_list: return - leave_applications = frappe.db.sql(""" + if not students_list: + return + leave_applications = frappe.db.sql( + """ select student, from_date, to_date from `tabStudent Leave Application` where @@ -100,29 +114,34 @@ def get_students_with_leave_application(from_date, to_date, students_list): or to_date between %(from_date)s and %(to_date)s or (%(from_date)s between from_date and to_date and %(to_date)s between from_date and to_date) ) - """, { - "students": students_list, - "from_date": from_date, - "to_date": to_date - }, as_dict=True) - students_with_leaves= {} + """, + {"students": students_list, "from_date": from_date, "to_date": to_date}, + as_dict=True, + ) + students_with_leaves = {} for application in leave_applications: for date in daterange(application.from_date, application.to_date): students_with_leaves.setdefault(date, []).append(application.student) return students_with_leaves + def daterange(d1, d2): import datetime + return (d1 + datetime.timedelta(days=i) for i in range((d2 - d1).days + 1)) + @frappe.whitelist() def get_attendance_years(): - year_list = frappe.db.sql_list('''select distinct YEAR(date) from `tabStudent Attendance` ORDER BY YEAR(date) DESC''') + year_list = frappe.db.sql_list( + """select distinct YEAR(date) from `tabStudent Attendance` ORDER BY YEAR(date) DESC""" + ) if not year_list: year_list = [getdate().year] return "\n".join(str(year) for year in year_list) + def mark_holidays(att_map, from_date, to_date, students_list): holiday_list = get_holiday_list() holidays = get_holidays(holiday_list) diff --git a/erpnext/education/setup.py b/erpnext/education/setup.py index 46620088dfd..98b4ddfc323 100644 --- a/erpnext/education/setup.py +++ b/erpnext/education/setup.py @@ -14,6 +14,7 @@ def setup_education(): return create_academic_sessions() + def create_academic_sessions(): data = [ {"doctype": "Academic Year", "academic_year_name": "2015-16"}, @@ -23,10 +24,11 @@ def create_academic_sessions(): {"doctype": "Academic Term", "academic_year": "2016-17", "term_name": "Semester 1"}, {"doctype": "Academic Term", "academic_year": "2016-17", "term_name": "Semester 2"}, {"doctype": "Academic Term", "academic_year": "2017-18", "term_name": "Semester 1"}, - {"doctype": "Academic Term", "academic_year": "2017-18", "term_name": "Semester 2"} + {"doctype": "Academic Term", "academic_year": "2017-18", "term_name": "Semester 2"}, ] insert_record(data) + def disable_desk_access_for_student_role(): try: student_role = frappe.get_doc("Role", "Student") @@ -37,11 +39,9 @@ def disable_desk_access_for_student_role(): student_role.desk_access = 0 student_role.save() + def create_student_role(): - student_role = frappe.get_doc({ - "doctype": "Role", - "role_name": "Student", - "desk_access": 0, - "restrict_to_domain": "Education" - }) + student_role = frappe.get_doc( + {"doctype": "Role", "role_name": "Student", "desk_access": 0, "restrict_to_domain": "Education"} + ) student_role.insert() diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py index a7a15d18ce8..e6eb1c99874 100644 --- a/erpnext/education/utils.py +++ b/erpnext/education/utils.py @@ -4,7 +4,9 @@ import frappe from frappe import _ -class OverlapError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + def validate_overlap_for(doc, doctype, fieldname, value=None): """Checks overlap for specified field. @@ -14,8 +16,16 @@ def validate_overlap_for(doc, doctype, fieldname, value=None): existing = get_overlap_for(doc, doctype, fieldname, value) if existing: - frappe.throw(_("This {0} conflicts with {1} for {2} {3}").format(doc.doctype, existing.name, - doc.meta.get_label(fieldname) if not value else fieldname , value or doc.get(fieldname)), OverlapError) + frappe.throw( + _("This {0} conflicts with {1} for {2} {3}").format( + doc.doctype, + existing.name, + doc.meta.get_label(fieldname) if not value else fieldname, + value or doc.get(fieldname), + ), + OverlapError, + ) + def get_overlap_for(doc, doctype, fieldname, value=None): """Returns overlaping document for specified field. @@ -23,45 +33,54 @@ def get_overlap_for(doc, doctype, fieldname, value=None): :param fieldname: Checks Overlap for this field """ - existing = frappe.db.sql("""select name, from_time, to_time from `tab{0}` + existing = frappe.db.sql( + """select name, from_time, to_time from `tab{0}` where `{1}`=%(val)s and schedule_date = %(schedule_date)s and ( (from_time > %(from_time)s and from_time < %(to_time)s) or (to_time > %(from_time)s and to_time < %(to_time)s) or (%(from_time)s > from_time and %(from_time)s < to_time) or (%(from_time)s = from_time and %(to_time)s = to_time)) - and name!=%(name)s and docstatus!=2""".format(doctype, fieldname), + and name!=%(name)s and docstatus!=2""".format( + doctype, fieldname + ), { "schedule_date": doc.schedule_date, "val": value or doc.get(fieldname), "from_time": doc.from_time, "to_time": doc.to_time, - "name": doc.name or "No Name" - }, as_dict=True) + "name": doc.name or "No Name", + }, + as_dict=True, + ) return existing[0] if existing else None def validate_duplicate_student(students): - unique_students= [] + unique_students = [] for stud in students: if stud.student in unique_students: - frappe.throw(_("Student {0} - {1} appears Multiple times in row {2} & {3}") - .format(stud.student, stud.student_name, unique_students.index(stud.student)+1, stud.idx)) + frappe.throw( + _("Student {0} - {1} appears Multiple times in row {2} & {3}").format( + stud.student, stud.student_name, unique_students.index(stud.student) + 1, stud.idx + ) + ) else: unique_students.append(stud.student) return None + # LMS Utils def get_current_student(): """Returns current student from frappe.session.user Returns: - object: Student Document + object: Student Document """ email = frappe.session.user - if email in ('Administrator', 'Guest'): + if email in ("Administrator", "Guest"): return None try: student_id = frappe.get_all("Student", {"student_email_id": email}, ["name"])[0].name @@ -69,74 +88,86 @@ def get_current_student(): except (IndexError, frappe.DoesNotExistError): return None + def get_portal_programs(): """Returns a list of all program to be displayed on the portal Programs are returned based on the following logic - is_published and (student_is_enrolled or student_can_self_enroll) + is_published and (student_is_enrolled or student_can_self_enroll) Returns: - list of dictionary: List of all programs and to be displayed on the portal along with access rights + list of dictionary: List of all programs and to be displayed on the portal along with access rights """ published_programs = frappe.get_all("Program", filters={"is_published": True}) if not published_programs: return None program_list = [frappe.get_doc("Program", program) for program in published_programs] - portal_programs = [{'program': program, 'has_access': allowed_program_access(program.name)} for program in program_list if allowed_program_access(program.name) or program.allow_self_enroll] + portal_programs = [ + {"program": program, "has_access": allowed_program_access(program.name)} + for program in program_list + if allowed_program_access(program.name) or program.allow_self_enroll + ] return portal_programs + def allowed_program_access(program, student=None): """Returns enrollment status for current student Args: - program (string): Name of the program - student (object): instance of Student document + program (string): Name of the program + student (object): instance of Student document Returns: - bool: Is current user enrolled or not + bool: Is current user enrolled or not """ if has_super_access(): return True if not student: student = get_current_student() - if student and get_enrollment('program', program, student.name): + if student and get_enrollment("program", program, student.name): return True else: return False + def get_enrollment(master, document, student): """Gets enrollment for course or program Args: - master (string): can either be program or course - document (string): program or course name - student (string): Student ID + master (string): can either be program or course + document (string): program or course name + student (string): Student ID Returns: - string: Enrollment Name if exists else returns empty string + string: Enrollment Name if exists else returns empty string """ - if master == 'program': - enrollments = frappe.get_all("Program Enrollment", filters={'student':student, 'program': document, 'docstatus': 1}) - if master == 'course': - enrollments = frappe.get_all("Course Enrollment", filters={'student':student, 'course': document}) + if master == "program": + enrollments = frappe.get_all( + "Program Enrollment", filters={"student": student, "program": document, "docstatus": 1} + ) + if master == "course": + enrollments = frappe.get_all( + "Course Enrollment", filters={"student": student, "course": document} + ) if enrollments: return enrollments[0].name else: return None + @frappe.whitelist() def enroll_in_program(program_name, student=None): """Enroll student in program Args: - program_name (string): Name of the program to be enrolled into - student (string, optional): name of student who has to be enrolled, if not - provided, a student will be created from the current user + program_name (string): Name of the program to be enrolled into + student (string, optional): name of student who has to be enrolled, if not + provided, a student will be created from the current user Returns: - string: name of the program enrollment document + string: name of the program enrollment document """ if has_super_access(): return @@ -145,7 +176,7 @@ def enroll_in_program(program_name, student=None): student = frappe.get_doc("Student", student) else: # Check if self enrollment in allowed - program = frappe.get_doc('Program', program_name) + program = frappe.get_doc("Program", program_name) if not program.allow_self_enroll: return frappe.throw(_("You are not allowed to enroll for this course")) @@ -154,12 +185,12 @@ def enroll_in_program(program_name, student=None): student = create_student_from_current_user() # Check if student is already enrolled in program - enrollment = get_enrollment('program', program_name, student.name) + enrollment = get_enrollment("program", program_name, student.name) if enrollment: return enrollment # Check if self enrollment in allowed - program = frappe.get_doc('Program', program_name) + program = frappe.get_doc("Program", program_name) if not program.allow_self_enroll: return frappe.throw(_("You are not allowed to enroll for this course")) @@ -167,15 +198,19 @@ def enroll_in_program(program_name, student=None): program_enrollment = student.enroll_in_program(program_name) return program_enrollment.name + def has_super_access(): """Check if user has a role that allows full access to LMS Returns: - bool: true if user has access to all lms content + bool: true if user has access to all lms content """ - current_user = frappe.get_doc('User', frappe.session.user) + current_user = frappe.get_doc("User", frappe.session.user) roles = set([role.role for role in current_user.roles]) - return bool(roles & {'Administrator', 'Instructor', 'Education Manager', 'System Manager', 'Academic User'}) + return bool( + roles & {"Administrator", "Instructor", "Education Manager", "System Manager", "Academic User"} + ) + @frappe.whitelist() def add_activity(course, content_type, content, program): @@ -184,14 +219,17 @@ def add_activity(course, content_type, content, program): student = get_current_student() if not student: - return frappe.throw(_("Student with email {0} does not exist").format(frappe.session.user), frappe.DoesNotExistError) + return frappe.throw( + _("Student with email {0} does not exist").format(frappe.session.user), frappe.DoesNotExistError + ) enrollment = get_or_create_course_enrollment(course, program) - if content_type == 'Quiz': + if content_type == "Quiz": return else: return enrollment.add_activity(content_type, content) + @frappe.whitelist() def evaluate_quiz(quiz_response, quiz_name, course, program, time_taken): import json @@ -203,16 +241,17 @@ def evaluate_quiz(quiz_response, quiz_name, course, program, time_taken): result, score, status = quiz.evaluate(quiz_response, quiz_name) if has_super_access(): - return {'result': result, 'score': score, 'status': status} + return {"result": result, "score": score, "status": status} if student: enrollment = get_or_create_course_enrollment(course, program) if quiz.allowed_attempt(enrollment, quiz_name): enrollment.add_quiz_activity(quiz_name, quiz_response, result, score, status, time_taken) - return {'result': result, 'score': score, 'status': status} + return {"result": result, "score": score, "status": status} else: return None + @frappe.whitelist() def get_quiz(quiz_name, course): try: @@ -222,37 +261,40 @@ def get_quiz(quiz_name, course): frappe.throw(_("Quiz {0} does not exist").format(quiz_name), frappe.DoesNotExistError) return None - questions = [{ - 'name': question.name, - 'question': question.question, - 'type': question.question_type, - 'options': [{'name': option.name, 'option': option.option} - for option in question.options], - } for question in questions] + questions = [ + { + "name": question.name, + "question": question.question, + "type": question.question_type, + "options": [{"name": option.name, "option": option.option} for option in question.options], + } + for question in questions + ] if has_super_access(): return { - 'questions': questions, - 'activity': None, - 'is_time_bound': quiz.is_time_bound, - 'duration': quiz.duration + "questions": questions, + "activity": None, + "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, - 'activity': {'is_complete': status, 'score': score, 'result': result, 'time_taken': time_taken}, - 'is_time_bound': quiz.is_time_bound, - 'duration': quiz.duration + "questions": questions, + "activity": {"is_complete": status, "score": score, "result": result, "time_taken": time_taken}, + "is_time_bound": quiz.is_time_bound, + "duration": quiz.duration, } + def get_topic_progress(topic, course_name, program): """ Return the porgress of a course in a program as well as the content to continue from. - :param topic_name: - :param course_name: + :param topic_name: + :param course_name: """ student = get_current_student() if not student: @@ -261,19 +303,20 @@ def get_topic_progress(topic, course_name, program): progress = student.get_topic_progress(course_enrollment.name, topic) if not progress: return None - count = sum([activity['is_complete'] for activity in progress]) + count = sum([activity["is_complete"] for activity in progress]) if count == 0: - return {'completed': False, 'started': False} + return {"completed": False, "started": False} elif count == len(progress): - return {'completed': True, 'started': True} + return {"completed": True, "started": True} elif count < len(progress): - return {'completed': False, 'started': True} + return {"completed": False, "started": True} + def get_course_progress(course, program): """ Return the porgress of a course in a program as well as the content to continue from. - :param topic_name: - :param course_name: + :param topic_name: + :param course_name: """ course_progress = [] for course_topic in course.topics: @@ -282,19 +325,20 @@ def get_course_progress(course, program): if progress: course_progress.append(progress) if course_progress: - number_of_completed_topics = sum([activity['completed'] for activity in course_progress]) + number_of_completed_topics = sum([activity["completed"] for activity in course_progress]) total_topics = len(course_progress) if total_topics == 1: return course_progress[0] if number_of_completed_topics == 0: - return {'completed': False, 'started': False} + return {"completed": False, "started": False} if number_of_completed_topics == total_topics: - return {'completed': True, 'started': True} + return {"completed": True, "started": True} if number_of_completed_topics < total_topics: - return {'completed': False, 'started': True} + return {"completed": False, "started": True} return None + def get_program_progress(program): program_progress = [] if not program.courses: @@ -303,8 +347,8 @@ def get_program_progress(program): course = frappe.get_doc("Course", program_course.course) progress = get_course_progress(course, program.name) if progress: - progress['name'] = course.name - progress['course'] = course.course_name + progress["name"] = course.name + progress["course"] = course.course_name program_progress.append(progress) if program_progress: @@ -312,81 +356,94 @@ def get_program_progress(program): return None + def get_program_completion(program): - topics = frappe.db.sql("""select `tabCourse Topic`.topic, `tabCourse Topic`.parent + topics = frappe.db.sql( + """select `tabCourse Topic`.topic, `tabCourse Topic`.parent from `tabCourse Topic`, `tabProgram Course` where `tabCourse Topic`.parent = `tabProgram Course`.course - and `tabProgram Course`.parent = %s""", program.name) + and `tabProgram Course`.parent = %s""", + program.name, + ) progress = [] for topic in topics: - topic_doc = frappe.get_doc('Topic', topic[0]) + topic_doc = frappe.get_doc("Topic", topic[0]) topic_progress = get_topic_progress(topic_doc, topic[1], program.name) if topic_progress: progress.append(topic_progress) if progress: - number_of_completed_topics = sum([activity['completed'] for activity in progress if activity]) + number_of_completed_topics = sum([activity["completed"] for activity in progress if activity]) total_topics = len(progress) try: - return int((float(number_of_completed_topics)/total_topics)*100) + return int((float(number_of_completed_topics) / total_topics) * 100) except ZeroDivisionError: return 0 return 0 + def create_student_from_current_user(): user = frappe.get_doc("User", frappe.session.user) - student = frappe.get_doc({ - "doctype": "Student", - "first_name": user.first_name, - "last_name": user.last_name, - "student_email_id": user.email, - "user": frappe.session.user - }) + student = frappe.get_doc( + { + "doctype": "Student", + "first_name": user.first_name, + "last_name": user.last_name, + "student_email_id": user.email, + "user": frappe.session.user, + } + ) student.save(ignore_permissions=True) return student + def get_or_create_course_enrollment(course, program): student = get_current_student() course_enrollment = get_enrollment("course", course, student.name) if not course_enrollment: - program_enrollment = get_enrollment('program', program.name, student.name) + program_enrollment = get_enrollment("program", program.name, student.name) if not program_enrollment: frappe.throw(_("You are not enrolled in program {0}").format(program)) return - return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name)) + return student.enroll_in_course( + course_name=course, program_enrollment=get_enrollment("program", program.name, student.name) + ) else: - return frappe.get_doc('Course Enrollment', course_enrollment) + return frappe.get_doc("Course Enrollment", course_enrollment) + def check_content_completion(content_name, content_type, enrollment_name): - activity = frappe.get_all("Course Activity", filters={'enrollment': enrollment_name, 'content_type': content_type, 'content': content_name}) + activity = frappe.get_all( + "Course Activity", + filters={"enrollment": enrollment_name, "content_type": content_type, "content": content_name}, + ) if activity: return True else: return False + def check_quiz_completion(quiz, enrollment_name): - attempts = frappe.get_all("Quiz Activity", - filters={ - 'enrollment': enrollment_name, - 'quiz': quiz.name - }, - fields=["name", "activity_date", "score", "status", "time_taken"] + attempts = frappe.get_all( + "Quiz Activity", + filters={"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) score = None result = None time_taken = None if attempts: - if quiz.grading_basis == 'Last Highest Score': - attempts = sorted(attempts, key = lambda i: int(i.score), reverse=True) - score = attempts[0]['score'] - result = attempts[0]['status'] - time_taken = attempts[0]['time_taken'] - if result == 'Pass': + if quiz.grading_basis == "Last Highest Score": + attempts = sorted(attempts, key=lambda i: int(i.score), reverse=True) + score = attempts[0]["score"] + result = attempts[0]["status"] + time_taken = attempts[0]["time_taken"] + if result == "Pass": status = True return status, score, result, time_taken diff --git a/erpnext/education/web_form/student_applicant/student_applicant.py b/erpnext/education/web_form/student_applicant/student_applicant.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/education/web_form/student_applicant/student_applicant.py +++ b/erpnext/education/web_form/student_applicant/student_applicant.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index dbf10c8491f..4579a274ffa 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -1,4 +1,3 @@ - import json import frappe @@ -22,20 +21,21 @@ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, @frappe.whitelist(allow_guest=True) -@validate_webhooks_request("Shopify Settings", 'X-Shopify-Hmac-Sha256', secret_key='shared_secret') +@validate_webhooks_request("Shopify Settings", "X-Shopify-Hmac-Sha256", secret_key="shared_secret") def store_request_data(order=None, event=None): if frappe.request: order = json.loads(frappe.request.data) - event = frappe.request.headers.get('X-Shopify-Topic') + event = frappe.request.headers.get("X-Shopify-Topic") dump_request_data(order, event) + def sync_sales_order(order, request_id=None, old_order_sync=False): - frappe.set_user('Administrator') + frappe.set_user("Administrator") shopify_settings = frappe.get_doc("Shopify Settings") frappe.flags.request_id = request_id - if not frappe.db.get_value("Sales Order", filters={"shopify_order_id": cstr(order['id'])}): + if not frappe.db.get_value("Sales Order", filters={"shopify_order_id": cstr(order["id"])}): try: validate_customer(order, shopify_settings) validate_item(order, shopify_settings) @@ -46,49 +46,57 @@ def sync_sales_order(order, request_id=None, old_order_sync=False): else: make_shopify_log(status="Success") + def prepare_sales_invoice(order, request_id=None): - frappe.set_user('Administrator') + frappe.set_user("Administrator") shopify_settings = frappe.get_doc("Shopify Settings") frappe.flags.request_id = request_id try: - sales_order = get_sales_order(cstr(order['id'])) + sales_order = get_sales_order(cstr(order["id"])) if sales_order: create_sales_invoice(order, shopify_settings, sales_order) make_shopify_log(status="Success") except Exception as e: make_shopify_log(status="Error", exception=e, rollback=True) + def prepare_delivery_note(order, request_id=None): - frappe.set_user('Administrator') + frappe.set_user("Administrator") shopify_settings = frappe.get_doc("Shopify Settings") frappe.flags.request_id = request_id try: - sales_order = get_sales_order(cstr(order['id'])) + sales_order = get_sales_order(cstr(order["id"])) if sales_order: create_delivery_note(order, shopify_settings, sales_order) make_shopify_log(status="Success") except Exception as e: make_shopify_log(status="Error", exception=e, rollback=True) + def get_sales_order(shopify_order_id): sales_order = frappe.db.get_value("Sales Order", filters={"shopify_order_id": shopify_order_id}) if sales_order: so = frappe.get_doc("Sales Order", sales_order) return so + def validate_customer(order, shopify_settings): customer_id = order.get("customer", {}).get("id") if customer_id: if not frappe.db.get_value("Customer", {"shopify_customer_id": customer_id}, "name"): create_customer(order.get("customer"), shopify_settings) + def validate_item(order, shopify_settings): for item in order.get("line_items"): - if item.get("product_id") and not frappe.db.get_value("Item", {"shopify_product_id": item.get("product_id")}, "name"): + if item.get("product_id") and not frappe.db.get_value( + "Item", {"shopify_product_id": item.get("product_id")}, "name" + ): sync_item_from_shopify(shopify_settings, item) + def create_order(order, shopify_settings, old_order_sync=False, company=None): so = create_sales_order(order, shopify_settings, company) if so: @@ -98,44 +106,48 @@ def create_order(order, shopify_settings, old_order_sync=False, company=None): if order.get("fulfillments") and not old_order_sync: create_delivery_note(order, shopify_settings, so) + def create_sales_order(shopify_order, shopify_settings, company=None): product_not_exists = [] - customer = frappe.db.get_value("Customer", {"shopify_customer_id": shopify_order.get("customer", {}).get("id")}, "name") + customer = frappe.db.get_value( + "Customer", {"shopify_customer_id": shopify_order.get("customer", {}).get("id")}, "name" + ) so = frappe.db.get_value("Sales Order", {"shopify_order_id": shopify_order.get("id")}, "name") if not so: - items = get_order_items(shopify_order.get("line_items"), shopify_settings, getdate(shopify_order.get('created_at'))) + items = get_order_items( + shopify_order.get("line_items"), shopify_settings, getdate(shopify_order.get("created_at")) + ) if not items: - message = 'Following items exists in the shopify order but relevant records were not found in the shopify Product master' + message = "Following items exists in the shopify order but relevant records were not found in the shopify Product master" message += "\n" + ", ".join(product_not_exists) make_shopify_log(status="Error", exception=message, rollback=True) - return '' + return "" - so = frappe.get_doc({ - "doctype": "Sales Order", - "naming_series": shopify_settings.sales_order_series or "SO-Shopify-", - "shopify_order_id": shopify_order.get("id"), - "shopify_order_number": shopify_order.get("name"), - "customer": customer or shopify_settings.default_customer, - "transaction_date": getdate(shopify_order.get("created_at")) or nowdate(), - "delivery_date": getdate(shopify_order.get("created_at")) or nowdate(), - "company": shopify_settings.company, - "selling_price_list": shopify_settings.price_list, - "ignore_pricing_rule": 1, - "items": items, - "taxes": get_order_taxes(shopify_order, shopify_settings), - "apply_discount_on": "Grand Total", - "discount_amount": get_discounted_amount(shopify_order), - }) + so = frappe.get_doc( + { + "doctype": "Sales Order", + "naming_series": shopify_settings.sales_order_series or "SO-Shopify-", + "shopify_order_id": shopify_order.get("id"), + "shopify_order_number": shopify_order.get("name"), + "customer": customer or shopify_settings.default_customer, + "transaction_date": getdate(shopify_order.get("created_at")) or nowdate(), + "delivery_date": getdate(shopify_order.get("created_at")) or nowdate(), + "company": shopify_settings.company, + "selling_price_list": shopify_settings.price_list, + "ignore_pricing_rule": 1, + "items": items, + "taxes": get_order_taxes(shopify_order, shopify_settings), + "apply_discount_on": "Grand Total", + "discount_amount": get_discounted_amount(shopify_order), + } + ) if company: - so.update({ - "company": company, - "status": "Draft" - }) + so.update({"company": company, "status": "Draft"}) so.flags.ignore_mandatory = True so.save(ignore_permissions=True) so.submit() @@ -146,12 +158,17 @@ def create_sales_order(shopify_order, shopify_settings, company=None): frappe.db.commit() return so + def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=False): - if not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name")\ - and so.docstatus==1 and not so.per_billed and cint(shopify_settings.sync_sales_invoice): + if ( + not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name") + and so.docstatus == 1 + and not so.per_billed + and cint(shopify_settings.sync_sales_invoice) + ): if old_order_sync: - posting_date = getdate(shopify_order.get('created_at')) + posting_date = getdate(shopify_order.get("created_at")) else: posting_date = nowdate() @@ -169,13 +186,18 @@ def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=Fal make_payament_entry_against_sales_invoice(si, shopify_settings, posting_date) frappe.db.commit() + def set_cost_center(items, cost_center): for item in items: item.cost_center = cost_center + def make_payament_entry_against_sales_invoice(doc, shopify_settings, posting_date=None): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account) + + payment_entry = get_payment_entry( + doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account + ) payment_entry.flags.ignore_mandatory = True payment_entry.reference_no = doc.name payment_entry.posting_date = posting_date or nowdate() @@ -183,13 +205,18 @@ def make_payament_entry_against_sales_invoice(doc, shopify_settings, posting_dat payment_entry.insert(ignore_permissions=True) payment_entry.submit() + def create_delivery_note(shopify_order, shopify_settings, so): if not cint(shopify_settings.sync_delivery_note): return for fulfillment in shopify_order.get("fulfillments"): - if not frappe.db.get_value("Delivery Note", {"shopify_fulfillment_id": fulfillment.get("id")}, "name")\ - and so.docstatus==1: + if ( + not frappe.db.get_value( + "Delivery Note", {"shopify_fulfillment_id": fulfillment.get("id")}, "name" + ) + and so.docstatus == 1 + ): dn = make_delivery_note(so.name) dn.shopify_order_id = fulfillment.get("order_id") @@ -204,9 +231,15 @@ def create_delivery_note(shopify_order, shopify_settings, so): dn.submit() frappe.db.commit() + def get_fulfillment_items(dn_items, fulfillment_items, shopify_settings): - return [dn_item.update({"qty": item.get("quantity")}) for item in fulfillment_items for dn_item in dn_items\ - if get_item_code(item) == dn_item.item_code] + return [ + dn_item.update({"qty": item.get("quantity")}) + for item in fulfillment_items + for dn_item in dn_items + if get_item_code(item) == dn_item.item_code + ] + def get_discounted_amount(order): discounted_amount = 0.0 @@ -214,98 +247,120 @@ def get_discounted_amount(order): discounted_amount += flt(discount.get("amount")) return discounted_amount + def get_order_items(order_items, shopify_settings, delivery_date): items = [] all_product_exists = True product_not_exists = [] for shopify_item in order_items: - if not shopify_item.get('product_exists'): + if not shopify_item.get("product_exists"): all_product_exists = False - product_not_exists.append({'title':shopify_item.get('title'), - 'shopify_order_id': shopify_item.get('id')}) + product_not_exists.append( + {"title": shopify_item.get("title"), "shopify_order_id": shopify_item.get("id")} + ) continue if all_product_exists: item_code = get_item_code(shopify_item) - items.append({ - "item_code": item_code, - "item_name": shopify_item.get("name"), - "rate": shopify_item.get("price"), - "delivery_date": delivery_date, - "qty": shopify_item.get("quantity"), - "stock_uom": shopify_item.get("uom") or _("Nos"), - "warehouse": shopify_settings.warehouse - }) + items.append( + { + "item_code": item_code, + "item_name": shopify_item.get("name"), + "rate": shopify_item.get("price"), + "delivery_date": delivery_date, + "qty": shopify_item.get("quantity"), + "stock_uom": shopify_item.get("uom") or _("Nos"), + "warehouse": shopify_settings.warehouse, + } + ) else: items = [] return items + def get_item_code(shopify_item): - item_code = frappe.db.get_value("Item", {"shopify_variant_id": shopify_item.get("variant_id")}, "item_code") + item_code = frappe.db.get_value( + "Item", {"shopify_variant_id": shopify_item.get("variant_id")}, "item_code" + ) if not item_code: - item_code = frappe.db.get_value("Item", {"shopify_product_id": shopify_item.get("product_id")}, "item_code") + item_code = frappe.db.get_value( + "Item", {"shopify_product_id": shopify_item.get("product_id")}, "item_code" + ) if not item_code: item_code = frappe.db.get_value("Item", {"item_name": shopify_item.get("title")}, "item_code") return item_code + def get_order_taxes(shopify_order, shopify_settings): taxes = [] for tax in shopify_order.get("tax_lines"): - taxes.append({ - "charge_type": _("On Net Total"), - "account_head": get_tax_account_head(tax), - "description": "{0} - {1}%".format(tax.get("title"), tax.get("rate") * 100.0), - "rate": tax.get("rate") * 100.00, - "included_in_print_rate": 1 if shopify_order.get("taxes_included") else 0, - "cost_center": shopify_settings.cost_center - }) + taxes.append( + { + "charge_type": _("On Net Total"), + "account_head": get_tax_account_head(tax), + "description": "{0} - {1}%".format(tax.get("title"), tax.get("rate") * 100.0), + "rate": tax.get("rate") * 100.00, + "included_in_print_rate": 1 if shopify_order.get("taxes_included") else 0, + "cost_center": shopify_settings.cost_center, + } + ) - taxes = update_taxes_with_shipping_lines(taxes, shopify_order.get("shipping_lines"), shopify_settings) + taxes = update_taxes_with_shipping_lines( + taxes, shopify_order.get("shipping_lines"), shopify_settings + ) return taxes + def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings): """Shipping lines represents the shipping details, - each such shipping detail consists of a list of tax_lines""" + each such shipping detail consists of a list of tax_lines""" for shipping_charge in shipping_lines: if shipping_charge.get("price"): - taxes.append({ - "charge_type": _("Actual"), - "account_head": get_tax_account_head(shipping_charge), - "description": shipping_charge["title"], - "tax_amount": shipping_charge["price"], - "cost_center": shopify_settings.cost_center - }) + taxes.append( + { + "charge_type": _("Actual"), + "account_head": get_tax_account_head(shipping_charge), + "description": shipping_charge["title"], + "tax_amount": shipping_charge["price"], + "cost_center": shopify_settings.cost_center, + } + ) for tax in shipping_charge.get("tax_lines"): - taxes.append({ - "charge_type": _("Actual"), - "account_head": get_tax_account_head(tax), - "description": tax["title"], - "tax_amount": tax["price"], - "cost_center": shopify_settings.cost_center - }) + taxes.append( + { + "charge_type": _("Actual"), + "account_head": get_tax_account_head(tax), + "description": tax["title"], + "tax_amount": tax["price"], + "cost_center": shopify_settings.cost_center, + } + ) return taxes + def get_tax_account_head(tax): tax_title = tax.get("title").encode("utf-8") - tax_account = frappe.db.get_value("Shopify Tax Account", \ - {"parent": "Shopify Settings", "shopify_tax": tax_title}, "tax_account") + tax_account = frappe.db.get_value( + "Shopify Tax Account", {"parent": "Shopify Settings", "shopify_tax": tax_title}, "tax_account" + ) if not tax_account: frappe.throw(_("Tax Account not specified for Shopify Tax {0}").format(tax.get("title"))) return tax_account + @frappe.whitelist(allow_guest=True) def sync_old_orders(): - frappe.set_user('Administrator') - shopify_settings = frappe.get_doc('Shopify Settings') + frappe.set_user("Administrator") + shopify_settings = frappe.get_doc("Shopify Settings") if not shopify_settings.sync_missing_orders: return @@ -324,7 +379,7 @@ def sync_old_orders(): return sync_sales_order(order=order, old_order_sync=True) - last_order_id = order.get('id') + last_order_id = order.get("id") if last_order_id: shopify_settings.load_from_db() @@ -335,29 +390,40 @@ def sync_old_orders(): except Exception as e: raise e + def stop_sync(shopify_settings): shopify_settings.sync_missing_orders = 0 - shopify_settings.last_order_id = '' + shopify_settings.last_order_id = "" shopify_settings.save() frappe.db.commit() + def get_url(shopify_settings): last_order_id = shopify_settings.last_order_id if not last_order_id: - if shopify_settings.sync_based_on == 'Date': - url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&created_at_min={0}&since_id=0".format( - get_datetime(shopify_settings.from_date)), shopify_settings) + if shopify_settings.sync_based_on == "Date": + url = get_shopify_url( + "admin/api/2021-04/orders.json?limit=250&created_at_min={0}&since_id=0".format( + get_datetime(shopify_settings.from_date) + ), + shopify_settings, + ) else: - url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format( - shopify_settings.from_order_id), shopify_settings) + url = get_shopify_url( + "admin/api/2021-04/orders.json?limit=250&since_id={0}".format(shopify_settings.from_order_id), + shopify_settings, + ) else: - url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings) + url = get_shopify_url( + "admin/api/2021-04/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings + ) return url + def is_sync_complete(shopify_settings, order): - if shopify_settings.sync_based_on == 'Date': - return getdate(shopify_settings.to_date) < getdate(order.get('created_at')) + if shopify_settings.sync_based_on == "Date": + return getdate(shopify_settings.to_date) < getdate(order.get("created_at")) else: - return cstr(order.get('id')) == cstr(shopify_settings.to_order_id) + return cstr(order.get("id")) == cstr(shopify_settings.to_order_id) diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 3e815e9bebf..46a4d3c69e4 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -1,5 +1,3 @@ - - import base64 import hashlib import hmac @@ -14,26 +12,30 @@ def verify_request(): woocommerce_settings = frappe.get_doc("Woocommerce Settings") sig = base64.b64encode( hmac.new( - woocommerce_settings.secret.encode('utf8'), - frappe.request.data, - hashlib.sha256 + woocommerce_settings.secret.encode("utf8"), frappe.request.data, hashlib.sha256 ).digest() ) - if frappe.request.data and \ - not sig == frappe.get_request_header("X-Wc-Webhook-Signature", "").encode(): - frappe.throw(_("Unverified Webhook Data")) + if ( + frappe.request.data + and not sig == frappe.get_request_header("X-Wc-Webhook-Signature", "").encode() + ): + frappe.throw(_("Unverified Webhook Data")) frappe.set_user(woocommerce_settings.creation_user) + @frappe.whitelist(allow_guest=True) def order(*args, **kwargs): try: _order(*args, **kwargs) except Exception: - error_message = frappe.get_traceback()+"\n\n Request Data: \n"+json.loads(frappe.request.data).__str__() + error_message = ( + frappe.get_traceback() + "\n\n Request Data: \n" + json.loads(frappe.request.data).__str__() + ) frappe.log_error(error_message, "WooCommerce Error") raise + def _order(*args, **kwargs): woocommerce_settings = frappe.get_doc("Woocommerce Settings") if frappe.flags.woocomm_test_order_data: @@ -45,7 +47,7 @@ def _order(*args, **kwargs): try: order = json.loads(frappe.request.data) except ValueError: - #woocommerce returns 'webhook_id=value' for the first request which is not JSON + # woocommerce returns 'webhook_id=value' for the first request which is not JSON order = frappe.request.data event = frappe.get_request_header("X-Wc-Webhook-Event") @@ -53,7 +55,7 @@ def _order(*args, **kwargs): return "success" if event == "created": - sys_lang = frappe.get_single("System Settings").language or 'en' + sys_lang = frappe.get_single("System Settings").language or "en" raw_billing_data = order.get("billing") raw_shipping_data = order.get("shipping") customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name") @@ -61,6 +63,7 @@ def _order(*args, **kwargs): link_items(order.get("line_items"), woocommerce_settings, sys_lang) create_sales_order(order, woocommerce_settings, customer_name, sys_lang) + def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name): customer_woo_com_email = raw_billing_data.get("email") customer_exists = frappe.get_value("Customer", {"woocommerce_email": customer_woo_com_email}) @@ -79,9 +82,14 @@ def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name if customer_exists: frappe.rename_doc("Customer", old_name, customer_name) - for address_type in ("Billing", "Shipping",): + for address_type in ( + "Billing", + "Shipping", + ): try: - address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email, "address_type": address_type}) + address = frappe.get_doc( + "Address", {"woocommerce_email": customer_woo_com_email, "address_type": address_type} + ) rename_address(address, customer) except ( frappe.DoesNotExistError, @@ -94,6 +102,7 @@ def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name create_address(raw_shipping_data, customer, "Shipping") create_contact(raw_billing_data, customer) + def create_contact(data, customer): email = data.get("email", None) phone = data.get("phone", None) @@ -113,14 +122,12 @@ def create_contact(data, customer): if email: contact.add_email(email, is_primary=1) - contact.append("links", { - "link_doctype": "Customer", - "link_name": customer.name - }) + contact.append("links", {"link_doctype": "Customer", "link_name": customer.name}) contact.flags.ignore_mandatory = True contact.save() + def create_address(raw_data, customer, address_type): address = frappe.new_doc("Address") @@ -134,14 +141,12 @@ def create_address(raw_data, customer, address_type): address.pincode = raw_data.get("postcode") address.phone = raw_data.get("phone") address.email_id = customer.woocommerce_email - address.append("links", { - "link_doctype": "Customer", - "link_name": customer.name - }) + address.append("links", {"link_doctype": "Customer", "link_name": customer.name}) address.flags.ignore_mandatory = True address.save() + def rename_address(address, customer): old_address_title = address.name new_address_title = customer.name + "-" + address.address_type @@ -150,12 +155,13 @@ def rename_address(address, customer): frappe.rename_doc("Address", old_address_title, new_address_title) + def link_items(items_list, woocommerce_settings, sys_lang): for item_data in items_list: item_woo_com_id = cstr(item_data.get("product_id")) - if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, 'name'): - #Create Item + if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, "name"): + # Create Item item = frappe.new_doc("Item") item.item_code = _("woocommerce - {0}", sys_lang).format(item_woo_com_id) item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang) @@ -166,6 +172,7 @@ def link_items(items_list, woocommerce_settings, sys_lang): item.flags.ignore_mandatory = True item.save() + def create_sales_order(order, woocommerce_settings, customer_name, sys_lang): new_sales_order = frappe.new_doc("Sales Order") new_sales_order.customer = customer_name @@ -187,12 +194,12 @@ def create_sales_order(order, woocommerce_settings, customer_name, sys_lang): frappe.db.commit() + def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang): - company_abbr = frappe.db.get_value('Company', woocommerce_settings.company, 'abbr') + company_abbr = frappe.db.get_value("Company", woocommerce_settings.company, "abbr") default_warehouse = _("Stores - {0}", sys_lang).format(company_abbr) - if not frappe.db.exists("Warehouse", default_warehouse) \ - and not woocommerce_settings.warehouse: + if not frappe.db.exists("Warehouse", default_warehouse) and not woocommerce_settings.warehouse: frappe.throw(_("Please set Warehouse in Woocommerce Settings")) for item in order.get("line_items"): @@ -201,28 +208,44 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l ordered_items_tax = item.get("total_tax") - new_sales_order.append("items", { - "item_code": found_item.name, - "item_name": found_item.item_name, - "description": found_item.item_name, - "delivery_date": new_sales_order.delivery_date, - "uom": woocommerce_settings.uom or _("Nos", sys_lang), - "qty": item.get("quantity"), - "rate": item.get("price"), - "warehouse": woocommerce_settings.warehouse or default_warehouse - }) + new_sales_order.append( + "items", + { + "item_code": found_item.name, + "item_name": found_item.item_name, + "description": found_item.item_name, + "delivery_date": new_sales_order.delivery_date, + "uom": woocommerce_settings.uom or _("Nos", sys_lang), + "qty": item.get("quantity"), + "rate": item.get("price"), + "warehouse": woocommerce_settings.warehouse or default_warehouse, + }, + ) - add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account) + add_tax_details( + new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account + ) # shipping_details = order.get("shipping_lines") # used for detailed order - add_tax_details(new_sales_order, order.get("shipping_tax"), "Shipping Tax", woocommerce_settings.f_n_f_account) - add_tax_details(new_sales_order, order.get("shipping_total"), "Shipping Total", woocommerce_settings.f_n_f_account) + add_tax_details( + new_sales_order, order.get("shipping_tax"), "Shipping Tax", woocommerce_settings.f_n_f_account + ) + add_tax_details( + new_sales_order, + order.get("shipping_total"), + "Shipping Total", + woocommerce_settings.f_n_f_account, + ) + def add_tax_details(sales_order, price, desc, tax_account_head): - sales_order.append("taxes", { - "charge_type":"Actual", - "account_head": tax_account_head, - "tax_amount": price, - "description": desc - }) + sales_order.append( + "taxes", + { + "charge_type": "Actual", + "account_head": tax_account_head, + "tax_amount": price, + "description": desc, + }, + ) diff --git a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py b/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py index d7ebf59ea8e..616ecfbac68 100644 --- a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py +++ b/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py @@ -1,13 +1,12 @@ - import frappe def pre_process(issue): - project = frappe.db.get_value('Project', filters={'project_name': issue.milestone}) + project = frappe.db.get_value("Project", filters={"project_name": issue.milestone}) return { - 'title': issue.title, - 'body': frappe.utils.md_to_html(issue.body or ''), - 'state': issue.state.title(), - 'project': project or '' + "title": issue.title, + "body": frappe.utils.md_to_html(issue.body or ""), + "state": issue.state.title(), + "project": project or "", } diff --git a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py b/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py index 7dd0c8658d1..d44fc0454ca 100644 --- a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py +++ b/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py @@ -1,8 +1,6 @@ - - def pre_process(milestone): return { - 'title': milestone.title, - 'description': milestone.description, - 'state': milestone.state.title() + "title": milestone.title, + "description": milestone.description, + "state": milestone.state.title(), } diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py index ff51959eab5..7210d5fa745 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py @@ -14,7 +14,7 @@ from six import StringIO import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws -#Get and Create Products +# Get and Create Products def get_products_details(): products = get_products_instance() reports = get_reports_instance() @@ -23,84 +23,93 @@ def get_products_details(): market_place_list = return_as_list(mws_settings.market_place_id) for marketplace in market_place_list: - report_id = request_and_fetch_report_id("_GET_FLAT_FILE_OPEN_LISTINGS_DATA_", None, None, market_place_list) + report_id = request_and_fetch_report_id( + "_GET_FLAT_FILE_OPEN_LISTINGS_DATA_", None, None, market_place_list + ) if report_id: listings_response = reports.get_report(report_id=report_id) - #Get ASIN Codes + # Get ASIN Codes string_io = StringIO(frappe.safe_decode(listings_response.original)) - csv_rows = list(csv.reader(string_io, delimiter=str('\t'))) + csv_rows = list(csv.reader(string_io, delimiter=str("\t"))) asin_list = list(set([row[1] for row in csv_rows[1:]])) - #break into chunks of 10 + # break into chunks of 10 asin_chunked_list = list(chunks(asin_list, 10)) - #Map ASIN Codes to SKUs - sku_asin = [{"asin":row[1],"sku":row[0]} for row in csv_rows[1:]] + # Map ASIN Codes to SKUs + sku_asin = [{"asin": row[1], "sku": row[0]} for row in csv_rows[1:]] - #Fetch Products List from ASIN + # Fetch Products List from ASIN for asin_list in asin_chunked_list: - products_response = call_mws_method(products.get_matching_product,marketplaceid=marketplace, - asins=asin_list) + products_response = call_mws_method( + products.get_matching_product, marketplaceid=marketplace, asins=asin_list + ) matching_products_list = products_response.parsed for product in matching_products_list: - skus = [row["sku"] for row in sku_asin if row["asin"]==product.ASIN] + skus = [row["sku"] for row in sku_asin if row["asin"] == product.ASIN] for sku in skus: create_item_code(product, sku) + def get_products_instance(): mws_settings = frappe.get_doc("Amazon MWS Settings") products = mws.Products( - account_id = mws_settings.seller_id, - access_key = mws_settings.aws_access_key_id, - secret_key = mws_settings.secret_key, - region = mws_settings.region, - domain = mws_settings.domain - ) + account_id=mws_settings.seller_id, + access_key=mws_settings.aws_access_key_id, + secret_key=mws_settings.secret_key, + region=mws_settings.region, + domain=mws_settings.domain, + ) return products + def get_reports_instance(): mws_settings = frappe.get_doc("Amazon MWS Settings") reports = mws.Reports( - account_id = mws_settings.seller_id, - access_key = mws_settings.aws_access_key_id, - secret_key = mws_settings.secret_key, - region = mws_settings.region, - domain = mws_settings.domain + account_id=mws_settings.seller_id, + access_key=mws_settings.aws_access_key_id, + secret_key=mws_settings.secret_key, + region=mws_settings.region, + domain=mws_settings.domain, ) return reports -#returns list as expected by amazon API + +# returns list as expected by amazon API def return_as_list(input_value): if isinstance(input_value, list): return input_value else: return [input_value] -#function to chunk product data + +# function to chunk product data def chunks(l, n): for i in range(0, len(l), n): - yield l[i:i+n] + yield l[i : i + n] + def request_and_fetch_report_id(report_type, start_date=None, end_date=None, marketplaceids=None): reports = get_reports_instance() - report_response = reports.request_report(report_type=report_type, - start_date=start_date, - end_date=end_date, - marketplaceids=marketplaceids) + report_response = reports.request_report( + report_type=report_type, start_date=start_date, end_date=end_date, marketplaceids=marketplaceids + ) report_request_id = report_response.parsed["ReportRequestInfo"]["ReportRequestId"]["value"] generated_report_id = None - #poll to get generated report - for x in range(1,10): + # poll to get generated report + for x in range(1, 10): report_request_list_response = reports.get_report_request_list(requestids=[report_request_id]) - report_status = report_request_list_response.parsed["ReportRequestInfo"]["ReportProcessingStatus"]["value"] + report_status = report_request_list_response.parsed["ReportRequestInfo"][ + "ReportProcessingStatus" + ]["value"] if report_status == "_SUBMITTED_" or report_status == "_IN_PROGRESS_": - #add time delay to wait for amazon to generate report + # add time delay to wait for amazon to generate report time.sleep(15) continue elif report_status == "_CANCELLED_": @@ -108,10 +117,13 @@ def request_and_fetch_report_id(report_type, start_date=None, end_date=None, mar elif report_status == "_DONE_NO_DATA_": break elif report_status == "_DONE_": - generated_report_id = report_request_list_response.parsed["ReportRequestInfo"]["GeneratedReportId"]["value"] + generated_report_id = report_request_list_response.parsed["ReportRequestInfo"][ + "GeneratedReportId" + ]["value"] break return generated_report_id + def call_mws_method(mws_method, *args, **kwargs): mws_settings = frappe.get_doc("Amazon MWS Settings") @@ -132,6 +144,7 @@ def call_mws_method(mws_method, *args, **kwargs): frappe.throw(_("Sync has been temporarily disabled because maximum retries have been exceeded")) + def create_item_code(amazon_item_json, sku): if frappe.db.get_value("Item", sku): return @@ -154,27 +167,30 @@ def create_item_code(amazon_item_json, sku): temp_item_group = amazon_item_json.Product.AttributeSets.ItemAttributes.ProductGroup - item_group = frappe.db.get_value("Item Group",filters={"item_group_name": temp_item_group}) + item_group = frappe.db.get_value("Item Group", filters={"item_group_name": temp_item_group}) if not item_group: igroup = frappe.new_doc("Item Group") igroup.item_group_name = temp_item_group - igroup.parent_item_group = mws_settings.item_group + igroup.parent_item_group = mws_settings.item_group igroup.insert() - item.append("item_defaults", {'company':mws_settings.company}) + item.append("item_defaults", {"company": mws_settings.company}) item.insert(ignore_permissions=True) create_item_price(amazon_item_json, item.item_code) return item.name + def create_manufacturer(amazon_item_json): if not amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer: return None - existing_manufacturer = frappe.db.get_value("Manufacturer", - filters={"short_name":amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer}) + existing_manufacturer = frappe.db.get_value( + "Manufacturer", + filters={"short_name": amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer}, + ) if not existing_manufacturer: manufacturer = frappe.new_doc("Manufacturer") @@ -184,12 +200,14 @@ def create_manufacturer(amazon_item_json): else: return existing_manufacturer + def create_brand(amazon_item_json): if not amazon_item_json.Product.AttributeSets.ItemAttributes.Brand: return None - existing_brand = frappe.db.get_value("Brand", - filters={"brand":amazon_item_json.Product.AttributeSets.ItemAttributes.Brand}) + existing_brand = frappe.db.get_value( + "Brand", filters={"brand": amazon_item_json.Product.AttributeSets.ItemAttributes.Brand} + ) if not existing_brand: brand = frappe.new_doc("Brand") brand.brand = amazon_item_json.Product.AttributeSets.ItemAttributes.Brand @@ -198,18 +216,24 @@ def create_brand(amazon_item_json): else: return existing_brand + def create_item_price(amazon_item_json, item_code): item_price = frappe.new_doc("Item Price") - item_price.price_list = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "price_list") - if not("ListPrice" in amazon_item_json.Product.AttributeSets.ItemAttributes): + item_price.price_list = frappe.db.get_value( + "Amazon MWS Settings", "Amazon MWS Settings", "price_list" + ) + if not ("ListPrice" in amazon_item_json.Product.AttributeSets.ItemAttributes): item_price.price_list_rate = 0 else: - item_price.price_list_rate = amazon_item_json.Product.AttributeSets.ItemAttributes.ListPrice.Amount + item_price.price_list_rate = ( + amazon_item_json.Product.AttributeSets.ItemAttributes.ListPrice.Amount + ) item_price.item_code = item_code item_price.insert() -#Get and create Orders + +# Get and create Orders def get_orders(after_date): try: orders = get_orders_instance() @@ -217,11 +241,14 @@ def get_orders(after_date): mws_settings = frappe.get_doc("Amazon MWS Settings") market_place_list = return_as_list(mws_settings.market_place_id) - orders_response = call_mws_method(orders.list_orders, marketplaceids=market_place_list, + orders_response = call_mws_method( + orders.list_orders, + marketplaceids=market_place_list, fulfillment_channels=["MFN", "AFN"], lastupdatedafter=after_date, orderstatus=statuses, - max_results='50') + max_results="50", + ) while True: orders_list = [] @@ -244,30 +271,34 @@ def get_orders(after_date): except Exception as e: frappe.log_error(title="get_orders", message=e) + def get_orders_instance(): mws_settings = frappe.get_doc("Amazon MWS Settings") orders = mws.Orders( - account_id = mws_settings.seller_id, - access_key = mws_settings.aws_access_key_id, - secret_key = mws_settings.secret_key, - region= mws_settings.region, - domain= mws_settings.domain, - version="2013-09-01" - ) + account_id=mws_settings.seller_id, + access_key=mws_settings.aws_access_key_id, + secret_key=mws_settings.secret_key, + region=mws_settings.region, + domain=mws_settings.domain, + version="2013-09-01", + ) return orders -def create_sales_order(order_json,after_date): + +def create_sales_order(order_json, after_date): customer_name = create_customer(order_json) create_address(order_json, customer_name) market_place_order_id = order_json.AmazonOrderId - so = frappe.db.get_value("Sales Order", - filters={"amazon_order_id": market_place_order_id}, - fieldname="name") + so = frappe.db.get_value( + "Sales Order", filters={"amazon_order_id": market_place_order_id}, fieldname="name" + ) - taxes_and_charges = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "taxes_charges") + taxes_and_charges = frappe.db.get_value( + "Amazon MWS Settings", "Amazon MWS Settings", "taxes_charges" + ) if so: return @@ -277,7 +308,8 @@ def create_sales_order(order_json,after_date): delivery_date = dateutil.parser.parse(order_json.LatestShipDate).strftime("%Y-%m-%d") transaction_date = dateutil.parser.parse(order_json.PurchaseDate).strftime("%Y-%m-%d") - so = frappe.get_doc({ + so = frappe.get_doc( + { "doctype": "Sales Order", "naming_series": "SO-", "amazon_order_id": market_place_order_id, @@ -286,42 +318,46 @@ def create_sales_order(order_json,after_date): "delivery_date": delivery_date, "transaction_date": transaction_date, "items": items, - "company": frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "company") - }) + "company": frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "company"), + } + ) try: if taxes_and_charges: charges_and_fees = get_charges_and_fees(market_place_order_id) for charge in charges_and_fees.get("charges"): - so.append('taxes', charge) + so.append("taxes", charge) for fee in charges_and_fees.get("fees"): - so.append('taxes', fee) + so.append("taxes", fee) so.insert(ignore_permissions=True) so.submit() except Exception as e: import traceback + frappe.log_error(message=traceback.format_exc(), title="Create Sales Order") + def create_customer(order_json): order_customer_name = "" - if not("BuyerName" in order_json): + if not ("BuyerName" in order_json): order_customer_name = "Buyer - " + order_json.AmazonOrderId else: order_customer_name = order_json.BuyerName - existing_customer_name = frappe.db.get_value("Customer", - filters={"name": order_customer_name}, fieldname="name") + existing_customer_name = frappe.db.get_value( + "Customer", filters={"name": order_customer_name}, fieldname="name" + ) if existing_customer_name: filters = [ - ["Dynamic Link", "link_doctype", "=", "Customer"], - ["Dynamic Link", "link_name", "=", existing_customer_name], - ["Dynamic Link", "parenttype", "=", "Contact"] - ] + ["Dynamic Link", "link_doctype", "=", "Customer"], + ["Dynamic Link", "link_name", "=", existing_customer_name], + ["Dynamic Link", "parenttype", "=", "Contact"], + ] existing_contacts = frappe.get_list("Contact", filters) @@ -330,10 +366,7 @@ def create_customer(order_json): else: new_contact = frappe.new_doc("Contact") new_contact.first_name = order_customer_name - new_contact.append('links', { - "link_doctype": "Customer", - "link_name": existing_customer_name - }) + new_contact.append("links", {"link_doctype": "Customer", "link_name": existing_customer_name}) new_contact.insert() return existing_customer_name @@ -348,26 +381,24 @@ def create_customer(order_json): new_contact = frappe.new_doc("Contact") new_contact.first_name = order_customer_name - new_contact.append('links', { - "link_doctype": "Customer", - "link_name": new_customer.name - }) + new_contact.append("links", {"link_doctype": "Customer", "link_name": new_customer.name}) new_contact.insert() return new_customer.name + def create_address(amazon_order_item_json, customer_name): filters = [ - ["Dynamic Link", "link_doctype", "=", "Customer"], - ["Dynamic Link", "link_name", "=", customer_name], - ["Dynamic Link", "parenttype", "=", "Address"] - ] + ["Dynamic Link", "link_doctype", "=", "Customer"], + ["Dynamic Link", "link_name", "=", customer_name], + ["Dynamic Link", "parenttype", "=", "Address"], + ] existing_address = frappe.get_list("Address", filters) - if not("ShippingAddress" in amazon_order_item_json): + if not ("ShippingAddress" in amazon_order_item_json): return None else: make_address = frappe.new_doc("Address") @@ -390,21 +421,23 @@ def create_address(amazon_order_item_json, customer_name): for address in existing_address: address_doc = frappe.get_doc("Address", address["name"]) - if (address_doc.address_line1 == make_address.address_line1 and - address_doc.pincode == make_address.pincode): + if ( + address_doc.address_line1 == make_address.address_line1 + and address_doc.pincode == make_address.pincode + ): return address - make_address.append("links", { - "link_doctype": "Customer", - "link_name": customer_name - }) + make_address.append("links", {"link_doctype": "Customer", "link_name": customer_name}) make_address.address_type = "Shipping" make_address.insert() + def get_order_items(market_place_order_id): mws_orders = get_orders_instance() - order_items_response = call_mws_method(mws_orders.list_order_items, amazon_order_id=market_place_order_id) + order_items_response = call_mws_method( + mws_orders.list_order_items, amazon_order_id=market_place_order_id + ) final_order_items = [] order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem) @@ -419,16 +452,18 @@ def get_order_items(market_place_order_id): else: price = order_item.ItemPrice.Amount - final_order_items.append({ - "item_code": get_item_code(order_item), - "item_name": order_item.SellerSKU, - "description": order_item.Title, - "rate": price, - "qty": order_item.QuantityOrdered, - "stock_uom": "Nos", - "warehouse": warehouse, - "conversion_factor": "1.0" - }) + final_order_items.append( + { + "item_code": get_item_code(order_item), + "item_name": order_item.SellerSKU, + "description": order_item.Title, + "rate": price, + "qty": order_item.QuantityOrdered, + "stock_uom": "Nos", + "warehouse": warehouse, + "conversion_factor": "1.0", + } + ) if not "NextToken" in order_items_response.parsed: break @@ -440,16 +475,18 @@ def get_order_items(market_place_order_id): return final_order_items + def get_item_code(order_item): sku = order_item.SellerSKU item_code = frappe.db.get_value("Item", {"item_code": sku}, "item_code") if item_code: return item_code + def get_charges_and_fees(market_place_order_id): finances = get_finances_instance() - charges_fees = {"charges":[], "fees":[]} + charges_fees = {"charges": [], "fees": []} response = call_mws_method(finances.list_financial_events, amazon_order_id=market_place_order_id) @@ -462,49 +499,55 @@ def get_charges_and_fees(market_place_order_id): for shipment_item in shipment_item_list: charges, fees = [], [] - if 'ItemChargeList' in shipment_item.keys(): + if "ItemChargeList" in shipment_item.keys(): charges = return_as_list(shipment_item.ItemChargeList.ChargeComponent) - if 'ItemFeeList' in shipment_item.keys(): + if "ItemFeeList" in shipment_item.keys(): fees = return_as_list(shipment_item.ItemFeeList.FeeComponent) for charge in charges: - if(charge.ChargeType != "Principal") and float(charge.ChargeAmount.CurrencyAmount) != 0: + if (charge.ChargeType != "Principal") and float(charge.ChargeAmount.CurrencyAmount) != 0: charge_account = get_account(charge.ChargeType) - charges_fees.get("charges").append({ - "charge_type":"Actual", - "account_head": charge_account, - "tax_amount": charge.ChargeAmount.CurrencyAmount, - "description": charge.ChargeType + " for " + shipment_item.SellerSKU - }) + charges_fees.get("charges").append( + { + "charge_type": "Actual", + "account_head": charge_account, + "tax_amount": charge.ChargeAmount.CurrencyAmount, + "description": charge.ChargeType + " for " + shipment_item.SellerSKU, + } + ) for fee in fees: if float(fee.FeeAmount.CurrencyAmount) != 0: fee_account = get_account(fee.FeeType) - charges_fees.get("fees").append({ - "charge_type":"Actual", - "account_head": fee_account, - "tax_amount": fee.FeeAmount.CurrencyAmount, - "description": fee.FeeType + " for " + shipment_item.SellerSKU - }) + charges_fees.get("fees").append( + { + "charge_type": "Actual", + "account_head": fee_account, + "tax_amount": fee.FeeAmount.CurrencyAmount, + "description": fee.FeeType + " for " + shipment_item.SellerSKU, + } + ) return charges_fees + def get_finances_instance(): mws_settings = frappe.get_doc("Amazon MWS Settings") finances = mws.Finances( - account_id = mws_settings.seller_id, - access_key = mws_settings.aws_access_key_id, - secret_key = mws_settings.secret_key, - region= mws_settings.region, - domain= mws_settings.domain, - version="2015-05-01" - ) + account_id=mws_settings.seller_id, + access_key=mws_settings.aws_access_key_id, + secret_key=mws_settings.secret_key, + region=mws_settings.region, + domain=mws_settings.domain, + version="2015-05-01", + ) return finances + def get_account(name): existing_account = frappe.db.get_value("Account", {"account_name": "Amazon {0}".format(name)}) account_name = existing_account diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py index 4caf137455a..ff034fb3564 100755 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py @@ -23,57 +23,57 @@ from requests import request from requests.exceptions import HTTPError __all__ = [ - 'Feeds', - 'Inventory', - 'MWSError', - 'Reports', - 'Orders', - 'Products', - 'Recommendations', - 'Sellers', - 'Finances' + "Feeds", + "Inventory", + "MWSError", + "Reports", + "Orders", + "Products", + "Recommendations", + "Sellers", + "Finances", ] # See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf page 8 # for a list of the end points and marketplace IDs MARKETPLACES = { - "CA": "https://mws.amazonservices.ca", #A2EUQ1WTGCTBG2 - "US": "https://mws.amazonservices.com", #ATVPDKIKX0DER", - "DE": "https://mws-eu.amazonservices.com", #A1PA6795UKMFR9 - "ES": "https://mws-eu.amazonservices.com", #A1RKKUPIHCS9HS - "FR": "https://mws-eu.amazonservices.com", #A13V1IB3VIYZZH - "IN": "https://mws.amazonservices.in", #A21TJRUUN4KGV - "IT": "https://mws-eu.amazonservices.com", #APJ6JRA9NG5V4 - "UK": "https://mws-eu.amazonservices.com", #A1F83G8C2ARO7P - "JP": "https://mws.amazonservices.jp", #A1VC38T7YXB528 - "CN": "https://mws.amazonservices.com.cn", #AAHKV2X7AFYLW - "AE": " https://mws.amazonservices.ae", #A2VIGQ35RCS4UG - "MX": "https://mws.amazonservices.com.mx", #A1AM78C64UM0Y8 - "BR": "https://mws.amazonservices.com", #A2Q3Y263D00KWC + "CA": "https://mws.amazonservices.ca", # A2EUQ1WTGCTBG2 + "US": "https://mws.amazonservices.com", # ATVPDKIKX0DER", + "DE": "https://mws-eu.amazonservices.com", # A1PA6795UKMFR9 + "ES": "https://mws-eu.amazonservices.com", # A1RKKUPIHCS9HS + "FR": "https://mws-eu.amazonservices.com", # A13V1IB3VIYZZH + "IN": "https://mws.amazonservices.in", # A21TJRUUN4KGV + "IT": "https://mws-eu.amazonservices.com", # APJ6JRA9NG5V4 + "UK": "https://mws-eu.amazonservices.com", # A1F83G8C2ARO7P + "JP": "https://mws.amazonservices.jp", # A1VC38T7YXB528 + "CN": "https://mws.amazonservices.com.cn", # AAHKV2X7AFYLW + "AE": " https://mws.amazonservices.ae", # A2VIGQ35RCS4UG + "MX": "https://mws.amazonservices.com.mx", # A1AM78C64UM0Y8 + "BR": "https://mws.amazonservices.com", # A2Q3Y263D00KWC } class MWSError(Exception): """ - Main MWS Exception class + Main MWS Exception class """ + # Allows quick access to the response object. # Do not rely on this attribute, always check if its not None. response = None + def calc_md5(string): - """Calculates the MD5 encryption for the given string - """ + """Calculates the MD5 encryption for the given string""" md = hashlib.md5() md.update(string) return base64.encodebytes(md.digest()).decode().strip() - def remove_empty(d): """ - Helper function that removes all keys from a dictionary (d), + Helper function that removes all keys from a dictionary (d), that have an empty value. """ for key in list(d): @@ -81,10 +81,12 @@ def remove_empty(d): del d[key] return d + def remove_namespace(xml): - xml = xml.decode('utf-8') + xml = xml.decode("utf-8") regex = re.compile(' xmlns(:ns2)?="[^"]+"|(ns2:)|(xml:)') - return regex.sub('', xml) + return regex.sub("", xml) + class DictWrapper(object): def __init__(self, xml, rootkey=None): @@ -100,23 +102,26 @@ class DictWrapper(object): else: return self._response_dict + class DataWrapper(object): """ - Text wrapper in charge of validating the hash sent by Amazon. + Text wrapper in charge of validating the hash sent by Amazon. """ + def __init__(self, data, header): self.original = data - if 'content-md5' in header: + if "content-md5" in header: hash_ = calc_md5(self.original) - if header['content-md5'] != hash_: + if header["content-md5"] != hash_: raise MWSError("Wrong Contentlength, maybe amazon error...") @property def parsed(self): return self.original + class MWS(object): - """ Base Amazon API class """ + """Base Amazon API class""" # This is used to post/get to the different uris used by amazon per api # ie. /Orders/2011-01-01 @@ -130,7 +135,7 @@ class MWS(object): # is recommended to define its namespace, so that it can be referenced # like so AmazonAPISubclass.NS. # For more information see http://stackoverflow.com/a/8719461/389453 - NS = '' + NS = "" # Some APIs are available only to either a "Merchant" or "Seller" # the type of account needs to be sent in every call to the amazon MWS. @@ -142,7 +147,9 @@ class MWS(object): # Which is the name of the parameter for that specific account type. ACCOUNT_TYPE = "SellerId" - def __init__(self, access_key, secret_key, account_id, region='US', domain='', uri="", version=""): + def __init__( + self, access_key, secret_key, account_id, region="US", domain="", uri="", version="" + ): self.access_key = access_key self.secret_key = secret_key self.account_id = account_id @@ -154,41 +161,45 @@ class MWS(object): elif region in MARKETPLACES: self.domain = MARKETPLACES[region] else: - error_msg = "Incorrect region supplied ('%(region)s'). Must be one of the following: %(marketplaces)s" % { - "marketplaces" : ', '.join(MARKETPLACES.keys()), - "region" : region, - } + error_msg = ( + "Incorrect region supplied ('%(region)s'). Must be one of the following: %(marketplaces)s" + % { + "marketplaces": ", ".join(MARKETPLACES.keys()), + "region": region, + } + ) raise MWSError(error_msg) def make_request(self, extra_data, method="GET", **kwargs): - """Make request to Amazon MWS API with these parameters - """ + """Make request to Amazon MWS API with these parameters""" # Remove all keys with an empty value because # Amazon's MWS does not allow such a thing. extra_data = remove_empty(extra_data) params = { - 'AWSAccessKeyId': self.access_key, + "AWSAccessKeyId": self.access_key, self.ACCOUNT_TYPE: self.account_id, - 'SignatureVersion': '2', - 'Timestamp': self.get_timestamp(), - 'Version': self.version, - 'SignatureMethod': 'HmacSHA256', + "SignatureVersion": "2", + "Timestamp": self.get_timestamp(), + "Version": self.version, + "SignatureMethod": "HmacSHA256", } params.update(extra_data) - request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)]) + request_description = "&".join( + ["%s=%s" % (k, quote(params[k], safe="-_.~")) for k in sorted(params)] + ) signature = self.calc_signature(method, request_description) - url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature)) - headers = {'User-Agent': 'python-amazon-mws/0.0.1 (Language=Python)'} - headers.update(kwargs.get('extra_headers', {})) + url = "%s%s?%s&Signature=%s" % (self.domain, self.uri, request_description, quote(signature)) + headers = {"User-Agent": "python-amazon-mws/0.0.1 (Language=Python)"} + headers.update(kwargs.get("extra_headers", {})) try: # Some might wonder as to why i don't pass the params dict as the params argument to request. # My answer is, here i have to get the url parsed string of params in order to sign it, so # if i pass the params dict as params to request, request will repeat that step because it will need # to convert the dict to a url parsed string, so why do it twice if i can just pass the full url :). - response = request(method, url, data=kwargs.get('body', ''), headers=headers) + response = request(method, url, data=kwargs.get("body", ""), headers=headers) response.raise_for_status() # When retrieving data from the response object, # be aware that response.content returns the content in bytes while response.text calls @@ -213,380 +224,417 @@ class MWS(object): def get_service_status(self): """ - Returns a GREEN, GREEN_I, YELLOW or RED status. - Depending on the status/availability of the API its being called from. + Returns a GREEN, GREEN_I, YELLOW or RED status. + Depending on the status/availability of the API its being called from. """ - return self.make_request(extra_data=dict(Action='GetServiceStatus')) + return self.make_request(extra_data=dict(Action="GetServiceStatus")) def calc_signature(self, method, request_description): - """Calculate MWS signature to interface with Amazon - """ - sig_data = method + '\n' + self.domain.replace('https://', '').lower() + '\n' + self.uri + '\n' + request_description - sig_data = sig_data.encode('utf-8') - secret_key = self.secret_key.encode('utf-8') + """Calculate MWS signature to interface with Amazon""" + sig_data = ( + method + + "\n" + + self.domain.replace("https://", "").lower() + + "\n" + + self.uri + + "\n" + + request_description + ) + sig_data = sig_data.encode("utf-8") + secret_key = self.secret_key.encode("utf-8") digest = hmac.new(secret_key, sig_data, hashlib.sha256).digest() - return base64.b64encode(digest).decode('utf-8') + return base64.b64encode(digest).decode("utf-8") def get_timestamp(self): """ - Returns the current timestamp in proper format. + Returns the current timestamp in proper format. """ return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) def enumerate_param(self, param, values): """ - Builds a dictionary of an enumerated parameter. - Takes any iterable and returns a dictionary. - ie. - enumerate_param('MarketplaceIdList.Id', (123, 345, 4343)) - returns - { - MarketplaceIdList.Id.1: 123, - MarketplaceIdList.Id.2: 345, - MarketplaceIdList.Id.3: 4343 - } + Builds a dictionary of an enumerated parameter. + Takes any iterable and returns a dictionary. + ie. + enumerate_param('MarketplaceIdList.Id', (123, 345, 4343)) + returns + { + MarketplaceIdList.Id.1: 123, + MarketplaceIdList.Id.2: 345, + MarketplaceIdList.Id.3: 4343 + } """ params = {} if values is not None: - if not param.endswith('.'): + if not param.endswith("."): param = "%s." % param for num, value in enumerate(values): - params['%s%d' % (param, (num + 1))] = value + params["%s%d" % (param, (num + 1))] = value return params class Feeds(MWS): - """ Amazon MWS Feeds API """ + """Amazon MWS Feeds API""" ACCOUNT_TYPE = "Merchant" - def submit_feed(self, feed, feed_type, marketplaceids=None, - content_type="text/xml", purge='false'): + def submit_feed( + self, feed, feed_type, marketplaceids=None, content_type="text/xml", purge="false" + ): """ Uploads a feed ( xml or .tsv ) to the seller's inventory. Can be used for creating/updating products on Amazon. """ - data = dict(Action='SubmitFeed', - FeedType=feed_type, - PurgeAndReplace=purge) - data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) + data = dict(Action="SubmitFeed", FeedType=feed_type, PurgeAndReplace=purge) + data.update(self.enumerate_param("MarketplaceIdList.Id.", marketplaceids)) md = calc_md5(feed) - return self.make_request(data, method="POST", body=feed, - extra_headers={'Content-MD5': md, 'Content-Type': content_type}) + return self.make_request( + data, method="POST", body=feed, extra_headers={"Content-MD5": md, "Content-Type": content_type} + ) - def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None, - processingstatuses=None, fromdate=None, todate=None): + def get_feed_submission_list( + self, + feedids=None, + max_count=None, + feedtypes=None, + processingstatuses=None, + fromdate=None, + todate=None, + ): """ Returns a list of all feed submissions submitted in the previous 90 days. That match the query parameters. """ - data = dict(Action='GetFeedSubmissionList', - MaxCount=max_count, - SubmittedFromDate=fromdate, - SubmittedToDate=todate,) - data.update(self.enumerate_param('FeedSubmissionIdList.Id', feedids)) - data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes)) - data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses)) + data = dict( + Action="GetFeedSubmissionList", + MaxCount=max_count, + SubmittedFromDate=fromdate, + SubmittedToDate=todate, + ) + data.update(self.enumerate_param("FeedSubmissionIdList.Id", feedids)) + data.update(self.enumerate_param("FeedTypeList.Type.", feedtypes)) + data.update(self.enumerate_param("FeedProcessingStatusList.Status.", processingstatuses)) return self.make_request(data) def get_submission_list_by_next_token(self, token): - data = dict(Action='GetFeedSubmissionListByNextToken', NextToken=token) + data = dict(Action="GetFeedSubmissionListByNextToken", NextToken=token) return self.make_request(data) - def get_feed_submission_count(self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None): - data = dict(Action='GetFeedSubmissionCount', - SubmittedFromDate=fromdate, - SubmittedToDate=todate) - data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes)) - data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses)) + def get_feed_submission_count( + self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None + ): + data = dict(Action="GetFeedSubmissionCount", SubmittedFromDate=fromdate, SubmittedToDate=todate) + data.update(self.enumerate_param("FeedTypeList.Type.", feedtypes)) + data.update(self.enumerate_param("FeedProcessingStatusList.Status.", processingstatuses)) return self.make_request(data) def cancel_feed_submissions(self, feedids=None, feedtypes=None, fromdate=None, todate=None): - data = dict(Action='CancelFeedSubmissions', - SubmittedFromDate=fromdate, - SubmittedToDate=todate) - data.update(self.enumerate_param('FeedSubmissionIdList.Id.', feedids)) - data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes)) + data = dict(Action="CancelFeedSubmissions", SubmittedFromDate=fromdate, SubmittedToDate=todate) + data.update(self.enumerate_param("FeedSubmissionIdList.Id.", feedids)) + data.update(self.enumerate_param("FeedTypeList.Type.", feedtypes)) return self.make_request(data) def get_feed_submission_result(self, feedid): - data = dict(Action='GetFeedSubmissionResult', FeedSubmissionId=feedid) + data = dict(Action="GetFeedSubmissionResult", FeedSubmissionId=feedid) return self.make_request(data) + class Reports(MWS): - """ Amazon MWS Reports API """ + """Amazon MWS Reports API""" ACCOUNT_TYPE = "Merchant" ## REPORTS ### def get_report(self, report_id): - data = dict(Action='GetReport', ReportId=report_id) + data = dict(Action="GetReport", ReportId=report_id) return self.make_request(data) def get_report_count(self, report_types=(), acknowledged=None, fromdate=None, todate=None): - data = dict(Action='GetReportCount', - Acknowledged=acknowledged, - AvailableFromDate=fromdate, - AvailableToDate=todate) - data.update(self.enumerate_param('ReportTypeList.Type.', report_types)) + data = dict( + Action="GetReportCount", + Acknowledged=acknowledged, + AvailableFromDate=fromdate, + AvailableToDate=todate, + ) + data.update(self.enumerate_param("ReportTypeList.Type.", report_types)) return self.make_request(data) - def get_report_list(self, requestids=(), max_count=None, types=(), acknowledged=None, - fromdate=None, todate=None): - data = dict(Action='GetReportList', - Acknowledged=acknowledged, - AvailableFromDate=fromdate, - AvailableToDate=todate, - MaxCount=max_count) - data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids)) - data.update(self.enumerate_param('ReportTypeList.Type.', types)) + def get_report_list( + self, requestids=(), max_count=None, types=(), acknowledged=None, fromdate=None, todate=None + ): + data = dict( + Action="GetReportList", + Acknowledged=acknowledged, + AvailableFromDate=fromdate, + AvailableToDate=todate, + MaxCount=max_count, + ) + data.update(self.enumerate_param("ReportRequestIdList.Id.", requestids)) + data.update(self.enumerate_param("ReportTypeList.Type.", types)) return self.make_request(data) def get_report_list_by_next_token(self, token): - data = dict(Action='GetReportListByNextToken', NextToken=token) + data = dict(Action="GetReportListByNextToken", NextToken=token) return self.make_request(data) - def get_report_request_count(self, report_types=(), processingstatuses=(), fromdate=None, todate=None): - data = dict(Action='GetReportRequestCount', - RequestedFromDate=fromdate, - RequestedToDate=todate) - data.update(self.enumerate_param('ReportTypeList.Type.', report_types)) - data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses)) + def get_report_request_count( + self, report_types=(), processingstatuses=(), fromdate=None, todate=None + ): + data = dict(Action="GetReportRequestCount", RequestedFromDate=fromdate, RequestedToDate=todate) + data.update(self.enumerate_param("ReportTypeList.Type.", report_types)) + data.update(self.enumerate_param("ReportProcessingStatusList.Status.", processingstatuses)) return self.make_request(data) - def get_report_request_list(self, requestids=(), types=(), processingstatuses=(), - max_count=None, fromdate=None, todate=None): - data = dict(Action='GetReportRequestList', - MaxCount=max_count, - RequestedFromDate=fromdate, - RequestedToDate=todate) - data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids)) - data.update(self.enumerate_param('ReportTypeList.Type.', types)) - data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses)) + def get_report_request_list( + self, requestids=(), types=(), processingstatuses=(), max_count=None, fromdate=None, todate=None + ): + data = dict( + Action="GetReportRequestList", + MaxCount=max_count, + RequestedFromDate=fromdate, + RequestedToDate=todate, + ) + data.update(self.enumerate_param("ReportRequestIdList.Id.", requestids)) + data.update(self.enumerate_param("ReportTypeList.Type.", types)) + data.update(self.enumerate_param("ReportProcessingStatusList.Status.", processingstatuses)) return self.make_request(data) def get_report_request_list_by_next_token(self, token): - data = dict(Action='GetReportRequestListByNextToken', NextToken=token) + data = dict(Action="GetReportRequestListByNextToken", NextToken=token) return self.make_request(data) def request_report(self, report_type, start_date=None, end_date=None, marketplaceids=()): - data = dict(Action='RequestReport', - ReportType=report_type, - StartDate=start_date, - EndDate=end_date) - data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) + data = dict( + Action="RequestReport", ReportType=report_type, StartDate=start_date, EndDate=end_date + ) + data.update(self.enumerate_param("MarketplaceIdList.Id.", marketplaceids)) return self.make_request(data) ### ReportSchedule ### def get_report_schedule_list(self, types=()): - data = dict(Action='GetReportScheduleList') - data.update(self.enumerate_param('ReportTypeList.Type.', types)) + data = dict(Action="GetReportScheduleList") + data.update(self.enumerate_param("ReportTypeList.Type.", types)) return self.make_request(data) def get_report_schedule_count(self, types=()): - data = dict(Action='GetReportScheduleCount') - data.update(self.enumerate_param('ReportTypeList.Type.', types)) + data = dict(Action="GetReportScheduleCount") + data.update(self.enumerate_param("ReportTypeList.Type.", types)) return self.make_request(data) class Orders(MWS): - """ Amazon Orders API """ + """Amazon Orders API""" URI = "/Orders/2013-09-01" VERSION = "2013-09-01" - NS = '{https://mws.amazonservices.com/Orders/2011-01-01}' + NS = "{https://mws.amazonservices.com/Orders/2011-01-01}" - def list_orders(self, marketplaceids, created_after=None, created_before=None, lastupdatedafter=None, - lastupdatedbefore=None, orderstatus=(), fulfillment_channels=(), - payment_methods=(), buyer_email=None, seller_orderid=None, max_results='100'): + def list_orders( + self, + marketplaceids, + created_after=None, + created_before=None, + lastupdatedafter=None, + lastupdatedbefore=None, + orderstatus=(), + fulfillment_channels=(), + payment_methods=(), + buyer_email=None, + seller_orderid=None, + max_results="100", + ): - data = dict(Action='ListOrders', - CreatedAfter=created_after, - CreatedBefore=created_before, - LastUpdatedAfter=lastupdatedafter, - LastUpdatedBefore=lastupdatedbefore, - BuyerEmail=buyer_email, - SellerOrderId=seller_orderid, - MaxResultsPerPage=max_results, - ) - data.update(self.enumerate_param('OrderStatus.Status.', orderstatus)) - data.update(self.enumerate_param('MarketplaceId.Id.', marketplaceids)) - data.update(self.enumerate_param('FulfillmentChannel.Channel.', fulfillment_channels)) - data.update(self.enumerate_param('PaymentMethod.Method.', payment_methods)) + data = dict( + Action="ListOrders", + CreatedAfter=created_after, + CreatedBefore=created_before, + LastUpdatedAfter=lastupdatedafter, + LastUpdatedBefore=lastupdatedbefore, + BuyerEmail=buyer_email, + SellerOrderId=seller_orderid, + MaxResultsPerPage=max_results, + ) + data.update(self.enumerate_param("OrderStatus.Status.", orderstatus)) + data.update(self.enumerate_param("MarketplaceId.Id.", marketplaceids)) + data.update(self.enumerate_param("FulfillmentChannel.Channel.", fulfillment_channels)) + data.update(self.enumerate_param("PaymentMethod.Method.", payment_methods)) return self.make_request(data) def list_orders_by_next_token(self, token): - data = dict(Action='ListOrdersByNextToken', NextToken=token) + data = dict(Action="ListOrdersByNextToken", NextToken=token) return self.make_request(data) def get_order(self, amazon_order_ids): - data = dict(Action='GetOrder') - data.update(self.enumerate_param('AmazonOrderId.Id.', amazon_order_ids)) + data = dict(Action="GetOrder") + data.update(self.enumerate_param("AmazonOrderId.Id.", amazon_order_ids)) return self.make_request(data) def list_order_items(self, amazon_order_id): - data = dict(Action='ListOrderItems', AmazonOrderId=amazon_order_id) + data = dict(Action="ListOrderItems", AmazonOrderId=amazon_order_id) return self.make_request(data) def list_order_items_by_next_token(self, token): - data = dict(Action='ListOrderItemsByNextToken', NextToken=token) + data = dict(Action="ListOrderItemsByNextToken", NextToken=token) return self.make_request(data) class Products(MWS): - """ Amazon MWS Products API """ + """Amazon MWS Products API""" - URI = '/Products/2011-10-01' - VERSION = '2011-10-01' - NS = '{http://mws.amazonservices.com/schema/Products/2011-10-01}' + URI = "/Products/2011-10-01" + VERSION = "2011-10-01" + NS = "{http://mws.amazonservices.com/schema/Products/2011-10-01}" def list_matching_products(self, marketplaceid, query, contextid=None): - """ Returns a list of products and their attributes, ordered by - relevancy, based on a search query that you specify. - Your search query can be a phrase that describes the product - or it can be a product identifier such as a UPC, EAN, ISBN, or JAN. + """Returns a list of products and their attributes, ordered by + relevancy, based on a search query that you specify. + Your search query can be a phrase that describes the product + or it can be a product identifier such as a UPC, EAN, ISBN, or JAN. """ - data = dict(Action='ListMatchingProducts', - MarketplaceId=marketplaceid, - Query=query, - QueryContextId=contextid) + data = dict( + Action="ListMatchingProducts", + MarketplaceId=marketplaceid, + Query=query, + QueryContextId=contextid, + ) return self.make_request(data) def get_matching_product(self, marketplaceid, asins): - """ Returns a list of products and their attributes, based on a list of - ASIN values that you specify. + """Returns a list of products and their attributes, based on a list of + ASIN values that you specify. """ - data = dict(Action='GetMatchingProduct', MarketplaceId=marketplaceid) - data.update(self.enumerate_param('ASINList.ASIN.', asins)) + data = dict(Action="GetMatchingProduct", MarketplaceId=marketplaceid) + data.update(self.enumerate_param("ASINList.ASIN.", asins)) return self.make_request(data) def get_matching_product_for_id(self, marketplaceid, type, id): - """ Returns a list of products and their attributes, based on a list of - product identifier values (asin, sellersku, upc, ean, isbn and JAN) - Added in Fourth Release, API version 2011-10-01 + """Returns a list of products and their attributes, based on a list of + product identifier values (asin, sellersku, upc, ean, isbn and JAN) + Added in Fourth Release, API version 2011-10-01 """ - data = dict(Action='GetMatchingProductForId', - MarketplaceId=marketplaceid, - IdType=type) - data.update(self.enumerate_param('IdList.Id', id)) + data = dict(Action="GetMatchingProductForId", MarketplaceId=marketplaceid, IdType=type) + data.update(self.enumerate_param("IdList.Id", id)) return self.make_request(data) def get_competitive_pricing_for_sku(self, marketplaceid, skus): - """ Returns the current competitive pricing of a product, - based on the SellerSKU and MarketplaceId that you specify. + """Returns the current competitive pricing of a product, + based on the SellerSKU and MarketplaceId that you specify. """ - data = dict(Action='GetCompetitivePricingForSKU', MarketplaceId=marketplaceid) - data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus)) + data = dict(Action="GetCompetitivePricingForSKU", MarketplaceId=marketplaceid) + data.update(self.enumerate_param("SellerSKUList.SellerSKU.", skus)) return self.make_request(data) def get_competitive_pricing_for_asin(self, marketplaceid, asins): - """ Returns the current competitive pricing of a product, - based on the ASIN and MarketplaceId that you specify. + """Returns the current competitive pricing of a product, + based on the ASIN and MarketplaceId that you specify. """ - data = dict(Action='GetCompetitivePricingForASIN', MarketplaceId=marketplaceid) - data.update(self.enumerate_param('ASINList.ASIN.', asins)) + data = dict(Action="GetCompetitivePricingForASIN", MarketplaceId=marketplaceid) + data.update(self.enumerate_param("ASINList.ASIN.", asins)) return self.make_request(data) - def get_lowest_offer_listings_for_sku(self, marketplaceid, skus, condition="Any", excludeme="False"): - data = dict(Action='GetLowestOfferListingsForSKU', - MarketplaceId=marketplaceid, - ItemCondition=condition, - ExcludeMe=excludeme) - data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus)) + def get_lowest_offer_listings_for_sku( + self, marketplaceid, skus, condition="Any", excludeme="False" + ): + data = dict( + Action="GetLowestOfferListingsForSKU", + MarketplaceId=marketplaceid, + ItemCondition=condition, + ExcludeMe=excludeme, + ) + data.update(self.enumerate_param("SellerSKUList.SellerSKU.", skus)) return self.make_request(data) - def get_lowest_offer_listings_for_asin(self, marketplaceid, asins, condition="Any", excludeme="False"): - data = dict(Action='GetLowestOfferListingsForASIN', - MarketplaceId=marketplaceid, - ItemCondition=condition, - ExcludeMe=excludeme) - data.update(self.enumerate_param('ASINList.ASIN.', asins)) + def get_lowest_offer_listings_for_asin( + self, marketplaceid, asins, condition="Any", excludeme="False" + ): + data = dict( + Action="GetLowestOfferListingsForASIN", + MarketplaceId=marketplaceid, + ItemCondition=condition, + ExcludeMe=excludeme, + ) + data.update(self.enumerate_param("ASINList.ASIN.", asins)) return self.make_request(data) def get_product_categories_for_sku(self, marketplaceid, sku): - data = dict(Action='GetProductCategoriesForSKU', - MarketplaceId=marketplaceid, - SellerSKU=sku) + data = dict(Action="GetProductCategoriesForSKU", MarketplaceId=marketplaceid, SellerSKU=sku) return self.make_request(data) def get_product_categories_for_asin(self, marketplaceid, asin): - data = dict(Action='GetProductCategoriesForASIN', - MarketplaceId=marketplaceid, - ASIN=asin) + data = dict(Action="GetProductCategoriesForASIN", MarketplaceId=marketplaceid, ASIN=asin) return self.make_request(data) def get_my_price_for_sku(self, marketplaceid, skus, condition=None): - data = dict(Action='GetMyPriceForSKU', - MarketplaceId=marketplaceid, - ItemCondition=condition) - data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus)) + data = dict(Action="GetMyPriceForSKU", MarketplaceId=marketplaceid, ItemCondition=condition) + data.update(self.enumerate_param("SellerSKUList.SellerSKU.", skus)) return self.make_request(data) def get_my_price_for_asin(self, marketplaceid, asins, condition=None): - data = dict(Action='GetMyPriceForASIN', - MarketplaceId=marketplaceid, - ItemCondition=condition) - data.update(self.enumerate_param('ASINList.ASIN.', asins)) + data = dict(Action="GetMyPriceForASIN", MarketplaceId=marketplaceid, ItemCondition=condition) + data.update(self.enumerate_param("ASINList.ASIN.", asins)) return self.make_request(data) class Sellers(MWS): - """ Amazon MWS Sellers API """ + """Amazon MWS Sellers API""" - URI = '/Sellers/2011-07-01' - VERSION = '2011-07-01' - NS = '{http://mws.amazonservices.com/schema/Sellers/2011-07-01}' + URI = "/Sellers/2011-07-01" + VERSION = "2011-07-01" + NS = "{http://mws.amazonservices.com/schema/Sellers/2011-07-01}" def list_marketplace_participations(self): """ - Returns a list of marketplaces a seller can participate in and - a list of participations that include seller-specific information in that marketplace. - The operation returns only those marketplaces where the seller's account is in an active state. + Returns a list of marketplaces a seller can participate in and + a list of participations that include seller-specific information in that marketplace. + The operation returns only those marketplaces where the seller's account is in an active state. """ - data = dict(Action='ListMarketplaceParticipations') + data = dict(Action="ListMarketplaceParticipations") return self.make_request(data) def list_marketplace_participations_by_next_token(self, token): """ - Takes a "NextToken" and returns the same information as "list_marketplace_participations". - Based on the "NextToken". + Takes a "NextToken" and returns the same information as "list_marketplace_participations". + Based on the "NextToken". """ - data = dict(Action='ListMarketplaceParticipations', NextToken=token) + data = dict(Action="ListMarketplaceParticipations", NextToken=token) return self.make_request(data) + #### Fulfillment APIs #### + class InboundShipments(MWS): URI = "/FulfillmentInboundShipment/2010-10-01" - VERSION = '2010-10-01' + VERSION = "2010-10-01" # To be completed class Inventory(MWS): - """ Amazon MWS Inventory Fulfillment API """ + """Amazon MWS Inventory Fulfillment API""" - URI = '/FulfillmentInventory/2010-10-01' - VERSION = '2010-10-01' + URI = "/FulfillmentInventory/2010-10-01" + VERSION = "2010-10-01" NS = "{http://mws.amazonaws.com/FulfillmentInventory/2010-10-01}" - def list_inventory_supply(self, skus=(), datetime=None, response_group='Basic'): - """ Returns information on available inventory """ + def list_inventory_supply(self, skus=(), datetime=None, response_group="Basic"): + """Returns information on available inventory""" - data = dict(Action='ListInventorySupply', - QueryStartDateTime=datetime, - ResponseGroup=response_group, - ) - data.update(self.enumerate_param('SellerSkus.member.', skus)) + data = dict( + Action="ListInventorySupply", + QueryStartDateTime=datetime, + ResponseGroup=response_group, + ) + data.update(self.enumerate_param("SellerSkus.member.", skus)) return self.make_request(data, "POST") def list_inventory_supply_by_next_token(self, token): - data = dict(Action='ListInventorySupplyByNextToken', NextToken=token) + data = dict(Action="ListInventorySupplyByNextToken", NextToken=token) return self.make_request(data, "POST") @@ -598,10 +646,10 @@ class OutboundShipments(MWS): class Recommendations(MWS): - """ Amazon MWS Recommendations API """ + """Amazon MWS Recommendations API""" - URI = '/Recommendations/2013-04-01' - VERSION = '2013-04-01' + URI = "/Recommendations/2013-04-01" + VERSION = "2013-04-01" NS = "{https://mws.amazonservices.com/Recommendations/2013-04-01}" def get_last_updated_time_for_recommendations(self, marketplaceid): @@ -610,8 +658,7 @@ class Recommendations(MWS): returns the time when recommendations were last updated for each category. """ - data = dict(Action='GetLastUpdatedTimeForRecommendations', - MarketplaceId=marketplaceid) + data = dict(Action="GetLastUpdatedTimeForRecommendations", MarketplaceId=marketplaceid) return self.make_request(data, "POST") def list_recommendations(self, marketplaceid, recommendationcategory=None): @@ -619,9 +666,11 @@ class Recommendations(MWS): Returns your active recommendations for a specific category or for all categories for a specific marketplace. """ - data = dict(Action="ListRecommendations", - MarketplaceId=marketplaceid, - RecommendationCategory=recommendationcategory) + data = dict( + Action="ListRecommendations", + MarketplaceId=marketplaceid, + RecommendationCategory=recommendationcategory, + ) return self.make_request(data, "POST") def list_recommendations_by_next_token(self, token): @@ -629,23 +678,26 @@ class Recommendations(MWS): Returns the next page of recommendations using the NextToken parameter. """ - data = dict(Action="ListRecommendationsByNextToken", - NextToken=token) + data = dict(Action="ListRecommendationsByNextToken", NextToken=token) return self.make_request(data, "POST") + class Finances(MWS): - """ Amazon Finances API""" - URI = '/Finances/2015-05-01' - VERSION = '2015-05-01' + """Amazon Finances API""" + + URI = "/Finances/2015-05-01" + VERSION = "2015-05-01" NS = "{https://mws.amazonservices.com/Finances/2015-05-01}" - def list_financial_events(self , posted_after=None, posted_before=None, - amazon_order_id=None, max_results='100'): + def list_financial_events( + self, posted_after=None, posted_before=None, amazon_order_id=None, max_results="100" + ): - data = dict(Action='ListFinancialEvents', - PostedAfter=posted_after, - PostedBefore=posted_before, - AmazonOrderId=amazon_order_id, - MaxResultsPerPage=max_results, - ) + data = dict( + Action="ListFinancialEvents", + PostedAfter=posted_after, + PostedBefore=posted_before, + AmazonOrderId=amazon_order_id, + MaxResultsPerPage=max_results, + ) return self.make_request(data) diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py index c1f460f49b6..c6af23d915f 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py @@ -21,26 +21,49 @@ class AmazonMWSSettings(Document): @frappe.whitelist() def get_products_details(self): if self.enable_amazon == 1: - frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details') + frappe.enqueue( + "erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details" + ) @frappe.whitelist() def get_order_details(self): if self.enable_amazon == 1: after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d") - frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_orders', after_date=after_date) + frappe.enqueue( + "erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_orders", + after_date=after_date, + ) + def schedule_get_order_details(): mws_settings = frappe.get_doc("Amazon MWS Settings") if mws_settings.enable_sync and mws_settings.enable_amazon: after_date = dateutil.parser.parse(mws_settings.after_date).strftime("%Y-%m-%d") - get_orders(after_date = after_date) + get_orders(after_date=after_date) + def setup_custom_fields(): custom_fields = { - "Item": [dict(fieldname='amazon_item_code', label='Amazon Item Code', - fieldtype='Data', insert_after='series', read_only=1, print_hide=1)], - "Sales Order": [dict(fieldname='amazon_order_id', label='Amazon Order ID', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1)] + "Item": [ + dict( + fieldname="amazon_item_code", + label="Amazon Item Code", + fieldtype="Data", + insert_after="series", + read_only=1, + print_hide=1, + ) + ], + "Sales Order": [ + dict( + fieldname="amazon_order_id", + label="Amazon Order ID", + fieldtype="Data", + insert_after="title", + read_only=1, + print_hide=1, + ) + ], } create_custom_fields(custom_fields) diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py index d9dfc6f72d4..f44be61f481 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py @@ -19,11 +19,12 @@ class object_dict(dict): >>> a['water'] = 'water' >>> a.water 'water' - >>> a.test = {'value': 1} + >>> a.test = {'value': 1} >>> a.test2 = object_dict({'name': 'test2', 'value': 2}) >>> a.test, a.test2.name, a.test2.value (1, 'test2', 2) """ + def __init__(self, initd=None): if initd is None: initd = {} @@ -36,8 +37,8 @@ class object_dict(dict): except KeyError: return None - if isinstance(d, dict) and 'value' in d and len(d) == 1: - return d['value'] + if isinstance(d, dict) and "value" in d and len(d) == 1: + return d["value"] else: return d @@ -49,11 +50,10 @@ class object_dict(dict): self.__setitem__(item, value) def getvalue(self, item, value=None): - return self.get(item, {}).get('value', value) + return self.get(item, {}).get("value", value) class xml2dict(object): - def __init__(self): pass @@ -63,12 +63,11 @@ class xml2dict(object): if node.text: node_tree.value = node.text for (k, v) in node.attrib.items(): - k, v = self._namespace_split(k, object_dict({'value':v})) + k, v = self._namespace_split(k, object_dict({"value": v})) node_tree[k] = v - #Save childrens + # Save childrens for child in node.getchildren(): - tag, tree = self._namespace_split(child.tag, - self._parse_node(child)) + tag, tree = self._namespace_split(child.tag, self._parse_node(child)) if tag not in node_tree: # the first time, so store it in dict node_tree[tag] = tree continue @@ -94,7 +93,7 @@ class xml2dict(object): def parse(self, file): """parse a xml file to a dict""" - f = open(file, 'r') + f = open(file, "r") return self.fromstring(f.read()) def fromstring(self, s): diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py index e84093cae09..4879cb56239 100644 --- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py +++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py @@ -14,7 +14,9 @@ class ExotelSettings(Document): def verify_credentials(self): if self.enabled: - response = requests.get('https://api.exotel.com/v1/Accounts/{sid}' - .format(sid = self.account_sid), auth=(self.api_key, self.api_token)) + response = requests.get( + "https://api.exotel.com/v1/Accounts/{sid}".format(sid=self.account_sid), + auth=(self.api_key, self.api_token), + ) if response.status_code != 200: frappe.throw(_("Invalid credentials")) diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py index bb62c395a5b..65be5993ffc 100644 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py +++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/__init__.py @@ -23,12 +23,15 @@ def webhooks(): set_status(event) return 200 + + def set_status(event): resource_type = event.get("resource_type", {}) if resource_type == "mandates": set_mandate_status(event) + def set_mandate_status(event): mandates = [] if isinstance(event["links"], (list,)): @@ -37,7 +40,12 @@ def set_mandate_status(event): else: mandates.append(event["links"]["mandate"]) - if event["action"] == "pending_customer_approval" or event["action"] == "pending_submission" or event["action"] == "submitted" or event["action"] == "active": + if ( + event["action"] == "pending_customer_approval" + or event["action"] == "pending_submission" + or event["action"] == "submitted" + or event["action"] == "active" + ): disabled = 0 else: disabled = 1 @@ -45,6 +53,7 @@ def set_mandate_status(event): for mandate in mandates: frappe.db.set_value("GoCardless Mandate", mandate, "disabled", disabled) + def authenticate_signature(r): """Returns True if the received signature matches the generated signature""" received_signature = frappe.get_request_header("Webhook-Signature") @@ -59,13 +68,22 @@ def authenticate_signature(r): return False + def get_webhook_keys(): def _get_webhook_keys(): - webhook_keys = [d.webhooks_secret for d in frappe.get_all("GoCardless Settings", fields=["webhooks_secret"],) if d.webhooks_secret] + webhook_keys = [ + d.webhooks_secret + for d in frappe.get_all( + "GoCardless Settings", + fields=["webhooks_secret"], + ) + if d.webhooks_secret + ] return webhook_keys return frappe.cache().get_value("gocardless_webhooks_secret", _get_webhook_keys) + def clear_cache(): frappe.cache().delete_value("gocardless_webhooks_secret") diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py index 1499d258fe6..797534a9bc7 100644 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py +++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py @@ -21,32 +21,35 @@ class GoCardlessSettings(Document): self.environment = self.get_environment() try: self.client = gocardless_pro.Client( - access_token=self.access_token, - environment=self.environment - ) + access_token=self.access_token, environment=self.environment + ) return self.client except Exception as e: frappe.throw(e) def on_update(self): - create_payment_gateway('GoCardless-' + self.gateway_name, settings='GoCardLess Settings', controller=self.gateway_name) - call_hook_method('payment_gateway_enabled', gateway='GoCardless-' + self.gateway_name) + create_payment_gateway( + "GoCardless-" + self.gateway_name, settings="GoCardLess Settings", controller=self.gateway_name + ) + call_hook_method("payment_gateway_enabled", gateway="GoCardless-" + self.gateway_name) def on_payment_request_submission(self, data): if data.reference_doctype != "Fees": - customer_data = frappe.db.get_value(data.reference_doctype, data.reference_name, ["company", "customer_name"], as_dict=1) + customer_data = frappe.db.get_value( + data.reference_doctype, data.reference_name, ["company", "customer_name"], as_dict=1 + ) data = { - "amount": flt(data.grand_total, data.precision("grand_total")), - "title": customer_data.company.encode("utf-8"), - "description": data.subject.encode("utf-8"), - "reference_doctype": data.doctype, - "reference_docname": data.name, - "payer_email": data.email_to or frappe.session.user, - "payer_name": customer_data.customer_name, - "order_id": data.name, - "currency": data.currency - } + "amount": flt(data.grand_total, data.precision("grand_total")), + "title": customer_data.company.encode("utf-8"), + "description": data.subject.encode("utf-8"), + "reference_doctype": data.doctype, + "reference_docname": data.name, + "payer_email": data.email_to or frappe.session.user, + "payer_name": customer_data.customer_name, + "order_id": data.name, + "currency": data.currency, + } valid_mandate = self.check_mandate_validity(data) if valid_mandate is not None: @@ -59,12 +62,19 @@ class GoCardlessSettings(Document): def check_mandate_validity(self, data): - if frappe.db.exists("GoCardless Mandate", dict(customer=data.get('payer_name'), disabled=0)): - registered_mandate = frappe.db.get_value("GoCardless Mandate", dict(customer=data.get('payer_name'), disabled=0), 'mandate') + if frappe.db.exists("GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0)): + registered_mandate = frappe.db.get_value( + "GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0), "mandate" + ) self.initialize_client() mandate = self.client.mandates.get(registered_mandate) - if mandate.status=="pending_customer_approval" or mandate.status=="pending_submission" or mandate.status=="submitted" or mandate.status=="active": + if ( + mandate.status == "pending_customer_approval" + or mandate.status == "pending_submission" + or mandate.status == "submitted" + or mandate.status == "active" + ): return {"mandate": registered_mandate} else: return None @@ -73,13 +83,17 @@ class GoCardlessSettings(Document): def get_environment(self): if self.use_sandbox: - return 'sandbox' + return "sandbox" else: - return 'live' + return "live" def validate_transaction_currency(self, currency): if currency not in self.supported_currencies: - frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency)) + frappe.throw( + _( + "Please select another payment method. Go Cardless does not support transactions in currency '{0}'" + ).format(currency) + ) def get_payment_url(self, **kwargs): return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs))) @@ -93,63 +107,85 @@ class GoCardlessSettings(Document): except Exception: frappe.log_error(frappe.get_traceback()) - return{ - "redirect_to": frappe.redirect_to_message(_('Server Error'), _("There seems to be an issue with the server's GoCardless configuration. Don't worry, in case of failure, the amount will get refunded to your account.")), - "status": 401 + return { + "redirect_to": frappe.redirect_to_message( + _("Server Error"), + _( + "There seems to be an issue with the server's GoCardless configuration. Don't worry, in case of failure, the amount will get refunded to your account." + ), + ), + "status": 401, } def create_charge_on_gocardless(self): - redirect_to = self.data.get('redirect_to') or None - redirect_message = self.data.get('redirect_message') or None + redirect_to = self.data.get("redirect_to") or None + redirect_message = self.data.get("redirect_message") or None - reference_doc = frappe.get_doc(self.data.get('reference_doctype'), self.data.get('reference_docname')) + reference_doc = frappe.get_doc( + self.data.get("reference_doctype"), self.data.get("reference_docname") + ) self.initialize_client() try: payment = self.client.payments.create( params={ - "amount" : cint(reference_doc.grand_total * 100), - "currency" : reference_doc.currency, - "links" : { - "mandate": self.data.get('mandate') - }, + "amount": cint(reference_doc.grand_total * 100), + "currency": reference_doc.currency, + "links": {"mandate": self.data.get("mandate")}, "metadata": { - "reference_doctype": reference_doc.doctype, - "reference_document": reference_doc.name - } - }, headers={ - 'Idempotency-Key' : self.data.get('reference_docname'), - }) + "reference_doctype": reference_doc.doctype, + "reference_document": reference_doc.name, + }, + }, + headers={ + "Idempotency-Key": self.data.get("reference_docname"), + }, + ) - if payment.status=="pending_submission" or payment.status=="pending_customer_approval" or payment.status=="submitted": - self.integration_request.db_set('status', 'Authorized', update_modified=False) + if ( + payment.status == "pending_submission" + or payment.status == "pending_customer_approval" + or payment.status == "submitted" + ): + self.integration_request.db_set("status", "Authorized", update_modified=False) self.flags.status_changed_to = "Completed" - self.integration_request.db_set('output', payment.status, update_modified=False) + self.integration_request.db_set("output", payment.status, update_modified=False) - elif payment.status=="confirmed" or payment.status=="paid_out": - self.integration_request.db_set('status', 'Completed', update_modified=False) + elif payment.status == "confirmed" or payment.status == "paid_out": + self.integration_request.db_set("status", "Completed", update_modified=False) self.flags.status_changed_to = "Completed" - self.integration_request.db_set('output', payment.status, update_modified=False) + self.integration_request.db_set("output", payment.status, update_modified=False) - elif payment.status=="cancelled" or payment.status=="customer_approval_denied" or payment.status=="charged_back": - self.integration_request.db_set('status', 'Cancelled', update_modified=False) - frappe.log_error(_("Payment Cancelled. Please check your GoCardless Account for more details"), "GoCardless Payment Error") - self.integration_request.db_set('error', payment.status, update_modified=False) + elif ( + payment.status == "cancelled" + or payment.status == "customer_approval_denied" + or payment.status == "charged_back" + ): + self.integration_request.db_set("status", "Cancelled", update_modified=False) + frappe.log_error( + _("Payment Cancelled. Please check your GoCardless Account for more details"), + "GoCardless Payment Error", + ) + self.integration_request.db_set("error", payment.status, update_modified=False) else: - self.integration_request.db_set('status', 'Failed', update_modified=False) - frappe.log_error(_("Payment Failed. Please check your GoCardless Account for more details"), "GoCardless Payment Error") - self.integration_request.db_set('error', payment.status, update_modified=False) + self.integration_request.db_set("status", "Failed", update_modified=False) + frappe.log_error( + _("Payment Failed. Please check your GoCardless Account for more details"), + "GoCardless Payment Error", + ) + self.integration_request.db_set("error", payment.status, update_modified=False) except Exception as e: frappe.log_error(e, "GoCardless Payment Error") if self.flags.status_changed_to == "Completed": - status = 'Completed' - if 'reference_doctype' in self.data and 'reference_docname' in self.data: + status = "Completed" + if "reference_doctype" in self.data and "reference_docname" in self.data: custom_redirect_to = None try: - custom_redirect_to = frappe.get_doc(self.data.get('reference_doctype'), - self.data.get('reference_docname')).run_method("on_payment_authorized", self.flags.status_changed_to) + custom_redirect_to = frappe.get_doc( + self.data.get("reference_doctype"), self.data.get("reference_docname") + ).run_method("on_payment_authorized", self.flags.status_changed_to) except Exception: frappe.log_error(frappe.get_traceback()) @@ -158,24 +194,25 @@ class GoCardlessSettings(Document): redirect_url = redirect_to else: - status = 'Error' - redirect_url = 'payment-failed' + status = "Error" + redirect_url = "payment-failed" if redirect_message: - redirect_url += '&' + urlencode({'redirect_message': redirect_message}) + redirect_url += "&" + urlencode({"redirect_message": redirect_message}) redirect_url = get_url(redirect_url) - return { - "redirect_to": redirect_url, - "status": status - } + return {"redirect_to": redirect_url, "status": status} + def get_gateway_controller(doc): payment_request = frappe.get_doc("Payment Request", doc) - gateway_controller = frappe.db.get_value("Payment Gateway", payment_request.payment_gateway, "gateway_controller") + gateway_controller = frappe.db.get_value( + "Payment Gateway", payment_request.payment_gateway, "gateway_controller" + ) return gateway_controller + def gocardless_initialization(doc): gateway_controller = get_gateway_controller(doc) settings = frappe.get_doc("GoCardless Settings", gateway_controller) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py index 6d46a1c884a..a577e7fa692 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py @@ -5,9 +5,15 @@ import requests from requests.auth import HTTPBasicAuth -class MpesaConnector(): - def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke", - live_url="https://api.safaricom.co.ke"): +class MpesaConnector: + def __init__( + self, + env="sandbox", + app_key=None, + app_secret=None, + sandbox_url="https://sandbox.safaricom.co.ke", + live_url="https://api.safaricom.co.ke", + ): """Setup configuration for Mpesa connector and generate new access token.""" self.env = env self.app_key = app_key @@ -23,36 +29,41 @@ class MpesaConnector(): This method is used to fetch the access token required by Mpesa. Returns: - access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa. + access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa. """ authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials" authenticate_url = "{0}{1}".format(self.base_url, authenticate_uri) - r = requests.get( - authenticate_url, - auth=HTTPBasicAuth(self.app_key, self.app_secret) - ) - self.authentication_token = r.json()['access_token'] - return r.json()['access_token'] + r = requests.get(authenticate_url, auth=HTTPBasicAuth(self.app_key, self.app_secret)) + self.authentication_token = r.json()["access_token"] + return r.json()["access_token"] - def get_balance(self, initiator=None, security_credential=None, party_a=None, identifier_type=None, - remarks=None, queue_timeout_url=None,result_url=None): + def get_balance( + self, + initiator=None, + security_credential=None, + party_a=None, + identifier_type=None, + remarks=None, + queue_timeout_url=None, + result_url=None, + ): """ This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number). Args: - initiator (str): Username used to authenticate the transaction. - security_credential (str): Generate from developer portal. - command_id (str): AccountBalance. - party_a (int): Till number being queried. - identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code) - remarks (str): Comments that are sent along with the transaction(maximum 100 characters). - queue_timeout_url (str): The url that handles information of timed out transactions. - result_url (str): The url that receives results from M-Pesa api call. + initiator (str): Username used to authenticate the transaction. + security_credential (str): Generate from developer portal. + command_id (str): AccountBalance. + party_a (int): Till number being queried. + identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code) + remarks (str): Comments that are sent along with the transaction(maximum 100 characters). + queue_timeout_url (str): The url that handles information of timed out transactions. + result_url (str): The url that receives results from M-Pesa api call. Returns: - OriginatorConverstionID (str): The unique request ID for tracking a transaction. - ConversationID (str): The unique request ID returned by mpesa for each request made - ResponseDescription (str): Response Description message + OriginatorConverstionID (str): The unique request ID for tracking a transaction. + ConversationID (str): The unique request ID returned by mpesa for each request made + ResponseDescription (str): Response Description message """ payload = { @@ -63,43 +74,56 @@ class MpesaConnector(): "IdentifierType": identifier_type, "Remarks": remarks, "QueueTimeOutURL": queue_timeout_url, - "ResultURL": result_url + "ResultURL": result_url, + } + headers = { + "Authorization": "Bearer {0}".format(self.authentication_token), + "Content-Type": "application/json", } - headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"} saf_url = "{0}{1}".format(self.base_url, "/mpesa/accountbalance/v1/query") r = requests.post(saf_url, headers=headers, json=payload) return r.json() - def stk_push(self, business_shortcode=None, passcode=None, amount=None, callback_url=None, reference_code=None, - phone_number=None, description=None): + def stk_push( + self, + business_shortcode=None, + passcode=None, + amount=None, + callback_url=None, + reference_code=None, + phone_number=None, + description=None, + ): """ This method uses Mpesa's Express API to initiate online payment on behalf of a customer. Args: - business_shortcode (int): The short code of the organization. - passcode (str): Get from developer portal - amount (int): The amount being transacted - callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API. - reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type. - phone_number(int): The Mobile Number to receive the STK Pin Prompt. - description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters + business_shortcode (int): The short code of the organization. + passcode (str): Get from developer portal + amount (int): The amount being transacted + callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API. + reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type. + phone_number(int): The Mobile Number to receive the STK Pin Prompt. + description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters Success Response: - CustomerMessage(str): Messages that customers can understand. - CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request. - ResponseDescription(str): Describes Success or failure - MerchantRequestID(str): This is a global unique Identifier for any submitted payment request. - ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03 + CustomerMessage(str): Messages that customers can understand. + CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request. + ResponseDescription(str): Describes Success or failure + MerchantRequestID(str): This is a global unique Identifier for any submitted payment request. + ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03 Error Reponse: - requestId(str): This is a unique requestID for the payment request - errorCode(str): This is a predefined code that indicates the reason for request failure. - errorMessage(str): This is a predefined code that indicates the reason for request failure. + requestId(str): This is a unique requestID for the payment request + errorCode(str): This is a predefined code that indicates the reason for request failure. + errorMessage(str): This is a predefined code that indicates the reason for request failure. """ - time = str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "") + time = ( + str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "") + ) password = "{0}{1}{2}".format(str(business_shortcode), str(passcode), time) - encoded = base64.b64encode(bytes(password, encoding='utf8')) + encoded = base64.b64encode(bytes(password, encoding="utf8")) payload = { "BusinessShortCode": business_shortcode, "Password": encoded.decode("utf-8"), @@ -111,9 +135,14 @@ class MpesaConnector(): "CallBackURL": callback_url, "AccountReference": reference_code, "TransactionDesc": description, - "TransactionType": "CustomerPayBillOnline" if self.env == "sandbox" else "CustomerBuyGoodsOnline" + "TransactionType": "CustomerPayBillOnline" + if self.env == "sandbox" + else "CustomerBuyGoodsOnline", + } + headers = { + "Authorization": "Bearer {0}".format(self.authentication_token), + "Content-Type": "application/json", } - headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"} saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest") r = requests.post(saf_url, headers=headers, json=payload) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py index 368139b872f..c92edc5efae 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py @@ -11,21 +11,22 @@ def create_custom_pos_fields(): "label": "Request for Payment", "fieldtype": "Button", "hidden": 1, - "insert_after": "contact_email" + "insert_after": "contact_email", }, { "fieldname": "mpesa_receipt_number", "label": "Mpesa Receipt Number", "fieldtype": "Data", "read_only": 1, - "insert_after": "company" - } + "insert_after": "company", + }, ] } if not frappe.get_meta("POS Invoice").has_field("request_for_payment"): create_custom_fields(pos_field) - record_dict = [{ + record_dict = [ + { "doctype": "POS Field", "fieldname": "contact_mobile", "label": "Mobile No", @@ -33,7 +34,7 @@ def create_custom_pos_fields(): "options": "Phone", "parenttype": "POS Settings", "parent": "POS Settings", - "parentfield": "invoice_fields" + "parentfield": "invoice_fields", }, { "doctype": "POS Field", @@ -42,11 +43,12 @@ def create_custom_pos_fields(): "fieldtype": "Button", "parenttype": "POS Settings", "parent": "POS Settings", - "parentfield": "invoice_fields" - } + "parentfield": "invoice_fields", + }, ] create_pos_settings(record_dict) + def create_pos_settings(record_dict): for record in record_dict: if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}): diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py index e7b4a30e0a5..78a598ce985 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py @@ -2,7 +2,6 @@ # For license information, please see license.txt - from json import dumps, loads import frappe @@ -23,16 +22,26 @@ class MpesaSettings(Document): def validate_transaction_currency(self, currency): if currency not in self.supported_currencies: - frappe.throw(_("Please select another payment method. Mpesa does not support transactions in currency '{0}'").format(currency)) + frappe.throw( + _( + "Please select another payment method. Mpesa does not support transactions in currency '{0}'" + ).format(currency) + ) def on_update(self): create_custom_pos_fields() - create_payment_gateway('Mpesa-' + self.payment_gateway_name, settings='Mpesa Settings', controller=self.payment_gateway_name) - call_hook_method('payment_gateway_enabled', gateway='Mpesa-' + self.payment_gateway_name, payment_channel="Phone") + create_payment_gateway( + "Mpesa-" + self.payment_gateway_name, + settings="Mpesa Settings", + controller=self.payment_gateway_name, + ) + call_hook_method( + "payment_gateway_enabled", gateway="Mpesa-" + self.payment_gateway_name, payment_channel="Phone" + ) # required to fetch the bank account details from the payment gateway account frappe.db.commit() - create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone") + create_mode_of_payment("Mpesa-" + self.payment_gateway_name, payment_type="Phone") def request_for_payment(self, **kwargs): args = frappe._dict(kwargs) @@ -44,6 +53,7 @@ class MpesaSettings(Document): from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import ( get_payment_request_response_payload, ) + response = frappe._dict(get_payment_request_response_payload(amount)) else: response = frappe._dict(generate_stk_push(**args)) @@ -55,11 +65,15 @@ class MpesaSettings(Document): if request_amount > self.transaction_limit: # make multiple requests request_amounts = [] - requests_to_be_made = frappe.utils.ceil(request_amount / self.transaction_limit) # 480/150 = ceil(3.2) = 4 + requests_to_be_made = frappe.utils.ceil( + request_amount / self.transaction_limit + ) # 480/150 = ceil(3.2) = 4 for i in range(requests_to_be_made): amount = self.transaction_limit if i == requests_to_be_made - 1: - amount = request_amount - (self.transaction_limit * i) # for 4th request, 480 - (150 * 3) = 30 + amount = request_amount - ( + self.transaction_limit * i + ) # for 4th request, 480 - (150 * 3) = 30 request_amounts.append(amount) else: request_amounts = [request_amount] @@ -69,15 +83,14 @@ class MpesaSettings(Document): @frappe.whitelist() def get_account_balance_info(self): payload = dict( - reference_doctype="Mpesa Settings", - reference_docname=self.name, - doc_details=vars(self) + reference_doctype="Mpesa Settings", reference_docname=self.name, doc_details=vars(self) ) if frappe.flags.in_test: from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import ( get_test_account_balance_response, ) + response = frappe._dict(get_test_account_balance_response()) else: response = frappe._dict(get_account_balance(payload)) @@ -95,46 +108,62 @@ class MpesaSettings(Document): req_name = getattr(response, global_id) error = None - if not frappe.db.exists('Integration Request', req_name): + if not frappe.db.exists("Integration Request", req_name): create_request_log(request_dict, "Host", "Mpesa", req_name, error) if error: frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error")) + def generate_stk_push(**kwargs): """Generate stk push by making a API call to the stk push API.""" args = frappe._dict(kwargs) try: - callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction" + callback_url = ( + get_request_site_address(True) + + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction" + ) mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:]) env = "production" if not mpesa_settings.sandbox else "sandbox" # for sandbox, business shortcode is same as till number - business_shortcode = mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number + business_shortcode = ( + mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number + ) - connector = MpesaConnector(env=env, + connector = MpesaConnector( + env=env, app_key=mpesa_settings.consumer_key, - app_secret=mpesa_settings.get_password("consumer_secret")) + app_secret=mpesa_settings.get_password("consumer_secret"), + ) mobile_number = sanitize_mobile_number(args.sender) response = connector.stk_push( - business_shortcode=business_shortcode, amount=args.request_amount, + business_shortcode=business_shortcode, + amount=args.request_amount, passcode=mpesa_settings.get_password("online_passkey"), - callback_url=callback_url, reference_code=mpesa_settings.till_number, - phone_number=mobile_number, description="POS Payment" + callback_url=callback_url, + reference_code=mpesa_settings.till_number, + phone_number=mobile_number, + description="POS Payment", ) return response except Exception: frappe.log_error(title=_("Mpesa Express Transaction Error")) - frappe.throw(_("Issue detected with Mpesa configuration, check the error logs for more details"), title=_("Mpesa Express Error")) + frappe.throw( + _("Issue detected with Mpesa configuration, check the error logs for more details"), + title=_("Mpesa Express Error"), + ) + def sanitize_mobile_number(number): """Add country code and strip leading zeroes from the phone number.""" return "254" + str(number).lstrip("0") + @frappe.whitelist(allow_guest=True) def verify_transaction(**kwargs): """Verify the transaction result received via callback from stk.""" @@ -146,28 +175,28 @@ def verify_transaction(**kwargs): integration_request = frappe.get_doc("Integration Request", checkout_id) transaction_data = frappe._dict(loads(integration_request.data)) - total_paid = 0 # for multiple integration request made against a pos invoice - success = False # for reporting successfull callback to point of sale ui + total_paid = 0 # for multiple integration request made against a pos invoice + success = False # for reporting successfull callback to point of sale ui - if transaction_response['ResultCode'] == 0: + if transaction_response["ResultCode"] == 0: if integration_request.reference_doctype and integration_request.reference_docname: try: item_response = transaction_response["CallbackMetadata"]["Item"] amount = fetch_param_value(item_response, "Amount", "Name") mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") - pr = frappe.get_doc(integration_request.reference_doctype, integration_request.reference_docname) + pr = frappe.get_doc( + integration_request.reference_doctype, integration_request.reference_docname + ) mpesa_receipts, completed_payments = get_completed_integration_requests_info( - integration_request.reference_doctype, - integration_request.reference_docname, - checkout_id + integration_request.reference_doctype, integration_request.reference_docname, checkout_id ) total_paid = amount + sum(completed_payments) - mpesa_receipts = ', '.join(mpesa_receipts + [mpesa_receipt]) + mpesa_receipts = ", ".join(mpesa_receipts + [mpesa_receipt]) if total_paid >= pr.grand_total: - pr.run_method("on_payment_authorized", 'Completed') + pr.run_method("on_payment_authorized", "Completed") success = True frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts) @@ -180,24 +209,31 @@ def verify_transaction(**kwargs): integration_request.handle_failure(transaction_response) frappe.publish_realtime( - event='process_phone_payment', + event="process_phone_payment", doctype="POS Invoice", docname=transaction_data.payment_reference, user=integration_request.owner, message={ - 'amount': total_paid, - 'success': success, - 'failure_message': transaction_response["ResultDesc"] if transaction_response['ResultCode'] != 0 else '' + "amount": total_paid, + "success": success, + "failure_message": transaction_response["ResultDesc"] + if transaction_response["ResultCode"] != 0 + else "", }, ) + def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id): - output_of_other_completed_requests = frappe.get_all("Integration Request", filters={ - 'name': ['!=', checkout_id], - 'reference_doctype': reference_doctype, - 'reference_docname': reference_docname, - 'status': 'Completed' - }, pluck="output") + output_of_other_completed_requests = frappe.get_all( + "Integration Request", + filters={ + "name": ["!=", checkout_id], + "reference_doctype": reference_doctype, + "reference_docname": reference_docname, + "status": "Completed", + }, + pluck="output", + ) mpesa_receipts, completed_payments = [], [] @@ -211,23 +247,38 @@ def get_completed_integration_requests_info(reference_doctype, reference_docname return mpesa_receipts, completed_payments + def get_account_balance(request_payload): """Call account balance API to send the request to the Mpesa Servers.""" try: mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname")) env = "production" if not mpesa_settings.sandbox else "sandbox" - connector = MpesaConnector(env=env, + connector = MpesaConnector( + env=env, app_key=mpesa_settings.consumer_key, - app_secret=mpesa_settings.get_password("consumer_secret")) + app_secret=mpesa_settings.get_password("consumer_secret"), + ) - callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info" + callback_url = ( + get_request_site_address(True) + + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info" + ) - response = connector.get_balance(mpesa_settings.initiator_name, mpesa_settings.security_credential, mpesa_settings.till_number, 4, mpesa_settings.name, callback_url, callback_url) + response = connector.get_balance( + mpesa_settings.initiator_name, + mpesa_settings.security_credential, + mpesa_settings.till_number, + 4, + mpesa_settings.name, + callback_url, + callback_url, + ) return response except Exception: frappe.log_error(title=_("Account Balance Processing Error")) frappe.throw(_("Please check your configuration and try again"), title=_("Error")) + @frappe.whitelist(allow_guest=True) def process_balance_info(**kwargs): """Process and store account balance information received via callback from the account balance API call.""" @@ -255,35 +306,43 @@ def process_balance_info(**kwargs): ref_doc.db_set("account_balance", balance_info) request.handle_success(account_balance_response) - frappe.publish_realtime("refresh_mpesa_dashboard", doctype="Mpesa Settings", - docname=transaction_data.reference_docname, user=transaction_data.owner) + frappe.publish_realtime( + "refresh_mpesa_dashboard", + doctype="Mpesa Settings", + docname=transaction_data.reference_docname, + user=transaction_data.owner, + ) except Exception: request.handle_failure(account_balance_response) - frappe.log_error(title=_("Mpesa Account Balance Processing Error"), message=account_balance_response) + frappe.log_error( + title=_("Mpesa Account Balance Processing Error"), message=account_balance_response + ) else: request.handle_failure(account_balance_response) + def format_string_to_json(balance_info): """ Format string to json. e.g: '''Working Account|KES|481000.00|481000.00|0.00|0.00''' => {'Working Account': {'current_balance': '481000.00', - 'available_balance': '481000.00', - 'reserved_balance': '0.00', - 'uncleared_balance': '0.00'}} + 'available_balance': '481000.00', + 'reserved_balance': '0.00', + 'uncleared_balance': '0.00'}} """ balance_dict = frappe._dict() for account_info in balance_info.split("&"): - account_info = account_info.split('|') + account_info = account_info.split("|") balance_dict[account_info[0]] = dict( current_balance=fmt_money(account_info[2], currency="KES"), available_balance=fmt_money(account_info[3], currency="KES"), reserved_balance=fmt_money(account_info[4], currency="KES"), - uncleared_balance=fmt_money(account_info[5], currency="KES") + uncleared_balance=fmt_money(account_info[5], currency="KES"), ) return dumps(balance_dict) + def fetch_param_value(response, key, key_field): """Fetch the specified key from list of dictionary. Key is identified via the key field.""" for param in response: 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 3945afab69a..17e332c7df3 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -22,12 +22,12 @@ class TestMpesaSettings(unittest.TestCase): create_mpesa_settings(payment_gateway_name="Payment") def tearDown(self): - frappe.db.sql('delete from `tabMpesa Settings`') + frappe.db.sql("delete from `tabMpesa Settings`") frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"') def test_creation_of_payment_gateway(self): - mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone") - self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"})) + mode_of_payment = create_mode_of_payment("Mpesa-_Test", payment_type="Phone") + self.assertTrue(frappe.db.exists("Payment Gateway Account", {"payment_gateway": "Mpesa-_Test"})) self.assertTrue(mode_of_payment.name) self.assertEqual(mode_of_payment.type, "Phone") @@ -45,24 +45,33 @@ class TestMpesaSettings(unittest.TestCase): # test formatting of account balance received as string to json with appropriate currency symbol mpesa_doc.reload() - self.assertEqual(mpesa_doc.account_balance, dumps({ - "Working Account": { - "current_balance": "Sh 481,000.00", - "available_balance": "Sh 481,000.00", - "reserved_balance": "Sh 0.00", - "uncleared_balance": "Sh 0.00" - } - })) + self.assertEqual( + mpesa_doc.account_balance, + dumps( + { + "Working Account": { + "current_balance": "Sh 481,000.00", + "available_balance": "Sh 481,000.00", + "reserved_balance": "Sh 0.00", + "uncleared_balance": "Sh 0.00", + } + } + ), + ) integration_request.delete() def test_processing_of_callback_payload(self): - mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") + mpesa_account = frappe.db.get_value( + "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" + ) frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") pos_invoice = create_pos_invoice(do_not_submit=1) - pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 500}) + pos_invoice.append( + "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 500} + ) pos_invoice.contact_mobile = "093456543894" pos_invoice.currency = "KES" pos_invoice.save() @@ -72,12 +81,18 @@ class TestMpesaSettings(unittest.TestCase): self.assertEqual(pr.payment_gateway, "Mpesa-Payment") # submitting payment request creates integration requests with random id - integration_req_ids = frappe.get_all("Integration Request", filters={ - 'reference_doctype': pr.doctype, - 'reference_docname': pr.name, - }, pluck="name") + integration_req_ids = frappe.get_all( + "Integration Request", + filters={ + "reference_doctype": pr.doctype, + "reference_docname": pr.name, + }, + pluck="name", + ) - callback_response = get_payment_callback_payload(Amount=500, CheckoutRequestID=integration_req_ids[0]) + callback_response = get_payment_callback_payload( + Amount=500, CheckoutRequestID=integration_req_ids[0] + ) verify_transaction(**callback_response) # test creation of integration request integration_request = frappe.get_doc("Integration Request", integration_req_ids[0]) @@ -99,13 +114,17 @@ class TestMpesaSettings(unittest.TestCase): pos_invoice.delete() def test_processing_of_multiple_callback_payload(self): - mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") + mpesa_account = frappe.db.get_value( + "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" + ) frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") pos_invoice = create_pos_invoice(do_not_submit=1) - pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000}) + pos_invoice.append( + "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 1000} + ) pos_invoice.contact_mobile = "093456543894" pos_invoice.currency = "KES" pos_invoice.save() @@ -115,10 +134,14 @@ class TestMpesaSettings(unittest.TestCase): self.assertEqual(pr.payment_gateway, "Mpesa-Payment") # submitting payment request creates integration requests with random id - integration_req_ids = frappe.get_all("Integration Request", filters={ - 'reference_doctype': pr.doctype, - 'reference_docname': pr.name, - }, pluck="name") + integration_req_ids = frappe.get_all( + "Integration Request", + filters={ + "reference_doctype": pr.doctype, + "reference_docname": pr.name, + }, + pluck="name", + ) # create random receipt nos and send it as response to callback handler mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids] @@ -128,7 +151,7 @@ class TestMpesaSettings(unittest.TestCase): callback_response = get_payment_callback_payload( Amount=500, CheckoutRequestID=integration_req_ids[i], - MpesaReceiptNumber=mpesa_receipt_numbers[i] + MpesaReceiptNumber=mpesa_receipt_numbers[i], ) # handle response manually verify_transaction(**callback_response) @@ -139,7 +162,7 @@ class TestMpesaSettings(unittest.TestCase): # check receipt number once all the integration requests are completed pos_invoice.reload() - self.assertEqual(pos_invoice.mpesa_receipt_number, ', '.join(mpesa_receipt_numbers)) + self.assertEqual(pos_invoice.mpesa_receipt_number, ", ".join(mpesa_receipt_numbers)) frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") [d.delete() for d in integration_requests] @@ -149,13 +172,17 @@ class TestMpesaSettings(unittest.TestCase): pos_invoice.delete() def test_processing_of_only_one_succes_callback_payload(self): - mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") + mpesa_account = frappe.db.get_value( + "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" + ) frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") pos_invoice = create_pos_invoice(do_not_submit=1) - pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000}) + pos_invoice.append( + "payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 1000} + ) pos_invoice.contact_mobile = "093456543894" pos_invoice.currency = "KES" pos_invoice.save() @@ -165,10 +192,14 @@ class TestMpesaSettings(unittest.TestCase): self.assertEqual(pr.payment_gateway, "Mpesa-Payment") # submitting payment request creates integration requests with random id - integration_req_ids = frappe.get_all("Integration Request", filters={ - 'reference_doctype': pr.doctype, - 'reference_docname': pr.name, - }, pluck="name") + integration_req_ids = frappe.get_all( + "Integration Request", + filters={ + "reference_doctype": pr.doctype, + "reference_docname": pr.name, + }, + pluck="name", + ) # create random receipt nos and send it as response to callback handler mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids] @@ -176,7 +207,7 @@ class TestMpesaSettings(unittest.TestCase): callback_response = get_payment_callback_payload( Amount=500, CheckoutRequestID=integration_req_ids[0], - MpesaReceiptNumber=mpesa_receipt_numbers[0] + MpesaReceiptNumber=mpesa_receipt_numbers[0], ) # handle response manually verify_transaction(**callback_response) @@ -188,11 +219,15 @@ class TestMpesaSettings(unittest.TestCase): # second integration request fails # now retrying payment request should make only one integration request again pr = pos_invoice.create_payment_request() - new_integration_req_ids = frappe.get_all("Integration Request", filters={ - 'reference_doctype': pr.doctype, - 'reference_docname': pr.name, - 'name': ['not in', integration_req_ids] - }, pluck="name") + new_integration_req_ids = frappe.get_all( + "Integration Request", + filters={ + "reference_doctype": pr.doctype, + "reference_docname": pr.name, + "name": ["not in", integration_req_ids], + }, + pluck="name", + ) self.assertEqual(len(new_integration_req_ids), 1) @@ -203,94 +238,56 @@ class TestMpesaSettings(unittest.TestCase): pr.delete() pos_invoice.delete() + def create_mpesa_settings(payment_gateway_name="Express"): if frappe.db.exists("Mpesa Settings", payment_gateway_name): return frappe.get_doc("Mpesa Settings", payment_gateway_name) - doc = frappe.get_doc(dict( #nosec - doctype="Mpesa Settings", - sandbox=1, - payment_gateway_name=payment_gateway_name, - consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn", - consumer_secret="VI1oS3oBGPJfh3JyvLHw", - online_passkey="LVI1oS3oBGPJfh3JyvLHwZOd", - till_number="174379" - )) + doc = frappe.get_doc( + dict( # nosec + doctype="Mpesa Settings", + sandbox=1, + payment_gateway_name=payment_gateway_name, + consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn", + consumer_secret="VI1oS3oBGPJfh3JyvLHw", + online_passkey="LVI1oS3oBGPJfh3JyvLHwZOd", + till_number="174379", + ) + ) doc.insert(ignore_permissions=True) return doc + def get_test_account_balance_response(): """Response received after calling the account balance API.""" return { - "ResultType":0, - "ResultCode":0, - "ResultDesc":"The service request has been accepted successfully.", - "OriginatorConversationID":"10816-694520-2", - "ConversationID":"AG_20200927_00007cdb1f9fb6494315", - "TransactionID":"LGR0000000", - "ResultParameters":{ - "ResultParameter":[ - { - "Key":"ReceiptNo", - "Value":"LGR919G2AV" - }, - { - "Key":"Conversation ID", - "Value":"AG_20170727_00004492b1b6d0078fbe" - }, - { - "Key":"FinalisedTime", - "Value":20170727101415 - }, - { - "Key":"Amount", - "Value":10 - }, - { - "Key":"TransactionStatus", - "Value":"Completed" - }, - { - "Key":"ReasonType", - "Value":"Salary Payment via API" - }, - { - "Key":"TransactionReason" - }, - { - "Key":"DebitPartyCharges", - "Value":"Fee For B2C Payment|KES|33.00" - }, - { - "Key":"DebitAccountType", - "Value":"Utility Account" - }, - { - "Key":"InitiatedTime", - "Value":20170727101415 - }, - { - "Key":"Originator Conversation ID", - "Value":"19455-773836-1" - }, - { - "Key":"CreditPartyName", - "Value":"254708374149 - John Doe" - }, - { - "Key":"DebitPartyName", - "Value":"600134 - Safaricom157" - } - ] - }, - "ReferenceData":{ - "ReferenceItem":{ - "Key":"Occasion", - "Value":"aaaa" + "ResultType": 0, + "ResultCode": 0, + "ResultDesc": "The service request has been accepted successfully.", + "OriginatorConversationID": "10816-694520-2", + "ConversationID": "AG_20200927_00007cdb1f9fb6494315", + "TransactionID": "LGR0000000", + "ResultParameters": { + "ResultParameter": [ + {"Key": "ReceiptNo", "Value": "LGR919G2AV"}, + {"Key": "Conversation ID", "Value": "AG_20170727_00004492b1b6d0078fbe"}, + {"Key": "FinalisedTime", "Value": 20170727101415}, + {"Key": "Amount", "Value": 10}, + {"Key": "TransactionStatus", "Value": "Completed"}, + {"Key": "ReasonType", "Value": "Salary Payment via API"}, + {"Key": "TransactionReason"}, + {"Key": "DebitPartyCharges", "Value": "Fee For B2C Payment|KES|33.00"}, + {"Key": "DebitAccountType", "Value": "Utility Account"}, + {"Key": "InitiatedTime", "Value": 20170727101415}, + {"Key": "Originator Conversation ID", "Value": "19455-773836-1"}, + {"Key": "CreditPartyName", "Value": "254708374149 - John Doe"}, + {"Key": "DebitPartyName", "Value": "600134 - Safaricom157"}, + ] + }, + "ReferenceData": {"ReferenceItem": {"Key": "Occasion", "Value": "aaaa"}}, } - } - } + def get_payment_request_response_payload(Amount=500): """Response received after successfully calling the stk push process request API.""" @@ -304,40 +301,44 @@ def get_payment_request_response_payload(Amount=500): "ResultDesc": "The service request is processed successfully.", "CallbackMetadata": { "Item": [ - { "Name": "Amount", "Value": Amount }, - { "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" }, - { "Name": "TransactionDate", "Value": 20201006113336 }, - { "Name": "PhoneNumber", "Value": 254723575670 } + {"Name": "Amount", "Value": Amount}, + {"Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R"}, + {"Name": "TransactionDate", "Value": 20201006113336}, + {"Name": "PhoneNumber", "Value": 254723575670}, ] - } + }, } -def get_payment_callback_payload(Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"): + +def get_payment_callback_payload( + Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R" +): """Response received from the server as callback after calling the stkpush process request API.""" return { - "Body":{ - "stkCallback":{ - "MerchantRequestID":"19465-780693-1", - "CheckoutRequestID":CheckoutRequestID, - "ResultCode":0, - "ResultDesc":"The service request is processed successfully.", - "CallbackMetadata":{ - "Item":[ - { "Name":"Amount", "Value":Amount }, - { "Name":"MpesaReceiptNumber", "Value":MpesaReceiptNumber }, - { "Name":"Balance" }, - { "Name":"TransactionDate", "Value":20170727154800 }, - { "Name":"PhoneNumber", "Value":254721566839 } + "Body": { + "stkCallback": { + "MerchantRequestID": "19465-780693-1", + "CheckoutRequestID": CheckoutRequestID, + "ResultCode": 0, + "ResultDesc": "The service request is processed successfully.", + "CallbackMetadata": { + "Item": [ + {"Name": "Amount", "Value": Amount}, + {"Name": "MpesaReceiptNumber", "Value": MpesaReceiptNumber}, + {"Name": "Balance"}, + {"Name": "TransactionDate", "Value": 20170727154800}, + {"Name": "PhoneNumber", "Value": 254721566839}, ] - } + }, } } } + def get_account_balance_callback_payload(): """Response received from the server as callback after calling the account balance API.""" return { - "Result":{ + "Result": { "ResultType": 0, "ResultCode": 0, "ResultDesc": "The service request is processed successfully.", @@ -346,18 +347,15 @@ def get_account_balance_callback_payload(): "TransactionID": "OIR0000000", "ResultParameters": { "ResultParameter": [ - { - "Key": "AccountBalance", - "Value": "Working Account|KES|481000.00|481000.00|0.00|0.00" - }, - { "Key": "BOCompletedTime", "Value": 20200927234123 } + {"Key": "AccountBalance", "Value": "Working Account|KES|481000.00|481000.00|0.00|0.00"}, + {"Key": "BOCompletedTime", "Value": 20200927234123}, ] }, "ReferenceData": { "ReferenceItem": { "Key": "QueueTimeoutURL", - "Value": "https://internalsandbox.safaricom.co.ke/mpesa/abresults/v1/submit" + "Value": "https://internalsandbox.safaricom.co.ke/mpesa/abresults/v1/submit", } - } + }, } } diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 0b552f9bbf4..625dd3110ea 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -8,7 +8,7 @@ from frappe import _ from plaid.errors import APIError, InvalidRequestError, ItemError -class PlaidConnector(): +class PlaidConnector: def __init__(self, access_token=None): self.access_token = access_token self.settings = frappe.get_single("Plaid Settings") @@ -18,7 +18,7 @@ class PlaidConnector(): client_id=self.settings.plaid_client_id, secret=self.settings.get_password("plaid_secret"), environment=self.settings.plaid_env, - api_version="2020-09-14" + api_version="2020-09-14", ) def get_access_token(self, public_token): @@ -29,25 +29,29 @@ class PlaidConnector(): return access_token def get_token_request(self, update_mode=False): - country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"] + country_codes = ( + ["US", "CA", "FR", "IE", "NL", "ES", "GB"] + if self.settings.enable_european_access + else ["US", "CA"] + ) args = { "client_name": self.client_name, # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", "country_codes": country_codes, - "user": { - "client_user_id": frappe.generate_hash(frappe.session.user, length=32) - } + "user": {"client_user_id": frappe.generate_hash(frappe.session.user, length=32)}, } if update_mode: args["access_token"] = self.access_token else: - args.update({ - "client_id": self.settings.plaid_client_id, - "secret": self.settings.plaid_secret, - "products": self.products, - }) + args.update( + { + "client_id": self.settings.plaid_client_id, + "secret": self.settings.plaid_secret, + "products": self.products, + } + ) return args @@ -82,11 +86,7 @@ class PlaidConnector(): def get_transactions(self, start_date, end_date, account_id=None): self.auth() - kwargs = dict( - access_token=self.access_token, - start_date=start_date, - end_date=end_date - ) + kwargs = dict(access_token=self.access_token, start_date=start_date, end_date=end_date) if account_id: kwargs.update(dict(account_ids=[account_id])) @@ -94,7 +94,9 @@ class PlaidConnector(): response = self.client.Transactions.get(**kwargs) transactions = response["transactions"] while len(transactions) < response["total_transactions"]: - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) + 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: diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 7e6f146ce3f..ce65f6c0ff7 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -29,7 +29,7 @@ def get_plaid_configuration(): return { "plaid_env": plaid_settings.plaid_env, "link_token": plaid_settings.get_link_token(), - "client_name": frappe.local.site + "client_name": frappe.local.site, } return "disabled" @@ -45,14 +45,16 @@ def add_institution(token, response): if not frappe.db.exists("Bank", response["institution"]["name"]): try: - bank = frappe.get_doc({ - "doctype": "Bank", - "bank_name": response["institution"]["name"], - "plaid_access_token": access_token - }) + bank = frappe.get_doc( + { + "doctype": "Bank", + "bank_name": response["institution"]["name"], + "plaid_access_token": access_token, + } + ) bank.insert() except Exception: - frappe.log_error(frappe.get_traceback(), title=_('Plaid Link Error')) + 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 @@ -89,65 +91,71 @@ def add_bank_accounts(response, bank, company): if not existing_bank_account: try: - new_account = frappe.get_doc({ - "doctype": "Bank Account", - "bank": bank["bank_name"], - "account": default_gl_account.account, - "account_name": account["name"], - "account_type": account.get("type", ""), - "account_subtype": account.get("subtype", ""), - "mask": account.get("mask", ""), - "integration_id": account["id"], - "is_company_account": 1, - "company": company - }) + new_account = frappe.get_doc( + { + "doctype": "Bank Account", + "bank": bank["bank_name"], + "account": default_gl_account.account, + "account_name": account["name"], + "account_type": account.get("type", ""), + "account_subtype": account.get("subtype", ""), + "mask": account.get("mask", ""), + "integration_id": account["id"], + "is_company_account": 1, + "company": company, + } + ) new_account.insert() result.append(new_account.name) except frappe.UniqueValidationError: - frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"])) + frappe.msgprint( + _("Bank account {0} already exists and could not be created again").format(account["name"]) + ) except Exception: 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")) + frappe.throw( + _("There was an error creating Bank Account while linking with Plaid."), + title=_("Plaid Link Failed"), + ) else: 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 = 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")) + frappe.throw( + _("There was an error updating Bank Account {} while linking with Plaid.").format( + existing_bank_account + ), + title=_("Plaid Link Failed"), + ) return result def add_account_type(account_type): try: - frappe.get_doc({ - "doctype": "Bank Account Type", - "account_type": account_type - }).insert() + frappe.get_doc({"doctype": "Bank Account Type", "account_type": account_type}).insert() except Exception: frappe.throw(frappe.get_traceback()) def add_account_subtype(account_subtype): try: - frappe.get_doc({ - "doctype": "Bank Account Subtype", - "account_subtype": account_subtype - }).insert() + frappe.get_doc({"doctype": "Bank Account Subtype", "account_subtype": account_subtype}).insert() except Exception: frappe.throw(frappe.get_traceback()) @@ -164,19 +172,26 @@ def sync_transactions(bank, bank_account): end_date = formatdate(today(), "YYYY-MM-dd") try: - transactions = get_transactions(bank=bank, bank_account=bank_account, start_date=start_date, end_date=end_date) + transactions = get_transactions( + bank=bank, bank_account=bank_account, start_date=start_date, end_date=end_date + ) result = [] for transaction in reversed(transactions): result += new_bank_transaction(transaction) if result: - last_transaction_date = frappe.db.get_value('Bank Transaction', result.pop(), 'date') + last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date") - frappe.logger().info("Plaid added {} new Bank Transactions from '{}' between {} and {}".format( - len(result), bank_account, start_date, end_date)) + frappe.logger().info( + "Plaid added {} new Bank Transactions from '{}' between {} and {}".format( + len(result), bank_account, start_date, end_date + ) + ) - frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date) + frappe.db.set_value( + "Bank Account", bank_account, "last_integration_date", last_transaction_date + ) except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) @@ -185,7 +200,9 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): access_token = None if bank_account: - related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True) + related_bank = frappe.db.get_values( + "Bank Account", bank_account, ["bank", "integration_id"], as_dict=True + ) access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token") account_id = related_bank[0].integration_id else: @@ -196,7 +213,9 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): transactions = [] try: - transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id) + 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.") + " " @@ -229,18 +248,20 @@ def new_bank_transaction(transaction): if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])): try: - new_transaction = frappe.get_doc({ - "doctype": "Bank Transaction", - "date": getdate(transaction["date"]), - "status": status, - "bank_account": bank_account, - "deposit": debit, - "withdrawal": credit, - "currency": transaction["iso_currency_code"], - "transaction_id": transaction["transaction_id"], - "reference_number": transaction["payment_meta"]["reference_number"], - "description": transaction["name"] - }) + new_transaction = frappe.get_doc( + { + "doctype": "Bank Transaction", + "date": getdate(transaction["date"]), + "status": status, + "bank_account": bank_account, + "deposit": debit, + "withdrawal": credit, + "currency": transaction["iso_currency_code"], + "transaction_id": transaction["transaction_id"], + "reference_number": transaction["payment_meta"]["reference_number"], + "description": transaction["name"], + } + ) new_transaction.insert() new_transaction.submit() @@ -250,7 +271,7 @@ def new_bank_transaction(transaction): result.append(new_transaction.name) except Exception: - frappe.throw(title=_('Bank transaction creation error')) + frappe.throw(title=_("Bank transaction creation error")) return result @@ -260,19 +281,21 @@ def automatic_synchronization(): if settings.enabled == 1 and settings.automatic_sync == 1: enqueue_synchronization() + @frappe.whitelist() def enqueue_synchronization(): - plaid_accounts = frappe.get_all("Bank Account", - filters={"integration_id": ["!=", ""]}, - fields=["name", "bank"]) + plaid_accounts = frappe.get_all( + "Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"] + ) for plaid_account in plaid_accounts: frappe.enqueue( "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, - bank_account=plaid_account.name + bank_account=plaid_account.name, ) + @frappe.whitelist() def get_link_token_for_update(access_token): plaid = PlaidConnector(access_token) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py index 535d7fa7997..e8dc3e258f6 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py @@ -45,111 +45,110 @@ class TestPlaidSettings(unittest.TestCase): def test_default_bank_account(self): if not frappe.db.exists("Bank", "Citi"): - frappe.get_doc({ - "doctype": "Bank", - "bank_name": "Citi" - }).insert() + frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert() bank_accounts = { - 'account': { - 'subtype': 'checking', - 'mask': '0000', - 'type': 'depository', - 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', - 'name': 'Plaid Checking' + "account": { + "subtype": "checking", + "mask": "0000", + "type": "depository", + "id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", + "name": "Plaid Checking", }, - 'account_id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', - 'link_session_id': 'db673d75-61aa-442a-864f-9b3f174f3725', - 'accounts': [{ - 'type': 'depository', - 'subtype': 'checking', - 'mask': '0000', - 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', - 'name': 'Plaid Checking' - }], - 'institution': { - 'institution_id': 'ins_6', - 'name': 'Citi' - } + "account_id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", + "link_session_id": "db673d75-61aa-442a-864f-9b3f174f3725", + "accounts": [ + { + "type": "depository", + "subtype": "checking", + "mask": "0000", + "id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", + "name": "Plaid Checking", + } + ], + "institution": {"institution_id": "ins_6", "name": "Citi"}, } bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler) - company = frappe.db.get_single_value('Global Defaults', 'default_company') + company = frappe.db.get_single_value("Global Defaults", "default_company") frappe.db.set_value("Company", company, "default_bank_account", None) - self.assertRaises(frappe.ValidationError, add_bank_accounts, response=bank_accounts, bank=bank, company=company) + self.assertRaises( + frappe.ValidationError, add_bank_accounts, response=bank_accounts, bank=bank, company=company + ) def test_new_transaction(self): if not frappe.db.exists("Bank", "Citi"): - frappe.get_doc({ - "doctype": "Bank", - "bank_name": "Citi" - }).insert() + frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert() bank_accounts = { - 'account': { - 'subtype': 'checking', - 'mask': '0000', - 'type': 'depository', - 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', - 'name': 'Plaid Checking' + "account": { + "subtype": "checking", + "mask": "0000", + "type": "depository", + "id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", + "name": "Plaid Checking", }, - 'account_id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', - 'link_session_id': 'db673d75-61aa-442a-864f-9b3f174f3725', - 'accounts': [{ - 'type': 'depository', - 'subtype': 'checking', - 'mask': '0000', - 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', - 'name': 'Plaid Checking' - }], - 'institution': { - 'institution_id': 'ins_6', - 'name': 'Citi' - } + "account_id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", + "link_session_id": "db673d75-61aa-442a-864f-9b3f174f3725", + "accounts": [ + { + "type": "depository", + "subtype": "checking", + "mask": "0000", + "id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK", + "name": "Plaid Checking", + } + ], + "institution": {"institution_id": "ins_6", "name": "Citi"}, } bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler) - company = frappe.db.get_single_value('Global Defaults', 'default_company') + company = frappe.db.get_single_value("Global Defaults", "default_company") if frappe.db.get_value("Company", company, "default_bank_account") is None: - frappe.db.set_value("Company", company, "default_bank_account", get_default_bank_cash_account(company, "Cash").get("account")) + frappe.db.set_value( + "Company", + company, + "default_bank_account", + get_default_bank_cash_account(company, "Cash").get("account"), + ) add_bank_accounts(bank_accounts, bank, company) transactions = { - 'account_owner': None, - 'category': ['Food and Drink', 'Restaurants'], - 'account_id': 'b4Jkp1LJDZiPgojpr1ansXJrj5Q6w9fVmv6ov', - 'pending_transaction_id': None, - 'transaction_id': 'x374xPa7DvUewqlR5mjNIeGK8r8rl3Sn647LM', - 'unofficial_currency_code': None, - 'name': 'INTRST PYMNT', - 'transaction_type': 'place', - 'amount': -4.22, - 'location': { - 'city': None, - 'zip': None, - 'store_number': None, - 'lon': None, - 'state': None, - 'address': None, - 'lat': None + "account_owner": None, + "category": ["Food and Drink", "Restaurants"], + "account_id": "b4Jkp1LJDZiPgojpr1ansXJrj5Q6w9fVmv6ov", + "pending_transaction_id": None, + "transaction_id": "x374xPa7DvUewqlR5mjNIeGK8r8rl3Sn647LM", + "unofficial_currency_code": None, + "name": "INTRST PYMNT", + "transaction_type": "place", + "amount": -4.22, + "location": { + "city": None, + "zip": None, + "store_number": None, + "lon": None, + "state": None, + "address": None, + "lat": None, }, - 'payment_meta': { - 'reference_number': None, - 'payer': None, - 'payment_method': None, - 'reason': None, - 'payee': None, - 'ppd_id': None, - 'payment_processor': None, - 'by_order_of': None + "payment_meta": { + "reference_number": None, + "payer": None, + "payment_method": None, + "reason": None, + "payee": None, + "ppd_id": None, + "payment_processor": None, + "by_order_of": None, }, - 'date': '2017-12-22', - 'category_id': '13005000', - 'pending': False, - 'iso_currency_code': 'USD' + "date": "2017-12-22", + "category_id": "13005000", + "pending": False, + "iso_currency_code": "USD", } new_bank_transaction(transactions) diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py index 5de568272a0..b93c5c4d38c 100644 --- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py +++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py @@ -37,30 +37,27 @@ class QuickBooksMigrator(Document): def __init__(self, *args, **kwargs): super(QuickBooksMigrator, self).__init__(*args, **kwargs) self.oauth = OAuth2Session( - client_id=self.client_id, - redirect_uri=self.redirect_url, - scope=self.scope + client_id=self.client_id, redirect_uri=self.redirect_url, scope=self.scope ) if not self.authorization_url and self.authorization_endpoint: self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0] - def on_update(self): if self.company: # We need a Cost Center corresponding to the selected erpnext Company - self.default_cost_center = frappe.db.get_value('Company', self.company, 'cost_center') - company_warehouses = frappe.get_all('Warehouse', filters={"company": self.company, "is_group": 0}) + self.default_cost_center = frappe.db.get_value("Company", self.company, "cost_center") + company_warehouses = frappe.get_all( + "Warehouse", filters={"company": self.company, "is_group": 0} + ) if company_warehouses: self.default_warehouse = company_warehouses[0].name if self.authorization_endpoint: self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0] - @frappe.whitelist() def migrate(self): frappe.enqueue_doc("QuickBooks Migrator", "QuickBooks Migrator", "_migrate", queue="long") - def _migrate(self): try: self.set_indicator("In Progress") @@ -86,18 +83,33 @@ class QuickBooksMigrator(Document): # Following entities are directly available from API # Invoice can be an exception sometimes though (as explained above). entities_for_normal_transform = [ - "Customer", "Item", "Vendor", + "Customer", + "Item", + "Vendor", "Preferences", - "JournalEntry", "Purchase", "Deposit", - "Invoice", "CreditMemo", "SalesReceipt", "RefundReceipt", - "Bill", "VendorCredit", - "Payment", "BillPayment", + "JournalEntry", + "Purchase", + "Deposit", + "Invoice", + "CreditMemo", + "SalesReceipt", + "RefundReceipt", + "Bill", + "VendorCredit", + "Payment", + "BillPayment", ] for entity in entities_for_normal_transform: self._migrate_entries(entity) # Following entries are not available directly from API, Need to be regenrated from GeneralLedger Report - entities_for_gl_transform = ["Advance Payment", "Tax Payment", "Sales Tax Payment", "Purchase Tax Payment", "Inventory Qty Adjust"] + entities_for_gl_transform = [ + "Advance Payment", + "Tax Payment", + "Sales Tax Payment", + "Purchase Tax Payment", + "Inventory Qty Adjust", + ] for entity in entities_for_gl_transform: self._migrate_entries_from_gl(entity) self.set_indicator("Complete") @@ -107,33 +119,37 @@ class QuickBooksMigrator(Document): frappe.db.commit() - def get_tokens(self): token = self.oauth.fetch_token( - token_url=self.token_endpoint, - client_secret=self.client_secret, - code=self.code + token_url=self.token_endpoint, client_secret=self.client_secret, code=self.code ) self.access_token = token["access_token"] self.refresh_token = token["refresh_token"] self.save() - def _refresh_tokens(self): token = self.oauth.refresh_token( token_url=self.token_endpoint, client_id=self.client_id, refresh_token=self.refresh_token, client_secret=self.client_secret, - code=self.code + code=self.code, ) self.access_token = token["access_token"] self.refresh_token = token["refresh_token"] self.save() - def _make_custom_fields(self): - doctypes_for_quickbooks_id_field = ["Account", "Customer", "Address", "Item", "Supplier", "Sales Invoice", "Journal Entry", "Purchase Invoice"] + doctypes_for_quickbooks_id_field = [ + "Account", + "Customer", + "Address", + "Item", + "Supplier", + "Sales Invoice", + "Journal Entry", + "Purchase Invoice", + ] for doctype in doctypes_for_quickbooks_id_field: self._make_custom_quickbooks_id_field(doctype) @@ -143,53 +159,60 @@ class QuickBooksMigrator(Document): frappe.db.commit() - def _make_custom_quickbooks_id_field(self, doctype): if not frappe.get_meta(doctype).has_field("quickbooks_id"): - frappe.get_doc({ - "doctype": "Custom Field", - "label": "QuickBooks ID", - "dt": doctype, - "fieldname": "quickbooks_id", - "fieldtype": "Data", - }).insert() - + frappe.get_doc( + { + "doctype": "Custom Field", + "label": "QuickBooks ID", + "dt": doctype, + "fieldname": "quickbooks_id", + "fieldtype": "Data", + } + ).insert() def _make_custom_company_field(self, doctype): if not frappe.get_meta(doctype).has_field("company"): - frappe.get_doc({ - "doctype": "Custom Field", - "label": "Company", - "dt": doctype, - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - }).insert() - + frappe.get_doc( + { + "doctype": "Custom Field", + "label": "Company", + "dt": doctype, + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + } + ).insert() def _migrate_accounts(self): self._make_root_accounts() for entity in ["Account", "TaxRate", "TaxCode"]: self._migrate_entries(entity) - def _make_root_accounts(self): roots = ["Asset", "Equity", "Expense", "Liability", "Income"] for root in roots: try: - if not frappe.db.exists({"doctype": "Account", "name": encode_company_abbr("{} - QB".format(root), self.company), "company": self.company}): - frappe.get_doc({ + if not frappe.db.exists( + { "doctype": "Account", - "account_name": "{} - QB".format(root), - "root_type": root, - "is_group": "1", + "name": encode_company_abbr("{} - QB".format(root), self.company), "company": self.company, - }).insert(ignore_mandatory=True) + } + ): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "{} - QB".format(root), + "root_type": root, + "is_group": "1", + "company": self.company, + } + ).insert(ignore_mandatory=True) except Exception as e: self._log_error(e, root) frappe.db.commit() - def _migrate_entries(self, entity): try: query_uri = "{}/company/{}/query".format( @@ -198,22 +221,19 @@ class QuickBooksMigrator(Document): ) max_result_count = 1000 # Count number of entries - response = self._get(query_uri, - params={ - "query": """SELECT COUNT(*) FROM {}""".format(entity) - } - ) + response = self._get(query_uri, params={"query": """SELECT COUNT(*) FROM {}""".format(entity)}) entry_count = response.json()["QueryResponse"]["totalCount"] # fetch pages and accumulate entries = [] for start_position in range(1, entry_count + 1, max_result_count): - response = self._get(query_uri, + response = self._get( + query_uri, params={ "query": """SELECT * FROM {} STARTPOSITION {} MAXRESULTS {}""".format( entity, start_position, max_result_count ) - } + }, ) entries.extend(response.json()["QueryResponse"][entity]) entries = self._preprocess_entries(entity, entries) @@ -221,16 +241,18 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, response.text) - def _fetch_general_ledger(self): try: - query_uri = "{}/company/{}/reports/GeneralLedger".format(self.api_endpoint, self.quickbooks_company_id) - response = self._get(query_uri, + query_uri = "{}/company/{}/reports/GeneralLedger".format( + self.api_endpoint, self.quickbooks_company_id + ) + response = self._get( + query_uri, params={ "columns": ",".join(["tx_date", "txn_type", "credit_amt", "debt_amt"]), "date_macro": "All", "minorversion": 3, - } + }, ) self.gl_entries = {} for section in response.json()["Rows"]["Row"]: @@ -250,7 +272,6 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, response.text) - def _create_fiscal_years(self): try: # Assumes that exactly one fiscal year has been created so far @@ -258,10 +279,12 @@ class QuickBooksMigrator(Document): from itertools import chain from frappe.utils.data import add_years, getdate - smallest_ledger_entry_date = getdate(min(entry["date"] for entry in chain(*self.gl_entries.values()) if entry["date"])) - oldest_fiscal_year = frappe.get_all("Fiscal Year", - fields=["year_start_date", "year_end_date"], - order_by="year_start_date" + + smallest_ledger_entry_date = getdate( + min(entry["date"] for entry in chain(*self.gl_entries.values()) if entry["date"]) + ) + oldest_fiscal_year = frappe.get_all( + "Fiscal Year", fields=["year_start_date", "year_end_date"], order_by="year_start_date" )[0] # Keep on creating fiscal years # until smallest_ledger_entry_date is no longer smaller than the oldest fiscal year's start date @@ -272,7 +295,9 @@ class QuickBooksMigrator(Document): if new_fiscal_year.year_start_date.year == new_fiscal_year.year_end_date.year: new_fiscal_year.year = new_fiscal_year.year_start_date.year else: - new_fiscal_year.year = "{}-{}".format(new_fiscal_year.year_start_date.year, new_fiscal_year.year_end_date.year) + new_fiscal_year.year = "{}-{}".format( + new_fiscal_year.year_start_date.year, new_fiscal_year.year_end_date.year + ) new_fiscal_year.save() oldest_fiscal_year = new_fiscal_year @@ -280,40 +305,30 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e) - def _migrate_entries_from_gl(self, entity): if entity in self.general_ledger: self._save_entries(entity, self.general_ledger[entity].values()) - def _save_entries(self, entity, entries): entity_method_map = { "Account": self._save_account, "TaxRate": self._save_tax_rate, "TaxCode": self._save_tax_code, - "Preferences": self._save_preference, - "Customer": self._save_customer, "Item": self._save_item, "Vendor": self._save_vendor, - "Invoice": self._save_invoice, "CreditMemo": self._save_credit_memo, "SalesReceipt": self._save_sales_receipt, "RefundReceipt": self._save_refund_receipt, - "JournalEntry": self._save_journal_entry, - "Bill": self._save_bill, "VendorCredit": self._save_vendor_credit, - "Payment": self._save_payment, "BillPayment": self._save_bill_payment, - "Purchase": self._save_purchase, "Deposit": self._save_deposit, - "Advance Payment": self._save_advance_payment, "Tax Payment": self._save_tax_payment, "Sales Tax Payment": self._save_tax_payment, @@ -322,11 +337,17 @@ class QuickBooksMigrator(Document): } total = len(entries) for index, entry in enumerate(entries, start=1): - self._publish({"event": "progress", "message": _("Saving {0}").format(entity), "count": index, "total": total}) + self._publish( + { + "event": "progress", + "message": _("Saving {0}").format(entity), + "count": index, + "total": total, + } + ) entity_method_map[entity](entry) frappe.db.commit() - def _preprocess_entries(self, entity, entries): entity_method_map = { "Account": self._preprocess_accounts, @@ -338,7 +359,6 @@ class QuickBooksMigrator(Document): entries = preprocessor(entries) return entries - def _get_gl_entries_from_section(self, section, account=None): if "Header" in section: if "id" in section["Header"]["ColData"][0]: @@ -359,19 +379,20 @@ class QuickBooksMigrator(Document): for row in section["Rows"]["Row"]: if row["type"] == "Data": data = row["ColData"] - entries.append({ - "account": account, - "date": data[0]["value"], - "type": data[1]["value"], - "id": data[1].get("id"), - "credit": frappe.utils.flt(data[2]["value"]), - "debit": frappe.utils.flt(data[3]["value"]), - }) + entries.append( + { + "account": account, + "date": data[0]["value"], + "type": data[1]["value"], + "id": data[1].get("id"), + "credit": frappe.utils.flt(data[2]["value"]), + "debit": frappe.utils.flt(data[3]["value"]), + } + ) if row["type"] == "Section": self._get_gl_entries_from_section(row, account) self.gl_entries.setdefault(account, []).extend(entries) - def _preprocess_accounts(self, accounts): self.accounts = {account["Name"]: account for account in accounts} for account in accounts: @@ -381,7 +402,6 @@ class QuickBooksMigrator(Document): account["is_group"] = 0 return sorted(accounts, key=lambda account: int(account["Id"])) - def _save_account(self, account): mapping = { "Bank": "Asset", @@ -389,24 +409,22 @@ class QuickBooksMigrator(Document): "Fixed Asset": "Asset", "Other Asset": "Asset", "Accounts Receivable": "Asset", - "Equity": "Equity", - "Expense": "Expense", "Other Expense": "Expense", "Cost of Goods Sold": "Expense", - "Accounts Payable": "Liability", "Credit Card": "Liability", "Long Term Liability": "Liability", "Other Current Liability": "Liability", - "Income": "Income", "Other Income": "Income", } # Map Quickbooks Account Types to ERPNext root_accunts and and root_type try: - if not frappe.db.exists({"doctype": "Account", "quickbooks_id": account["Id"], "company": self.company}): + if not frappe.db.exists( + {"doctype": "Account", "quickbooks_id": account["Id"], "company": self.company} + ): is_child = account["SubAccount"] is_group = account["is_group"] # Create Two Accounts for every Group Account @@ -416,103 +434,125 @@ class QuickBooksMigrator(Document): account_id = account["Id"] if is_child: - parent_account = self._get_account_name_by_id("Group - {}".format(account["ParentRef"]["value"])) + parent_account = self._get_account_name_by_id( + "Group - {}".format(account["ParentRef"]["value"]) + ) else: - parent_account = encode_company_abbr("{} - QB".format(mapping[account["AccountType"]]), self.company) + parent_account = encode_company_abbr( + "{} - QB".format(mapping[account["AccountType"]]), self.company + ) - frappe.get_doc({ - "doctype": "Account", - "quickbooks_id": account_id, - "account_name": self._get_unique_account_name(account["Name"]), - "root_type": mapping[account["AccountType"]], - "account_type": self._get_account_type(account), - "account_currency": account["CurrencyRef"]["value"], - "parent_account": parent_account, - "is_group": is_group, - "company": self.company, - }).insert() - - if is_group: - # Create a Leaf account corresponding to the group account - frappe.get_doc({ + frappe.get_doc( + { "doctype": "Account", - "quickbooks_id": account["Id"], + "quickbooks_id": account_id, "account_name": self._get_unique_account_name(account["Name"]), "root_type": mapping[account["AccountType"]], "account_type": self._get_account_type(account), "account_currency": account["CurrencyRef"]["value"], - "parent_account": self._get_account_name_by_id(account_id), - "is_group": 0, + "parent_account": parent_account, + "is_group": is_group, "company": self.company, - }).insert() + } + ).insert() + + if is_group: + # Create a Leaf account corresponding to the group account + frappe.get_doc( + { + "doctype": "Account", + "quickbooks_id": account["Id"], + "account_name": self._get_unique_account_name(account["Name"]), + "root_type": mapping[account["AccountType"]], + "account_type": self._get_account_type(account), + "account_currency": account["CurrencyRef"]["value"], + "parent_account": self._get_account_name_by_id(account_id), + "is_group": 0, + "company": self.company, + } + ).insert() if account.get("AccountSubType") == "UndepositedFunds": self.undeposited_funds_account = self._get_account_name_by_id(account["Id"]) self.save() except Exception as e: self._log_error(e, account) - def _get_account_type(self, account): account_subtype_mapping = {"UndepositedFunds": "Cash"} account_type = account_subtype_mapping.get(account.get("AccountSubType")) if account_type is None: - account_type_mapping = {"Accounts Payable": "Payable", "Accounts Receivable": "Receivable", "Bank": "Bank", "Credit Card": "Bank"} + account_type_mapping = { + "Accounts Payable": "Payable", + "Accounts Receivable": "Receivable", + "Bank": "Bank", + "Credit Card": "Bank", + } account_type = account_type_mapping.get(account["AccountType"]) return account_type - def _preprocess_tax_rates(self, tax_rates): self.tax_rates = {tax_rate["Id"]: tax_rate for tax_rate in tax_rates} return tax_rates - def _save_tax_rate(self, tax_rate): try: - if not frappe.db.exists({"doctype": "Account", "quickbooks_id": "TaxRate - {}".format(tax_rate["Id"]), "company": self.company}): - frappe.get_doc({ + if not frappe.db.exists( + { "doctype": "Account", "quickbooks_id": "TaxRate - {}".format(tax_rate["Id"]), - "account_name": "{} - QB".format(tax_rate["Name"]), - "root_type": "Liability", - "parent_account": encode_company_abbr("{} - QB".format("Liability"), self.company), - "is_group": "0", "company": self.company, - }).insert() + } + ): + frappe.get_doc( + { + "doctype": "Account", + "quickbooks_id": "TaxRate - {}".format(tax_rate["Id"]), + "account_name": "{} - QB".format(tax_rate["Name"]), + "root_type": "Liability", + "parent_account": encode_company_abbr("{} - QB".format("Liability"), self.company), + "is_group": "0", + "company": self.company, + } + ).insert() except Exception as e: self._log_error(e, tax_rate) - def _preprocess_tax_codes(self, tax_codes): self.tax_codes = {tax_code["Id"]: tax_code for tax_code in tax_codes} return tax_codes - def _save_tax_code(self, tax_code): pass - def _save_customer(self, customer): try: - if not frappe.db.exists({"doctype": "Customer", "quickbooks_id": customer["Id"], "company": self.company}): + if not frappe.db.exists( + {"doctype": "Customer", "quickbooks_id": customer["Id"], "company": self.company} + ): try: - receivable_account = frappe.get_all("Account", filters={ - "account_type": "Receivable", - "account_currency": customer["CurrencyRef"]["value"], - "company": self.company, - })[0]["name"] + receivable_account = frappe.get_all( + "Account", + filters={ + "account_type": "Receivable", + "account_currency": customer["CurrencyRef"]["value"], + "company": self.company, + }, + )[0]["name"] except Exception: receivable_account = None - erpcustomer = frappe.get_doc({ - "doctype": "Customer", - "quickbooks_id": customer["Id"], - "customer_name": encode_company_abbr(customer["DisplayName"], self.company), - "customer_type": "Individual", - "customer_group": "Commercial", - "default_currency": customer["CurrencyRef"]["value"], - "accounts": [{"company": self.company, "account": receivable_account}], - "territory": "All Territories", - "company": self.company, - }).insert() + erpcustomer = frappe.get_doc( + { + "doctype": "Customer", + "quickbooks_id": customer["Id"], + "customer_name": encode_company_abbr(customer["DisplayName"], self.company), + "customer_type": "Individual", + "customer_group": "Commercial", + "default_currency": customer["CurrencyRef"]["value"], + "accounts": [{"company": self.company, "account": receivable_account}], + "territory": "All Territories", + "company": self.company, + } + ).insert() if "BillAddr" in customer: self._create_address(erpcustomer, "Customer", customer["BillAddr"], "Billing") if "ShipAddr" in customer: @@ -520,10 +560,11 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, customer) - def _save_item(self, item): try: - if not frappe.db.exists({"doctype": "Item", "quickbooks_id": item["Id"], "company": self.company}): + if not frappe.db.exists( + {"doctype": "Item", "quickbooks_id": item["Id"], "company": self.company} + ): if item["Type"] in ("Service", "Inventory"): item_dict = { "doctype": "Item", @@ -533,7 +574,7 @@ class QuickBooksMigrator(Document): "is_stock_item": 0, "item_group": "All Item Groups", "company": self.company, - "item_defaults": [{"company": self.company, "default_warehouse": self.default_warehouse}] + "item_defaults": [{"company": self.company, "default_warehouse": self.default_warehouse}], } if "ExpenseAccountRef" in item: expense_account = self._get_account_name_by_id(item["ExpenseAccountRef"]["value"]) @@ -545,21 +586,23 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, item) - def _allow_fraction_in_unit(self): frappe.db.set_value("UOM", "Unit", "must_be_whole_number", 0) - def _save_vendor(self, vendor): try: - if not frappe.db.exists({"doctype": "Supplier", "quickbooks_id": vendor["Id"], "company": self.company}): - erpsupplier = frappe.get_doc({ - "doctype": "Supplier", - "quickbooks_id": vendor["Id"], - "supplier_name": encode_company_abbr(vendor["DisplayName"], self.company), - "supplier_group": "All Supplier Groups", - "company": self.company, - }).insert() + if not frappe.db.exists( + {"doctype": "Supplier", "quickbooks_id": vendor["Id"], "company": self.company} + ): + erpsupplier = frappe.get_doc( + { + "doctype": "Supplier", + "quickbooks_id": vendor["Id"], + "supplier_name": encode_company_abbr(vendor["DisplayName"], self.company), + "supplier_group": "All Supplier Groups", + "company": self.company, + } + ).insert() if "BillAddr" in vendor: self._create_address(erpsupplier, "Supplier", vendor["BillAddr"], "Billing") if "ShipAddr" in vendor: @@ -567,7 +610,6 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e) - def _save_preference(self, preference): try: if preference["SalesFormsPrefs"]["AllowShipping"]: @@ -577,7 +619,6 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, preference) - def _save_invoice(self, invoice): # Invoice can be Linked with Another Transactions # If any of these transactions is a "StatementCharge" or "ReimburseCharge" then in the UI @@ -585,59 +626,56 @@ class QuickBooksMigrator(Document): # Also as of now there is no way of fetching the corresponding transaction from api # We in order to correctly reflect account balance make an equivalent Journal Entry quickbooks_id = "Invoice - {}".format(invoice["Id"]) - if any(linked["TxnType"] in ("StatementCharge", "ReimburseCharge") for linked in invoice["LinkedTxn"]): + if any( + linked["TxnType"] in ("StatementCharge", "ReimburseCharge") for linked in invoice["LinkedTxn"] + ): self._save_invoice_as_journal_entry(invoice, quickbooks_id) else: self._save_sales_invoice(invoice, quickbooks_id) - def _save_credit_memo(self, credit_memo): # Credit Memo is equivalent to a return Sales Invoice quickbooks_id = "Credit Memo - {}".format(credit_memo["Id"]) self._save_sales_invoice(credit_memo, quickbooks_id, is_return=True) - def _save_sales_receipt(self, sales_receipt): # Sales Receipt is equivalent to a POS Sales Invoice quickbooks_id = "Sales Receipt - {}".format(sales_receipt["Id"]) self._save_sales_invoice(sales_receipt, quickbooks_id, is_pos=True) - def _save_refund_receipt(self, refund_receipt): # Refund Receipt is equivalent to a return POS Sales Invoice quickbooks_id = "Refund Receipt - {}".format(refund_receipt["Id"]) self._save_sales_invoice(refund_receipt, quickbooks_id, is_return=True, is_pos=True) - def _save_sales_invoice(self, invoice, quickbooks_id, is_return=False, is_pos=False): try: - if not frappe.db.exists({"doctype": "Sales Invoice", "quickbooks_id": quickbooks_id, "company": self.company}): + if not frappe.db.exists( + {"doctype": "Sales Invoice", "quickbooks_id": quickbooks_id, "company": self.company} + ): invoice_dict = { "doctype": "Sales Invoice", "quickbooks_id": quickbooks_id, - # Quickbooks uses ISO 4217 Code # of course this gonna come back to bite me "currency": invoice["CurrencyRef"]["value"], - # Exchange Rate is provided if multicurrency is enabled # It is not provided if multicurrency is not enabled "conversion_rate": invoice.get("ExchangeRate", 1), "posting_date": invoice["TxnDate"], - # QuickBooks doesn't make Due Date a mandatory field this is a hack "due_date": invoice.get("DueDate", invoice["TxnDate"]), - "customer": frappe.get_all("Customer", + "customer": frappe.get_all( + "Customer", filters={ "quickbooks_id": invoice["CustomerRef"]["value"], "company": self.company, - })[0]["name"], + }, + )[0]["name"], "items": self._get_si_items(invoice, is_return=is_return), "taxes": self._get_taxes(invoice), - # Do not change posting_date upon submission "set_posting_time": 1, - # QuickBooks doesn't round total "disable_rounded_total": 1, "is_return": is_return, @@ -659,7 +697,6 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, [invoice, invoice_dict, json.loads(invoice_doc.as_json())]) - def _get_si_items(self, invoice, is_return=False): items = [] for line in invoice["Line"]: @@ -672,48 +709,56 @@ class QuickBooksMigrator(Document): else: tax_code = "NON" if line["SalesItemLineDetail"]["ItemRef"]["value"] != "SHIPPING_ITEM_ID": - item = frappe.db.get_all("Item", + item = frappe.db.get_all( + "Item", filters={ "quickbooks_id": line["SalesItemLineDetail"]["ItemRef"]["value"], "company": self.company, }, - fields=["name", "stock_uom"] + fields=["name", "stock_uom"], )[0] - items.append({ - "item_code": item["name"], - "conversion_factor": 1, - "uom": item["stock_uom"], - "description": line.get("Description", line["SalesItemLineDetail"]["ItemRef"]["name"]), - "qty": line["SalesItemLineDetail"]["Qty"], - "price_list_rate": line["SalesItemLineDetail"]["UnitPrice"], - "cost_center": self.default_cost_center, - "warehouse": self.default_warehouse, - "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)) - }) + items.append( + { + "item_code": item["name"], + "conversion_factor": 1, + "uom": item["stock_uom"], + "description": line.get("Description", line["SalesItemLineDetail"]["ItemRef"]["name"]), + "qty": line["SalesItemLineDetail"]["Qty"], + "price_list_rate": line["SalesItemLineDetail"]["UnitPrice"], + "cost_center": self.default_cost_center, + "warehouse": self.default_warehouse, + "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), + } + ) else: - items.append({ - "item_name": "Shipping", - "conversion_factor": 1, - "expense_account": self._get_account_name_by_id("TaxRate - {}".format(line["SalesItemLineDetail"]["TaxCodeRef"]["value"])), - "uom": "Unit", - "description": "Shipping", - "income_account": self.default_shipping_account, - "qty": 1, - "price_list_rate": line["Amount"], - "cost_center": self.default_cost_center, - "warehouse": self.default_warehouse, - "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)) - }) + items.append( + { + "item_name": "Shipping", + "conversion_factor": 1, + "expense_account": self._get_account_name_by_id( + "TaxRate - {}".format(line["SalesItemLineDetail"]["TaxCodeRef"]["value"]) + ), + "uom": "Unit", + "description": "Shipping", + "income_account": self.default_shipping_account, + "qty": 1, + "price_list_rate": line["Amount"], + "cost_center": self.default_cost_center, + "warehouse": self.default_warehouse, + "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), + } + ) if is_return: items[-1]["qty"] *= -1 elif line["DetailType"] == "DescriptionOnly": - items[-1].update({ - "margin_type": "Percentage", - "margin_rate_or_amount": int(line["Description"].split("%")[0]), - }) + items[-1].update( + { + "margin_type": "Percentage", + "margin_rate_or_amount": int(line["Description"].split("%")[0]), + } + ) return items - def _get_item_taxes(self, tax_code): tax_rates = self.tax_rates item_taxes = {} @@ -723,30 +768,31 @@ class QuickBooksMigrator(Document): if rate_list_type in tax_code: for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]: if tax_rate_detail["TaxTypeApplicable"] == "TaxOnAmount": - tax_head = self._get_account_name_by_id("TaxRate - {}".format(tax_rate_detail["TaxRateRef"]["value"])) + tax_head = self._get_account_name_by_id( + "TaxRate - {}".format(tax_rate_detail["TaxRateRef"]["value"]) + ) tax_rate = tax_rates[tax_rate_detail["TaxRateRef"]["value"]] item_taxes[tax_head] = tax_rate["RateValue"] return item_taxes - def _get_invoice_payments(self, invoice, is_return=False, is_pos=False): if is_pos: amount = invoice["TotalAmt"] if is_return: amount = -amount - return [{ - "mode_of_payment": "Cash", - "account": self._get_account_name_by_id(invoice["DepositToAccountRef"]["value"]), - "amount": amount, - }] - + return [ + { + "mode_of_payment": "Cash", + "account": self._get_account_name_by_id(invoice["DepositToAccountRef"]["value"]), + "amount": amount, + } + ] def _get_discount(self, lines): for line in lines: if line["DetailType"] == "DiscountLineDetail" and "Amount" in line["DiscountLineDetail"]: return line - def _save_invoice_as_journal_entry(self, invoice, quickbooks_id): try: accounts = [] @@ -758,8 +804,9 @@ class QuickBooksMigrator(Document): account_line["credit_in_account_currency"] = line["credit"] if frappe.db.get_value("Account", line["account"], "account_type") == "Receivable": account_line["party_type"] = "Customer" - account_line["party"] = frappe.get_all("Customer", - filters={"quickbooks_id": invoice["CustomerRef"]["value"], "company": self.company} + account_line["party"] = frappe.get_all( + "Customer", + filters={"quickbooks_id": invoice["CustomerRef"]["value"], "company": self.company}, )[0]["name"] accounts.append(account_line) @@ -769,7 +816,6 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, [invoice, accounts]) - def _save_journal_entry(self, journal_entry): # JournalEntry is equivalent to a Journal Entry @@ -782,13 +828,17 @@ class QuickBooksMigrator(Document): accounts = [] for line in lines: if line["DetailType"] == "JournalEntryLineDetail": - account_name = self._get_account_name_by_id(line["JournalEntryLineDetail"]["AccountRef"]["value"]) + account_name = self._get_account_name_by_id( + line["JournalEntryLineDetail"]["AccountRef"]["value"] + ) posting_type = line["JournalEntryLineDetail"]["PostingType"] - accounts.append({ - "account": account_name, - posting_type_field_mapping[posting_type]: line["Amount"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "account": account_name, + posting_type_field_mapping[posting_type]: line["Amount"], + "cost_center": self.default_cost_center, + } + ) return accounts quickbooks_id = "Journal Entry - {}".format(journal_entry["Id"]) @@ -796,39 +846,41 @@ class QuickBooksMigrator(Document): posting_date = journal_entry["TxnDate"] self.__save_journal_entry(quickbooks_id, accounts, posting_date) - def __save_journal_entry(self, quickbooks_id, accounts, posting_date): try: - if not frappe.db.exists({"doctype": "Journal Entry", "quickbooks_id": quickbooks_id, "company": self.company}): - je = frappe.get_doc({ - "doctype": "Journal Entry", - "quickbooks_id": quickbooks_id, - "company": self.company, - "posting_date": posting_date, - "accounts": accounts, - "multi_currency": 1, - }) + if not frappe.db.exists( + {"doctype": "Journal Entry", "quickbooks_id": quickbooks_id, "company": self.company} + ): + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "quickbooks_id": quickbooks_id, + "company": self.company, + "posting_date": posting_date, + "accounts": accounts, + "multi_currency": 1, + } + ) je.insert() je.submit() except Exception as e: self._log_error(e, [accounts, json.loads(je.as_json())]) - def _save_bill(self, bill): # Bill is equivalent to a Purchase Invoice quickbooks_id = "Bill - {}".format(bill["Id"]) self.__save_purchase_invoice(bill, quickbooks_id) - def _save_vendor_credit(self, vendor_credit): # Vendor Credit is equivalent to a return Purchase Invoice quickbooks_id = "Vendor Credit - {}".format(vendor_credit["Id"]) self.__save_purchase_invoice(vendor_credit, quickbooks_id, is_return=True) - def __save_purchase_invoice(self, invoice, quickbooks_id, is_return=False): try: - if not frappe.db.exists({"doctype": "Purchase Invoice", "quickbooks_id": quickbooks_id, "company": self.company}): + if not frappe.db.exists( + {"doctype": "Purchase Invoice", "quickbooks_id": quickbooks_id, "company": self.company} + ): credit_to_account = self._get_account_name_by_id(invoice["APAccountRef"]["value"]) invoice_dict = { "doctype": "Purchase Invoice", @@ -838,11 +890,13 @@ class QuickBooksMigrator(Document): "posting_date": invoice["TxnDate"], "due_date": invoice.get("DueDate", invoice["TxnDate"]), "credit_to": credit_to_account, - "supplier": frappe.get_all("Supplier", + "supplier": frappe.get_all( + "Supplier", filters={ "quickbooks_id": invoice["VendorRef"]["value"], "company": self.company, - })[0]["name"], + }, + )[0]["name"], "items": self._get_pi_items(invoice, is_return=is_return), "taxes": self._get_taxes(invoice), "set_posting_time": 1, @@ -857,7 +911,6 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, [invoice, invoice_dict, json.loads(invoice_doc.as_json())]) - def _get_pi_items(self, purchase_invoice, is_return=False): items = [] for line in purchase_invoice["Line"]: @@ -869,24 +922,29 @@ class QuickBooksMigrator(Document): tax_code = purchase_invoice["TxnTaxDetail"]["TxnTaxCodeRef"]["value"] else: tax_code = "NON" - item = frappe.db.get_all("Item", + item = frappe.db.get_all( + "Item", filters={ "quickbooks_id": line["ItemBasedExpenseLineDetail"]["ItemRef"]["value"], - "company": self.company + "company": self.company, }, - fields=["name", "stock_uom"] + fields=["name", "stock_uom"], )[0] - items.append({ - "item_code": item["name"], - "conversion_factor": 1, - "uom": item["stock_uom"], - "description": line.get("Description", line["ItemBasedExpenseLineDetail"]["ItemRef"]["name"]), - "qty": line["ItemBasedExpenseLineDetail"]["Qty"], - "price_list_rate": line["ItemBasedExpenseLineDetail"]["UnitPrice"], - "warehouse": self.default_warehouse, - "cost_center": self.default_cost_center, - "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), - }) + items.append( + { + "item_code": item["name"], + "conversion_factor": 1, + "uom": item["stock_uom"], + "description": line.get( + "Description", line["ItemBasedExpenseLineDetail"]["ItemRef"]["name"] + ), + "qty": line["ItemBasedExpenseLineDetail"]["Qty"], + "price_list_rate": line["ItemBasedExpenseLineDetail"]["UnitPrice"], + "warehouse": self.default_warehouse, + "cost_center": self.default_cost_center, + "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), + } + ) elif line["DetailType"] == "AccountBasedExpenseLineDetail": if line["AccountBasedExpenseLineDetail"]["TaxCodeRef"]["value"] != "TAX": tax_code = line["AccountBasedExpenseLineDetail"]["TaxCodeRef"]["value"] @@ -895,23 +953,30 @@ class QuickBooksMigrator(Document): tax_code = purchase_invoice["TxnTaxDetail"]["TxnTaxCodeRef"]["value"] else: tax_code = "NON" - items.append({ - "item_name": line.get("Description", line["AccountBasedExpenseLineDetail"]["AccountRef"]["name"]), - "conversion_factor": 1, - "expense_account": self._get_account_name_by_id(line["AccountBasedExpenseLineDetail"]["AccountRef"]["value"]), - "uom": "Unit", - "description": line.get("Description", line["AccountBasedExpenseLineDetail"]["AccountRef"]["name"]), - "qty": 1, - "price_list_rate": line["Amount"], - "warehouse": self.default_warehouse, - "cost_center": self.default_cost_center, - "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), - }) + items.append( + { + "item_name": line.get( + "Description", line["AccountBasedExpenseLineDetail"]["AccountRef"]["name"] + ), + "conversion_factor": 1, + "expense_account": self._get_account_name_by_id( + line["AccountBasedExpenseLineDetail"]["AccountRef"]["value"] + ), + "uom": "Unit", + "description": line.get( + "Description", line["AccountBasedExpenseLineDetail"]["AccountRef"]["name"] + ), + "qty": 1, + "price_list_rate": line["Amount"], + "warehouse": self.default_warehouse, + "cost_center": self.default_cost_center, + "item_tax_rate": json.dumps(self._get_item_taxes(tax_code)), + } + ) if is_return: items[-1]["qty"] *= -1 return items - def _save_payment(self, payment): try: quickbooks_id = "Payment - {}".format(payment["Id"]) @@ -928,8 +993,11 @@ class QuickBooksMigrator(Document): if linked_transaction["TxnType"] == "Invoice": si_quickbooks_id = "Invoice - {}".format(linked_transaction["TxnId"]) # Invoice could have been saved as a Sales Invoice or a Journal Entry - if frappe.db.exists({"doctype": "Sales Invoice", "quickbooks_id": si_quickbooks_id, "company": self.company}): - sales_invoice = frappe.get_all("Sales Invoice", + if frappe.db.exists( + {"doctype": "Sales Invoice", "quickbooks_id": si_quickbooks_id, "company": self.company} + ): + sales_invoice = frappe.get_all( + "Sales Invoice", filters={ "quickbooks_id": si_quickbooks_id, "company": self.company, @@ -941,43 +1009,51 @@ class QuickBooksMigrator(Document): party = sales_invoice["customer"] party_account = sales_invoice["debit_to"] - if frappe.db.exists({"doctype": "Journal Entry", "quickbooks_id": si_quickbooks_id, "company": self.company}): - journal_entry = frappe.get_doc("Journal Entry", + if frappe.db.exists( + {"doctype": "Journal Entry", "quickbooks_id": si_quickbooks_id, "company": self.company} + ): + journal_entry = frappe.get_doc( + "Journal Entry", { "quickbooks_id": si_quickbooks_id, "company": self.company, - } + }, ) # Invoice saved as a Journal Entry must have party and party_type set on line containing Receivable Account - customer_account_line = list(filter(lambda acc: acc.party_type == "Customer", journal_entry.accounts))[0] + customer_account_line = list( + filter(lambda acc: acc.party_type == "Customer", journal_entry.accounts) + )[0] reference_type = "Journal Entry" reference_name = journal_entry.name party = customer_account_line.party party_account = customer_account_line.account - accounts.append({ - "party_type": "Customer", - "party": party, - "reference_type": reference_type, - "reference_name": reference_name, - "account": party_account, - "credit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "party_type": "Customer", + "party": party, + "reference_type": reference_type, + "reference_name": reference_name, + "account": party_account, + "credit_in_account_currency": line["Amount"], + "cost_center": self.default_cost_center, + } + ) deposit_account = self._get_account_name_by_id(payment["DepositToAccountRef"]["value"]) - accounts.append({ - "account": deposit_account, - "debit_in_account_currency": payment["TotalAmt"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "account": deposit_account, + "debit_in_account_currency": payment["TotalAmt"], + "cost_center": self.default_cost_center, + } + ) posting_date = payment["TxnDate"] self.__save_journal_entry(quickbooks_id, accounts, posting_date) except Exception as e: self._log_error(e, [payment, accounts]) - def _save_bill_payment(self, bill_payment): try: quickbooks_id = "BillPayment - {}".format(bill_payment["Id"]) @@ -987,8 +1063,11 @@ class QuickBooksMigrator(Document): linked_transaction = line["LinkedTxn"][0] if linked_transaction["TxnType"] == "Bill": pi_quickbooks_id = "Bill - {}".format(linked_transaction["TxnId"]) - if frappe.db.exists({"doctype": "Purchase Invoice", "quickbooks_id": pi_quickbooks_id, "company": self.company}): - purchase_invoice = frappe.get_all("Purchase Invoice", + if frappe.db.exists( + {"doctype": "Purchase Invoice", "quickbooks_id": pi_quickbooks_id, "company": self.company} + ): + purchase_invoice = frappe.get_all( + "Purchase Invoice", filters={ "quickbooks_id": pi_quickbooks_id, "company": self.company, @@ -999,15 +1078,17 @@ class QuickBooksMigrator(Document): reference_name = purchase_invoice["name"] party = purchase_invoice["supplier"] party_account = purchase_invoice["credit_to"] - accounts.append({ - "party_type": "Supplier", - "party": party, - "reference_type": reference_type, - "reference_name": reference_name, - "account": party_account, - "debit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "party_type": "Supplier", + "party": party, + "reference_type": reference_type, + "reference_name": reference_name, + "account": party_account, + "debit_in_account_currency": line["Amount"], + "cost_center": self.default_cost_center, + } + ) if bill_payment["PayType"] == "Check": bank_account_id = bill_payment["CheckPayment"]["BankAccountRef"]["value"] @@ -1015,49 +1096,68 @@ class QuickBooksMigrator(Document): bank_account_id = bill_payment["CreditCardPayment"]["CCAccountRef"]["value"] bank_account = self._get_account_name_by_id(bank_account_id) - accounts.append({ - "account": bank_account, - "credit_in_account_currency": bill_payment["TotalAmt"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "account": bank_account, + "credit_in_account_currency": bill_payment["TotalAmt"], + "cost_center": self.default_cost_center, + } + ) posting_date = bill_payment["TxnDate"] self.__save_journal_entry(quickbooks_id, accounts, posting_date) except Exception as e: self._log_error(e, [bill_payment, accounts]) - def _save_purchase(self, purchase): try: quickbooks_id = "Purchase - {}".format(purchase["Id"]) # Credit Bank Account - accounts = [{ - "account": self._get_account_name_by_id(purchase["AccountRef"]["value"]), - "credit_in_account_currency": purchase["TotalAmt"], - "cost_center": self.default_cost_center, - }] + accounts = [ + { + "account": self._get_account_name_by_id(purchase["AccountRef"]["value"]), + "credit_in_account_currency": purchase["TotalAmt"], + "cost_center": self.default_cost_center, + } + ] # Debit Mentioned Accounts for line in purchase["Line"]: if line["DetailType"] == "AccountBasedExpenseLineDetail": - account = self._get_account_name_by_id(line["AccountBasedExpenseLineDetail"]["AccountRef"]["value"]) + account = self._get_account_name_by_id( + line["AccountBasedExpenseLineDetail"]["AccountRef"]["value"] + ) elif line["DetailType"] == "ItemBasedExpenseLineDetail": - account = frappe.get_doc("Item", - {"quickbooks_id": line["ItemBasedExpenseLineDetail"]["ItemRef"]["value"], "company": self.company} - ).item_defaults[0].expense_account - accounts.append({ - "account": account, - "debit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - }) + account = ( + frappe.get_doc( + "Item", + { + "quickbooks_id": line["ItemBasedExpenseLineDetail"]["ItemRef"]["value"], + "company": self.company, + }, + ) + .item_defaults[0] + .expense_account + ) + accounts.append( + { + "account": account, + "debit_in_account_currency": line["Amount"], + "cost_center": self.default_cost_center, + } + ) # Debit Tax Accounts if "TxnTaxDetail" in purchase: for line in purchase["TxnTaxDetail"]["TaxLine"]: - accounts.append({ - "account": self._get_account_name_by_id("TaxRate - {}".format(line["TaxLineDetail"]["TaxRateRef"]["value"])), - "debit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "account": self._get_account_name_by_id( + "TaxRate - {}".format(line["TaxLineDetail"]["TaxRateRef"]["value"]) + ), + "debit_in_account_currency": line["Amount"], + "cost_center": self.default_cost_center, + } + ) # If purchase["Credit"] is set to be True then it represents a refund if purchase.get("Credit"): @@ -1074,61 +1174,64 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, [purchase, accounts]) - def _save_deposit(self, deposit): try: quickbooks_id = "Deposit - {}".format(deposit["Id"]) # Debit Bank Account - accounts = [{ - "account": self._get_account_name_by_id(deposit["DepositToAccountRef"]["value"]), - "debit_in_account_currency": deposit["TotalAmt"], - "cost_center": self.default_cost_center, - }] + accounts = [ + { + "account": self._get_account_name_by_id(deposit["DepositToAccountRef"]["value"]), + "debit_in_account_currency": deposit["TotalAmt"], + "cost_center": self.default_cost_center, + } + ] # Credit Mentioned Accounts for line in deposit["Line"]: if "LinkedTxn" in line: - accounts.append({ - "account": self.undeposited_funds_account, - "credit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "account": self.undeposited_funds_account, + "credit_in_account_currency": line["Amount"], + "cost_center": self.default_cost_center, + } + ) else: - accounts.append({ - "account": self._get_account_name_by_id(line["DepositLineDetail"]["AccountRef"]["value"]), - "credit_in_account_currency": line["Amount"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "account": self._get_account_name_by_id(line["DepositLineDetail"]["AccountRef"]["value"]), + "credit_in_account_currency": line["Amount"], + "cost_center": self.default_cost_center, + } + ) # Debit Cashback if mentioned if "CashBack" in deposit: - accounts.append({ - "account": self._get_account_name_by_id(deposit["CashBack"]["AccountRef"]["value"]), - "debit_in_account_currency": deposit["CashBack"]["Amount"], - "cost_center": self.default_cost_center, - }) + accounts.append( + { + "account": self._get_account_name_by_id(deposit["CashBack"]["AccountRef"]["value"]), + "debit_in_account_currency": deposit["CashBack"]["Amount"], + "cost_center": self.default_cost_center, + } + ) posting_date = deposit["TxnDate"] self.__save_journal_entry(quickbooks_id, accounts, posting_date) except Exception as e: self._log_error(e, [deposit, accounts]) - def _save_advance_payment(self, advance_payment): quickbooks_id = "Advance Payment - {}".format(advance_payment["id"]) self.__save_ledger_entry_as_je(advance_payment, quickbooks_id) - def _save_tax_payment(self, tax_payment): quickbooks_id = "Tax Payment - {}".format(tax_payment["id"]) self.__save_ledger_entry_as_je(tax_payment, quickbooks_id) - def _save_inventory_qty_adjust(self, inventory_qty_adjust): quickbooks_id = "Inventory Qty Adjust - {}".format(inventory_qty_adjust["id"]) self.__save_ledger_entry_as_je(inventory_qty_adjust, quickbooks_id) - def __save_ledger_entry_as_je(self, ledger_entry, quickbooks_id): try: accounts = [] @@ -1145,7 +1248,6 @@ class QuickBooksMigrator(Document): except Exception as e: self._log_error(e, ledger_entry) - def _get_taxes(self, entry): taxes = [] if "TxnTaxDetail" not in entry or "TaxLine" not in entry["TxnTaxDetail"]: @@ -1155,27 +1257,30 @@ class QuickBooksMigrator(Document): account_head = self._get_account_name_by_id("TaxRate - {}".format(tax_rate)) tax_type_applicable = self._get_tax_type(tax_rate) if tax_type_applicable == "TaxOnAmount": - taxes.append({ - "charge_type": "On Net Total", - "account_head": account_head, - "description": account_head, - "cost_center": self.default_cost_center, - "rate": 0, - }) + taxes.append( + { + "charge_type": "On Net Total", + "account_head": account_head, + "description": account_head, + "cost_center": self.default_cost_center, + "rate": 0, + } + ) else: parent_tax_rate = self._get_parent_tax_rate(tax_rate) parent_row_id = self._get_parent_row_id(parent_tax_rate, taxes) - taxes.append({ - "charge_type": "On Previous Row Amount", - "row_id": parent_row_id, - "account_head": account_head, - "description": account_head, - "cost_center": self.default_cost_center, - "rate": line["TaxLineDetail"]["TaxPercent"], - }) + taxes.append( + { + "charge_type": "On Previous Row Amount", + "row_id": parent_row_id, + "account_head": account_head, + "description": account_head, + "cost_center": self.default_cost_center, + "rate": line["TaxLineDetail"]["TaxPercent"], + } + ) return taxes - def _get_tax_type(self, tax_rate): for tax_code in self.tax_codes.values(): for rate_list_type in ("SalesTaxRateList", "PurchaseTaxRateList"): @@ -1184,7 +1289,6 @@ class QuickBooksMigrator(Document): if tax_rate_detail["TaxRateRef"]["value"] == tax_rate: return tax_rate_detail["TaxTypeApplicable"] - def _get_parent_tax_rate(self, tax_rate): parent = None for tax_code in self.tax_codes.values(): @@ -1198,34 +1302,33 @@ class QuickBooksMigrator(Document): if tax_rate_detail["TaxOrder"] == parent: return tax_rate_detail["TaxRateRef"]["value"] - def _get_parent_row_id(self, tax_rate, taxes): tax_account = self._get_account_name_by_id("TaxRate - {}".format(tax_rate)) for index, tax in enumerate(taxes): if tax["account_head"] == tax_account: return index + 1 - def _create_address(self, entity, doctype, address, address_type): try: if not frappe.db.exists({"doctype": "Address", "quickbooks_id": address["Id"]}): - frappe.get_doc({ - "doctype": "Address", - "quickbooks_address_id": address["Id"], - "address_title": entity.name, - "address_type": address_type, - "address_line1": address["Line1"], - "city": address["City"], - "links": [{"link_doctype": doctype, "link_name": entity.name}] - }).insert() + frappe.get_doc( + { + "doctype": "Address", + "quickbooks_address_id": address["Id"], + "address_title": entity.name, + "address_type": address_type, + "address_line1": address["Line1"], + "city": address["City"], + "links": [{"link_doctype": doctype, "link_name": entity.name}], + } + ).insert() except Exception as e: self._log_error(e, address) - def _get(self, *args, **kwargs): kwargs["headers"] = { "Accept": "application/json", - "Authorization": "Bearer {}".format(self.access_token) + "Authorization": "Bearer {}".format(self.access_token), } response = requests.get(*args, **kwargs) # HTTP Status code 401 here means that the access_token is expired @@ -1236,43 +1339,41 @@ class QuickBooksMigrator(Document): response = self._get(*args, **kwargs) return response - def _get_account_name_by_id(self, quickbooks_id): - return frappe.get_all("Account", filters={"quickbooks_id": quickbooks_id, "company": self.company})[0]["name"] - + return frappe.get_all( + "Account", filters={"quickbooks_id": quickbooks_id, "company": self.company} + )[0]["name"] def _publish(self, *args, **kwargs): frappe.publish_realtime("quickbooks_progress_update", *args, **kwargs) - def _get_unique_account_name(self, quickbooks_name, number=0): if number: quickbooks_account_name = "{} - {} - QB".format(quickbooks_name, number) else: quickbooks_account_name = "{} - QB".format(quickbooks_name) company_encoded_account_name = encode_company_abbr(quickbooks_account_name, self.company) - if frappe.db.exists({"doctype": "Account", "name": company_encoded_account_name, "company": self.company}): + if frappe.db.exists( + {"doctype": "Account", "name": company_encoded_account_name, "company": self.company} + ): unique_account_name = self._get_unique_account_name(quickbooks_name, number + 1) else: unique_account_name = quickbooks_account_name return unique_account_name - def _log_error(self, execption, data=""): - frappe.log_error(title="QuickBooks Migration Error", - message="\n".join([ - "Data", - json.dumps(data, - sort_keys=True, - indent=4, - separators=(',', ': ') - ), - "Exception", - traceback.format_exc() - ]) + frappe.log_error( + title="QuickBooks Migration Error", + message="\n".join( + [ + "Data", + json.dumps(data, sort_keys=True, indent=4, separators=(",", ": ")), + "Exception", + traceback.format_exc(), + ] + ), ) - def set_indicator(self, status): self.status = status self.save() diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py index 5d17ccfb8c4..181afb3f5fb 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py +++ b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.py @@ -25,7 +25,7 @@ def make_shopify_log(status="Queued", exception=None, rollback=False): frappe.db.rollback() if make_new: - log = frappe.get_doc({"doctype":"Shopify Log"}).insert(ignore_permissions=True) + log = frappe.get_doc({"doctype": "Shopify Log"}).insert(ignore_permissions=True) else: log = log = frappe.get_doc("Shopify Log", frappe.flags.request_id) @@ -35,33 +35,49 @@ def make_shopify_log(status="Queued", exception=None, rollback=False): log.save(ignore_permissions=True) frappe.db.commit() + def get_message(exception): message = None - if hasattr(exception, 'message'): + if hasattr(exception, "message"): message = exception.message - elif hasattr(exception, '__str__'): + elif hasattr(exception, "__str__"): message = exception.__str__() else: message = "Something went wrong while syncing" return message + def dump_request_data(data, event="create/order"): event_mapper = { - "orders/create": get_webhook_address(connector_name='shopify_connection', method="sync_sales_order", exclude_uri=True), - "orders/paid" : get_webhook_address(connector_name='shopify_connection', method="prepare_sales_invoice", exclude_uri=True), - "orders/fulfilled": get_webhook_address(connector_name='shopify_connection', method="prepare_delivery_note", exclude_uri=True) + "orders/create": get_webhook_address( + connector_name="shopify_connection", method="sync_sales_order", exclude_uri=True + ), + "orders/paid": get_webhook_address( + connector_name="shopify_connection", method="prepare_sales_invoice", exclude_uri=True + ), + "orders/fulfilled": get_webhook_address( + connector_name="shopify_connection", method="prepare_delivery_note", exclude_uri=True + ), } - log = frappe.get_doc({ - "doctype": "Shopify Log", - "request_data": json.dumps(data, indent=1), - "method": event_mapper[event] - }).insert(ignore_permissions=True) + log = frappe.get_doc( + { + "doctype": "Shopify Log", + "request_data": json.dumps(data, indent=1), + "method": event_mapper[event], + } + ).insert(ignore_permissions=True) frappe.db.commit() - frappe.enqueue(method=event_mapper[event], queue='short', timeout=300, is_async=True, - **{"order": data, "request_id": log.name}) + frappe.enqueue( + method=event_mapper[event], + queue="short", + timeout=300, + is_async=True, + **{"order": data, "request_id": log.name} + ) + @frappe.whitelist() def resync(method, name, request_data): @@ -69,5 +85,10 @@ def resync(method, name, request_data): if not method.startswith("erpnext.erpnext_integrations.connectors.shopify_connection"): return - frappe.enqueue(method=method, queue='short', timeout=300, is_async=True, - **{"order": json.loads(request_data), "request_id": name}) + frappe.enqueue( + method=method, + queue="short", + timeout=300, + is_async=True, + **{"order": json.loads(request_data), "request_id": name} + ) diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py b/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py index 18adeb83aef..8d4506a421f 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py +++ b/erpnext/erpnext_integrations/doctype/shopify_log/test_shopify_log.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Shopify Log') + class TestShopifyLog(unittest.TestCase): pass diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js index a926a7e52a5..a5b676e2e39 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js @@ -37,7 +37,7 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){ } - let app_link = "Ecommerce Integrations" + let app_link = "Ecommerce Integrations" frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true); }) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py index c8ce7d32f70..9628b4d4351 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py @@ -26,28 +26,38 @@ class ShopifySettings(Document): def validate_access_credentials(self): if not (self.get_password(raise_exception=False) and self.api_key and self.shopify_url): - frappe.msgprint(_("Missing value for Password, API Key or Shopify URL"), raise_exception=frappe.ValidationError) + frappe.msgprint( + _("Missing value for Password, API Key or Shopify URL"), raise_exception=frappe.ValidationError + ) def register_webhooks(self): webhooks = ["orders/create", "orders/paid", "orders/fulfilled"] # url = get_shopify_url('admin/webhooks.json', self) created_webhooks = [d.method for d in self.webhooks] - url = get_shopify_url('admin/api/2021-04/webhooks.json', self) + url = get_shopify_url("admin/api/2021-04/webhooks.json", self) for method in webhooks: session = get_request_session() try: - res = session.post(url, data=json.dumps({ - "webhook": { - "topic": method, - "address": get_webhook_address(connector_name='shopify_connection', method='store_request_data', force_https=True), - "format": "json" + res = session.post( + url, + data=json.dumps( + { + "webhook": { + "topic": method, + "address": get_webhook_address( + connector_name="shopify_connection", method="store_request_data", force_https=True + ), + "format": "json", + } } - }), headers=get_header(self)) + ), + headers=get_header(self), + ) res.raise_for_status() self.update_webhook_table(method, res.json()) except HTTPError as e: - error_message = res.json().get('errors', e) + error_message = res.json().get("errors", e) make_shopify_log(status="Warning", exception=error_message, rollback=True) except Exception as e: @@ -58,89 +68,173 @@ class ShopifySettings(Document): deleted_webhooks = [] for d in self.webhooks: - url = get_shopify_url('admin/api/2021-04/webhooks/{0}.json'.format(d.webhook_id), self) + url = get_shopify_url("admin/api/2021-04/webhooks/{0}.json".format(d.webhook_id), self) try: res = session.delete(url, headers=get_header(self)) res.raise_for_status() deleted_webhooks.append(d) except HTTPError as e: - error_message = res.json().get('errors', e) + error_message = res.json().get("errors", e) make_shopify_log(status="Warning", exception=error_message, rollback=True) except Exception as e: - frappe.log_error(message=e, title='Shopify Webhooks Issue') + frappe.log_error(message=e, title="Shopify Webhooks Issue") for d in deleted_webhooks: self.remove(d) def update_webhook_table(self, method, res): - self.append("webhooks", { - "webhook_id": res['webhook']['id'], - "method": method - }) + self.append("webhooks", {"webhook_id": res["webhook"]["id"], "method": method}) + def get_shopify_url(path, settings): if settings.app_type == "Private": - return 'https://{}:{}@{}/{}'.format(settings.api_key, settings.get_password('password'), settings.shopify_url, path) + return "https://{}:{}@{}/{}".format( + settings.api_key, settings.get_password("password"), settings.shopify_url, path + ) else: - return 'https://{}/{}'.format(settings.shopify_url, path) + return "https://{}/{}".format(settings.shopify_url, path) + def get_header(settings): - header = {'Content-Type': 'application/json'} + header = {"Content-Type": "application/json"} return header + @frappe.whitelist() def get_series(): return { - "sales_order_series" : frappe.get_meta("Sales Order").get_options("naming_series") or "SO-Shopify-", - "sales_invoice_series" : frappe.get_meta("Sales Invoice").get_options("naming_series") or "SI-Shopify-", - "delivery_note_series" : frappe.get_meta("Delivery Note").get_options("naming_series") or "DN-Shopify-" + "sales_order_series": frappe.get_meta("Sales Order").get_options("naming_series") + or "SO-Shopify-", + "sales_invoice_series": frappe.get_meta("Sales Invoice").get_options("naming_series") + or "SI-Shopify-", + "delivery_note_series": frappe.get_meta("Delivery Note").get_options("naming_series") + or "DN-Shopify-", } + def setup_custom_fields(): custom_fields = { "Customer": [ - dict(fieldname='shopify_customer_id', label='Shopify Customer Id', - fieldtype='Data', insert_after='series', read_only=1, print_hide=1) + dict( + fieldname="shopify_customer_id", + label="Shopify Customer Id", + fieldtype="Data", + insert_after="series", + read_only=1, + print_hide=1, + ) ], "Supplier": [ - dict(fieldname='shopify_supplier_id', label='Shopify Supplier Id', - fieldtype='Data', insert_after='supplier_name', read_only=1, print_hide=1) + dict( + fieldname="shopify_supplier_id", + label="Shopify Supplier Id", + fieldtype="Data", + insert_after="supplier_name", + read_only=1, + print_hide=1, + ) ], "Address": [ - dict(fieldname='shopify_address_id', label='Shopify Address Id', - fieldtype='Data', insert_after='fax', read_only=1, print_hide=1) + dict( + fieldname="shopify_address_id", + label="Shopify Address Id", + fieldtype="Data", + insert_after="fax", + read_only=1, + print_hide=1, + ) ], "Item": [ - dict(fieldname='shopify_variant_id', label='Shopify Variant Id', - fieldtype='Data', insert_after='item_code', read_only=1, print_hide=1), - dict(fieldname='shopify_product_id', label='Shopify Product Id', - fieldtype='Data', insert_after='item_code', read_only=1, print_hide=1), - dict(fieldname='shopify_description', label='Shopify Description', - fieldtype='Text Editor', insert_after='description', read_only=1, print_hide=1) + dict( + fieldname="shopify_variant_id", + label="Shopify Variant Id", + fieldtype="Data", + insert_after="item_code", + read_only=1, + print_hide=1, + ), + dict( + fieldname="shopify_product_id", + label="Shopify Product Id", + fieldtype="Data", + insert_after="item_code", + read_only=1, + print_hide=1, + ), + dict( + fieldname="shopify_description", + label="Shopify Description", + fieldtype="Text Editor", + insert_after="description", + read_only=1, + print_hide=1, + ), ], "Sales Order": [ - dict(fieldname='shopify_order_id', label='Shopify Order Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1), - dict(fieldname='shopify_order_number', label='Shopify Order Number', - fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1) + dict( + fieldname="shopify_order_id", + label="Shopify Order Id", + fieldtype="Data", + insert_after="title", + read_only=1, + print_hide=1, + ), + dict( + fieldname="shopify_order_number", + label="Shopify Order Number", + fieldtype="Data", + insert_after="shopify_order_id", + read_only=1, + print_hide=1, + ), ], - "Delivery Note":[ - dict(fieldname='shopify_order_id', label='Shopify Order Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1), - dict(fieldname='shopify_order_number', label='Shopify Order Number', - fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1), - dict(fieldname='shopify_fulfillment_id', label='Shopify Fulfillment Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1) + "Delivery Note": [ + dict( + fieldname="shopify_order_id", + label="Shopify Order Id", + fieldtype="Data", + insert_after="title", + read_only=1, + print_hide=1, + ), + dict( + fieldname="shopify_order_number", + label="Shopify Order Number", + fieldtype="Data", + insert_after="shopify_order_id", + read_only=1, + print_hide=1, + ), + dict( + fieldname="shopify_fulfillment_id", + label="Shopify Fulfillment Id", + fieldtype="Data", + insert_after="title", + read_only=1, + print_hide=1, + ), ], "Sales Invoice": [ - dict(fieldname='shopify_order_id', label='Shopify Order Id', - fieldtype='Data', insert_after='title', read_only=1, print_hide=1), - dict(fieldname='shopify_order_number', label='Shopify Order Number', - fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1) - ] + dict( + fieldname="shopify_order_id", + label="Shopify Order Id", + fieldtype="Data", + insert_after="title", + read_only=1, + print_hide=1, + ), + dict( + fieldname="shopify_order_number", + label="Shopify Order Number", + fieldtype="Data", + insert_after="shopify_order_id", + read_only=1, + print_hide=1, + ), + ], } create_custom_fields(custom_fields) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py index 945862ddbc8..6f76d8a89e7 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_customer.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ @@ -6,21 +5,29 @@ from frappe import _ def create_customer(shopify_customer, shopify_settings): import frappe.utils.nestedset - cust_name = (shopify_customer.get("first_name") + " " + (shopify_customer.get("last_name") \ - and shopify_customer.get("last_name") or "")) if shopify_customer.get("first_name")\ + cust_name = ( + ( + shopify_customer.get("first_name") + + " " + + (shopify_customer.get("last_name") and shopify_customer.get("last_name") or "") + ) + if shopify_customer.get("first_name") else shopify_customer.get("email") + ) try: - customer = frappe.get_doc({ - "doctype": "Customer", - "name": shopify_customer.get("id"), - "customer_name" : cust_name, - "shopify_customer_id": shopify_customer.get("id"), - "sync_with_shopify": 1, - "customer_group": shopify_settings.customer_group, - "territory": frappe.utils.nestedset.get_root_of("Territory"), - "customer_type": _("Individual") - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "name": shopify_customer.get("id"), + "customer_name": cust_name, + "shopify_customer_id": shopify_customer.get("id"), + "sync_with_shopify": 1, + "customer_group": shopify_settings.customer_group, + "territory": frappe.utils.nestedset.get_root_of("Territory"), + "customer_type": _("Individual"), + } + ) customer.flags.ignore_mandatory = True customer.insert(ignore_permissions=True) @@ -32,6 +39,7 @@ def create_customer(shopify_customer, shopify_settings): except Exception as e: raise e + def create_customer_address(customer, shopify_customer): addresses = shopify_customer.get("addresses", []) @@ -40,29 +48,29 @@ def create_customer_address(customer, shopify_customer): for i, address in enumerate(addresses): address_title, address_type = get_address_title_and_type(customer.customer_name, i) - try : - frappe.get_doc({ - "doctype": "Address", - "shopify_address_id": address.get("id"), - "address_title": address_title, - "address_type": address_type, - "address_line1": address.get("address1") or "Address 1", - "address_line2": address.get("address2"), - "city": address.get("city") or "City", - "state": address.get("province"), - "pincode": address.get("zip"), - "country": address.get("country"), - "phone": address.get("phone"), - "email_id": shopify_customer.get("email"), - "links": [{ - "link_doctype": "Customer", - "link_name": customer.name - }] - }).insert(ignore_mandatory=True) + try: + frappe.get_doc( + { + "doctype": "Address", + "shopify_address_id": address.get("id"), + "address_title": address_title, + "address_type": address_type, + "address_line1": address.get("address1") or "Address 1", + "address_line2": address.get("address2"), + "city": address.get("city") or "City", + "state": address.get("province"), + "pincode": address.get("zip"), + "country": address.get("country"), + "phone": address.get("phone"), + "email_id": shopify_customer.get("email"), + "links": [{"link_doctype": "Customer", "link_name": customer.name}], + } + ).insert(ignore_mandatory=True) except Exception as e: raise e + def get_address_title_and_type(customer_name, index): address_type = _("Billing") address_title = customer_name diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py index 0fcf20c596e..6b0f59774a5 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ from frappe.utils import cint, cstr, get_request_session @@ -11,8 +10,11 @@ from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings impo shopify_variants_attr_list = ["option1", "option2", "option3"] + def sync_item_from_shopify(shopify_settings, item): - url = get_shopify_url("admin/api/2021-04/products/{0}.json".format(item.get("product_id")), shopify_settings) + url = get_shopify_url( + "admin/api/2021-04/products/{0}.json".format(item.get("product_id")), shopify_settings + ) session = get_request_session() try: @@ -24,6 +26,7 @@ def sync_item_from_shopify(shopify_settings, item): except Exception as e: raise e + def make_item(warehouse, shopify_item): add_item_weight(shopify_item) @@ -33,34 +36,38 @@ def make_item(warehouse, shopify_item): create_item_variants(shopify_item, warehouse, attributes, shopify_variants_attr_list) else: - shopify_item["variant_id"] = shopify_item['variants'][0]["id"] + shopify_item["variant_id"] = shopify_item["variants"][0]["id"] create_item(shopify_item, warehouse) + def add_item_weight(shopify_item): - shopify_item["weight"] = shopify_item['variants'][0]["weight"] - shopify_item["weight_unit"] = shopify_item['variants'][0]["weight_unit"] + shopify_item["weight"] = shopify_item["variants"][0]["weight"] + shopify_item["weight_unit"] = shopify_item["variants"][0]["weight_unit"] + def has_variants(shopify_item): - if len(shopify_item.get("options")) >= 1 and "Default Title" not in shopify_item.get("options")[0]["values"]: + if ( + len(shopify_item.get("options")) >= 1 + and "Default Title" not in shopify_item.get("options")[0]["values"] + ): return True return False + def create_attribute(shopify_item): attribute = [] # shopify item dict - for attr in shopify_item.get('options'): + for attr in shopify_item.get("options"): if not frappe.db.get_value("Item Attribute", attr.get("name"), "name"): - frappe.get_doc({ - "doctype": "Item Attribute", - "attribute_name": attr.get("name"), - "item_attribute_values": [ - { - "attribute_value": attr_value, - "abbr":attr_value - } - for attr_value in attr.get("values") - ] - }).insert() + frappe.get_doc( + { + "doctype": "Item Attribute", + "attribute_name": attr.get("name"), + "item_attribute_values": [ + {"attribute_value": attr_value, "abbr": attr_value} for attr_value in attr.get("values") + ], + } + ).insert() attribute.append({"attribute": attr.get("name")}) else: @@ -72,26 +79,29 @@ def create_attribute(shopify_item): attribute.append({"attribute": attr.get("name")}) else: - attribute.append({ - "attribute": attr.get("name"), - "from_range": item_attr.get("from_range"), - "to_range": item_attr.get("to_range"), - "increment": item_attr.get("increment"), - "numeric_values": item_attr.get("numeric_values") - }) + attribute.append( + { + "attribute": attr.get("name"), + "from_range": item_attr.get("from_range"), + "to_range": item_attr.get("to_range"), + "increment": item_attr.get("increment"), + "numeric_values": item_attr.get("numeric_values"), + } + ) return attribute + def set_new_attribute_values(item_attr, values): for attr_value in values: - if not any((d.abbr.lower() == attr_value.lower() or d.attribute_value.lower() == attr_value.lower())\ - for d in item_attr.item_attribute_values): - item_attr.append("item_attribute_values", { - "attribute_value": attr_value, - "abbr": attr_value - }) + if not any( + (d.abbr.lower() == attr_value.lower() or d.attribute_value.lower() == attr_value.lower()) + for d in item_attr.item_attribute_values + ): + item_attr.append("item_attribute_values", {"attribute_value": attr_value, "abbr": attr_value}) -def create_item(shopify_item, warehouse, has_variant=0, attributes=None,variant_of=None): + +def create_item(shopify_item, warehouse, has_variant=0, attributes=None, variant_of=None): item_dict = { "doctype": "Item", "shopify_product_id": shopify_item.get("id"), @@ -100,12 +110,12 @@ def create_item(shopify_item, warehouse, has_variant=0, attributes=None,variant_ "sync_with_shopify": 1, "is_stock_item": 1, "item_code": cstr(shopify_item.get("item_code")) or cstr(shopify_item.get("id")), - "item_name": shopify_item.get("title", '').strip(), + "item_name": shopify_item.get("title", "").strip(), "description": shopify_item.get("body_html") or shopify_item.get("title"), "shopify_description": shopify_item.get("body_html") or shopify_item.get("title"), "item_group": get_item_group(shopify_item.get("product_type")), "has_variants": has_variant, - "attributes":attributes or [], + "attributes": attributes or [], "stock_uom": shopify_item.get("uom") or _("Nos"), "stock_keeping_unit": shopify_item.get("sku") or get_sku(shopify_item), "default_warehouse": warehouse, @@ -113,16 +123,12 @@ def create_item(shopify_item, warehouse, has_variant=0, attributes=None,variant_ "weight_uom": shopify_item.get("weight_unit"), "weight_per_unit": shopify_item.get("weight"), "default_supplier": get_supplier(shopify_item), - "item_defaults": [ - { - "company": get_default_company() - } - ] + "item_defaults": [{"company": get_default_company()}], } if not is_item_exists(item_dict, attributes, variant_of=variant_of): item_details = get_item_details(shopify_item) - name = '' + name = "" if not item_details: new_item = frappe.get_doc(item_dict) @@ -137,14 +143,19 @@ def create_item(shopify_item, warehouse, has_variant=0, attributes=None,variant_ frappe.db.commit() + def create_item_variants(shopify_item, warehouse, attributes, shopify_variants_attr_list): - template_item = frappe.db.get_value("Item", filters={"shopify_product_id": shopify_item.get("id")}, - fieldname=["name", "stock_uom"], as_dict=True) + template_item = frappe.db.get_value( + "Item", + filters={"shopify_product_id": shopify_item.get("id")}, + fieldname=["name", "stock_uom"], + as_dict=True, + ) if template_item: for variant in shopify_item.get("variants"): shopify_item_variant = { - "id" : variant.get("id"), + "id": variant.get("id"), "item_code": variant.get("id"), "title": variant.get("title"), "product_type": shopify_item.get("product_type"), @@ -153,32 +164,42 @@ def create_item_variants(shopify_item, warehouse, attributes, shopify_variants_a "item_price": variant.get("price"), "variant_id": variant.get("id"), "weight_unit": variant.get("weight_unit"), - "weight": variant.get("weight") + "weight": variant.get("weight"), } for i, variant_attr in enumerate(shopify_variants_attr_list): if variant.get(variant_attr): - attributes[i].update({"attribute_value": get_attribute_value(variant.get(variant_attr), attributes[i])}) + attributes[i].update( + {"attribute_value": get_attribute_value(variant.get(variant_attr), attributes[i])} + ) create_item(shopify_item_variant, warehouse, 0, attributes, template_item.name) + def get_attribute_value(variant_attr_val, attribute): - attribute_value = frappe.db.sql("""select attribute_value from `tabItem Attribute Value` - where parent = %s and (abbr = %s or attribute_value = %s)""", (attribute["attribute"], variant_attr_val, - variant_attr_val), as_list=1) - return attribute_value[0][0] if len(attribute_value)>0 else cint(variant_attr_val) + attribute_value = frappe.db.sql( + """select attribute_value from `tabItem Attribute Value` + where parent = %s and (abbr = %s or attribute_value = %s)""", + (attribute["attribute"], variant_attr_val, variant_attr_val), + as_list=1, + ) + return attribute_value[0][0] if len(attribute_value) > 0 else cint(variant_attr_val) + def get_item_group(product_type=None): import frappe.utils.nestedset + parent_item_group = frappe.utils.nestedset.get_root_of("Item Group") if product_type: if not frappe.db.get_value("Item Group", product_type, "name"): - item_group = frappe.get_doc({ - "doctype": "Item Group", - "item_group_name": product_type, - "parent_item_group": parent_item_group, - "is_group": "No" - }).insert() + item_group = frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": product_type, + "parent_item_group": parent_item_group, + "is_group": "No", + } + ).insert() return item_group.name else: return product_type @@ -191,74 +212,97 @@ def get_sku(item): return item.get("variants")[0].get("sku") return "" + def add_to_price_list(item, name): - shopify_settings = frappe.db.get_value("Shopify Settings", None, ["price_list", "update_price_in_erpnext_price_list"], as_dict=1) + shopify_settings = frappe.db.get_value( + "Shopify Settings", None, ["price_list", "update_price_in_erpnext_price_list"], as_dict=1 + ) if not shopify_settings.update_price_in_erpnext_price_list: return - item_price_name = frappe.db.get_value("Item Price", - {"item_code": name, "price_list": shopify_settings.price_list}, "name") + item_price_name = frappe.db.get_value( + "Item Price", {"item_code": name, "price_list": shopify_settings.price_list}, "name" + ) if not item_price_name: - frappe.get_doc({ - "doctype": "Item Price", - "price_list": shopify_settings.price_list, - "item_code": name, - "price_list_rate": item.get("item_price") or item.get("variants")[0].get("price") - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": shopify_settings.price_list, + "item_code": name, + "price_list_rate": item.get("item_price") or item.get("variants")[0].get("price"), + } + ).insert() else: item_rate = frappe.get_doc("Item Price", item_price_name) item_rate.price_list_rate = item.get("item_price") or item.get("variants")[0].get("price") item_rate.save() + def get_item_image(shopify_item): if shopify_item.get("image"): return shopify_item.get("image").get("src") return None + def get_supplier(shopify_item): if shopify_item.get("vendor"): - supplier = frappe.db.sql("""select name from tabSupplier - where name = %s or shopify_supplier_id = %s """, (shopify_item.get("vendor"), - shopify_item.get("vendor").lower()), as_list=1) + supplier = frappe.db.sql( + """select name from tabSupplier + where name = %s or shopify_supplier_id = %s """, + (shopify_item.get("vendor"), shopify_item.get("vendor").lower()), + as_list=1, + ) if not supplier: - supplier = frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": shopify_item.get("vendor"), - "shopify_supplier_id": shopify_item.get("vendor").lower(), - "supplier_group": get_supplier_group() - }).insert() + supplier = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": shopify_item.get("vendor"), + "shopify_supplier_id": shopify_item.get("vendor").lower(), + "supplier_group": get_supplier_group(), + } + ).insert() return supplier.name else: return shopify_item.get("vendor") else: return "" + def get_supplier_group(): supplier_group = frappe.db.get_value("Supplier Group", _("Shopify Supplier")) if not supplier_group: - supplier_group = frappe.get_doc({ - "doctype": "Supplier Group", - "supplier_group_name": _("Shopify Supplier") - }).insert() + supplier_group = frappe.get_doc( + {"doctype": "Supplier Group", "supplier_group_name": _("Shopify Supplier")} + ).insert() return supplier_group.name return supplier_group + def get_item_details(shopify_item): item_details = {} - item_details = frappe.db.get_value("Item", {"shopify_product_id": shopify_item.get("id")}, - ["name", "stock_uom", "item_name"], as_dict=1) + item_details = frappe.db.get_value( + "Item", + {"shopify_product_id": shopify_item.get("id")}, + ["name", "stock_uom", "item_name"], + as_dict=1, + ) if item_details: return item_details else: - item_details = frappe.db.get_value("Item", {"shopify_variant_id": shopify_item.get("id")}, - ["name", "stock_uom", "item_name"], as_dict=1) + item_details = frappe.db.get_value( + "Item", + {"shopify_variant_id": shopify_item.get("id")}, + ["name", "stock_uom", "item_name"], + as_dict=1, + ) return item_details + def is_item_exists(shopify_item, attributes=None, variant_of=None): if variant_of: name = variant_of @@ -267,7 +311,7 @@ def is_item_exists(shopify_item, attributes=None, variant_of=None): if name: item = frappe.get_doc("Item", name) - item.flags.ignore_mandatory=True + item.flags.ignore_mandatory = True if not variant_of and not item.shopify_product_id: item.shopify_product_id = shopify_item.get("shopify_product_id") @@ -277,23 +321,35 @@ def is_item_exists(shopify_item, attributes=None, variant_of=None): if item.shopify_product_id and attributes and attributes[0].get("attribute_value"): if not variant_of: - variant_of = frappe.db.get_value("Item", - {"shopify_product_id": item.shopify_product_id}, "variant_of") + variant_of = frappe.db.get_value( + "Item", {"shopify_product_id": item.shopify_product_id}, "variant_of" + ) # create conditions for all item attributes, # as we are putting condition basis on OR it will fetch all items matching either of conditions # thus comparing matching conditions with len(attributes) # which will give exact matching variant item. - conditions = ["(iv.attribute='{0}' and iv.attribute_value = '{1}')"\ - .format(attr.get("attribute"), attr.get("attribute_value")) for attr in attributes] + conditions = [ + "(iv.attribute='{0}' and iv.attribute_value = '{1}')".format( + attr.get("attribute"), attr.get("attribute_value") + ) + for attr in attributes + ] - conditions = "( {0} ) and iv.parent = it.name ) = {1}".format(" or ".join(conditions), len(attributes)) + conditions = "( {0} ) and iv.parent = it.name ) = {1}".format( + " or ".join(conditions), len(attributes) + ) - parent = frappe.db.sql(""" select * from tabItem it where + parent = frappe.db.sql( + """ select * from tabItem it where ( select count(*) from `tabItem Variant Attribute` iv - where {conditions} and it.variant_of = %s """.format(conditions=conditions) , - variant_of, as_list=1) + where {conditions} and it.variant_of = %s """.format( + conditions=conditions + ), + variant_of, + as_list=1, + ) if parent: variant = frappe.get_doc("Item", parent[0][0]) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py index 41e26f3067f..7cc45d2115f 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py @@ -22,9 +22,11 @@ class ShopifySettings(unittest.TestCase): def setUpClass(cls): frappe.set_user("Administrator") - cls.allow_negative_stock = cint(frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')) + cls.allow_negative_stock = cint( + frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + ) if not cls.allow_negative_stock: - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) setup_custom_fields() @@ -38,74 +40,86 @@ class ShopifySettings(unittest.TestCase): @classmethod def tearDownClass(cls): if not cls.allow_negative_stock: - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0) @classmethod def setup_shopify(cls): shopify_settings = frappe.get_doc("Shopify Settings") shopify_settings.taxes = [] - shopify_settings.update({ - "app_type": "Private", - "shopify_url": "test.myshopify.com", - "api_key": "17702c7c4452b9c5d235240b6e7a39da", - "password": "17702c7c4452b9c5d235240b6e7a39da", - "shared_secret": "17702c7c4452b9c5d235240b6e7a39da", - "price_list": "_Test Price List", - "warehouse": "_Test Warehouse - _TC", - "cash_bank_account": "Cash - _TC", - "account": "Cash - _TC", - "customer_group": "_Test Customer Group", - "cost_center": "Main - _TC", - "taxes": [ - { - "shopify_tax": "International Shipping", - "tax_account":"Legal Expenses - _TC" - } - ], - "enable_shopify": 0, - "sales_order_series": "SO-", - "sync_sales_invoice": 1, - "sales_invoice_series": "SINV-", - "sync_delivery_note": 1, - "delivery_note_series": "DN-" - }).save(ignore_permissions=True) + shopify_settings.update( + { + "app_type": "Private", + "shopify_url": "test.myshopify.com", + "api_key": "17702c7c4452b9c5d235240b6e7a39da", + "password": "17702c7c4452b9c5d235240b6e7a39da", + "shared_secret": "17702c7c4452b9c5d235240b6e7a39da", + "price_list": "_Test Price List", + "warehouse": "_Test Warehouse - _TC", + "cash_bank_account": "Cash - _TC", + "account": "Cash - _TC", + "customer_group": "_Test Customer Group", + "cost_center": "Main - _TC", + "taxes": [{"shopify_tax": "International Shipping", "tax_account": "Legal Expenses - _TC"}], + "enable_shopify": 0, + "sales_order_series": "SO-", + "sync_sales_invoice": 1, + "sales_invoice_series": "SINV-", + "sync_delivery_note": 1, + "delivery_note_series": "DN-", + } + ).save(ignore_permissions=True) cls.shopify_settings = shopify_settings def test_order(self): # Create Customer - with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer: + with open( + os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json") + ) as shopify_customer: shopify_customer = json.load(shopify_customer) create_customer(shopify_customer.get("customer"), self.shopify_settings) # Create Item - with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_item.json")) as shopify_item: + with open( + os.path.join(os.path.dirname(__file__), "test_data", "shopify_item.json") + ) as shopify_item: shopify_item = json.load(shopify_item) make_item("_Test Warehouse - _TC", shopify_item.get("product")) # Create Order - with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order: + with open( + os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json") + ) as shopify_order: shopify_order = json.load(shopify_order) create_order(shopify_order.get("order"), self.shopify_settings, False, company="_Test Company") - sales_order = frappe.get_doc("Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))}) + sales_order = frappe.get_doc( + "Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))} + ) self.assertEqual(cstr(shopify_order.get("order").get("id")), sales_order.shopify_order_id) # Check for customer shopify_order_customer_id = cstr(shopify_order.get("order").get("customer").get("id")) - sales_order_customer_id = frappe.get_value("Customer", sales_order.customer, "shopify_customer_id") + sales_order_customer_id = frappe.get_value( + "Customer", sales_order.customer, "shopify_customer_id" + ) self.assertEqual(shopify_order_customer_id, sales_order_customer_id) # Check sales invoice - sales_invoice = frappe.get_doc("Sales Invoice", {"shopify_order_id": sales_order.shopify_order_id}) + sales_invoice = frappe.get_doc( + "Sales Invoice", {"shopify_order_id": sales_order.shopify_order_id} + ) self.assertEqual(sales_invoice.rounded_total, sales_order.rounded_total) # Check delivery note - delivery_note_count = frappe.db.sql("""select count(*) from `tabDelivery Note` - where shopify_order_id = %s""", sales_order.shopify_order_id)[0][0] + delivery_note_count = frappe.db.sql( + """select count(*) from `tabDelivery Note` + where shopify_order_id = %s""", + sales_order.shopify_order_id, + )[0][0] self.assertEqual(delivery_note_count, len(shopify_order.get("order").get("fulfillments"))) diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index 54ed6f7d115..833bbdfa3f3 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -36,6 +36,7 @@ def new_doc(document): return doc + class TallyMigration(Document): def validate(self): failed_import_log = json.loads(self.failed_import_log) @@ -73,14 +74,16 @@ class TallyMigration(Document): def dump_processed_data(self, data): for key, value in data.items(): - f = frappe.get_doc({ - "doctype": "File", - "file_name": key + ".json", - "attached_to_doctype": self.doctype, - "attached_to_name": self.name, - "content": json.dumps(value), - "is_private": True - }) + f = frappe.get_doc( + { + "doctype": "File", + "file_name": key + ".json", + "attached_to_doctype": self.doctype, + "attached_to_name": self.name, + "content": json.dumps(value), + "is_private": True, + } + ) try: f.insert() except frappe.DuplicateEntryError: @@ -88,8 +91,12 @@ class TallyMigration(Document): setattr(self, key, f.file_url) def set_account_defaults(self): - self.default_cost_center, self.default_round_off_account = frappe.db.get_value("Company", self.erpnext_company, ["cost_center", "round_off_account"]) - self.default_warehouse = frappe.db.get_value("Stock Settings", "Stock Settings", "default_warehouse") + self.default_cost_center, self.default_round_off_account = frappe.db.get_value( + "Company", self.erpnext_company, ["cost_center", "round_off_account"] + ) + self.default_warehouse = frappe.db.get_value( + "Stock Settings", "Stock Settings", "default_warehouse" + ) def _process_master_data(self): def get_company_name(collection): @@ -100,18 +107,24 @@ class TallyMigration(Document): "Application of Funds (Assets)": "Asset", "Expenses": "Expense", "Income": "Income", - "Source of Funds (Liabilities)": "Liability" + "Source of Funds (Liabilities)": "Liability", } roots = set(root_type_map.keys()) - accounts = list(get_groups(collection.find_all("GROUP"))) + list(get_ledgers(collection.find_all("LEDGER"))) + accounts = list(get_groups(collection.find_all("GROUP"))) + list( + get_ledgers(collection.find_all("LEDGER")) + ) children, parents = get_children_and_parent_dict(accounts) - group_set = [acc[1] for acc in accounts if acc[2]] + group_set = [acc[1] for acc in accounts if acc[2]] children, customers, suppliers = remove_parties(parents, children, group_set) try: coa = traverse({}, children, roots, roots, group_set) except RecursionError: - self.log(_("Error occured while parsing Chart of Accounts: Please make sure that no two accounts have the same name")) + self.log( + _( + "Error occured while parsing Chart of Accounts: Please make sure that no two accounts have the same name" + ) + ) for account in coa: coa[account]["root_type"] = root_type_map[account] @@ -185,42 +198,48 @@ class TallyMigration(Document): links = [] if account.NAME.string.strip() in customers: party_type = "Customer" - parties.append({ - "doctype": party_type, - "customer_name": account.NAME.string.strip(), - "tax_id": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None, - "customer_group": "All Customer Groups", - "territory": "All Territories", - "customer_type": "Individual", - }) + parties.append( + { + "doctype": party_type, + "customer_name": account.NAME.string.strip(), + "tax_id": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None, + "customer_group": "All Customer Groups", + "territory": "All Territories", + "customer_type": "Individual", + } + ) links.append({"link_doctype": party_type, "link_name": account["NAME"]}) if account.NAME.string.strip() in suppliers: party_type = "Supplier" - parties.append({ - "doctype": party_type, - "supplier_name": account.NAME.string.strip(), - "pan": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None, - "supplier_group": "All Supplier Groups", - "supplier_type": "Individual", - }) + parties.append( + { + "doctype": party_type, + "supplier_name": account.NAME.string.strip(), + "pan": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None, + "supplier_group": "All Supplier Groups", + "supplier_type": "Individual", + } + ) links.append({"link_doctype": party_type, "link_name": account["NAME"]}) if party_type: address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")]) - addresses.append({ - "doctype": "Address", - "address_line1": address[:140].strip(), - "address_line2": address[140:].strip(), - "country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None, - "state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None, - "gst_state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None, - "pin_code": account.PINCODE.string.strip() if account.PINCODE else None, - "mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, - "phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, - "gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None, - "links": links - }) + addresses.append( + { + "doctype": "Address", + "address_line1": address[:140].strip(), + "address_line2": address[140:].strip(), + "country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None, + "state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None, + "gst_state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None, + "pin_code": account.PINCODE.string.strip() if account.PINCODE else None, + "mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, + "phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, + "gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None, + "links": links, + } + ) return parties, addresses def get_stock_items_uoms(collection): @@ -231,14 +250,16 @@ class TallyMigration(Document): items = [] for item in collection.find_all("STOCKITEM"): stock_uom = item.BASEUNITS.string.strip() if item.BASEUNITS else self.default_uom - items.append({ - "doctype": "Item", - "item_code" : item.NAME.string.strip(), - "stock_uom": stock_uom.strip(), - "is_stock_item": 0, - "item_group": "All Item Groups", - "item_defaults": [{"company": self.erpnext_company}] - }) + items.append( + { + "doctype": "Item", + "item_code": item.NAME.string.strip(), + "stock_uom": stock_uom.strip(), + "is_stock_item": 0, + "item_group": "All Item Groups", + "item_defaults": [{"company": self.erpnext_company}], + } + ) return items, uoms @@ -257,7 +278,13 @@ class TallyMigration(Document): self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5) items, uoms = get_stock_items_uoms(collection) - data = {"chart_of_accounts": chart_of_accounts, "parties": parties, "addresses": addresses, "items": items, "uoms": uoms} + data = { + "chart_of_accounts": chart_of_accounts, + "parties": parties, + "addresses": addresses, + "items": items, + "uoms": uoms, + } self.publish("Process Master Data", _("Done"), 5, 5) self.dump_processed_data(data) @@ -272,7 +299,10 @@ class TallyMigration(Document): self.set_status() def publish(self, title, message, count, total): - frappe.publish_realtime("tally_migration_progress_update", {"title": title, "message": message, "count": count, "total": total}) + frappe.publish_realtime( + "tally_migration_progress_update", + {"title": title, "message": message, "count": count, "total": total}, + ) def _import_master_data(self): def create_company_and_coa(coa_file_url): @@ -280,12 +310,14 @@ class TallyMigration(Document): frappe.local.flags.ignore_chart_of_accounts = True try: - company = frappe.get_doc({ - "doctype": "Company", - "company_name": self.erpnext_company, - "default_currency": "INR", - "enable_perpetual_inventory": 0, - }).insert() + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": self.erpnext_company, + "default_currency": "INR", + "enable_perpetual_inventory": 0, + } + ).insert() except frappe.DuplicateEntryError: company = frappe.get_doc("Company", self.erpnext_company) unset_existing_data(self.erpnext_company) @@ -358,8 +390,16 @@ class TallyMigration(Document): for voucher in collection.find_all("VOUCHER"): if voucher.ISCANCELLED.string.strip() == "Yes": continue - inventory_entries = voucher.find_all("INVENTORYENTRIES.LIST") + voucher.find_all("ALLINVENTORYENTRIES.LIST") + voucher.find_all("INVENTORYENTRIESIN.LIST") + voucher.find_all("INVENTORYENTRIESOUT.LIST") - if voucher.VOUCHERTYPENAME.string.strip() not in ["Journal", "Receipt", "Payment", "Contra"] and inventory_entries: + inventory_entries = ( + voucher.find_all("INVENTORYENTRIES.LIST") + + voucher.find_all("ALLINVENTORYENTRIES.LIST") + + voucher.find_all("INVENTORYENTRIESIN.LIST") + + voucher.find_all("INVENTORYENTRIESOUT.LIST") + ) + if ( + voucher.VOUCHERTYPENAME.string.strip() not in ["Journal", "Receipt", "Payment", "Contra"] + and inventory_entries + ): function = voucher_to_invoice else: function = voucher_to_journal_entry @@ -375,9 +415,14 @@ class TallyMigration(Document): def voucher_to_journal_entry(voucher): accounts = [] - ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST") + ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all( + "LEDGERENTRIES.LIST" + ) for entry in ledger_entries: - account = {"account": encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company), "cost_center": self.default_cost_center} + account = { + "account": encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company), + "cost_center": self.default_cost_center, + } if entry.ISPARTYLEDGER.string.strip() == "Yes": party_details = get_party(entry.LEDGERNAME.string.strip()) if party_details: @@ -438,7 +483,12 @@ class TallyMigration(Document): return invoice def get_voucher_items(voucher, doctype): - inventory_entries = voucher.find_all("INVENTORYENTRIES.LIST") + voucher.find_all("ALLINVENTORYENTRIES.LIST") + voucher.find_all("INVENTORYENTRIESIN.LIST") + voucher.find_all("INVENTORYENTRIESOUT.LIST") + inventory_entries = ( + voucher.find_all("INVENTORYENTRIES.LIST") + + voucher.find_all("ALLINVENTORYENTRIES.LIST") + + voucher.find_all("INVENTORYENTRIESIN.LIST") + + voucher.find_all("INVENTORYENTRIESOUT.LIST") + ) if doctype == "Sales Invoice": account_field = "income_account" elif doctype == "Purchase Invoice": @@ -446,32 +496,41 @@ class TallyMigration(Document): items = [] for entry in inventory_entries: qty, uom = entry.ACTUALQTY.string.strip().split() - items.append({ - "item_code": entry.STOCKITEMNAME.string.strip(), - "description": entry.STOCKITEMNAME.string.strip(), - "qty": qty.strip(), - "uom": uom.strip(), - "conversion_factor": 1, - "price_list_rate": entry.RATE.string.strip().split("/")[0], - "cost_center": self.default_cost_center, - "warehouse": self.default_warehouse, - account_field: encode_company_abbr(entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string.strip(), self.erpnext_company), - }) + items.append( + { + "item_code": entry.STOCKITEMNAME.string.strip(), + "description": entry.STOCKITEMNAME.string.strip(), + "qty": qty.strip(), + "uom": uom.strip(), + "conversion_factor": 1, + "price_list_rate": entry.RATE.string.strip().split("/")[0], + "cost_center": self.default_cost_center, + "warehouse": self.default_warehouse, + account_field: encode_company_abbr( + entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string.strip(), + self.erpnext_company, + ), + } + ) return items def get_voucher_taxes(voucher): - ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST") + ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all( + "LEDGERENTRIES.LIST" + ) taxes = [] for entry in ledger_entries: if entry.ISPARTYLEDGER.string.strip() == "No": tax_account = encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company) - taxes.append({ - "charge_type": "Actual", - "account_head": tax_account, - "description": tax_account, - "tax_amount": entry.AMOUNT.string.strip(), - "cost_center": self.default_cost_center, - }) + taxes.append( + { + "charge_type": "Actual", + "account_head": tax_account, + "description": tax_account, + "tax_amount": entry.AMOUNT.string.strip(), + "cost_center": self.default_cost_center, + } + ) return taxes def get_party(party): @@ -502,8 +561,11 @@ class TallyMigration(Document): def _import_day_book_data(self): def create_fiscal_years(vouchers): from frappe.utils.data import add_years, getdate + earliest_date = getdate(min(voucher["posting_date"] for voucher in vouchers)) - oldest_year = frappe.get_all("Fiscal Year", fields=["year_start_date", "year_end_date"], order_by="year_start_date")[0] + oldest_year = frappe.get_all( + "Fiscal Year", fields=["year_start_date", "year_end_date"], order_by="year_start_date" + )[0] while earliest_date < oldest_year.year_start_date: new_year = frappe.get_doc({"doctype": "Fiscal Year"}) new_year.year_start_date = add_years(oldest_year.year_start_date, -1) @@ -520,32 +582,46 @@ class TallyMigration(Document): "fieldtype": "Data", "fieldname": "tally_guid", "read_only": 1, - "label": "Tally GUID" + "label": "Tally GUID", } tally_voucher_no_df = { "fieldtype": "Data", "fieldname": "tally_voucher_no", "read_only": 1, - "label": "Tally Voucher Number" + "label": "Tally Voucher Number", } for df in [tally_guid_df, tally_voucher_no_df]: for doctype in doctypes: create_custom_field(doctype, df) def create_price_list(): - frappe.get_doc({ - "doctype": "Price List", - "price_list_name": "Tally Price List", - "selling": 1, - "buying": 1, - "enabled": 1, - "currency": "INR" - }).insert() + frappe.get_doc( + { + "doctype": "Price List", + "price_list_name": "Tally Price List", + "selling": 1, + "buying": 1, + "enabled": 1, + "currency": "INR", + } + ).insert() try: - frappe.db.set_value("Account", encode_company_abbr(self.tally_creditors_account, self.erpnext_company), "account_type", "Payable") - frappe.db.set_value("Account", encode_company_abbr(self.tally_debtors_account, self.erpnext_company), "account_type", "Receivable") - frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.default_round_off_account) + frappe.db.set_value( + "Account", + encode_company_abbr(self.tally_creditors_account, self.erpnext_company), + "account_type", + "Payable", + ) + frappe.db.set_value( + "Account", + encode_company_abbr(self.tally_debtors_account, self.erpnext_company), + "account_type", + "Receivable", + ) + frappe.db.set_value( + "Company", self.erpnext_company, "round_off_account", self.default_round_off_account + ) vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers}) vouchers = json.loads(vouchers_file.get_content()) @@ -560,7 +636,16 @@ class TallyMigration(Document): for index in range(0, total, VOUCHER_CHUNK_SIZE): if index + VOUCHER_CHUNK_SIZE >= total: is_last = True - frappe.enqueue_doc(self.doctype, self.name, "_import_vouchers", queue="long", timeout=3600, start=index+1, total=total, is_last=is_last) + frappe.enqueue_doc( + self.doctype, + self.name, + "_import_vouchers", + queue="long", + timeout=3600, + start=index + 1, + total=total, + is_last=is_last, + ) except Exception: self.log() @@ -572,7 +657,7 @@ class TallyMigration(Document): frappe.flags.in_migrate = True vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers}) vouchers = json.loads(vouchers_file.get_content()) - chunk = vouchers[start: start + VOUCHER_CHUNK_SIZE] + chunk = vouchers[start : start + VOUCHER_CHUNK_SIZE] for index, voucher in enumerate(chunk, start=start): try: @@ -617,17 +702,22 @@ class TallyMigration(Document): if sys.exc_info()[1].__class__ != frappe.DuplicateEntryError: failed_import_log = json.loads(self.failed_import_log) doc = data.as_dict() - failed_import_log.append({ - "doc": doc, - "exc": traceback.format_exc() - }) - self.failed_import_log = json.dumps(failed_import_log, separators=(',', ':')) + failed_import_log.append({"doc": doc, "exc": traceback.format_exc()}) + self.failed_import_log = json.dumps(failed_import_log, separators=(",", ":")) self.save() frappe.db.commit() else: data = data or self.status - message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) + message = "\n".join( + [ + "Data:", + json.dumps(data, default=str, indent=4), + "--" * 50, + "\nException:", + traceback.format_exc(), + ] + ) return frappe.log_error(title="Tally Migration Error", message=message) def set_status(self, status=""): diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py index d4bbe881d0c..2148863c556 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py @@ -14,27 +14,31 @@ from erpnext.erpnext_integrations.taxjar_integration import get_client class TaxJarSettings(Document): - def on_update(self): TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax TAXJAR_SANDBOX_MODE = self.is_sandbox - fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'}) - fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden') + fields_already_exist = frappe.db.exists( + "Custom Field", + {"dt": ("in", ["Item", "Sales Invoice Item"]), "fieldname": "product_tax_category"}, + ) + fields_hidden = frappe.get_value( + "Custom Field", {"dt": ("in", ["Sales Invoice Item"])}, "hidden" + ) - if (TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE): + if TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE: if not fields_already_exist: add_product_tax_categories() make_custom_fields() add_permissions() - frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False) + frappe.enqueue("erpnext.regional.united_states.setup.add_product_tax_categories", now=False) elif fields_already_exist and fields_hidden: - toggle_tax_category_fields(hidden='0') + toggle_tax_category_fields(hidden="0") elif fields_already_exist: - toggle_tax_category_fields(hidden='1') + toggle_tax_category_fields(hidden="1") def validate(self): self.calculate_taxes_validation_for_create_transactions() @@ -46,54 +50,97 @@ class TaxJarSettings(Document): new_nexus_list = [frappe._dict(address) for address in nexus] - self.set('nexus', []) - self.set('nexus', new_nexus_list) + self.set("nexus", []) + self.set("nexus", new_nexus_list) self.save() def calculate_taxes_validation_for_create_transactions(self): if not self.taxjar_calculate_tax and (self.taxjar_create_transactions or self.is_sandbox): - frappe.throw(frappe._('Before enabling Create Transaction or Sandbox Mode, you need to check the Enable Tax Calculation box')) + frappe.throw( + frappe._( + "Before enabling Create Transaction or Sandbox Mode, you need to check the Enable Tax Calculation box" + ) + ) def toggle_tax_category_fields(hidden): - frappe.set_value('Custom Field', {'dt':'Sales Invoice Item', 'fieldname':'product_tax_category'}, 'hidden', hidden) - frappe.set_value('Custom Field', {'dt':'Item', 'fieldname':'product_tax_category'}, 'hidden', hidden) + frappe.set_value( + "Custom Field", + {"dt": "Sales Invoice Item", "fieldname": "product_tax_category"}, + "hidden", + hidden, + ) + frappe.set_value( + "Custom Field", {"dt": "Item", "fieldname": "product_tax_category"}, "hidden", hidden + ) def add_product_tax_categories(): - with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "product_tax_category_data.json"), "r") as f: tax_categories = json.loads(f.read()) - create_tax_categories(tax_categories['categories']) + create_tax_categories(tax_categories["categories"]) + def create_tax_categories(data): for d in data: - if not frappe.db.exists('Product Tax Category',{'product_tax_code':d.get('product_tax_code')}): - tax_category = frappe.new_doc('Product Tax Category') + if not frappe.db.exists("Product Tax Category", {"product_tax_code": d.get("product_tax_code")}): + tax_category = frappe.new_doc("Product Tax Category") tax_category.description = d.get("description") tax_category.product_tax_code = d.get("product_tax_code") tax_category.category_name = d.get("name") tax_category.db_insert() + def make_custom_fields(update=True): custom_fields = { - 'Sales Invoice Item': [ - dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category', - label='Product Tax Category', fetch_from='item_code.product_tax_category'), - dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount', - label='Tax Collectable', read_only=1, options='currency'), - dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable', - label='Taxable Amount', read_only=1, options='currency') + "Sales Invoice Item": [ + dict( + fieldname="product_tax_category", + fieldtype="Link", + insert_after="description", + options="Product Tax Category", + label="Product Tax Category", + fetch_from="item_code.product_tax_category", + ), + dict( + fieldname="tax_collectable", + fieldtype="Currency", + insert_after="net_amount", + label="Tax Collectable", + read_only=1, + options="currency", + ), + dict( + fieldname="taxable_amount", + fieldtype="Currency", + insert_after="tax_collectable", + label="Taxable Amount", + read_only=1, + options="currency", + ), + ], + "Item": [ + dict( + fieldname="product_tax_category", + fieldtype="Link", + insert_after="item_group", + options="Product Tax Category", + label="Product Tax Category", + ) ], - 'Item': [ - dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category', - label='Product Tax Category') - ] } create_custom_fields(custom_fields, update=update) + def add_permissions(): doctype = "Product Tax Category" - for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'): + for role in ( + "Accounts Manager", + "Accounts User", + "System Manager", + "Item Manager", + "Stock Manager", + ): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py index 8da52f49f1c..ab95b0cda9d 100644 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py @@ -21,11 +21,23 @@ class WoocommerceSettings(Document): custom_fields = {} # create for doctype in ["Customer", "Sales Order", "Item", "Address"]: - df = dict(fieldname='woocommerce_id', label='Woocommerce ID', fieldtype='Data', read_only=1, print_hide=1) + df = dict( + fieldname="woocommerce_id", + label="Woocommerce ID", + fieldtype="Data", + read_only=1, + print_hide=1, + ) create_custom_field(doctype, df) for doctype in ["Customer", "Address"]: - df = dict(fieldname='woocommerce_email', label='Woocommerce Email', fieldtype='Data', read_only=1, print_hide=1) + df = dict( + fieldname="woocommerce_email", + label="Woocommerce Email", + fieldtype="Data", + read_only=1, + print_hide=1, + ) create_custom_field(doctype, df) if not frappe.get_value("Item Group", {"name": _("WooCommerce Products")}): @@ -57,21 +69,21 @@ class WoocommerceSettings(Document): # for CI Test to work url = "http://localhost:8000" - server_url = '{uri.scheme}://{uri.netloc}'.format( - uri=urlparse(url) - ) + server_url = "{uri.scheme}://{uri.netloc}".format(uri=urlparse(url)) delivery_url = server_url + endpoint self.endpoint = delivery_url + @frappe.whitelist() def generate_secret(): woocommerce_settings = frappe.get_doc("Woocommerce Settings") woocommerce_settings.secret = frappe.generate_hash() woocommerce_settings.save() + @frappe.whitelist() def get_series(): return { - "sales_order_series" : frappe.get_meta("Sales Order").get_options("naming_series") or "SO-WOO-", + "sales_order_series": frappe.get_meta("Sales Order").get_options("naming_series") or "SO-WOO-", } diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index 167fcb71652..1f5df67e197 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -6,15 +6,17 @@ from frappe import _ # api/method/erpnext.erpnext_integrations.exotel_integration.handle_end_call # api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call + @frappe.whitelist(allow_guest=True) def handle_incoming_call(**kwargs): try: exotel_settings = get_exotel_settings() - if not exotel_settings.enabled: return + if not exotel_settings.enabled: + return call_payload = kwargs - status = call_payload.get('Status') - if status == 'free': + status = call_payload.get("Status") + if status == "free": return call_log = get_call_log(call_payload) @@ -24,87 +26,100 @@ def handle_incoming_call(**kwargs): update_call_log(call_payload, call_log=call_log) except Exception as e: frappe.db.rollback() - frappe.log_error(title=_('Error in Exotel incoming call')) + frappe.log_error(title=_("Error in Exotel incoming call")) frappe.db.commit() + @frappe.whitelist(allow_guest=True) def handle_end_call(**kwargs): - update_call_log(kwargs, 'Completed') + update_call_log(kwargs, "Completed") + @frappe.whitelist(allow_guest=True) def handle_missed_call(**kwargs): - update_call_log(kwargs, 'Missed') + update_call_log(kwargs, "Missed") -def update_call_log(call_payload, status='Ringing', call_log=None): + +def update_call_log(call_payload, status="Ringing", call_log=None): call_log = call_log or get_call_log(call_payload) if call_log: call_log.status = status - call_log.to = call_payload.get('DialWhomNumber') - call_log.duration = call_payload.get('DialCallDuration') or 0 - call_log.recording_url = call_payload.get('RecordingUrl') + call_log.to = call_payload.get("DialWhomNumber") + call_log.duration = call_payload.get("DialCallDuration") or 0 + call_log.recording_url = call_payload.get("RecordingUrl") call_log.save(ignore_permissions=True) frappe.db.commit() return call_log + def get_call_log(call_payload): - call_log = frappe.get_all('Call Log', { - 'id': call_payload.get('CallSid'), - }, limit=1) + call_log = frappe.get_all( + "Call Log", + { + "id": call_payload.get("CallSid"), + }, + limit=1, + ) if call_log: - return frappe.get_doc('Call Log', call_log[0].name) + return frappe.get_doc("Call Log", call_log[0].name) + def create_call_log(call_payload): - call_log = frappe.new_doc('Call Log') - call_log.id = call_payload.get('CallSid') - call_log.to = call_payload.get('DialWhomNumber') - call_log.medium = call_payload.get('To') - call_log.status = 'Ringing' - setattr(call_log, 'from', call_payload.get('CallFrom')) + call_log = frappe.new_doc("Call Log") + call_log.id = call_payload.get("CallSid") + call_log.to = call_payload.get("DialWhomNumber") + call_log.medium = call_payload.get("To") + call_log.status = "Ringing" + setattr(call_log, "from", call_payload.get("CallFrom")) call_log.save(ignore_permissions=True) frappe.db.commit() return call_log + @frappe.whitelist() def get_call_status(call_id): - endpoint = get_exotel_endpoint('Calls/{call_id}.json'.format(call_id=call_id)) + endpoint = get_exotel_endpoint("Calls/{call_id}.json".format(call_id=call_id)) response = requests.get(endpoint) - status = response.json().get('Call', {}).get('Status') + status = response.json().get("Call", {}).get("Status") return status + @frappe.whitelist() def make_a_call(from_number, to_number, caller_id): - endpoint = get_exotel_endpoint('Calls/connect.json?details=true') - response = requests.post(endpoint, data={ - 'From': from_number, - 'To': to_number, - 'CallerId': caller_id - }) + endpoint = get_exotel_endpoint("Calls/connect.json?details=true") + response = requests.post( + endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id} + ) return response.json() + def get_exotel_settings(): - return frappe.get_single('Exotel Settings') + return frappe.get_single("Exotel Settings") + def whitelist_numbers(numbers, caller_id): - endpoint = get_exotel_endpoint('CustomerWhitelist') - response = requests.post(endpoint, data={ - 'VirtualNumber': caller_id, - 'Number': numbers, - }) + endpoint = get_exotel_endpoint("CustomerWhitelist") + response = requests.post( + endpoint, + data={ + "VirtualNumber": caller_id, + "Number": numbers, + }, + ) return response + def get_all_exophones(): - endpoint = get_exotel_endpoint('IncomingPhoneNumbers') + endpoint = get_exotel_endpoint("IncomingPhoneNumbers") response = requests.post(endpoint) return response + def get_exotel_endpoint(action): settings = get_exotel_settings() - return 'https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}'.format( - api_key=settings.api_key, - api_token=settings.api_token, - sid=settings.account_sid, - action=action + return "https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}".format( + api_key=settings.api_key, api_token=settings.api_token, sid=settings.account_sid, action=action ) diff --git a/erpnext/erpnext_integrations/stripe_integration.py b/erpnext/erpnext_integrations/stripe_integration.py index 502cb5f00a4..b12adc1d3df 100644 --- a/erpnext/erpnext_integrations/stripe_integration.py +++ b/erpnext/erpnext_integrations/stripe_integration.py @@ -16,17 +16,21 @@ def create_stripe_subscription(gateway_controller, data): try: stripe_settings.integration_request = create_request_log(stripe_settings.data, "Host", "Stripe") - stripe_settings.payment_plans = frappe.get_doc("Payment Request", stripe_settings.data.reference_docname).subscription_plans + stripe_settings.payment_plans = frappe.get_doc( + "Payment Request", stripe_settings.data.reference_docname + ).subscription_plans return create_subscription_on_stripe(stripe_settings) except Exception: frappe.log_error(frappe.get_traceback()) - return{ + return { "redirect_to": frappe.redirect_to_message( - _('Server Error'), - _("It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.") + _("Server Error"), + _( + "It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account." + ), ), - "status": 401 + "status": 401, } @@ -40,20 +44,20 @@ def create_subscription_on_stripe(stripe_settings): customer = stripe.Customer.create( source=stripe_settings.data.stripe_token_id, description=stripe_settings.data.payer_name, - email=stripe_settings.data.payer_email + email=stripe_settings.data.payer_email, ) subscription = stripe.Subscription.create(customer=customer, items=items) if subscription.status == "active": - stripe_settings.integration_request.db_set('status', 'Completed', update_modified=False) + stripe_settings.integration_request.db_set("status", "Completed", update_modified=False) stripe_settings.flags.status_changed_to = "Completed" else: - stripe_settings.integration_request.db_set('status', 'Failed', update_modified=False) - frappe.log_error('Subscription N°: ' + subscription.id, 'Stripe Payment not completed') + stripe_settings.integration_request.db_set("status", "Failed", update_modified=False) + frappe.log_error("Subscription N°: " + subscription.id, "Stripe Payment not completed") except Exception: - stripe_settings.integration_request.db_set('status', 'Failed', update_modified=False) + stripe_settings.integration_request.db_set("status", "Failed", update_modified=False) frappe.log_error(frappe.get_traceback()) return stripe_settings.finalize_request() diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index 14c86d56328..b8893aa7732 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -8,14 +8,92 @@ from frappe.utils import cint, flt from erpnext import get_default_company, get_region -SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", - "FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO", - "SE", "SI", "SK", "US"] -SUPPORTED_STATE_CODES = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL', - 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', - 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', - 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'] - +SUPPORTED_COUNTRY_CODES = [ + "AT", + "AU", + "BE", + "BG", + "CA", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GB", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK", + "US", +] +SUPPORTED_STATE_CODES = [ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DE", + "DC", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WV", + "WI", + "WY", +] def get_client(): @@ -30,14 +108,14 @@ def get_client(): if api_key and api_url: client = taxjar.Client(api_key=api_key, api_url=api_url) - client.set_api_config('headers', { - 'x-api-version': '2022-01-24' - }) + client.set_api_config("headers", {"x-api-version": "2022-01-24"}) return client def create_transaction(doc, method): - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value( + "TaxJar Settings", "taxjar_create_transactions" + ) """Create an order transaction in TaxJar""" @@ -60,10 +138,10 @@ def create_transaction(doc, method): if not tax_dict: return - tax_dict['transaction_id'] = doc.name - tax_dict['transaction_date'] = frappe.utils.today() - tax_dict['sales_tax'] = sales_tax - tax_dict['amount'] = doc.total + tax_dict['shipping'] + tax_dict["transaction_id"] = doc.name + tax_dict["transaction_date"] = frappe.utils.today() + tax_dict["sales_tax"] = sales_tax + tax_dict["amount"] = doc.total + tax_dict["shipping"] try: if doc.is_return: @@ -78,7 +156,9 @@ def create_transaction(doc, method): def delete_transaction(doc, method): """Delete an existing TaxJar order transaction""" - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value( + "TaxJar Settings", "taxjar_create_transactions" + ) if not TAXJAR_CREATE_TRANSACTIONS: return @@ -109,10 +189,10 @@ def get_tax_data(doc): line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items] if from_shipping_state not in SUPPORTED_STATE_CODES: - from_shipping_state = get_state_code(from_address, 'Company') + from_shipping_state = get_state_code(from_address, "Company") if to_shipping_state not in SUPPORTED_STATE_CODES: - to_shipping_state = get_state_code(to_address, 'Shipping') + to_shipping_state = get_state_code(to_address, "Shipping") tax_dict = { "from_country": from_country_code, @@ -128,10 +208,11 @@ def get_tax_data(doc): "shipping": shipping, "amount": doc.net_total, "plugin": "erpnext", - "line_items": line_items + "line_items": line_items, } return tax_dict + def get_state_code(address, location): if address is not None: state_code = get_iso_3166_2_state_code(address) @@ -142,21 +223,21 @@ def get_state_code(address, location): return state_code + def get_line_item_dict(item, docstatus): tax_dict = dict( - id = item.get('idx'), - quantity = item.get('qty'), - unit_price = item.get('rate'), - product_tax_code = item.get('product_tax_category') + id=item.get("idx"), + quantity=item.get("qty"), + unit_price=item.get("rate"), + product_tax_code=item.get("product_tax_category"), ) if docstatus == 1: - tax_dict.update({ - 'sales_tax':item.get('tax_collectable') - }) + tax_dict.update({"sales_tax": item.get("tax_collectable")}) return tax_dict + def set_sales_tax(doc, method): TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") @@ -164,7 +245,7 @@ def set_sales_tax(doc, method): if not TAXJAR_CALCULATE_TAX: return - if get_region(doc.company) != 'United States': + if get_region(doc.company) != "United States": return if not doc.items: @@ -197,22 +278,26 @@ def set_sales_tax(doc, method): doc.run_method("calculate_taxes_and_totals") break else: - doc.append("taxes", { - "charge_type": "Actual", - "description": "Sales Tax", - "account_head": TAX_ACCOUNT_HEAD, - "tax_amount": tax_data.amount_to_collect - }) + doc.append( + "taxes", + { + "charge_type": "Actual", + "description": "Sales Tax", + "account_head": TAX_ACCOUNT_HEAD, + "tax_amount": tax_data.amount_to_collect, + }, + ) # Assigning values to tax_collectable and taxable_amount fields in sales item table for item in tax_data.breakdown.line_items: - doc.get('items')[cint(item.id)-1].tax_collectable = item.tax_collectable - doc.get('items')[cint(item.id)-1].taxable_amount = item.taxable_amount + doc.get("items")[cint(item.id) - 1].tax_collectable = item.tax_collectable + doc.get("items")[cint(item.id) - 1].taxable_amount = item.taxable_amount doc.run_method("calculate_taxes_and_totals") + def check_for_nexus(doc, tax_dict): TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") - if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}): + if not frappe.db.get_value("TaxJar Nexus", {"region_code": tax_dict["to_state"]}): for item in doc.get("items"): item.tax_collectable = flt(0) item.taxable_amount = flt(0) @@ -222,13 +307,17 @@ def check_for_nexus(doc, tax_dict): doc.taxes.remove(tax) return + def check_sales_tax_exemption(doc): # if the party is exempt from sales tax, then set all tax account heads to zero TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") - sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \ - or frappe.db.has_column("Customer", "exempt_from_sales_tax") \ + sales_tax_exempted = ( + hasattr(doc, "exempt_from_sales_tax") + and doc.exempt_from_sales_tax + or frappe.db.has_column("Customer", "exempt_from_sales_tax") and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax") + ) if sales_tax_exempted: for tax in doc.taxes: @@ -240,6 +329,7 @@ def check_sales_tax_exemption(doc): else: return False + def validate_tax_request(tax_dict): """Return the sales tax that should be collected for a given order.""" @@ -283,9 +373,12 @@ def get_shipping_address_details(doc): def get_iso_3166_2_state_code(address): import pycountry + country_code = frappe.db.get_value("Country", address.get("country"), "code") - error_message = _("""{0} is not a valid state! Check for typos or enter the ISO code for your state.""").format(address.get("state")) + error_message = _( + """{0} is not a valid state! Check for typos or enter the ISO code for your state.""" + ).format(address.get("state")) state = address.get("state").upper().strip() # The max length for ISO state codes is 3, excluding the country code @@ -306,7 +399,7 @@ def get_iso_3166_2_state_code(address): except LookupError: frappe.throw(_(error_message)) else: - return lookup_state.code.split('-')[1] + return lookup_state.code.split("-")[1] def sanitize_error_response(response): @@ -317,7 +410,7 @@ def sanitize_error_response(response): "to zip": "Zipcode", "to city": "City", "to state": "State", - "to country": "Country" + "to country": "Country", } for k, v in sanitized_responses.items(): diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index 44039933405..406751f1a62 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -1,4 +1,3 @@ - import base64 import hashlib import hmac @@ -10,28 +9,24 @@ from six.moves.urllib.parse import urlparse from erpnext import get_default_company -def validate_webhooks_request(doctype, hmac_key, secret_key='secret'): +def validate_webhooks_request(doctype, hmac_key, secret_key="secret"): def innerfn(fn): settings = frappe.get_doc(doctype) if frappe.request and settings and settings.get(secret_key) and not frappe.flags.in_test: sig = base64.b64encode( - hmac.new( - settings.get(secret_key).encode('utf8'), - frappe.request.data, - hashlib.sha256 - ).digest() + hmac.new(settings.get(secret_key).encode("utf8"), frappe.request.data, hashlib.sha256).digest() ) - if frappe.request.data and \ - not sig == bytes(frappe.get_request_header(hmac_key).encode()): - frappe.throw(_("Unverified Webhook Data")) + if frappe.request.data and not sig == bytes(frappe.get_request_header(hmac_key).encode()): + frappe.throw(_("Unverified Webhook Data")) frappe.set_user(settings.modified_by) return fn return innerfn + def get_webhook_address(connector_name, method, exclude_uri=False, force_https=False): endpoint = "erpnext.erpnext_integrations.connectors.{0}.{1}".format(connector_name, method) @@ -51,34 +46,40 @@ def get_webhook_address(connector_name, method, exclude_uri=False, force_https=F return server_url + def create_mode_of_payment(gateway, payment_type="General"): - payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { - "payment_gateway": gateway - }, ['payment_account']) + payment_gateway_account = frappe.db.get_value( + "Payment Gateway Account", {"payment_gateway": gateway}, ["payment_account"] + ) mode_of_payment = frappe.db.exists("Mode of Payment", gateway) if not mode_of_payment and payment_gateway_account: - mode_of_payment = frappe.get_doc({ - "doctype": "Mode of Payment", - "mode_of_payment": gateway, - "enabled": 1, - "type": payment_type, - "accounts": [{ - "doctype": "Mode of Payment Account", - "company": get_default_company(), - "default_account": payment_gateway_account - }] - }) + mode_of_payment = frappe.get_doc( + { + "doctype": "Mode of Payment", + "mode_of_payment": gateway, + "enabled": 1, + "type": payment_type, + "accounts": [ + { + "doctype": "Mode of Payment Account", + "company": get_default_company(), + "default_account": payment_gateway_account, + } + ], + } + ) mode_of_payment.insert(ignore_permissions=True) return mode_of_payment elif mode_of_payment: return frappe.get_doc("Mode of Payment", mode_of_payment) + def get_tracking_url(carrier, tracking_number): # Return the formatted Tracking URL. - tracking_url = '' - url_reference = frappe.get_value('Parcel Service', carrier, 'url_reference') + tracking_url = "" + url_reference = frappe.get_value("Parcel Service", carrier, "url_reference") if url_reference: - tracking_url = frappe.render_template(url_reference, {'tracking_number': tracking_number}) + tracking_url = frappe.render_template(url_reference, {"tracking_number": tracking_number}) return tracking_url diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py index cfd2a7ce049..86c29d476b2 100644 --- a/erpnext/exceptions.py +++ b/erpnext/exceptions.py @@ -1,11 +1,26 @@ - import frappe # accounts -class PartyFrozen(frappe.ValidationError): pass -class InvalidAccountCurrency(frappe.ValidationError): pass -class InvalidCurrency(frappe.ValidationError): pass -class PartyDisabled(frappe.ValidationError):pass -class InvalidAccountDimensionError(frappe.ValidationError): pass -class MandatoryAccountDimensionError(frappe.ValidationError): pass +class PartyFrozen(frappe.ValidationError): + pass + + +class InvalidAccountCurrency(frappe.ValidationError): + pass + + +class InvalidCurrency(frappe.ValidationError): + pass + + +class PartyDisabled(frappe.ValidationError): + pass + + +class InvalidAccountDimensionError(frappe.ValidationError): + pass + + +class MandatoryAccountDimensionError(frappe.ValidationError): + pass diff --git a/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py index ed6951dacbc..9ffe1979597 100644 --- a/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py +++ b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py @@ -8,30 +8,39 @@ from frappe.utils.dashboard import cache_source @frappe.whitelist() @cache_source -def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None, heatmap_year = None): +def get( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, +): if chart_name: - chart = frappe.get_doc('Dashboard Chart', chart_name) + chart = frappe.get_doc("Dashboard Chart", chart_name) else: chart = frappe._dict(frappe.parse_json(chart)) filters = frappe.parse_json(filters) - data = frappe.db.get_list('Medical Department', fields=['name']) + data = frappe.db.get_list("Medical Department", fields=["name"]) if not filters: filters = {} - status = ['Open', 'Scheduled', 'Closed', 'Cancelled'] + status = ["Open", "Scheduled", "Closed", "Cancelled"] for department in data: - filters['department'] = department.name - department['total_appointments'] = frappe.db.count('Patient Appointment', filters=filters) + filters["department"] = department.name + department["total_appointments"] = frappe.db.count("Patient Appointment", filters=filters) for entry in status: - filters['status'] = entry - department[frappe.scrub(entry)] = frappe.db.count('Patient Appointment', filters=filters) - filters.pop('status') + filters["status"] = entry + department[frappe.scrub(entry)] = frappe.db.count("Patient Appointment", filters=filters) + filters.pop("status") - sorted_department_map = sorted(data, key = lambda i: i['total_appointments'], reverse=True) + sorted_department_map = sorted(data, key=lambda i: i["total_appointments"], reverse=True) if len(sorted_department_map) > 10: sorted_department_map = sorted_department_map[:10] @@ -50,24 +59,12 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d cancelled.append(department.cancelled) return { - 'labels': labels, - 'datasets': [ - { - 'name': 'Open', - 'values': open_appointments - }, - { - 'name': 'Scheduled', - 'values': scheduled - }, - { - 'name': 'Closed', - 'values': closed - }, - { - 'name': 'Cancelled', - 'values': cancelled - } + "labels": labels, + "datasets": [ + {"name": "Open", "values": open_appointments}, + {"name": "Scheduled", "values": scheduled}, + {"name": "Closed", "values": closed}, + {"name": "Cancelled", "values": cancelled}, ], - 'type': 'bar' + "type": "bar", } diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.py b/erpnext/healthcare/doctype/appointment_type/appointment_type.py index f77511328f8..db4a19e32d6 100644 --- a/erpnext/healthcare/doctype/appointment_type/appointment_type.py +++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.py @@ -10,47 +10,65 @@ class AppointmentType(Document): def validate(self): if self.items and self.price_list: for item in self.items: - existing_op_item_price = frappe.db.exists('Item Price', { - 'item_code': item.op_consulting_charge_item, - 'price_list': self.price_list - }) + existing_op_item_price = frappe.db.exists( + "Item Price", {"item_code": item.op_consulting_charge_item, "price_list": self.price_list} + ) if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge: make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge) - existing_ip_item_price = frappe.db.exists('Item Price', { - 'item_code': item.inpatient_visit_charge_item, - 'price_list': self.price_list - }) + existing_ip_item_price = frappe.db.exists( + "Item Price", {"item_code": item.inpatient_visit_charge_item, "price_list": self.price_list} + ) + + if ( + not existing_ip_item_price + and item.inpatient_visit_charge_item + and item.inpatient_visit_charge + ): + make_item_price( + self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge + ) - if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge: - make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge) @frappe.whitelist() def get_service_item_based_on_department(appointment_type, department): - item_list = frappe.db.get_value('Appointment Type Service Item', - filters = {'medical_department': department, 'parent': appointment_type}, - fieldname = ['op_consulting_charge_item', - 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'], - as_dict = 1 + item_list = frappe.db.get_value( + "Appointment Type Service Item", + filters={"medical_department": department, "parent": appointment_type}, + fieldname=[ + "op_consulting_charge_item", + "inpatient_visit_charge_item", + "op_consulting_charge", + "inpatient_visit_charge", + ], + as_dict=1, ) # if department wise items are not set up # use the generic items if not item_list: - item_list = frappe.db.get_value('Appointment Type Service Item', - filters = {'parent': appointment_type}, - fieldname = ['op_consulting_charge_item', - 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'], - as_dict = 1 + item_list = frappe.db.get_value( + "Appointment Type Service Item", + filters={"parent": appointment_type}, + fieldname=[ + "op_consulting_charge_item", + "inpatient_visit_charge_item", + "op_consulting_charge", + "inpatient_visit_charge", + ], + as_dict=1, ) return item_list + def make_item_price(price_list, item, item_price): - frappe.get_doc({ - 'doctype': 'Item Price', - 'price_list': price_list, - 'item_code': item, - 'price_list_rate': item_price - }).insert(ignore_permissions=True, ignore_mandatory=True) + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list, + "item_code": item, + "price_list_rate": item_price, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type_dashboard.py b/erpnext/healthcare/doctype/appointment_type/appointment_type_dashboard.py index de5b9af2242..04bc04acb3c 100644 --- a/erpnext/healthcare/doctype/appointment_type/appointment_type_dashboard.py +++ b/erpnext/healthcare/doctype/appointment_type/appointment_type_dashboard.py @@ -1,14 +1,10 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'appointment_type', - 'transactions': [ - { - 'label': _('Patient Appointments'), - 'items': ['Patient Appointment'] - }, - ] + "fieldname": "appointment_type", + "transactions": [ + {"label": _("Patient Appointments"), "items": ["Patient Appointment"]}, + ], } diff --git a/erpnext/healthcare/doctype/appointment_type/test_appointment_type.py b/erpnext/healthcare/doctype/appointment_type/test_appointment_type.py index b98575c376a..e5d1a1162ac 100644 --- a/erpnext/healthcare/doctype/appointment_type/test_appointment_type.py +++ b/erpnext/healthcare/doctype/appointment_type/test_appointment_type.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Appointment Type') + class TestAppointmentType(unittest.TestCase): pass diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index a53f72a35d3..421db118399 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -33,27 +33,29 @@ class ClinicalProcedure(Document): def after_insert(self): if self.prescription: - frappe.db.set_value('Procedure Prescription', self.prescription, 'procedure_created', 1) + frappe.db.set_value("Procedure Prescription", self.prescription, "procedure_created", 1) if self.appointment: - frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') - template = frappe.get_doc('Clinical Procedure Template', self.procedure_template) + frappe.db.set_value("Patient Appointment", self.appointment, "status", "Closed") + template = frappe.get_doc("Clinical Procedure Template", self.procedure_template) if template.sample: - patient = frappe.get_doc('Patient', self.patient) + patient = frappe.get_doc("Patient", self.patient) sample_collection = create_sample_doc(template, patient, None, self.company) - frappe.db.set_value('Clinical Procedure', self.name, 'sample', sample_collection.name) + frappe.db.set_value("Clinical Procedure", self.name, "sample", sample_collection.name) self.reload() def set_status(self): if self.docstatus == 0: - self.status = 'Draft' + self.status = "Draft" elif self.docstatus == 1: - if self.status not in ['In Progress', 'Completed']: - self.status = 'Pending' + if self.status not in ["In Progress", "Completed"]: + self.status = "Pending" elif self.docstatus == 2: - self.status = 'Cancelled' + self.status = "Cancelled" def set_title(self): - self.title = _('{0} - {1}').format(self.patient_name or self.patient, self.procedure_template)[:100] + self.title = _("{0} - {1}").format(self.patient_name or self.patient, self.procedure_template)[ + :100 + ] @frappe.whitelist() def complete_procedure(self): @@ -63,37 +65,48 @@ class ClinicalProcedure(Document): if self.items: consumable_total_amount = 0 consumption_details = False - customer = frappe.db.get_value('Patient', self.patient, 'customer') + customer = frappe.db.get_value("Patient", self.patient, "customer") if customer: for item in self.items: if item.invoice_separately_as_consumables: - price_list, price_list_currency = frappe.db.get_values('Price List', {'selling': 1}, ['name', 'currency'])[0] + price_list, price_list_currency = frappe.db.get_values( + "Price List", {"selling": 1}, ["name", "currency"] + )[0] args = { - 'doctype': 'Sales Invoice', - 'item_code': item.item_code, - 'company': self.company, - 'warehouse': self.warehouse, - 'customer': customer, - 'selling_price_list': price_list, - 'price_list_currency': price_list_currency, - 'plc_conversion_rate': 1.0, - 'conversion_rate': 1.0 + "doctype": "Sales Invoice", + "item_code": item.item_code, + "company": self.company, + "warehouse": self.warehouse, + "customer": customer, + "selling_price_list": price_list, + "price_list_currency": price_list_currency, + "plc_conversion_rate": 1.0, + "conversion_rate": 1.0, } item_details = get_item_details(args) item_price = item_details.price_list_rate * item.qty - item_consumption_details = item_details.item_name + ' ' + str(item.qty) + ' ' + item.uom + ' ' + str(item_price) + item_consumption_details = ( + item_details.item_name + " " + str(item.qty) + " " + item.uom + " " + str(item_price) + ) consumable_total_amount += item_price if not consumption_details: - consumption_details = _('Clinical Procedure ({0}):').format(self.name) - consumption_details += '\n\t' + item_consumption_details + consumption_details = _("Clinical Procedure ({0}):").format(self.name) + consumption_details += "\n\t" + item_consumption_details if consumable_total_amount > 0: - frappe.db.set_value('Clinical Procedure', self.name, 'consumable_total_amount', consumable_total_amount) - frappe.db.set_value('Clinical Procedure', self.name, 'consumption_details', consumption_details) + frappe.db.set_value( + "Clinical Procedure", self.name, "consumable_total_amount", consumable_total_amount + ) + frappe.db.set_value( + "Clinical Procedure", self.name, "consumption_details", consumption_details + ) else: - frappe.throw(_('Please set Customer in Patient {0}').format(frappe.bold(self.patient)), title=_('Customer Not Found')) + frappe.throw( + _("Please set Customer in Patient {0}").format(frappe.bold(self.patient)), + title=_("Customer Not Found"), + ) - self.db_set('status', 'Completed') + self.db_set("status", "Completed") if self.consume_stock and self.items: return stock_entry @@ -102,15 +115,15 @@ class ClinicalProcedure(Document): def start_procedure(self): allow_start = self.set_actual_qty() if allow_start: - self.db_set('status', 'In Progress') - return 'success' - return 'insufficient stock' + self.db_set("status", "In Progress") + return "success" + return "insufficient stock" def set_actual_qty(self): - allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') + allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") allow_start = True - for d in self.get('items'): + for d in self.get("items"): d.actual_qty = get_stock_qty(d.item_code, self.warehouse) # validate qty if not allow_negative_stock and d.actual_qty < d.qty: @@ -121,15 +134,15 @@ class ClinicalProcedure(Document): @frappe.whitelist() def make_material_receipt(self, submit=False): - stock_entry = frappe.new_doc('Stock Entry') + stock_entry = frappe.new_doc("Stock Entry") - stock_entry.stock_entry_type = 'Material Receipt' + stock_entry.stock_entry_type = "Material Receipt" stock_entry.to_warehouse = self.warehouse stock_entry.company = self.company - expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company) + expense_account = get_account(None, "expense_account", "Healthcare Settings", self.company) for item in self.items: if item.qty > item.actual_qty: - se_child = stock_entry.append('items') + se_child = stock_entry.append("items") se_child.item_code = item.item_code se_child.item_name = item.item_name se_child.uom = item.uom @@ -139,7 +152,7 @@ class ClinicalProcedure(Document): # in stock uom se_child.transfer_qty = flt(item.transfer_qty) se_child.conversion_factor = flt(item.conversion_factor) - cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') + cost_center = frappe.get_cached_value("Company", self.company, "cost_center") se_child.cost_center = cost_center se_child.expense_account = expense_account if submit: @@ -149,25 +162,30 @@ class ClinicalProcedure(Document): def get_stock_qty(item_code, warehouse): - return get_previous_sle({ - 'item_code': item_code, - 'warehouse': warehouse, - 'posting_date': nowdate(), - 'posting_time': nowtime() - }).get('qty_after_transaction') or 0 + return ( + get_previous_sle( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": nowdate(), + "posting_time": nowtime(), + } + ).get("qty_after_transaction") + or 0 + ) @frappe.whitelist() def get_procedure_consumables(procedure_template): - return get_items('Clinical Procedure Item', procedure_template, 'Clinical Procedure Template') + return get_items("Clinical Procedure Item", procedure_template, "Clinical Procedure Template") @frappe.whitelist() def set_stock_items(doc, stock_detail_parent, parenttype): - items = get_items('Clinical Procedure Item', stock_detail_parent, parenttype) + items = get_items("Clinical Procedure Item", stock_detail_parent, parenttype) for item in items: - se_child = doc.append('items') + se_child = doc.append("items") se_child.item_code = item.item_code se_child.item_name = item.item_name se_child.uom = item.uom @@ -178,32 +196,31 @@ def set_stock_items(doc, stock_detail_parent, parenttype): se_child.conversion_factor = flt(item.conversion_factor) if item.batch_no: se_child.batch_no = item.batch_no - if parenttype == 'Clinical Procedure Template': + if parenttype == "Clinical Procedure Template": se_child.invoice_separately_as_consumables = item.invoice_separately_as_consumables return doc def get_items(table, parent, parenttype): - items = frappe.db.get_all(table, filters={ - 'parent': parent, - 'parenttype': parenttype - }, fields=['*']) + items = frappe.db.get_all( + table, filters={"parent": parent, "parenttype": parenttype}, fields=["*"] + ) return items @frappe.whitelist() def make_stock_entry(doc): - stock_entry = frappe.new_doc('Stock Entry') - stock_entry = set_stock_items(stock_entry, doc.name, 'Clinical Procedure') - stock_entry.stock_entry_type = 'Material Issue' + stock_entry = frappe.new_doc("Stock Entry") + stock_entry = set_stock_items(stock_entry, doc.name, "Clinical Procedure") + stock_entry.stock_entry_type = "Material Issue" stock_entry.from_warehouse = doc.warehouse stock_entry.company = doc.company - expense_account = get_account(None, 'expense_account', 'Healthcare Settings', doc.company) + expense_account = get_account(None, "expense_account", "Healthcare Settings", doc.company) for item_line in stock_entry.items: - cost_center = frappe.get_cached_value('Company', doc.company, 'cost_center') + cost_center = frappe.get_cached_value("Company", doc.company, "cost_center") item_line.cost_center = cost_center item_line.expense_account = expense_account @@ -215,39 +232,47 @@ def make_stock_entry(doc): @frappe.whitelist() def make_procedure(source_name, target_doc=None): def set_missing_values(source, target): - consume_stock = frappe.db.get_value('Clinical Procedure Template', source.procedure_template, 'consume_stock') + consume_stock = frappe.db.get_value( + "Clinical Procedure Template", source.procedure_template, "consume_stock" + ) if consume_stock: target.consume_stock = 1 warehouse = None if source.service_unit: - warehouse = frappe.db.get_value('Healthcare Service Unit', source.service_unit, 'warehouse') + warehouse = frappe.db.get_value("Healthcare Service Unit", source.service_unit, "warehouse") if not warehouse: - warehouse = frappe.db.get_value('Stock Settings', None, 'default_warehouse') + warehouse = frappe.db.get_value("Stock Settings", None, "default_warehouse") if warehouse: target.warehouse = warehouse - set_stock_items(target, source.procedure_template, 'Clinical Procedure Template') + set_stock_items(target, source.procedure_template, "Clinical Procedure Template") - doc = get_mapped_doc('Patient Appointment', source_name, { - 'Patient Appointment': { - 'doctype': 'Clinical Procedure', - 'field_map': [ - ['appointment', 'name'], - ['patient', 'patient'], - ['patient_age', 'patient_age'], - ['patient_sex', 'patient_sex'], - ['procedure_template', 'procedure_template'], - ['prescription', 'procedure_prescription'], - ['practitioner', 'practitioner'], - ['medical_department', 'department'], - ['start_date', 'appointment_date'], - ['start_time', 'appointment_time'], - ['notes', 'notes'], - ['service_unit', 'service_unit'], - ['company', 'company'], - ['invoiced', 'invoiced'] - ] + doc = get_mapped_doc( + "Patient Appointment", + source_name, + { + "Patient Appointment": { + "doctype": "Clinical Procedure", + "field_map": [ + ["appointment", "name"], + ["patient", "patient"], + ["patient_age", "patient_age"], + ["patient_sex", "patient_sex"], + ["procedure_template", "procedure_template"], + ["prescription", "procedure_prescription"], + ["practitioner", "practitioner"], + ["medical_department", "department"], + ["start_date", "appointment_date"], + ["start_time", "appointment_time"], + ["notes", "notes"], + ["service_unit", "service_unit"], + ["company", "company"], + ["invoiced", "invoiced"], + ], } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return doc diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py index 76a958ecc3a..a241ba7a401 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py @@ -1,4 +1,4 @@ - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # Copyright (c) 2017, ESS LLP and Contributors # See license.txt @@ -11,54 +11,59 @@ from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment imp create_healthcare_docs, ) -test_dependencies = ['Item'] +test_dependencies = ["Item"] + class TestClinicalProcedure(unittest.TestCase): def test_procedure_template_item(self): patient, practitioner = create_healthcare_docs() procedure_template = create_clinical_procedure_template() - self.assertTrue(frappe.db.exists('Item', procedure_template.item)) + self.assertTrue(frappe.db.exists("Item", procedure_template.item)) procedure_template.disabled = 1 procedure_template.save() - self.assertEqual(frappe.db.get_value('Item', procedure_template.item, 'disabled'), 1) + self.assertEqual(frappe.db.get_value("Item", procedure_template.item, "disabled"), 1) def test_consumables(self): patient, practitioner = create_healthcare_docs() procedure_template = create_clinical_procedure_template() procedure_template.allow_stock_consumption = 1 consumable = create_consumable() - procedure_template.append('items', { - 'item_code': consumable.item_code, - 'qty': 1, - 'uom': consumable.stock_uom, - 'stock_uom': consumable.stock_uom - }) + procedure_template.append( + "items", + { + "item_code": consumable.item_code, + "qty": 1, + "uom": consumable.stock_uom, + "stock_uom": consumable.stock_uom, + }, + ) procedure_template.save() procedure = create_procedure(procedure_template, patient, practitioner) result = procedure.start_procedure() - if result == 'insufficient stock': + if result == "insufficient stock": procedure.make_material_receipt(submit=True) result = procedure.start_procedure() - self.assertEqual(procedure.status, 'In Progress') + self.assertEqual(procedure.status, "In Progress") result = procedure.complete_procedure() # check consumption - self.assertTrue(frappe.db.exists('Stock Entry', result)) + self.assertTrue(frappe.db.exists("Stock Entry", result)) def create_consumable(): - if frappe.db.exists('Item', 'Syringe'): - return frappe.get_doc('Item', 'Syringe') - consumable = frappe.new_doc('Item') - consumable.item_code = 'Syringe' - consumable.item_group = '_Test Item Group' - consumable.stock_uom = 'Nos' + if frappe.db.exists("Item", "Syringe"): + return frappe.get_doc("Item", "Syringe") + consumable = frappe.new_doc("Item") + consumable.item_code = "Syringe" + consumable.item_group = "_Test Item Group" + consumable.stock_uom = "Nos" consumable.valuation_rate = 5.00 consumable.save() return consumable + def create_procedure(procedure_template, patient, practitioner): - procedure = frappe.new_doc('Clinical Procedure') + procedure = frappe.new_doc("Clinical Procedure") procedure.procedure_template = procedure_template.name procedure.patient = patient procedure.practitioner = practitioner diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py index 5104eaacb23..bfe4e75d4c9 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py @@ -24,13 +24,13 @@ class ClinicalProcedureTemplate(Document): def enable_disable_item(self): if self.is_billable: if self.disabled: - frappe.db.set_value('Item', self.item, 'disabled', 1) + frappe.db.set_value("Item", self.item, "disabled", 1) else: - frappe.db.set_value('Item', self.item, 'disabled', 0) + frappe.db.set_value("Item", self.item, "disabled", 0) def update_item_and_item_price(self): if self.is_billable and self.item: - item_doc = frappe.get_doc('Item', {'item_code': self.item}) + item_doc = frappe.get_doc("Item", {"item_code": self.item}) item_doc.item_name = self.template item_doc.item_group = self.item_group item_doc.description = self.description @@ -38,15 +38,15 @@ class ClinicalProcedureTemplate(Document): item_doc.save(ignore_permissions=True) if self.rate: - item_price = frappe.get_doc('Item Price', {'item_code': self.item}) + item_price = frappe.get_doc("Item Price", {"item_code": self.item}) item_price.item_name = self.template item_price.price_list_rate = self.rate item_price.save() elif not self.is_billable and self.item: - frappe.db.set_value('Item', self.item, 'disabled', 1) + frappe.db.set_value("Item", self.item, "disabled", 1) - self.db_set('change_in_item', 0) + self.db_set("change_in_item", 0) @frappe.whitelist() @@ -54,69 +54,72 @@ def get_item_details(args=None): if not isinstance(args, dict): args = json.loads(args) - item = frappe.db.get_all('Item', - filters={ - 'disabled': 0, - 'name': args.get('item_code') - }, - fields=['stock_uom', 'item_name'] + item = frappe.db.get_all( + "Item", filters={"disabled": 0, "name": args.get("item_code")}, fields=["stock_uom", "item_name"] ) if not item: - frappe.throw(_('Item {0} is not active').format(args.get('item_code'))) + frappe.throw(_("Item {0} is not active").format(args.get("item_code"))) item = item[0] ret = { - 'uom': item.stock_uom, - 'stock_uom': item.stock_uom, - 'item_name': item.item_name, - 'qty': 1, - 'transfer_qty': 0, - 'conversion_factor': 1 + "uom": item.stock_uom, + "stock_uom": item.stock_uom, + "item_name": item.item_name, + "qty": 1, + "transfer_qty": 0, + "conversion_factor": 1, } return ret + def create_item_from_template(doc): disabled = doc.disabled if doc.is_billable and not doc.disabled: disabled = 0 - uom = frappe.db.exists('UOM', 'Unit') or frappe.db.get_single_value('Stock Settings', 'stock_uom') - item = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': doc.template, - 'item_name':doc.template, - 'item_group': doc.item_group, - 'description':doc.description, - 'is_sales_item': 1, - 'is_service_item': 1, - 'is_purchase_item': 0, - 'is_stock_item': 0, - 'show_in_website': 0, - 'is_pro_applicable': 0, - 'disabled': disabled, - 'stock_uom': uom - }).insert(ignore_permissions=True, ignore_mandatory=True) + uom = frappe.db.exists("UOM", "Unit") or frappe.db.get_single_value("Stock Settings", "stock_uom") + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": doc.template, + "item_name": doc.template, + "item_group": doc.item_group, + "description": doc.description, + "is_sales_item": 1, + "is_service_item": 1, + "is_purchase_item": 0, + "is_stock_item": 0, + "show_in_website": 0, + "is_pro_applicable": 0, + "disabled": disabled, + "stock_uom": uom, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) make_item_price(item.name, doc.rate) - doc.db_set('item', item.name) + doc.db_set("item", item.name) + def make_item_price(item, item_price): - price_list_name = frappe.db.get_value('Price List', {'selling': 1}) - frappe.get_doc({ - 'doctype': 'Item Price', - 'price_list': price_list_name, - 'item_code': item, - 'price_list_rate': item_price - }).insert(ignore_permissions=True, ignore_mandatory=True) + price_list_name = frappe.db.get_value("Price List", {"selling": 1}) + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list_name, + "item_code": item, + "price_list_rate": item_price, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + @frappe.whitelist() def change_item_code_from_template(item_code, doc): doc = frappe._dict(json.loads(doc)) - if frappe.db.exists('Item', {'item_code': item_code}): - frappe.throw(_('Item with Item Code {0} already exists').format(item_code)) + if frappe.db.exists("Item", {"item_code": item_code}): + frappe.throw(_("Item with Item Code {0} already exists").format(item_code)) else: - rename_doc('Item', doc.item_code, item_code, ignore_permissions=True) - frappe.db.set_value('Clinical Procedure Template', doc.name, 'item_code', item_code) + rename_doc("Item", doc.item_code, item_code, ignore_permissions=True) + frappe.db.set_value("Clinical Procedure Template", doc.name, "item_code", item_code) return diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template_dashboard.py b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template_dashboard.py index e82855201ad..bc16756695a 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template_dashboard.py +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'procedure_template', - 'transactions': [ - { - 'label': _('Consultations'), - 'items': ['Clinical Procedure'] - } - ] + "fieldname": "procedure_template", + "transactions": [{"label": _("Consultations"), "items": ["Clinical Procedure"]}], } diff --git a/erpnext/healthcare/doctype/diagnosis/test_diagnosis.py b/erpnext/healthcare/doctype/diagnosis/test_diagnosis.py index ff9fdba76ab..3a6a3af313d 100644 --- a/erpnext/healthcare/doctype/diagnosis/test_diagnosis.py +++ b/erpnext/healthcare/doctype/diagnosis/test_diagnosis.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Diagnosis') + class TestDiagnosis(unittest.TestCase): pass diff --git a/erpnext/healthcare/doctype/drug_prescription/drug_prescription.py b/erpnext/healthcare/doctype/drug_prescription/drug_prescription.py index 7ae5b2322db..de3b34b1d57 100755 --- a/erpnext/healthcare/doctype/drug_prescription/drug_prescription.py +++ b/erpnext/healthcare/doctype/drug_prescription/drug_prescription.py @@ -13,21 +13,21 @@ class DrugPrescription(Document): period = None if self.dosage: - dosage = frappe.get_doc('Prescription Dosage', self.dosage) + dosage = frappe.get_doc("Prescription Dosage", self.dosage) for item in dosage.dosage_strength: quantity += item.strength if self.period and self.interval: - period = frappe.get_doc('Prescription Duration', self.period) + period = frappe.get_doc("Prescription Duration", self.period) if self.interval < period.get_days(): - quantity = quantity * (period.get_days()/self.interval) + quantity = quantity * (period.get_days() / self.interval) elif self.interval and self.interval_uom and self.period: - period = frappe.get_doc('Prescription Duration', self.period) + period = frappe.get_doc("Prescription Duration", self.period) interval_in = self.interval_uom - if interval_in == 'Day' and self.interval < period.get_days(): - quantity = period.get_days()/self.interval - elif interval_in == 'Hour' and self.interval < period.get_hours(): - quantity = period.get_hours()/self.interval + if interval_in == "Day" and self.interval < period.get_days(): + quantity = period.get_days() / self.interval + elif interval_in == "Hour" and self.interval < period.get_hours(): + quantity = period.get_hours() / self.interval if quantity > 0: return quantity else: diff --git a/erpnext/healthcare/doctype/exercise_type/exercise_type.py b/erpnext/healthcare/doctype/exercise_type/exercise_type.py index b76a3ca37a9..e789d042ebd 100644 --- a/erpnext/healthcare/doctype/exercise_type/exercise_type.py +++ b/erpnext/healthcare/doctype/exercise_type/exercise_type.py @@ -9,6 +9,6 @@ from frappe.model.document import Document class ExerciseType(Document): def autoname(self): if self.difficulty_level: - self.name = ' - '.join(filter(None, [self.exercise_name, self.difficulty_level])) + self.name = " - ".join(filter(None, [self.exercise_name, self.difficulty_level])) else: self.name = self.exercise_name diff --git a/erpnext/healthcare/doctype/fee_validity/fee_validity.py b/erpnext/healthcare/doctype/fee_validity/fee_validity.py index 0274e6ac4f9..1b66266fac0 100644 --- a/erpnext/healthcare/doctype/fee_validity/fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/fee_validity.py @@ -15,40 +15,45 @@ class FeeValidity(Document): def update_status(self): if self.visited >= self.max_visits: - self.status = 'Completed' + self.status = "Completed" else: - self.status = 'Pending' + self.status = "Pending" def create_fee_validity(appointment): if not check_is_new_patient(appointment): return - fee_validity = frappe.new_doc('Fee Validity') + fee_validity = frappe.new_doc("Fee Validity") fee_validity.practitioner = appointment.practitioner fee_validity.patient = appointment.patient - fee_validity.max_visits = frappe.db.get_single_value('Healthcare Settings', 'max_visits') or 1 - valid_days = frappe.db.get_single_value('Healthcare Settings', 'valid_days') or 1 + fee_validity.max_visits = frappe.db.get_single_value("Healthcare Settings", "max_visits") or 1 + valid_days = frappe.db.get_single_value("Healthcare Settings", "valid_days") or 1 fee_validity.visited = 0 fee_validity.start_date = getdate(appointment.appointment_date) - fee_validity.valid_till = getdate(appointment.appointment_date) + datetime.timedelta(days=int(valid_days)) + fee_validity.valid_till = getdate(appointment.appointment_date) + datetime.timedelta( + days=int(valid_days) + ) fee_validity.save(ignore_permissions=True) return fee_validity + def check_is_new_patient(appointment): - validity_exists = frappe.db.exists('Fee Validity', { - 'practitioner': appointment.practitioner, - 'patient': appointment.patient - }) + validity_exists = frappe.db.exists( + "Fee Validity", {"practitioner": appointment.practitioner, "patient": appointment.patient} + ) if validity_exists: return False - appointment_exists = frappe.db.get_all('Patient Appointment', { - 'name': ('!=', appointment.name), - 'status': ('!=', 'Cancelled'), - 'patient': appointment.patient, - 'practitioner': appointment.practitioner - }) + appointment_exists = frappe.db.get_all( + "Patient Appointment", + { + "name": ("!=", appointment.name), + "status": ("!=", "Cancelled"), + "patient": appointment.patient, + "practitioner": appointment.practitioner, + }, + ) if len(appointment_exists) and appointment_exists[0]: return False return True diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index 7eafc8dfe79..c720139c474 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -15,6 +15,7 @@ from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment imp test_dependencies = ["Company"] + class TestFeeValidity(unittest.TestCase): def setUp(self): frappe.db.sql("""delete from `tabPatient Appointment`""") diff --git a/erpnext/healthcare/doctype/healthcare.py b/erpnext/healthcare/doctype/healthcare.py index 87e208289c9..5ff63b9a581 100644 --- a/erpnext/healthcare/doctype/healthcare.py +++ b/erpnext/healthcare/doctype/healthcare.py @@ -1,5 +1,3 @@ - - def get_data(): return [] diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py index ba1c557f14e..52e12725818 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py @@ -22,71 +22,87 @@ class HealthcarePractitioner(Document): # concat first and last name self.name = self.practitioner_name - if frappe.db.exists('Healthcare Practitioner', self.name): - self.name = append_number_if_name_exists('Contact', self.name) + if frappe.db.exists("Healthcare Practitioner", self.name): + self.name = append_number_if_name_exists("Contact", self.name) def validate(self): self.set_full_name() validate_party_accounts(self) if self.inpatient_visit_charge_item: - validate_service_item(self.inpatient_visit_charge_item, 'Configure a service Item for Inpatient Consulting Charge Item') + validate_service_item( + self.inpatient_visit_charge_item, + "Configure a service Item for Inpatient Consulting Charge Item", + ) if self.op_consulting_charge_item: - validate_service_item(self.op_consulting_charge_item, 'Configure a service Item for Out Patient Consulting Charge Item') + validate_service_item( + self.op_consulting_charge_item, + "Configure a service Item for Out Patient Consulting Charge Item", + ) if self.user_id: self.validate_user_id() else: - existing_user_id = frappe.db.get_value('Healthcare Practitioner', self.name, 'user_id') + existing_user_id = frappe.db.get_value("Healthcare Practitioner", self.name, "user_id") if existing_user_id: frappe.permissions.remove_user_permission( - 'Healthcare Practitioner', self.name, existing_user_id) + "Healthcare Practitioner", self.name, existing_user_id + ) def on_update(self): if self.user_id: - frappe.permissions.add_user_permission('Healthcare Practitioner', self.name, self.user_id) + frappe.permissions.add_user_permission("Healthcare Practitioner", self.name, self.user_id) def set_full_name(self): if self.last_name: - self.practitioner_name = ' '.join(filter(None, [self.first_name, self.last_name])) + self.practitioner_name = " ".join(filter(None, [self.first_name, self.last_name])) else: self.practitioner_name = self.first_name def validate_user_id(self): - if not frappe.db.exists('User', self.user_id): - frappe.throw(_('User {0} does not exist').format(self.user_id)) - elif not frappe.db.exists('User', self.user_id, 'enabled'): - frappe.throw(_('User {0} is disabled').format(self.user_id)) + if not frappe.db.exists("User", self.user_id): + frappe.throw(_("User {0} does not exist").format(self.user_id)) + elif not frappe.db.exists("User", self.user_id, "enabled"): + frappe.throw(_("User {0} is disabled").format(self.user_id)) # check duplicate - practitioner = frappe.db.exists('Healthcare Practitioner', { - 'user_id': self.user_id, - 'name': ('!=', self.name) - }) + practitioner = frappe.db.exists( + "Healthcare Practitioner", {"user_id": self.user_id, "name": ("!=", self.name)} + ) if practitioner: - frappe.throw(_('User {0} is already assigned to Healthcare Practitioner {1}').format( - self.user_id, practitioner)) + frappe.throw( + _("User {0} is already assigned to Healthcare Practitioner {1}").format( + self.user_id, practitioner + ) + ) def on_trash(self): - delete_contact_and_address('Healthcare Practitioner', self.name) + delete_contact_and_address("Healthcare Practitioner", self.name) + def validate_service_item(item, msg): - if frappe.db.get_value('Item', item, 'is_stock_item'): + if frappe.db.get_value("Item", item, "is_stock_item"): frappe.throw(_(msg)) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_practitioner_list(doctype, txt, searchfield, start, page_len, filters=None): - active_filter = {'status': 'Active'} + active_filter = {"status": "Active"} filters = {**active_filter, **filters} if filters else active_filter - fields = ['name', 'practitioner_name', 'mobile_phone'] + fields = ["name", "practitioner_name", "mobile_phone"] - text_in = { - 'name': ('like', '%%%s%%' % txt), - 'practitioner_name': ('like', '%%%s%%' % txt) - } + text_in = {"name": ("like", "%%%s%%" % txt), "practitioner_name": ("like", "%%%s%%" % txt)} - return frappe.get_all('Healthcare Practitioner', fields = fields, - filters = filters, or_filters = text_in, start=start, page_length=page_len, order_by='name, practitioner_name', as_list=1) + return frappe.get_all( + "Healthcare Practitioner", + fields=fields, + filters=filters, + or_filters=text_in, + start=start, + page_length=page_len, + order_by="name, practitioner_name", + as_list=1, + ) diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner_dashboard.py b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner_dashboard.py index e41284b4727..ba6b86bb892 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner_dashboard.py +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner_dashboard.py @@ -1,20 +1,16 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Healthcare Practitioner.'), - 'fieldname': 'practitioner', - 'transactions': [ + "heatmap": True, + "heatmap_message": _("This is based on transactions against this Healthcare Practitioner."), + "fieldname": "practitioner", + "transactions": [ { - 'label': _('Appointments and Patient Encounters'), - 'items': ['Patient Appointment', 'Patient Encounter', 'Fee Validity'] + "label": _("Appointments and Patient Encounters"), + "items": ["Patient Appointment", "Patient Encounter", "Fee Validity"], }, - { - 'label': _('Consultation'), - 'items': ['Clinical Procedure', 'Lab Test'] - } - ] + {"label": _("Consultation"), "items": ["Clinical Procedure", "Lab Test"]}, + ], } diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json index 8935ec7d3c9..9366b09d27e 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json @@ -2,7 +2,6 @@ "actions": [], "allow_import": 1, "allow_rename": 1, - "autoname": "field:healthcare_service_unit_name", "beta": 1, "creation": "2016-09-21 13:48:14.731437", "description": "Healthcare Service Unit", @@ -207,7 +206,7 @@ ], "is_tree": 1, "links": [], - "modified": "2021-08-19 14:09:11.643464", + "modified": "2022-04-07 03:11:36.023277", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Service Unit", diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py index 6eeceacbc9e..1a6aed46730 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py @@ -11,14 +11,14 @@ from frappe.utils.nestedset import NestedSet class HealthcareServiceUnit(NestedSet): - nsm_parent_field = 'parent_healthcare_service_unit' + nsm_parent_field = "parent_healthcare_service_unit" def validate(self): self.set_service_unit_properties() def autoname(self): if self.company: - suffix = " - " + frappe.get_cached_value('Company', self.company, 'abbr') + suffix = " - " + frappe.get_cached_value("Company", self.company, "abbr") if not self.healthcare_service_unit_name.endswith(suffix): self.name = self.healthcare_service_unit_name + suffix else: @@ -34,15 +34,15 @@ class HealthcareServiceUnit(NestedSet): self.overlap_appointments = False self.inpatient_occupancy = False self.service_unit_capacity = 0 - self.occupancy_status = '' - self.service_unit_type = '' - elif self.service_unit_type != '': - service_unit_type = frappe.get_doc('Healthcare Service Unit Type', self.service_unit_type) + self.occupancy_status = "" + self.service_unit_type = "" + elif self.service_unit_type != "": + service_unit_type = frappe.get_doc("Healthcare Service Unit Type", self.service_unit_type) self.allow_appointments = service_unit_type.allow_appointments self.inpatient_occupancy = service_unit_type.inpatient_occupancy - if self.inpatient_occupancy and self.occupancy_status != '': - self.occupancy_status = 'Vacant' + if self.inpatient_occupancy and self.occupancy_status != "": + self.occupancy_status = "Vacant" if service_unit_type.overlap_appointments: self.overlap_appointments = True @@ -52,62 +52,78 @@ class HealthcareServiceUnit(NestedSet): if self.overlap_appointments: if not self.service_unit_capacity: - frappe.throw(_('Please set a valid Service Unit Capacity to enable Overlapping Appointments'), - title=_('Mandatory')) + frappe.throw( + _("Please set a valid Service Unit Capacity to enable Overlapping Appointments"), + title=_("Mandatory"), + ) @frappe.whitelist() def add_multiple_service_units(parent, data): - ''' + """ parent - parent service unit under which the service units are to be created data (dict) - company, healthcare_service_unit_name, count, service_unit_type, warehouse, service_unit_capacity - ''' + """ if not parent or not data: return data = json.loads(data) - company = data.get('company') or \ - frappe.defaults.get_defaults().get('company') or \ - frappe.db.get_single_value('Global Defaults', 'default_company') + company = ( + data.get("company") + or frappe.defaults.get_defaults().get("company") + or frappe.db.get_single_value("Global Defaults", "default_company") + ) - if not data.get('healthcare_service_unit_name') or not company: - frappe.throw(_('Service Unit Name and Company are mandatory to create Healthcare Service Units'), - title=_('Missing Required Fields')) + if not data.get("healthcare_service_unit_name") or not company: + frappe.throw( + _("Service Unit Name and Company are mandatory to create Healthcare Service Units"), + title=_("Missing Required Fields"), + ) - count = cint(data.get('count') or 0) + count = cint(data.get("count") or 0) if count <= 0: - frappe.throw(_('Number of Service Units to be created should at least be 1'), - title=_('Invalid Number of Service Units')) + frappe.throw( + _("Number of Service Units to be created should at least be 1"), + title=_("Invalid Number of Service Units"), + ) - capacity = cint(data.get('service_unit_capacity') or 1) + capacity = cint(data.get("service_unit_capacity") or 1) service_unit = { - 'doctype': 'Healthcare Service Unit', - 'parent_healthcare_service_unit': parent, - 'service_unit_type': data.get('service_unit_type') or None, - 'service_unit_capacity': capacity if capacity > 0 else 1, - 'warehouse': data.get('warehouse') or None, - 'company': company + "doctype": "Healthcare Service Unit", + "parent_healthcare_service_unit": parent, + "service_unit_type": data.get("service_unit_type") or None, + "service_unit_capacity": capacity if capacity > 0 else 1, + "warehouse": data.get("warehouse") or None, + "company": company, } - service_unit_name = '{}'.format(data.get('healthcare_service_unit_name').strip(' -')) + service_unit_name = "{}".format(data.get("healthcare_service_unit_name").strip(" -")) - last_suffix = frappe.db.sql("""SELECT + last_suffix = frappe.db.sql( + """SELECT IFNULL(MAX(CAST(SUBSTRING(name FROM %(start)s FOR 4) AS UNSIGNED)), 0) FROM `tabHealthcare Service Unit` WHERE name like %(prefix)s AND company=%(company)s""", - {'start': len(service_unit_name)+2, 'prefix': '{}-%'.format(service_unit_name), 'company': company}, - as_list=1)[0][0] + { + "start": len(service_unit_name) + 2, + "prefix": "{}-%".format(service_unit_name), + "company": company, + }, + as_list=1, + )[0][0] start_suffix = cint(last_suffix) + 1 failed_list = [] for i in range(start_suffix, count + start_suffix): # name to be in the form WARD-#### - service_unit['healthcare_service_unit_name'] = '{}-{}'.format(service_unit_name, cstr('%0*d' % (4, i))) + service_unit["healthcare_service_unit_name"] = "{}-{}".format( + service_unit_name, cstr("%0*d" % (4, i)) + ) service_unit_doc = frappe.get_doc(service_unit) try: service_unit_doc.insert() except Exception: - failed_list.append(service_unit['healthcare_service_unit_name']) + failed_list.append(service_unit["healthcare_service_unit_name"]) return failed_list diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.py b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.py index 1db6b66c44b..f95036d82f8 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.py @@ -12,15 +12,21 @@ class HealthcareServiceUnitType(Document): def validate(self): if self.allow_appointments and self.inpatient_occupancy: frappe.msgprint( - _('Healthcare Service Unit Type cannot have both {0} and {1}').format( - frappe.bold('Allow Appointments'), frappe.bold('Inpatient Occupancy')), - raise_exception=1, title=_('Validation Error'), indicator='red' + _("Healthcare Service Unit Type cannot have both {0} and {1}").format( + frappe.bold("Allow Appointments"), frappe.bold("Inpatient Occupancy") + ), + raise_exception=1, + title=_("Validation Error"), + indicator="red", ) elif not self.allow_appointments and not self.inpatient_occupancy: frappe.msgprint( - _('Healthcare Service Unit Type must allow atleast one among {0} and {1}').format( - frappe.bold('Allow Appointments'), frappe.bold('Inpatient Occupancy')), - raise_exception=1, title=_('Validation Error'), indicator='red' + _("Healthcare Service Unit Type must allow atleast one among {0} and {1}").format( + frappe.bold("Allow Appointments"), frappe.bold("Inpatient Occupancy") + ), + raise_exception=1, + title=_("Validation Error"), + indicator="red", ) if not self.allow_appointments: @@ -28,9 +34,9 @@ class HealthcareServiceUnitType(Document): if self.is_billable: if self.disabled: - frappe.db.set_value('Item', self.item, 'disabled', 1) + frappe.db.set_value("Item", self.item, "disabled", 1) else: - frappe.db.set_value('Item', self.item, 'disabled', 0) + frappe.db.set_value("Item", self.item, "disabled", 0) def after_insert(self): if self.inpatient_occupancy and self.is_billable: @@ -40,10 +46,10 @@ class HealthcareServiceUnitType(Document): if self.item: try: item = self.item - self.db_set('item', '') - frappe.delete_doc('Item', item) + self.db_set("item", "") + frappe.delete_doc("Item", item) except Exception: - frappe.throw(_('Not permitted. Please disable the Service Unit Type')) + frappe.throw(_("Not permitted. Please disable the Service Unit Type")) def on_update(self): if self.change_in_item and self.is_billable and self.item: @@ -52,47 +58,50 @@ class HealthcareServiceUnitType(Document): item_price = item_price_exists(self) if not item_price: - price_list_name = frappe.db.get_value('Price List', {'selling': 1}) + price_list_name = frappe.db.get_value("Price List", {"selling": 1}) if self.rate: make_item_price(self.item_code, price_list_name, self.rate) else: make_item_price(self.item_code, price_list_name, 0.0) else: - frappe.db.set_value('Item Price', item_price, 'price_list_rate', self.rate) + frappe.db.set_value("Item Price", item_price, "price_list_rate", self.rate) - frappe.db.set_value(self.doctype, self.name, 'change_in_item',0) + frappe.db.set_value(self.doctype, self.name, "change_in_item", 0) elif not self.is_billable and self.item: - frappe.db.set_value('Item', self.item, 'disabled', 1) + frappe.db.set_value("Item", self.item, "disabled", 1) self.reload() def item_price_exists(doc): - item_price = frappe.db.exists({'doctype': 'Item Price', 'item_code': doc.item_code}) + item_price = frappe.db.exists({"doctype": "Item Price", "item_code": doc.item_code}) if len(item_price): return item_price[0][0] return False + def create_item(doc): # insert item - item = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': doc.item_code, - 'item_name': doc.service_unit_type, - 'item_group': doc.item_group, - 'description': doc.description or doc.item_code, - 'is_sales_item': 1, - 'is_service_item': 1, - 'is_purchase_item': 0, - 'is_stock_item': 0, - 'show_in_website': 0, - 'is_pro_applicable': 0, - 'disabled': 0, - 'stock_uom': doc.uom - }).insert(ignore_permissions=True, ignore_mandatory=True) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": doc.item_code, + "item_name": doc.service_unit_type, + "item_group": doc.item_group, + "description": doc.description or doc.item_code, + "is_sales_item": 1, + "is_service_item": 1, + "is_purchase_item": 0, + "is_stock_item": 0, + "show_in_website": 0, + "is_pro_applicable": 0, + "disabled": 0, + "stock_uom": doc.uom, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) # insert item price # get item price list to insert item price - price_list_name = frappe.db.get_value('Price List', {'selling': 1}) + price_list_name = frappe.db.get_value("Price List", {"selling": 1}) if doc.rate: make_item_price(item.name, price_list_name, doc.rate) item.standard_rate = doc.rate @@ -103,32 +112,39 @@ def create_item(doc): item.save(ignore_permissions=True) # Set item in the doc - doc.db_set('item', item.name) + doc.db_set("item", item.name) + def make_item_price(item, price_list_name, item_price): - frappe.get_doc({ - 'doctype': 'Item Price', - 'price_list': price_list_name, - 'item_code': item, - 'price_list_rate': item_price - }).insert(ignore_permissions=True, ignore_mandatory=True) + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list_name, + "item_code": item, + "price_list_rate": item_price, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + def update_item(doc): item = frappe.get_doc("Item", doc.item) if item: - item.update({ - "item_name": doc.service_unit_type, - "item_group": doc.item_group, - "disabled": 0, - "standard_rate": doc.rate, - "description": doc.description - }) + item.update( + { + "item_name": doc.service_unit_type, + "item_group": doc.item_group, + "disabled": 0, + "standard_rate": doc.rate, + "description": doc.description, + } + ) item.db_update() + @frappe.whitelist() def change_item_code(item, item_code, doc_name): - if frappe.db.exists({'doctype': 'Item', 'item_code': item_code}): - frappe.throw(_('Item with Item Code {0} already exists').format(item_code)) + if frappe.db.exists({"doctype": "Item", "item_code": item_code}): + frappe.throw(_("Item with Item Code {0} already exists").format(item_code)) else: - rename_doc('Item', item, item_code, ignore_permissions=True) - frappe.db.set_value('Healthcare Service Unit Type', doc_name, 'item_code', item_code) + rename_doc("Item", item, item_code, ignore_permissions=True) + frappe.db.set_value("Healthcare Service Unit Type", doc_name, "item_code", item_code) diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type_dashboard.py b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type_dashboard.py index a72bb6e6bc7..c14efb72121 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type_dashboard.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type_dashboard.py @@ -1,14 +1,10 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'service_unit_type', - 'transactions': [ - { - 'label': _('Healthcare Service Units'), - 'items': ['Healthcare Service Unit'] - }, - ] + "fieldname": "service_unit_type", + "transactions": [ + {"label": _("Healthcare Service Units"), "items": ["Healthcare Service Unit"]}, + ], } diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.py b/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.py index 2acd8a1c391..73f54d235f7 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.py @@ -9,25 +9,25 @@ import frappe class TestHealthcareServiceUnitType(unittest.TestCase): def test_item_creation(self): unit_type = get_unit_type() - self.assertTrue(frappe.db.exists('Item', unit_type.item)) + self.assertTrue(frappe.db.exists("Item", unit_type.item)) # check item disabled unit_type.disabled = 1 unit_type.save() - self.assertEqual(frappe.db.get_value('Item', unit_type.item, 'disabled'), 1) + self.assertEqual(frappe.db.get_value("Item", unit_type.item, "disabled"), 1) def get_unit_type(): - if frappe.db.exists('Healthcare Service Unit Type', 'Inpatient Rooms'): - return frappe.get_doc('Healthcare Service Unit Type', 'Inpatient Rooms') + if frappe.db.exists("Healthcare Service Unit Type", "Inpatient Rooms"): + return frappe.get_doc("Healthcare Service Unit Type", "Inpatient Rooms") - unit_type = frappe.new_doc('Healthcare Service Unit Type') - unit_type.service_unit_type = 'Inpatient Rooms' + unit_type = frappe.new_doc("Healthcare Service Unit Type") + unit_type.service_unit_type = "Inpatient Rooms" unit_type.inpatient_occupancy = 1 unit_type.is_billable = 1 - unit_type.item_code = 'Inpatient Rooms' - unit_type.item_group = 'Services' - unit_type.uom = 'Hour' + unit_type.item_code = "Inpatient Rooms" + unit_type.item_group = "Services" + unit_type.uom = "Hour" unit_type.no_of_hours = 1 unit_type.rate = 4000 unit_type.save() diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py index 2a9792e874e..f8061e9dd15 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py @@ -12,13 +12,19 @@ from frappe.model.document import Document class HealthcareSettings(Document): def validate(self): - for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', - 'lab_test_approval_required', 'create_sample_collection_for_lab_test', 'default_medical_code_standard']: + for key in [ + "collect_registration_fee", + "link_customer_to_patient", + "patient_name_by", + "lab_test_approval_required", + "create_sample_collection_for_lab_test", + "default_medical_code_standard", + ]: frappe.db.set_default(key, self.get(key, "")) if self.collect_registration_fee: if self.registration_fee <= 0: - frappe.throw(_('Registration Fee cannot be negative or zero')) + frappe.throw(_("Registration Fee cannot be negative or zero")) if self.inpatient_visit_charge_item: validate_service_item(self.inpatient_visit_charge_item) @@ -29,63 +35,70 @@ class HealthcareSettings(Document): def validate_service_item(item): - if frappe.db.get_value('Item', item, 'is_stock_item'): - frappe.throw(_('Configure a service Item for {0}').format(item)) + if frappe.db.get_value("Item", item, "is_stock_item"): + frappe.throw(_("Configure a service Item for {0}").format(item)) + @frappe.whitelist() def get_sms_text(doc): sms_text = {} - doc = frappe.get_doc('Lab Test', doc) - context = {'doc': doc, 'alert': doc, 'comments': None} + doc = frappe.get_doc("Lab Test", doc) + context = {"doc": doc, "alert": doc, "comments": None} - emailed = frappe.db.get_value('Healthcare Settings', None, 'sms_emailed') - sms_text['emailed'] = frappe.render_template(emailed, context) + emailed = frappe.db.get_value("Healthcare Settings", None, "sms_emailed") + sms_text["emailed"] = frappe.render_template(emailed, context) - printed = frappe.db.get_value('Healthcare Settings', None, 'sms_printed') - sms_text['printed'] = frappe.render_template(printed, context) + printed = frappe.db.get_value("Healthcare Settings", None, "sms_printed") + sms_text["printed"] = frappe.render_template(printed, context) return sms_text + def send_registration_sms(doc): - if frappe.db.get_single_value('Healthcare Settings', 'send_registration_msg'): + if frappe.db.get_single_value("Healthcare Settings", "send_registration_msg"): if doc.mobile: - context = {'doc': doc, 'alert': doc, 'comments': None} - if doc.get('_comments'): - context['comments'] = json.loads(doc.get('_comments')) - messages = frappe.db.get_single_value('Healthcare Settings', 'registration_msg') + context = {"doc": doc, "alert": doc, "comments": None} + if doc.get("_comments"): + context["comments"] = json.loads(doc.get("_comments")) + messages = frappe.db.get_single_value("Healthcare Settings", "registration_msg") messages = frappe.render_template(messages, context) number = [doc.mobile] - send_sms(number,messages) + send_sms(number, messages) else: - frappe.msgprint(doc.name + ' has no mobile number to send registration SMS', alert=True) + frappe.msgprint(doc.name + " has no mobile number to send registration SMS", alert=True) + def get_receivable_account(company): - receivable_account = get_account(None, 'receivable_account', 'Healthcare Settings', company) + receivable_account = get_account(None, "receivable_account", "Healthcare Settings", company) if receivable_account: return receivable_account - return frappe.get_cached_value('Company', company, 'default_receivable_account') + return frappe.get_cached_value("Company", company, "default_receivable_account") + def get_income_account(practitioner, company): # check income account in Healthcare Practitioner if practitioner: - income_account = get_account('Healthcare Practitioner', None, practitioner, company) + income_account = get_account("Healthcare Practitioner", None, practitioner, company) if income_account: return income_account # else check income account in Healthcare Settings - income_account = get_account(None, 'income_account', 'Healthcare Settings', company) + income_account = get_account(None, "income_account", "Healthcare Settings", company) if income_account: return income_account # else return default income account of company - return frappe.get_cached_value('Company', company, 'default_income_account') + return frappe.get_cached_value("Company", company, "default_income_account") + def get_account(parent_type, parent_field, parent, company): if parent_type: - return frappe.db.get_value('Party Account', - {'parenttype': parent_type, 'parent': parent, 'company': company}, 'account') + return frappe.db.get_value( + "Party Account", {"parenttype": parent_type, "parent": parent, "company": company}, "account" + ) if parent_field: - return frappe.db.get_value('Party Account', - {'parentfield': parent_field, 'parent': parent, 'company': company}, 'account') + return frappe.db.get_value( + "Party Account", {"parentfield": parent_field, "parent": parent, "company": company}, "account" + ) diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py index 3cd8cb42a77..d78a6d82155 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -24,59 +24,68 @@ class InpatientMedicationEntry(Document): self.add_mo_to_table(orders) return self else: - self.set('medication_orders', []) - frappe.msgprint(_('No pending medication orders found for selected criteria')) + self.set("medication_orders", []) + frappe.msgprint(_("No pending medication orders found for selected criteria")) def add_mo_to_table(self, orders): # Add medication orders in the child table - self.set('medication_orders', []) + self.set("medication_orders", []) for data in orders: - self.append('medication_orders', { - 'patient': data.patient, - 'patient_name': data.patient_name, - 'inpatient_record': data.inpatient_record, - 'service_unit': data.service_unit, - 'datetime': "%s %s" % (data.date, data.time or "00:00:00"), - 'drug_code': data.drug, - 'drug_name': data.drug_name, - 'dosage': data.dosage, - 'dosage_form': data.dosage_form, - 'against_imo': data.parent, - 'against_imoe': data.name - }) + self.append( + "medication_orders", + { + "patient": data.patient, + "patient_name": data.patient_name, + "inpatient_record": data.inpatient_record, + "service_unit": data.service_unit, + "datetime": "%s %s" % (data.date, data.time or "00:00:00"), + "drug_code": data.drug, + "drug_name": data.drug_name, + "dosage": data.dosage, + "dosage_form": data.dosage_form, + "against_imo": data.parent, + "against_imoe": data.name, + }, + ) def on_submit(self): self.validate_medication_orders() success_msg = "" if self.update_stock: stock_entry = self.process_stock() - success_msg += _('Stock Entry {0} created and ').format( - frappe.bold(get_link_to_form('Stock Entry', stock_entry))) + success_msg += _("Stock Entry {0} created and ").format( + frappe.bold(get_link_to_form("Stock Entry", stock_entry)) + ) self.update_medication_orders() - success_msg += _('Inpatient Medication Orders updated successfully') - frappe.msgprint(success_msg, title=_('Success'), indicator='green') + success_msg += _("Inpatient Medication Orders updated successfully") + frappe.msgprint(success_msg, title=_("Success"), indicator="green") def validate_medication_orders(self): for entry in self.medication_orders: - docstatus, is_completed = frappe.db.get_value('Inpatient Medication Order Entry', entry.against_imoe, - ['docstatus', 'is_completed']) + docstatus, is_completed = frappe.db.get_value( + "Inpatient Medication Order Entry", entry.against_imoe, ["docstatus", "is_completed"] + ) if docstatus == 2: - frappe.throw(_('Row {0}: Cannot create Inpatient Medication Entry against cancelled Inpatient Medication Order {1}').format( - entry.idx, get_link_to_form(entry.against_imo))) + frappe.throw( + _( + "Row {0}: Cannot create Inpatient Medication Entry against cancelled Inpatient Medication Order {1}" + ).format(entry.idx, get_link_to_form(entry.against_imo)) + ) if is_completed: - frappe.throw(_('Row {0}: This Medication Order is already marked as completed').format( - entry.idx)) + frappe.throw( + _("Row {0}: This Medication Order is already marked as completed").format(entry.idx) + ) def on_cancel(self): self.cancel_stock_entries() self.update_medication_orders(on_cancel=True) def process_stock(self): - allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') + allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") if not allow_negative_stock: self.check_stock_qty() @@ -89,24 +98,27 @@ class InpatientMedicationEntry(Document): if on_cancel: is_completed = 0 - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabInpatient Medication Order Entry` SET is_completed = %(is_completed)s WHERE name IN %(orders)s - """, {'orders': orders, 'is_completed': is_completed}) + """, + {"orders": orders, "is_completed": is_completed}, + ) # update status and completed orders count for order, count in order_entry_map.items(): - medication_order = frappe.get_doc('Inpatient Medication Order', order) + medication_order = frappe.get_doc("Inpatient Medication Order", order) completed_orders = flt(count) - current_value = frappe.db.get_value('Inpatient Medication Order', order, 'completed_orders') + current_value = frappe.db.get_value("Inpatient Medication Order", order, "completed_orders") if on_cancel: completed_orders = flt(current_value) - flt(count) else: completed_orders = flt(current_value) + flt(count) - medication_order.db_set('completed_orders', completed_orders) + medication_order.db_set("completed_orders", completed_orders) medication_order.set_status() def get_order_entry_map(self): @@ -129,17 +141,23 @@ class InpatientMedicationEntry(Document): drug_shortage = get_drug_shortage_map(self.medication_orders, self.warehouse) if drug_shortage: - message = _('Quantity not available for the following items in warehouse {0}. ').format(frappe.bold(self.warehouse)) - message += _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.') + message = _("Quantity not available for the following items in warehouse {0}. ").format( + frappe.bold(self.warehouse) + ) + message += _( + "Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed." + ) - formatted_item_rows = '' + formatted_item_rows = "" for drug, shortage_qty in drug_shortage.items(): - item_link = get_link_to_form('Item', drug) + item_link = get_link_to_form("Item", drug) formatted_item_rows += """ {0} {1} - """.format(item_link, frappe.bold(shortage_qty)) + """.format( + item_link, frappe.bold(shortage_qty) + ) message += """ @@ -149,25 +167,27 @@ class InpatientMedicationEntry(Document): {2}
- """.format(_('Drug Code'), _('Shortage Qty'), formatted_item_rows) + """.format( + _("Drug Code"), _("Shortage Qty"), formatted_item_rows + ) - frappe.throw(message, title=_('Insufficient Stock'), is_minimizable=True, wide=True) + frappe.throw(message, title=_("Insufficient Stock"), is_minimizable=True, wide=True) def make_stock_entry(self): - stock_entry = frappe.new_doc('Stock Entry') - stock_entry.purpose = 'Material Issue' + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.purpose = "Material Issue" stock_entry.set_stock_entry_type() stock_entry.from_warehouse = self.warehouse stock_entry.company = self.company stock_entry.inpatient_medication_entry = self.name - cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') - expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company) + cost_center = frappe.get_cached_value("Company", self.company, "cost_center") + expense_account = get_account(None, "expense_account", "Healthcare Settings", self.company) for entry in self.medication_orders: - se_child = stock_entry.append('items') + se_child = stock_entry.append("items") se_child.item_code = entry.drug_code se_child.item_name = entry.drug_name - se_child.uom = frappe.db.get_value('Item', entry.drug_code, 'stock_uom') + se_child.uom = frappe.db.get_value("Item", entry.drug_code, "stock_uom") se_child.stock_uom = se_child.uom se_child.qty = flt(entry.dosage) # in stock uom @@ -182,9 +202,9 @@ class InpatientMedicationEntry(Document): return stock_entry.name def cancel_stock_entries(self): - stock_entries = frappe.get_all('Stock Entry', {'inpatient_medication_entry': self.name}) + stock_entries = frappe.get_all("Stock Entry", {"inpatient_medication_entry": self.name}) for entry in stock_entries: - doc = frappe.get_doc('Stock Entry', entry.name) + doc = frappe.get_doc("Stock Entry", entry.name) doc.cancel() @@ -192,7 +212,8 @@ def get_pending_medication_orders(entry): filters, values = get_filters(entry) to_remove = [] - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT ip.inpatient_record, ip.patient, ip.patient_name, entry.name, entry.parent, entry.drug, entry.drug_name, @@ -210,12 +231,17 @@ def get_pending_medication_orders(entry): {0} ORDER BY entry.date, entry.time - """.format(filters), values, as_dict=1) + """.format( + filters + ), + values, + as_dict=1, + ) for doc in data: inpatient_record = doc.inpatient_record if inpatient_record: - doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record) + doc["service_unit"] = get_current_healthcare_service_unit(inpatient_record) if entry.service_unit and doc.service_unit != entry.service_unit: to_remove.append(doc) @@ -227,53 +253,53 @@ def get_pending_medication_orders(entry): def get_filters(entry): - filters = '' + filters = "" values = dict(company=entry.company) if entry.from_date: - filters += ' and entry.date >= %(from_date)s' - values['from_date'] = entry.from_date + filters += " and entry.date >= %(from_date)s" + values["from_date"] = entry.from_date if entry.to_date: - filters += ' and entry.date <= %(to_date)s' - values['to_date'] = entry.to_date + filters += " and entry.date <= %(to_date)s" + values["to_date"] = entry.to_date if entry.from_time: - filters += ' and entry.time >= %(from_time)s' - values['from_time'] = entry.from_time + filters += " and entry.time >= %(from_time)s" + values["from_time"] = entry.from_time if entry.to_time: - filters += ' and entry.time <= %(to_time)s' - values['to_time'] = entry.to_time + filters += " and entry.time <= %(to_time)s" + values["to_time"] = entry.to_time if entry.patient: - filters += ' and ip.patient = %(patient)s' - values['patient'] = entry.patient + filters += " and ip.patient = %(patient)s" + values["patient"] = entry.patient if entry.practitioner: - filters += ' and ip.practitioner = %(practitioner)s' - values['practitioner'] = entry.practitioner + filters += " and ip.practitioner = %(practitioner)s" + values["practitioner"] = entry.practitioner if entry.item_code: - filters += ' and entry.drug = %(item_code)s' - values['item_code'] = entry.item_code + filters += " and entry.drug = %(item_code)s" + values["item_code"] = entry.item_code if entry.assigned_to_practitioner: - filters += ' and ip._assign LIKE %(assigned_to)s' - values['assigned_to'] = '%' + entry.assigned_to_practitioner + '%' + filters += " and ip._assign LIKE %(assigned_to)s" + values["assigned_to"] = "%" + entry.assigned_to_practitioner + "%" return filters, values def get_current_healthcare_service_unit(inpatient_record): - ip_record = frappe.get_doc('Inpatient Record', inpatient_record) - if ip_record.status in ['Admitted', 'Discharge Scheduled'] and ip_record.inpatient_occupancies: + ip_record = frappe.get_doc("Inpatient Record", inpatient_record) + if ip_record.status in ["Admitted", "Discharge Scheduled"] and ip_record.inpatient_occupancies: return ip_record.inpatient_occupancies[-1].service_unit return def get_drug_shortage_map(medication_orders, warehouse): """ - Returns a dict like { drug_code: shortage_qty } + Returns a dict like { drug_code: shortage_qty } """ drug_requirement = dict() for d in medication_orders: @@ -292,25 +318,25 @@ def get_drug_shortage_map(medication_orders, warehouse): @frappe.whitelist() def make_difference_stock_entry(docname): - doc = frappe.get_doc('Inpatient Medication Entry', docname) + doc = frappe.get_doc("Inpatient Medication Entry", docname) drug_shortage = get_drug_shortage_map(doc.medication_orders, doc.warehouse) if not drug_shortage: return None - stock_entry = frappe.new_doc('Stock Entry') - stock_entry.purpose = 'Material Transfer' + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.purpose = "Material Transfer" stock_entry.set_stock_entry_type() stock_entry.to_warehouse = doc.warehouse stock_entry.company = doc.company - cost_center = frappe.get_cached_value('Company', doc.company, 'cost_center') - expense_account = get_account(None, 'expense_account', 'Healthcare Settings', doc.company) + cost_center = frappe.get_cached_value("Company", doc.company, "cost_center") + expense_account = get_account(None, "expense_account", "Healthcare Settings", doc.company) for drug, shortage_qty in drug_shortage.items(): - se_child = stock_entry.append('items') + se_child = stock_entry.append("items") se_child.item_code = drug - se_child.item_name = frappe.db.get_value('Item', drug, 'stock_uom') - se_child.uom = frappe.db.get_value('Item', drug, 'stock_uom') + se_child.item_name = frappe.db.get_value("Item", drug, "stock_uom") + se_child.uom = frappe.db.get_value("Item", drug, "stock_uom") se_child.stock_uom = se_child.uom se_child.qty = flt(shortage_qty) se_child.t_warehouse = doc.warehouse diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py index 23e899a8d9a..07d634e9dd4 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py @@ -1,17 +1,9 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'against_imoe', - 'internal_links': { - 'Inpatient Medication Order': ['medication_orders', 'against_imo'] - }, - 'transactions': [ - { - 'label': _('Reference'), - 'items': ['Inpatient Medication Order'] - } - ] + "fieldname": "against_imoe", + "internal_links": {"Inpatient Medication Order": ["medication_orders", "against_imo"]}, + "transactions": [{"label": _("Reference"), "items": ["Inpatient Medication Order"]}], } diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py index 6da116447b9..ee21f2bd0c0 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py @@ -53,10 +53,10 @@ class TestInpatientMedicationEntry(unittest.TestCase): filters = frappe._dict( from_date=date, to_date=date, - from_time='', - to_time='', - item_code='Dextromethorphan', - patient=self.patient + from_time="", + to_time="", + item_code="Dextromethorphan", + patient=self.patient, ) ipme = create_ipme(filters, update_stock=0) @@ -74,10 +74,10 @@ class TestInpatientMedicationEntry(unittest.TestCase): filters = frappe._dict( from_date=date, to_date=date, - from_time='', - to_time='', - item_code='Dextromethorphan', - patient=self.patient + from_time="", + to_time="", + item_code="Dextromethorphan", + patient=self.patient, ) make_stock_entry() @@ -86,18 +86,21 @@ class TestInpatientMedicationEntry(unittest.TestCase): ipme.reload() # test order completed - is_order_completed = frappe.db.get_value('Inpatient Medication Order Entry', - ipme.medication_orders[0].against_imoe, 'is_completed') + is_order_completed = frappe.db.get_value( + "Inpatient Medication Order Entry", ipme.medication_orders[0].against_imoe, "is_completed" + ) self.assertEqual(is_order_completed, 1) # test stock entry - stock_entry = frappe.db.exists('Stock Entry', {'inpatient_medication_entry': ipme.name}) + stock_entry = frappe.db.exists("Stock Entry", {"inpatient_medication_entry": ipme.name}) self.assertTrue(stock_entry) # check references - stock_entry = frappe.get_doc('Stock Entry', stock_entry) + stock_entry = frappe.get_doc("Stock Entry", stock_entry) self.assertEqual(stock_entry.items[0].patient, self.patient) - self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name) + self.assertEqual( + stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name + ) def test_drug_shortage_stock_entry(self): ipmo = create_ipmo(self.patient) @@ -108,25 +111,25 @@ class TestInpatientMedicationEntry(unittest.TestCase): filters = frappe._dict( from_date=date, to_date=date, - from_time='', - to_time='', - item_code='Dextromethorphan', - patient=self.patient + from_time="", + to_time="", + item_code="Dextromethorphan", + patient=self.patient, ) # check drug shortage ipme = create_ipme(filters, update_stock=1) - ipme.warehouse = 'Finished Goods - _TC' + ipme.warehouse = "Finished Goods - _TC" ipme.save() drug_shortage = get_drug_shortage_map(ipme.medication_orders, ipme.warehouse) - self.assertEqual(drug_shortage.get('Dextromethorphan'), 3) + self.assertEqual(drug_shortage.get("Dextromethorphan"), 3) # check material transfer for drug shortage make_stock_entry() stock_entry = make_difference_stock_entry(ipme.name) - self.assertEqual(stock_entry.items[0].item_code, 'Dextromethorphan') + self.assertEqual(stock_entry.items[0].item_code, "Dextromethorphan") self.assertEqual(stock_entry.items[0].qty, 3) - stock_entry.from_warehouse = 'Stores - _TC' + stock_entry.from_warehouse = "Stores - _TC" stock_entry.submit() ipme.reload() @@ -134,38 +137,45 @@ class TestInpatientMedicationEntry(unittest.TestCase): def tearDown(self): # cleanup - Discharge - schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) + schedule_discharge( + frappe.as_json({"patient": self.patient, "discharge_ordered_datetime": now_datetime()}) + ) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() discharge_patient(self.ip_record, now_datetime()) - for entry in frappe.get_all('Inpatient Medication Entry'): - doc = frappe.get_doc('Inpatient Medication Entry', entry.name) + for entry in frappe.get_all("Inpatient Medication Entry"): + doc = frappe.get_doc("Inpatient Medication Entry", entry.name) doc.cancel() - for entry in frappe.get_all('Inpatient Medication Order'): - doc = frappe.get_doc('Inpatient Medication Order', entry.name) + for entry in frappe.get_all("Inpatient Medication Order"): + doc = frappe.get_doc("Inpatient Medication Order", entry.name) doc.cancel() + def make_stock_entry(warehouse=None): - frappe.db.set_value('Company', '_Test Company', { - 'stock_adjustment_account': 'Stock Adjustment - _TC', - 'default_inventory_account': 'Stock In Hand - _TC' - }) - stock_entry = frappe.new_doc('Stock Entry') - stock_entry.stock_entry_type = 'Material Receipt' - stock_entry.company = '_Test Company' - stock_entry.to_warehouse = warehouse or 'Stores - _TC' - expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company') - se_child = stock_entry.append('items') - se_child.item_code = 'Dextromethorphan' - se_child.item_name = 'Dextromethorphan' - se_child.uom = 'Nos' - se_child.stock_uom = 'Nos' + frappe.db.set_value( + "Company", + "_Test Company", + { + "stock_adjustment_account": "Stock Adjustment - _TC", + "default_inventory_account": "Stock In Hand - _TC", + }, + ) + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.stock_entry_type = "Material Receipt" + stock_entry.company = "_Test Company" + stock_entry.to_warehouse = warehouse or "Stores - _TC" + expense_account = get_account(None, "expense_account", "Healthcare Settings", "_Test Company") + se_child = stock_entry.append("items") + se_child.item_code = "Dextromethorphan" + se_child.item_name = "Dextromethorphan" + se_child.uom = "Nos" + se_child.stock_uom = "Nos" se_child.qty = 6 - se_child.t_warehouse = 'Stores - _TC' + se_child.t_warehouse = "Stores - _TC" # in stock uom se_child.conversion_factor = 1.0 se_child.expense_account = expense_account diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py index 60c57f9c45d..8360132e3e9 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py +++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py @@ -26,50 +26,53 @@ class InpatientMedicationOrder(Document): def validate_inpatient(self): if not self.inpatient_record: - frappe.throw(_('No Inpatient Record found against patient {0}').format(self.patient)) + frappe.throw(_("No Inpatient Record found against patient {0}").format(self.patient)) def validate_duplicate(self): - existing_mo = frappe.db.exists('Inpatient Medication Order', { - 'patient_encounter': self.patient_encounter, - 'docstatus': ('!=', 2), - 'name': ('!=', self.name) - }) + existing_mo = frappe.db.exists( + "Inpatient Medication Order", + { + "patient_encounter": self.patient_encounter, + "docstatus": ("!=", 2), + "name": ("!=", self.name), + }, + ) if existing_mo: - frappe.throw(_('An Inpatient Medication Order {0} against Patient Encounter {1} already exists.').format( - existing_mo, self.patient_encounter), frappe.DuplicateEntryError) + frappe.throw( + _("An Inpatient Medication Order {0} against Patient Encounter {1} already exists.").format( + existing_mo, self.patient_encounter + ), + frappe.DuplicateEntryError, + ) def set_total_orders(self): - self.db_set('total_orders', len(self.medication_orders)) + self.db_set("total_orders", len(self.medication_orders)) def set_status(self): - status = { - "0": "Draft", - "1": "Submitted", - "2": "Cancelled" - }[cstr(self.docstatus or 0)] + status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[cstr(self.docstatus or 0)] if self.docstatus == 1: if not self.completed_orders: - status = 'Pending' + status = "Pending" elif self.completed_orders < self.total_orders: - status = 'In Process' + status = "In Process" else: - status = 'Completed' + status = "Completed" - self.db_set('status', status) + self.db_set("status", status) @frappe.whitelist() def add_order_entries(self, order): - if order.get('drug_code'): - dosage = frappe.get_doc('Prescription Dosage', order.get('dosage')) - dates = get_prescription_dates(order.get('period'), self.start_date) + if order.get("drug_code"): + dosage = frappe.get_doc("Prescription Dosage", order.get("dosage")) + dates = get_prescription_dates(order.get("period"), self.start_date) for date in dates: for dose in dosage.dosage_strength: - entry = self.append('medication_orders') - entry.drug = order.get('drug_code') - entry.drug_name = frappe.db.get_value('Item', order.get('drug_code'), 'item_name') + entry = self.append("medication_orders") + entry.drug = order.get("drug_code") + entry.drug_name = frappe.db.get_value("Item", order.get("drug_code"), "item_name") entry.dosage = dose.strength - entry.dosage_form = order.get('dosage_form') + entry.dosage_form = order.get("dosage_form") entry.date = date entry.time = dose.strength_time self.end_date = dates[-1] diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py index 5ebe6fa594b..8f777b5008b 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py +++ b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py @@ -42,15 +42,19 @@ class TestInpatientMedicationOrder(unittest.TestCase): self.assertEqual(len(ipmo.medication_orders), 6) self.assertEqual(ipmo.medication_orders[0].date, add_days(getdate(), -1)) - prescription_dosage = frappe.get_doc('Prescription Dosage', '1-1-1') + prescription_dosage = frappe.get_doc("Prescription Dosage", "1-1-1") for i in range(len(prescription_dosage.dosage_strength)): - self.assertEqual(ipmo.medication_orders[i].time, prescription_dosage.dosage_strength[i].strength_time) + self.assertEqual( + ipmo.medication_orders[i].time, prescription_dosage.dosage_strength[i].strength_time + ) self.assertEqual(ipmo.medication_orders[3].date, getdate()) def test_inpatient_validation(self): # Discharge - schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) + schedule_discharge( + frappe.as_json({"patient": self.patient, "discharge_ordered_datetime": now_datetime()}) + ) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) @@ -67,24 +71,28 @@ class TestInpatientMedicationOrder(unittest.TestCase): ipmo.submit() ipmo.reload() - self.assertEqual(ipmo.status, 'Pending') + self.assertEqual(ipmo.status, "Pending") - filters = frappe._dict(from_date=add_days(getdate(), -1), to_date=add_days(getdate(), -1), from_time='', to_time='') + filters = frappe._dict( + from_date=add_days(getdate(), -1), to_date=add_days(getdate(), -1), from_time="", to_time="" + ) ipme = create_ipme(filters) ipme.submit() ipmo.reload() - self.assertEqual(ipmo.status, 'In Process') + self.assertEqual(ipmo.status, "In Process") - filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='') + filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time="", to_time="") ipme = create_ipme(filters) ipme.submit() ipmo.reload() - self.assertEqual(ipmo.status, 'Completed') + self.assertEqual(ipmo.status, "Completed") def tearDown(self): - if frappe.db.get_value('Patient', self.patient, 'inpatient_record'): + if frappe.db.get_value("Patient", self.patient, "inpatient_record"): # cleanup - Discharge - schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) + schedule_discharge( + frappe.as_json({"patient": self.patient, "discharge_ordered_datetime": now_datetime()}) + ) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) @@ -94,57 +102,61 @@ class TestInpatientMedicationOrder(unittest.TestCase): for doctype in ["Inpatient Medication Entry", "Inpatient Medication Order"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) + def create_dosage_form(): - if not frappe.db.exists('Dosage Form', 'Tablet'): - frappe.get_doc({ - 'doctype': 'Dosage Form', - 'dosage_form': 'Tablet' - }).insert() + if not frappe.db.exists("Dosage Form", "Tablet"): + frappe.get_doc({"doctype": "Dosage Form", "dosage_form": "Tablet"}).insert() + def create_drug(item=None): if not item: - item = 'Dextromethorphan' - drug = frappe.db.exists('Item', {'item_code': 'Dextromethorphan'}) + item = "Dextromethorphan" + drug = frappe.db.exists("Item", {"item_code": "Dextromethorphan"}) if not drug: - drug = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': 'Dextromethorphan', - 'item_name': 'Dextromethorphan', - 'item_group': 'Products', - 'stock_uom': 'Nos', - 'is_stock_item': 1, - 'valuation_rate': 50, - 'opening_stock': 20 - }).insert() + drug = frappe.get_doc( + { + "doctype": "Item", + "item_code": "Dextromethorphan", + "item_name": "Dextromethorphan", + "item_group": "Products", + "stock_uom": "Nos", + "is_stock_item": 1, + "valuation_rate": 50, + "opening_stock": 20, + } + ).insert() + def get_orders(): create_dosage_form() create_drug() return { - 'drug_code': 'Dextromethorphan', - 'drug_name': 'Dextromethorphan', - 'dosage': '1-1-1', - 'dosage_form': 'Tablet', - 'period': '2 Day' + "drug_code": "Dextromethorphan", + "drug_name": "Dextromethorphan", + "dosage": "1-1-1", + "dosage_form": "Tablet", + "period": "2 Day", } + def create_ipmo(patient): orders = get_orders() - ipmo = frappe.new_doc('Inpatient Medication Order') + ipmo = frappe.new_doc("Inpatient Medication Order") ipmo.patient = patient - ipmo.company = '_Test Company' + ipmo.company = "_Test Company" ipmo.start_date = add_days(getdate(), -1) ipmo.add_order_entries(orders) return ipmo + def create_ipme(filters, update_stock=0): - ipme = frappe.new_doc('Inpatient Medication Entry') - ipme.company = '_Test Company' + ipme = frappe.new_doc("Inpatient Medication Entry") + ipme.company = "_Test Company" ipme.posting_date = getdate() ipme.update_stock = update_stock if update_stock: - ipme.warehouse = 'Stores - _TC' + ipme.warehouse = "Stores - _TC" for key, value in filters.items(): ipme.set(key, value) ipme = ipme.get_medication_orders() diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json index 03ecf4fb018..e826ecf80bb 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json @@ -24,13 +24,12 @@ "expected_discharge", "references", "admission_encounter", - "admission_practitioner", + "primary_practitioner", "medical_department", "admission_ordered_for", "expected_length_of_stay", "admission_service_unit_type", "cb_admission", - "primary_practitioner", "secondary_practitioner", "admission_instruction", "encounter_details_section", @@ -134,11 +133,11 @@ "read_only": 1 }, { + "fetch_from": "primary_practitioner.department", "fieldname": "medical_department", "fieldtype": "Link", "label": "Medical Department", - "options": "Medical Department", - "set_only_once": 1 + "options": "Medical Department" }, { "fieldname": "primary_practitioner", @@ -211,13 +210,6 @@ "fieldname": "cb_admission", "fieldtype": "Column Break" }, - { - "fieldname": "admission_practitioner", - "fieldtype": "Link", - "label": "Healthcare Practitioner", - "options": "Healthcare Practitioner", - "read_only": 1 - }, { "fieldname": "admission_encounter", "fieldtype": "Link", @@ -412,7 +404,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-08-09 22:49:07.419692", + "modified": "2022-02-22 12:15:02.843426", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Record", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index 322ad0e4f26..fc7ce5fd4b8 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -13,12 +13,16 @@ from frappe.utils import get_datetime, get_link_to_form, getdate, now_datetime, class InpatientRecord(Document): def after_insert(self): - frappe.db.set_value('Patient', self.patient, 'inpatient_record', self.name) - frappe.db.set_value('Patient', self.patient, 'inpatient_status', self.status) + frappe.db.set_value("Patient", self.patient, "inpatient_record", self.name) + frappe.db.set_value("Patient", self.patient, "inpatient_status", self.status) - if self.admission_encounter: # Update encounter - frappe.db.set_value('Patient Encounter', self.admission_encounter, 'inpatient_record', self.name) - frappe.db.set_value('Patient Encounter', self.admission_encounter, 'inpatient_status', self.status) + if self.admission_encounter: # Update encounter + frappe.db.set_value( + "Patient Encounter", self.admission_encounter, "inpatient_record", self.name + ) + frappe.db.set_value( + "Patient Encounter", self.admission_encounter, "inpatient_status", self.status + ) def validate(self): self.validate_dates() @@ -28,14 +32,20 @@ class InpatientRecord(Document): frappe.db.set_value("Patient", self.patient, "inpatient_record", None) def validate_dates(self): - if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or \ - (getdate(self.discharge_ordered_datetime) < getdate(self.scheduled_date)): - frappe.throw(_('Expected and Discharge dates cannot be less than Admission Schedule date')) + if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or ( + getdate(self.discharge_ordered_datetime) < getdate(self.scheduled_date) + ): + frappe.throw(_("Expected and Discharge dates cannot be less than Admission Schedule date")) for entry in self.inpatient_occupancies: - if entry.check_in and entry.check_out and \ - get_datetime(entry.check_in) > get_datetime(entry.check_out): - frappe.throw(_('Row #{0}: Check Out datetime cannot be less than Check In datetime').format(entry.idx)) + if ( + entry.check_in + and entry.check_out + and get_datetime(entry.check_in) > get_datetime(entry.check_out) + ): + frappe.throw( + _("Row #{0}: Check Out datetime cannot be less than Check In datetime").format(entry.idx) + ) def validate_already_scheduled_or_admitted(self): query = """ @@ -45,14 +55,13 @@ class InpatientRecord(Document): and name != %(name)s and patient = %(patient)s """ - ip_record = frappe.db.sql(query,{ - "name": self.name, - "patient": self.patient - }, as_dict = 1) + ip_record = frappe.db.sql(query, {"name": self.name, "patient": self.patient}, as_dict=1) if ip_record: - msg = _(("Already {0} Patient {1} with Inpatient Record ").format(ip_record[0].status, self.patient) \ - + """ {0}""".format(ip_record[0].name)) + msg = _( + ("Already {0} Patient {1} with Inpatient Record ").format(ip_record[0].status, self.patient) + + """ {0}""".format(ip_record[0].name) + ) frappe.throw(msg) @frappe.whitelist() @@ -63,8 +72,8 @@ class InpatientRecord(Document): def discharge(self, check_out=None): if not check_out: check_out = now_datetime() - if (getdate(check_out) < getdate(self.admitted_datetime)): - frappe.throw(_('Discharge date cannot be less than Admission date')) + if getdate(check_out) < getdate(self.admitted_datetime): + frappe.throw(_("Discharge date cannot be less than Admission date")) discharge_patient(self, check_out) @frappe.whitelist() @@ -77,17 +86,21 @@ class InpatientRecord(Document): @frappe.whitelist() def schedule_inpatient(args): - admission_order = json.loads(args) # admission order via Encounter - if not admission_order or not admission_order['patient'] or not admission_order['admission_encounter']: - frappe.throw(_('Missing required details, did not create Inpatient Record')) + admission_order = json.loads(args) # admission order via Encounter + if ( + not admission_order + or not admission_order["patient"] + or not admission_order["admission_encounter"] + ): + frappe.throw(_("Missing required details, did not create Inpatient Record")) - inpatient_record = frappe.new_doc('Inpatient Record') + inpatient_record = frappe.new_doc("Inpatient Record") # Admission order details set_details_from_ip_order(inpatient_record, admission_order) # Patient details - patient = frappe.get_doc('Patient', admission_order['patient']) + patient = frappe.get_doc("Patient", admission_order["patient"]) inpatient_record.patient = patient.name inpatient_record.patient_name = patient.patient_name inpatient_record.gender = patient.sex @@ -99,45 +112,60 @@ def schedule_inpatient(args): inpatient_record.scheduled_date = today() # Set encounter detials - encounter = frappe.get_doc('Patient Encounter', admission_order['admission_encounter']) - if encounter and encounter.symptoms: # Symptoms - set_ip_child_records(inpatient_record, 'chief_complaint', encounter.symptoms) + encounter = frappe.get_doc("Patient Encounter", admission_order["admission_encounter"]) + if encounter and encounter.symptoms: # Symptoms + set_ip_child_records(inpatient_record, "chief_complaint", encounter.symptoms) - if encounter and encounter.diagnosis: # Diagnosis - set_ip_child_records(inpatient_record, 'diagnosis', encounter.diagnosis) + if encounter and encounter.diagnosis: # Diagnosis + set_ip_child_records(inpatient_record, "diagnosis", encounter.diagnosis) - if encounter and encounter.drug_prescription: # Medication - set_ip_child_records(inpatient_record, 'drug_prescription', encounter.drug_prescription) + if encounter and encounter.drug_prescription: # Medication + set_ip_child_records(inpatient_record, "drug_prescription", encounter.drug_prescription) - if encounter and encounter.lab_test_prescription: # Lab Tests - set_ip_child_records(inpatient_record, 'lab_test_prescription', encounter.lab_test_prescription) + if encounter and encounter.lab_test_prescription: # Lab Tests + set_ip_child_records(inpatient_record, "lab_test_prescription", encounter.lab_test_prescription) - if encounter and encounter.procedure_prescription: # Procedure Prescription - set_ip_child_records(inpatient_record, 'procedure_prescription', encounter.procedure_prescription) + if encounter and encounter.procedure_prescription: # Procedure Prescription + set_ip_child_records( + inpatient_record, "procedure_prescription", encounter.procedure_prescription + ) - if encounter and encounter.therapies: # Therapies + if encounter and encounter.therapies: # Therapies inpatient_record.therapy_plan = encounter.therapy_plan - set_ip_child_records(inpatient_record, 'therapies', encounter.therapies) + set_ip_child_records(inpatient_record, "therapies", encounter.therapies) - inpatient_record.status = 'Admission Scheduled' - inpatient_record.save(ignore_permissions = True) + inpatient_record.status = "Admission Scheduled" + inpatient_record.save(ignore_permissions=True) @frappe.whitelist() def schedule_discharge(args): discharge_order = json.loads(args) - if not discharge_order or not discharge_order['patient'] or not discharge_order['discharge_ordered_datetime']: - frappe.throw(_('Missing required details, did not create schedule discharge')) + if ( + not discharge_order + or not discharge_order["patient"] + or not discharge_order["discharge_ordered_datetime"] + ): + frappe.throw(_("Missing required details, did not create schedule discharge")) - inpatient_record_id = frappe.db.get_value('Patient', discharge_order['patient'], 'inpatient_record') + inpatient_record_id = frappe.db.get_value( + "Patient", discharge_order["patient"], "inpatient_record" + ) if inpatient_record_id: - inpatient_record = frappe.get_doc('Inpatient Record', inpatient_record_id) - check_out_inpatient(inpatient_record, discharge_order['discharge_ordered_datetime']) + inpatient_record = frappe.get_doc("Inpatient Record", inpatient_record_id) + check_out_inpatient(inpatient_record, discharge_order["discharge_ordered_datetime"]) set_details_from_ip_order(inpatient_record, discharge_order) - inpatient_record.status = 'Discharge Scheduled' - inpatient_record.save(ignore_permissions = True) - frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status) - frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status) + inpatient_record.status = "Discharge Scheduled" + inpatient_record.save(ignore_permissions=True) + frappe.db.set_value( + "Patient", discharge_order["patient"], "inpatient_status", inpatient_record.status + ) + frappe.db.set_value( + "Patient Encounter", + inpatient_record.discharge_encounter, + "inpatient_status", + inpatient_record.status, + ) def set_details_from_ip_order(inpatient_record, ip_order): @@ -148,7 +176,7 @@ def set_details_from_ip_order(inpatient_record, ip_order): def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child): for item in encounter_child: table = inpatient_record.append(inpatient_record_child) - for df in table.meta.get('fields'): + for df in table.meta.get("fields"): table.set(df.fieldname, item.get(df.fieldname)) @@ -158,7 +186,9 @@ def check_out_inpatient(inpatient_record, discharge_ordered_datetime): if inpatient_occupancy.left != 1: inpatient_occupancy.left = True inpatient_occupancy.check_out = discharge_ordered_datetime - frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") + frappe.db.set_value( + "Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant" + ) def discharge_patient(inpatient_record, check_out): @@ -166,7 +196,7 @@ def discharge_patient(inpatient_record, check_out): inpatient_record.discharge_datetime = check_out inpatient_record.status = "Discharged" - inpatient_record.save(ignore_permissions = True) + inpatient_record.save(ignore_permissions=True) def validate_inpatient_invoicing(inpatient_record): @@ -178,13 +208,15 @@ def validate_inpatient_invoicing(inpatient_record): if pending_invoices: message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ") - formatted_doc_rows = '' + formatted_doc_rows = "" for doctype, docnames in pending_invoices.items(): formatted_doc_rows += """ {0} {1} - """.format(doctype, docnames) + """.format( + doctype, docnames + ) message += """ @@ -194,7 +226,9 @@ def validate_inpatient_invoicing(inpatient_record): {2}
- """.format(_("Healthcare Service"), _("Documents"), formatted_doc_rows) + """.format( + _("Healthcare Service"), _("Documents"), formatted_doc_rows + ) frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True) @@ -232,34 +266,43 @@ def get_pending_doc(doc, doc_name_list, pending_invoices): else: doc_ids = doc_link if doc_ids: - pending_invoices[doc] = doc_ids + pending_invoices[doc] = doc_ids return pending_invoices def get_unbilled_inpatient_docs(doc, inpatient_record): - return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient, - 'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0}) + return frappe.db.get_list( + doc, + filters={ + "patient": inpatient_record.patient, + "inpatient_record": inpatient_record.name, + "docstatus": 1, + "invoiced": 0, + }, + ) def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None): inpatient_record.admitted_datetime = check_in - inpatient_record.status = 'Admitted' + inpatient_record.status = "Admitted" inpatient_record.expected_discharge = expected_discharge - inpatient_record.set('inpatient_occupancies', []) + inpatient_record.set("inpatient_occupancies", []) transfer_patient(inpatient_record, service_unit, check_in) - frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted') - frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name) + frappe.db.set_value("Patient", inpatient_record.patient, "inpatient_status", "Admitted") + frappe.db.set_value( + "Patient", inpatient_record.patient, "inpatient_record", inpatient_record.name + ) def transfer_patient(inpatient_record, service_unit, check_in): - item_line = inpatient_record.append('inpatient_occupancies', {}) + item_line = inpatient_record.append("inpatient_occupancies", {}) item_line.service_unit = service_unit item_line.check_in = check_in - inpatient_record.save(ignore_permissions = True) + inpatient_record.save(ignore_permissions=True) frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied") @@ -270,27 +313,25 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from): if inpatient_occupancy.left != 1 and inpatient_occupancy.service_unit == leave_from: inpatient_occupancy.left = True inpatient_occupancy.check_out = check_out - frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") - inpatient_record.save(ignore_permissions = True) + frappe.db.set_value( + "Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant" + ) + inpatient_record.save(ignore_permissions=True) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_leave_from(doctype, txt, searchfield, start, page_len, filters): - docname = filters['docname'] + docname = filters["docname"] - query = '''select io.service_unit + query = """select io.service_unit from `tabInpatient Occupancy` io, `tabInpatient Record` ir where io.parent = '{docname}' and io.parentfield = 'inpatient_occupancies' - and io.left!=1 and io.parent = ir.name''' + and io.left!=1 and io.parent = ir.name""" - return frappe.db.sql(query.format(**{ - "docname": docname, - "searchfield": searchfield, - "mcond": get_match_cond(doctype) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) + return frappe.db.sql( + query.format( + **{"docname": docname, "searchfield": searchfield, "mcond": get_match_cond(doctype)} + ), + {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + ) diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record_dashboard.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record_dashboard.py index 979f18be408..0d4b850172f 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record_dashboard.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record_dashboard.py @@ -1,18 +1,17 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'inpatient_record', - 'transactions': [ + "fieldname": "inpatient_record", + "transactions": [ { - 'label': _('Appointments and Encounters'), - 'items': ['Patient Appointment', 'Patient Encounter'] + "label": _("Appointments and Encounters"), + "items": ["Patient Appointment", "Patient Encounter"], }, { - 'label': _('Lab Tests and Vital Signs'), - 'items': ['Lab Test', 'Clinical Procedure', 'Sample Collection', 'Vital Signs'] - } - ] + "label": _("Lab Tests and Vital Signs"), + "items": ["Lab Test", "Clinical Procedure", "Sample Collection", "Vital Signs"], + }, + ], } diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index bab778be630..571a77cd186 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -23,7 +23,7 @@ class TestInpatientRecord(unittest.TestCase): # Schedule Admission ip_record = create_inpatient(patient) ip_record.expected_length_of_stay = 0 - ip_record.save(ignore_permissions = True) + ip_record.save(ignore_permissions=True) self.assertEqual(ip_record.name, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(ip_record.status, frappe.db.get_value("Patient", patient, "inpatient_status")) @@ -31,11 +31,17 @@ class TestInpatientRecord(unittest.TestCase): service_unit = get_healthcare_service_unit() admit_patient(ip_record, service_unit, now_datetime()) self.assertEqual("Admitted", frappe.db.get_value("Patient", patient, "inpatient_status")) - self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) + self.assertEqual( + "Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status") + ) # Discharge - schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) - self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) + schedule_discharge( + frappe.as_json({"patient": patient, "discharge_ordered_datetime": now_datetime()}) + ) + self.assertEqual( + "Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status") + ) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) # Validate Pending Invoices @@ -54,15 +60,19 @@ class TestInpatientRecord(unittest.TestCase): # Schedule Admission ip_record = create_inpatient(patient) ip_record.expected_length_of_stay = 0 - ip_record.save(ignore_permissions = True) + ip_record.save(ignore_permissions=True) # Admit service_unit = get_healthcare_service_unit() admit_patient(ip_record, service_unit, now_datetime()) # Discharge - schedule_discharge(frappe.as_json({"patient": patient, 'discharge_ordered_datetime': now_datetime()})) - self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) + schedule_discharge( + frappe.as_json({"patient": patient, "discharge_ordered_datetime": now_datetime()}) + ) + self.assertEqual( + "Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status") + ) ip_record = frappe.get_doc("Inpatient Record", ip_record.name) # Should not validate Pending Invoices @@ -80,7 +90,7 @@ class TestInpatientRecord(unittest.TestCase): # Schedule Admission ip_record = create_inpatient(patient) ip_record.expected_length_of_stay = 0 - ip_record.save(ignore_permissions = True) + ip_record.save(ignore_permissions=True) # Admit service_unit = get_healthcare_service_unit() @@ -93,8 +103,12 @@ class TestInpatientRecord(unittest.TestCase): self.assertFalse(patient_encounter.name in encounter_ids) # Discharge - schedule_discharge(frappe.as_json({"patient": patient, 'discharge_ordered_datetime': now_datetime()})) - self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) + schedule_discharge( + frappe.as_json({"patient": patient, "discharge_ordered_datetime": now_datetime()}) + ) + self.assertEqual( + "Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status") + ) ip_record = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record) @@ -107,7 +121,7 @@ class TestInpatientRecord(unittest.TestCase): ip_record = create_inpatient(patient) ip_record.expected_length_of_stay = 0 - ip_record.save(ignore_permissions = True) + ip_record.save(ignore_permissions=True) ip_record_new = create_inpatient(patient) ip_record_new.expected_length_of_stay = 0 self.assertRaises(frappe.ValidationError, ip_record_new.save) @@ -118,11 +132,12 @@ class TestInpatientRecord(unittest.TestCase): self.assertRaises(frappe.ValidationError, ip_record_new.save) frappe.db.sql("""delete from `tabInpatient Record`""") + def mark_invoiced_inpatient_occupancy(ip_record): if ip_record.inpatient_occupancies: for inpatient_occupancy in ip_record.inpatient_occupancies: inpatient_occupancy.invoiced = 1 - ip_record.save(ignore_permissions = True) + ip_record.save(ignore_permissions=True) def setup_inpatient_settings(key, value): @@ -132,8 +147,8 @@ def setup_inpatient_settings(key, value): def create_inpatient(patient): - patient_obj = frappe.get_doc('Patient', patient) - inpatient_record = frappe.new_doc('Inpatient Record') + patient_obj = frappe.get_doc("Patient", patient) + inpatient_record = frappe.new_doc("Inpatient Record") inpatient_record.patient = patient inpatient_record.patient_name = patient_obj.patient_name inpatient_record.gender = patient_obj.sex @@ -150,9 +165,13 @@ def create_inpatient(patient): def get_healthcare_service_unit(unit_name=None): if not unit_name: - service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1, "company": "_Test Company"}) + service_unit = get_random( + "Healthcare Service Unit", filters={"inpatient_occupancy": 1, "company": "_Test Company"} + ) else: - service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name}) + service_unit = frappe.db.exists( + "Healthcare Service Unit", {"healthcare_service_unit_name": unit_name} + ) if not service_unit: service_unit = frappe.new_doc("Healthcare Service Unit") @@ -162,20 +181,22 @@ def get_healthcare_service_unit(unit_name=None): service_unit.inpatient_occupancy = 1 service_unit.occupancy_status = "Vacant" service_unit.is_group = 0 - service_unit_parent_name = frappe.db.exists({ + service_unit_parent_name = frappe.db.exists( + { "doctype": "Healthcare Service Unit", "healthcare_service_unit_name": "_Test All Healthcare Service Units", - "is_group": 1 - }) + "is_group": 1, + } + ) if not service_unit_parent_name: parent_service_unit = frappe.new_doc("Healthcare Service Unit") parent_service_unit.healthcare_service_unit_name = "_Test All Healthcare Service Units" parent_service_unit.is_group = 1 - parent_service_unit.save(ignore_permissions = True) + parent_service_unit.save(ignore_permissions=True) service_unit.parent_healthcare_service_unit = parent_service_unit.name else: service_unit.parent_healthcare_service_unit = service_unit_parent_name[0][0] - service_unit.save(ignore_permissions = True) + service_unit.save(ignore_permissions=True) return service_unit.name return service_unit @@ -187,17 +208,17 @@ def get_service_unit_type(): service_unit_type = frappe.new_doc("Healthcare Service Unit Type") service_unit_type.service_unit_type = "_Test Service Unit Type Ip Occupancy" service_unit_type.inpatient_occupancy = 1 - service_unit_type.save(ignore_permissions = True) + service_unit_type.save(ignore_permissions=True) return service_unit_type.name return service_unit_type def create_patient(): - patient = frappe.db.exists('Patient', '_Test IPD Patient') + patient = frappe.db.exists("Patient", "_Test IPD Patient") if not patient: - patient = frappe.new_doc('Patient') - patient.first_name = '_Test IPD Patient' - patient.sex = 'Female' + patient = frappe.new_doc("Patient") + patient.first_name = "_Test IPD Patient" + patient.sex = "Female" patient.save(ignore_permissions=True) patient = patient.name return patient diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index e322a74f488..9a8ad9382d8 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -15,11 +15,11 @@ class LabTest(Document): def on_submit(self): self.validate_result_values() - self.db_set('submitted_date', getdate()) - self.db_set('status', 'Completed') + self.db_set("submitted_date", getdate()) + self.db_set("status", "Completed") def on_cancel(self): - self.db_set('status', 'Cancelled') + self.db_set("status", "Cancelled") self.reload() def on_update(self): @@ -31,8 +31,8 @@ class LabTest(Document): def after_insert(self): if self.prescription: - frappe.db.set_value('Lab Prescription', self.prescription, 'lab_test_created', 1) - if frappe.db.get_value('Lab Prescription', self.prescription, 'invoiced'): + frappe.db.set_value("Lab Prescription", self.prescription, "lab_test_created", 1) + if frappe.db.get_value("Lab Prescription", self.prescription, "invoiced"): self.invoiced = True if self.template: self.load_test_from_template() @@ -49,26 +49,36 @@ class LabTest(Document): try: item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor) except Exception: - item.secondary_uom_result = '' - frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated').format(item.idx), title = _('Warning')) + item.secondary_uom_result = "" + frappe.msgprint( + _("Row #{0}: Result for Secondary UOM not calculated").format(item.idx), title=_("Warning") + ) def validate_result_values(self): if self.normal_test_items: for item in self.normal_test_items: if not item.result_value and not item.allow_blank and item.require_result_value: - frappe.throw(_('Row #{0}: Please enter the result value for {1}').format( - item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results')) + frappe.throw( + _("Row #{0}: Please enter the result value for {1}").format( + item.idx, frappe.bold(item.lab_test_name) + ), + title=_("Mandatory Results"), + ) if self.descriptive_test_items: for item in self.descriptive_test_items: if not item.result_value and not item.allow_blank and item.require_result_value: - frappe.throw(_('Row #{0}: Please enter the result value for {1}').format( - item.idx, frappe.bold(item.lab_test_particulars)), title=_('Mandatory Results')) + frappe.throw( + _("Row #{0}: Please enter the result value for {1}").format( + item.idx, frappe.bold(item.lab_test_particulars) + ), + title=_("Mandatory Results"), + ) def create_test_from_template(lab_test): - template = frappe.get_doc('Lab Test Template', lab_test.template) - patient = frappe.get_doc('Patient', lab_test.patient) + template = frappe.get_doc("Lab Test Template", lab_test.template) + patient = frappe.get_doc("Patient", lab_test.patient) lab_test.lab_test_name = template.lab_test_name lab_test.result_date = getdate() @@ -81,85 +91,98 @@ def create_test_from_template(lab_test): lab_test = create_sample_collection(lab_test, template, patient, None) lab_test = load_result_format(lab_test, template, None, None) + @frappe.whitelist() def update_status(status, name): if name and status: - frappe.db.set_value('Lab Test', name, { - 'status': status, - 'approved_date': getdate() - }) + frappe.db.set_value("Lab Test", name, {"status": status, "approved_date": getdate()}) + @frappe.whitelist() def create_multiple(doctype, docname): if not doctype or not docname: - frappe.throw(_('Sales Invoice or Patient Encounter is required to create Lab Tests'), title=_('Insufficient Data')) + frappe.throw( + _("Sales Invoice or Patient Encounter is required to create Lab Tests"), + title=_("Insufficient Data"), + ) lab_test_created = False - if doctype == 'Sales Invoice': + if doctype == "Sales Invoice": lab_test_created = create_lab_test_from_invoice(docname) - elif doctype == 'Patient Encounter': + elif doctype == "Patient Encounter": lab_test_created = create_lab_test_from_encounter(docname) if lab_test_created: - frappe.msgprint(_('Lab Test(s) {0} created successfully').format(lab_test_created), indicator='green') + frappe.msgprint( + _("Lab Test(s) {0} created successfully").format(lab_test_created), indicator="green" + ) else: - frappe.msgprint(_('No Lab Tests created')) + frappe.msgprint(_("No Lab Tests created")) + def create_lab_test_from_encounter(encounter): lab_test_created = False - encounter = frappe.get_doc('Patient Encounter', encounter) + encounter = frappe.get_doc("Patient Encounter", encounter) if encounter and encounter.lab_test_prescription: - patient = frappe.get_doc('Patient', encounter.patient) + patient = frappe.get_doc("Patient", encounter.patient) for item in encounter.lab_test_prescription: if not item.lab_test_created: template = get_lab_test_template(item.lab_test_code) if template: - lab_test = create_lab_test_doc(item.invoiced, encounter.practitioner, patient, template, encounter.company) - lab_test.save(ignore_permissions = True) - frappe.db.set_value('Lab Prescription', item.name, 'lab_test_created', 1) + lab_test = create_lab_test_doc( + item.invoiced, encounter.practitioner, patient, template, encounter.company + ) + lab_test.save(ignore_permissions=True) + frappe.db.set_value("Lab Prescription", item.name, "lab_test_created", 1) if not lab_test_created: lab_test_created = lab_test.name else: - lab_test_created += ', ' + lab_test.name + lab_test_created += ", " + lab_test.name return lab_test_created def create_lab_test_from_invoice(sales_invoice): lab_tests_created = False - invoice = frappe.get_doc('Sales Invoice', sales_invoice) + invoice = frappe.get_doc("Sales Invoice", sales_invoice) if invoice and invoice.patient: - patient = frappe.get_doc('Patient', invoice.patient) + patient = frappe.get_doc("Patient", invoice.patient) for item in invoice.items: lab_test_created = 0 - if item.reference_dt == 'Lab Prescription': - lab_test_created = frappe.db.get_value('Lab Prescription', item.reference_dn, 'lab_test_created') - elif item.reference_dt == 'Lab Test': + if item.reference_dt == "Lab Prescription": + lab_test_created = frappe.db.get_value( + "Lab Prescription", item.reference_dn, "lab_test_created" + ) + elif item.reference_dt == "Lab Test": lab_test_created = 1 if lab_test_created != 1: template = get_lab_test_template(item.item_code) if template: - lab_test = create_lab_test_doc(True, invoice.ref_practitioner, patient, template, invoice.company) - if item.reference_dt == 'Lab Prescription': + lab_test = create_lab_test_doc( + True, invoice.ref_practitioner, patient, template, invoice.company + ) + if item.reference_dt == "Lab Prescription": lab_test.prescription = item.reference_dn - lab_test.save(ignore_permissions = True) - if item.reference_dt != 'Lab Prescription': - frappe.db.set_value('Sales Invoice Item', item.name, 'reference_dt', 'Lab Test') - frappe.db.set_value('Sales Invoice Item', item.name, 'reference_dn', lab_test.name) + lab_test.save(ignore_permissions=True) + if item.reference_dt != "Lab Prescription": + frappe.db.set_value("Sales Invoice Item", item.name, "reference_dt", "Lab Test") + frappe.db.set_value("Sales Invoice Item", item.name, "reference_dn", lab_test.name) if not lab_tests_created: lab_tests_created = lab_test.name else: - lab_tests_created += ', ' + lab_test.name + lab_tests_created += ", " + lab_test.name return lab_tests_created + def get_lab_test_template(item): - template_id = frappe.db.exists('Lab Test Template', {'item': item}) + template_id = frappe.db.exists("Lab Test Template", {"item": item}) if template_id: - return frappe.get_doc('Lab Test Template', template_id) + return frappe.get_doc("Lab Test Template", template_id) return False + def create_lab_test_doc(invoiced, practitioner, patient, template, company): - lab_test = frappe.new_doc('Lab Test') + lab_test = frappe.new_doc("Lab Test") lab_test.invoiced = invoiced lab_test.practitioner = practitioner lab_test.patient = patient.name @@ -175,9 +198,10 @@ def create_lab_test_doc(invoiced, practitioner, patient, template, company): lab_test.company = company return lab_test + def create_normals(template, lab_test): lab_test.normal_toggle = 1 - normal = lab_test.append('normal_test_items') + normal = lab_test.append("normal_test_items") normal.lab_test_name = template.lab_test_name normal.lab_test_uom = template.lab_test_uom normal.secondary_uom = template.secondary_uom @@ -187,10 +211,11 @@ def create_normals(template, lab_test): normal.allow_blank = 0 normal.template = template.name + def create_compounds(template, lab_test, is_group): lab_test.normal_toggle = 1 for normal_test_template in template.normal_test_templates: - normal = lab_test.append('normal_test_items') + normal = lab_test.append("normal_test_items") if is_group: normal.lab_test_event = normal_test_template.lab_test_event else: @@ -204,41 +229,47 @@ def create_compounds(template, lab_test, is_group): normal.allow_blank = normal_test_template.allow_blank normal.template = template.name + def create_descriptives(template, lab_test): lab_test.descriptive_toggle = 1 if template.sensitivity: lab_test.sensitivity_toggle = 1 for descriptive_test_template in template.descriptive_test_templates: - descriptive = lab_test.append('descriptive_test_items') + descriptive = lab_test.append("descriptive_test_items") descriptive.lab_test_particulars = descriptive_test_template.particulars descriptive.require_result_value = 1 descriptive.allow_blank = descriptive_test_template.allow_blank descriptive.template = template.name -def create_sample_doc(template, patient, invoice, company = None): + +def create_sample_doc(template, patient, invoice, company=None): if template.sample: - sample_exists = frappe.db.exists({ - 'doctype': 'Sample Collection', - 'patient': patient.name, - 'docstatus': 0, - 'sample': template.sample - }) + sample_exists = frappe.db.exists( + { + "doctype": "Sample Collection", + "patient": patient.name, + "docstatus": 0, + "sample": template.sample, + } + ) if sample_exists: # update sample collection by adding quantity - sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0]) + sample_collection = frappe.get_doc("Sample Collection", sample_exists[0][0]) quantity = int(sample_collection.sample_qty) + int(template.sample_qty) if template.sample_details: - sample_details = sample_collection.sample_details + '\n-\n' + _('Test :') - sample_details += (template.get('lab_test_name') or template.get('template')) + '\n' - sample_details += _('Collection Details:') + '\n\t' + template.sample_details - frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_details', sample_details) + sample_details = sample_collection.sample_details + "\n-\n" + _("Test :") + sample_details += (template.get("lab_test_name") or template.get("template")) + "\n" + sample_details += _("Collection Details:") + "\n\t" + template.sample_details + frappe.db.set_value( + "Sample Collection", sample_collection.name, "sample_details", sample_details + ) - frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_qty', quantity) + frappe.db.set_value("Sample Collection", sample_collection.name, "sample_qty", quantity) else: # Create Sample Collection for template, copy vals from Invoice - sample_collection = frappe.new_doc('Sample Collection') + sample_collection = frappe.new_doc("Sample Collection") if invoice: sample_collection.invoiced = True @@ -251,59 +282,70 @@ def create_sample_doc(template, patient, invoice, company = None): sample_collection.company = company if template.sample_details: - sample_collection.sample_details = _('Test :') + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details + sample_collection.sample_details = ( + _("Test :") + + (template.get("lab_test_name") or template.get("template")) + + "\n" + + "Collection Detials:\n\t" + + template.sample_details + ) sample_collection.save(ignore_permissions=True) return sample_collection + def create_sample_collection(lab_test, template, patient, invoice): - if frappe.get_cached_value('Healthcare Settings', None, 'create_sample_collection_for_lab_test'): + if frappe.get_cached_value("Healthcare Settings", None, "create_sample_collection_for_lab_test"): sample_collection = create_sample_doc(template, patient, invoice, lab_test.company) if sample_collection: lab_test.sample = sample_collection.name - sample_collection_doc = get_link_to_form('Sample Collection', sample_collection.name) - frappe.msgprint(_('Sample Collection {0} has been created').format(sample_collection_doc), - title=_('Sample Collection'), indicator='green') + sample_collection_doc = get_link_to_form("Sample Collection", sample_collection.name) + frappe.msgprint( + _("Sample Collection {0} has been created").format(sample_collection_doc), + title=_("Sample Collection"), + indicator="green", + ) return lab_test + def load_result_format(lab_test, template, prescription, invoice): - if template.lab_test_template_type == 'Single': + if template.lab_test_template_type == "Single": create_normals(template, lab_test) - elif template.lab_test_template_type == 'Compound': + elif template.lab_test_template_type == "Compound": create_compounds(template, lab_test, False) - elif template.lab_test_template_type == 'Descriptive': + elif template.lab_test_template_type == "Descriptive": create_descriptives(template, lab_test) - elif template.lab_test_template_type == 'Grouped': + elif template.lab_test_template_type == "Grouped": # Iterate for each template in the group and create one result for all. for lab_test_group in template.lab_test_groups: # Template_in_group = None if lab_test_group.lab_test_template: - template_in_group = frappe.get_doc('Lab Test Template', lab_test_group.lab_test_template) + template_in_group = frappe.get_doc("Lab Test Template", lab_test_group.lab_test_template) if template_in_group: - if template_in_group.lab_test_template_type == 'Single': + if template_in_group.lab_test_template_type == "Single": create_normals(template_in_group, lab_test) - elif template_in_group.lab_test_template_type == 'Compound': - normal_heading = lab_test.append('normal_test_items') + elif template_in_group.lab_test_template_type == "Compound": + normal_heading = lab_test.append("normal_test_items") normal_heading.lab_test_name = template_in_group.lab_test_name normal_heading.require_result_value = 0 normal_heading.allow_blank = 1 normal_heading.template = template_in_group.name create_compounds(template_in_group, lab_test, True) - elif template_in_group.lab_test_template_type == 'Descriptive': - descriptive_heading = lab_test.append('descriptive_test_items') + elif template_in_group.lab_test_template_type == "Descriptive": + descriptive_heading = lab_test.append("descriptive_test_items") descriptive_heading.lab_test_name = template_in_group.lab_test_name descriptive_heading.require_result_value = 0 descriptive_heading.allow_blank = 1 descriptive_heading.template = template_in_group.name create_descriptives(template_in_group, lab_test) - else: # Lab Test Group - Add New Line - normal = lab_test.append('normal_test_items') + else: # Lab Test Group - Add New Line + normal = lab_test.append("normal_test_items") normal.lab_test_name = lab_test_group.group_event normal.lab_test_uom = lab_test_group.group_test_uom normal.secondary_uom = lab_test_group.secondary_uom @@ -313,26 +355,27 @@ def load_result_format(lab_test, template, prescription, invoice): normal.require_result_value = 1 normal.template = template.name - if template.lab_test_template_type != 'No Result': + if template.lab_test_template_type != "No Result": if prescription: lab_test.prescription = prescription if invoice: - frappe.db.set_value('Lab Prescription', prescription, 'invoiced', True) - lab_test.save(ignore_permissions=True) # Insert the result + frappe.db.set_value("Lab Prescription", prescription, "invoiced", True) + lab_test.save(ignore_permissions=True) # Insert the result return lab_test + @frappe.whitelist() def get_employee_by_user_id(user_id): - emp_id = frappe.db.exists('Employee', { 'user_id': user_id }) + emp_id = frappe.db.exists("Employee", {"user_id": user_id}) if emp_id: - return frappe.get_doc('Employee', emp_id) + return frappe.get_doc("Employee", emp_id) return None @frappe.whitelist() def get_lab_test_prescribed(patient): return frappe.db.sql( - ''' + """ select lp.name, lp.lab_test_code, @@ -347,4 +390,6 @@ def get_lab_test_prescribed(patient): pe.patient=%s and lp.parent=pe.name and lp.lab_test_created=0 - ''', (patient)) + """, + (patient), + ) diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index 3ec4412ab0c..06c02d1ea07 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -20,12 +20,15 @@ from erpnext.healthcare.doctype.patient_medical_record.test_patient_medical_reco class TestLabTest(unittest.TestCase): def test_lab_test_item(self): lab_template = create_lab_test_template() - self.assertTrue(frappe.db.exists('Item', lab_template.item)) - self.assertEqual(frappe.db.get_value('Item Price', {'item_code':lab_template.item}, 'price_list_rate'), lab_template.lab_test_rate) + self.assertTrue(frappe.db.exists("Item", lab_template.item)) + self.assertEqual( + frappe.db.get_value("Item Price", {"item_code": lab_template.item}, "price_list_rate"), + lab_template.lab_test_rate, + ) lab_template.disabled = 1 lab_template.save() - self.assertEqual(frappe.db.get_value('Item', lab_template.item, 'disabled'), 1) + self.assertEqual(frappe.db.get_value("Item", lab_template.item, "disabled"), 1) lab_template.reload() @@ -43,7 +46,9 @@ class TestLabTest(unittest.TestCase): self.assertRaises(frappe.ValidationError, lab_test.submit) def test_sample_collection(self): - frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 1) + frappe.db.set_value( + "Healthcare Settings", "Healthcare Settings", "create_sample_collection_for_lab_test", 1 + ) lab_template = create_lab_test_template() lab_test = create_lab_test(lab_template) @@ -53,9 +58,11 @@ class TestLabTest(unittest.TestCase): lab_test.save() # check sample collection created - self.assertTrue(frappe.db.exists('Sample Collection', {'sample': lab_template.sample})) + self.assertTrue(frappe.db.exists("Sample Collection", {"sample": lab_template.sample})) - frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 0) + frappe.db.set_value( + "Healthcare Settings", "Healthcare Settings", "create_sample_collection_for_lab_test", 0 + ) lab_test = create_lab_test(lab_template) lab_test.descriptive_test_items[0].result_value = 12 lab_test.descriptive_test_items[1].result_value = 1 @@ -68,14 +75,14 @@ class TestLabTest(unittest.TestCase): def test_create_lab_tests_from_sales_invoice(self): sales_invoice = create_sales_invoice() - create_multiple('Sales Invoice', sales_invoice.name) + create_multiple("Sales Invoice", sales_invoice.name) sales_invoice.reload() self.assertIsNotNone(sales_invoice.items[0].reference_dn) self.assertIsNotNone(sales_invoice.items[1].reference_dn) def test_create_lab_tests_from_patient_encounter(self): patient_encounter = create_patient_encounter() - create_multiple('Patient Encounter', patient_encounter.name) + create_multiple("Patient Encounter", patient_encounter.name) patient_encounter.reload() self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created) self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created) @@ -83,23 +90,22 @@ class TestLabTest(unittest.TestCase): def create_lab_test_template(test_sensitivity=0, sample_collection=1): medical_department = create_medical_department() - if frappe.db.exists('Lab Test Template', 'Insulin Resistance'): - return frappe.get_doc('Lab Test Template', 'Insulin Resistance') - template = frappe.new_doc('Lab Test Template') - template.lab_test_name = 'Insulin Resistance' - template.lab_test_template_type = 'Descriptive' - template.lab_test_code = 'Insulin Resistance' - template.lab_test_group = 'Services' + if frappe.db.exists("Lab Test Template", "Insulin Resistance"): + return frappe.get_doc("Lab Test Template", "Insulin Resistance") + template = frappe.new_doc("Lab Test Template") + template.lab_test_name = "Insulin Resistance" + template.lab_test_template_type = "Descriptive" + template.lab_test_code = "Insulin Resistance" + template.lab_test_group = "Services" template.department = medical_department template.is_billable = 1 - template.lab_test_description = 'Insulin Resistance' + template.lab_test_description = "Insulin Resistance" template.lab_test_rate = 2000 - for entry in ['FBS', 'Insulin', 'IR']: - template.append('descriptive_test_templates', { - 'particulars': entry, - 'allow_blank': 1 if entry=='IR' else 0 - }) + for entry in ["FBS", "Insulin", "IR"]: + template.append( + "descriptive_test_templates", {"particulars": entry, "allow_blank": 1 if entry == "IR" else 0} + ) if test_sensitivity: template.sensitivity = 1 @@ -111,77 +117,85 @@ def create_lab_test_template(test_sensitivity=0, sample_collection=1): template.save() return template + def create_medical_department(): - medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') + medical_department = frappe.db.exists("Medical Department", "_Test Medical Department") if not medical_department: - medical_department = frappe.new_doc('Medical Department') - medical_department.department = '_Test Medical Department' + medical_department = frappe.new_doc("Medical Department") + medical_department.department = "_Test Medical Department" medical_department.save() medical_department = medical_department.name return medical_department + def create_lab_test(lab_template): patient = create_patient() - lab_test = frappe.new_doc('Lab Test') + lab_test = frappe.new_doc("Lab Test") lab_test.template = lab_template.name lab_test.patient = patient - lab_test.patient_sex = 'Female' + lab_test.patient_sex = "Female" lab_test.save() return lab_test + def create_lab_test_sample(): - blood_sample = frappe.db.exists('Lab Test Sample', 'Blood Sample') + blood_sample = frappe.db.exists("Lab Test Sample", "Blood Sample") if blood_sample: return blood_sample - sample = frappe.new_doc('Lab Test Sample') - sample.sample = 'Blood Sample' - sample.sample_uom = 'U/ml' + sample = frappe.new_doc("Lab Test Sample") + sample.sample = "Blood Sample" + sample.sample_uom = "U/ml" sample.save() return sample.name + def create_sales_invoice(): patient = create_patient() medical_department = create_medical_department() insulin_resistance_template = create_lab_test_template() blood_test_template = create_blood_test_template(medical_department) - sales_invoice = frappe.new_doc('Sales Invoice') + sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = patient - sales_invoice.customer = frappe.db.get_value('Patient', patient, 'customer') + sales_invoice.customer = frappe.db.get_value("Patient", patient, "customer") sales_invoice.due_date = getdate() - sales_invoice.company = '_Test Company' - sales_invoice.debit_to = get_receivable_account('_Test Company') + sales_invoice.company = "_Test Company" + sales_invoice.debit_to = get_receivable_account("_Test Company") tests = [insulin_resistance_template, blood_test_template] for entry in tests: - sales_invoice.append('items', { - 'item_code': entry.item, - 'item_name': entry.lab_test_name, - 'description': entry.lab_test_description, - 'qty': 1, - 'uom': 'Nos', - 'conversion_factor': 1, - 'income_account': get_income_account(None, '_Test Company'), - 'rate': entry.lab_test_rate, - 'amount': entry.lab_test_rate - }) + sales_invoice.append( + "items", + { + "item_code": entry.item, + "item_name": entry.lab_test_name, + "description": entry.lab_test_description, + "qty": 1, + "uom": "Nos", + "conversion_factor": 1, + "income_account": get_income_account(None, "_Test Company"), + "rate": entry.lab_test_rate, + "amount": entry.lab_test_rate, + }, + ) sales_invoice.set_missing_values() sales_invoice.submit() return sales_invoice + def create_patient_encounter(): patient = create_patient() medical_department = create_medical_department() insulin_resistance_template = create_lab_test_template() blood_test_template = create_blood_test_template(medical_department) - patient_encounter = frappe.new_doc('Patient Encounter') + patient_encounter = frappe.new_doc("Patient Encounter") patient_encounter.patient = patient patient_encounter.practitioner = create_practitioner() patient_encounter.encounter_date = getdate() @@ -189,22 +203,21 @@ def create_patient_encounter(): tests = [insulin_resistance_template, blood_test_template] for entry in tests: - patient_encounter.append('lab_test_prescription', { - 'lab_test_code': entry.item, - 'lab_test_name': entry.lab_test_name - }) + patient_encounter.append( + "lab_test_prescription", {"lab_test_code": entry.item, "lab_test_name": entry.lab_test_name} + ) patient_encounter.submit() return patient_encounter def create_practitioner(): - practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') + practitioner = frappe.db.exists("Healthcare Practitioner", "_Test Healthcare Practitioner") if not practitioner: - practitioner = frappe.new_doc('Healthcare Practitioner') - practitioner.first_name = '_Test Healthcare Practitioner' - practitioner.gender = 'Female' + practitioner = frappe.new_doc("Healthcare Practitioner") + practitioner.first_name = "_Test Healthcare Practitioner" + practitioner.gender = "Female" practitioner.op_consulting_charge = 500 practitioner.inpatient_visit_charge = 500 practitioner.save(ignore_permissions=True) diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py index 2cd3a4d20fa..0f3561f6f9f 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py @@ -17,7 +17,7 @@ class LabTestTemplate(Document): def validate(self): if self.is_billable and (not self.lab_test_rate or self.lab_test_rate <= 0.0): - frappe.throw(_('Standard Selling Rate should be greater than zero.')) + frappe.throw(_("Standard Selling Rate should be greater than zero.")) self.validate_conversion_factor() self.enable_disable_item() @@ -29,15 +29,17 @@ class LabTestTemplate(Document): item_price = self.item_price_exists() if not item_price: if self.lab_test_rate and self.lab_test_rate > 0.0: - price_list_name = frappe.db.get_value('Selling Settings', None, 'selling_price_list') or frappe.db.get_value('Price List', {'selling': 1}) + price_list_name = frappe.db.get_value( + "Selling Settings", None, "selling_price_list" + ) or frappe.db.get_value("Price List", {"selling": 1}) make_item_price(self.lab_test_code, price_list_name, self.lab_test_rate) else: - frappe.db.set_value('Item Price', item_price, 'price_list_rate', self.lab_test_rate) + frappe.db.set_value("Item Price", item_price, "price_list_rate", self.lab_test_rate) - self.db_set('change_in_item', 0) + self.db_set("change_in_item", 0) elif not self.is_billable and self.item: - frappe.db.set_value('Item', self.item, 'disabled', 1) + frappe.db.set_value("Item", self.item, "disabled", 1) self.reload() @@ -46,99 +48,113 @@ class LabTestTemplate(Document): if self.item: try: item = self.item - self.db_set('item', '') - frappe.delete_doc('Item', item) + self.db_set("item", "") + frappe.delete_doc("Item", item) except Exception: - frappe.throw(_('Not permitted. Please disable the Lab Test Template')) + frappe.throw(_("Not permitted. Please disable the Lab Test Template")) def enable_disable_item(self): if self.is_billable: if self.disabled: - frappe.db.set_value('Item', self.item, 'disabled', 1) + frappe.db.set_value("Item", self.item, "disabled", 1) else: - frappe.db.set_value('Item', self.item, 'disabled', 0) + frappe.db.set_value("Item", self.item, "disabled", 0) def update_item(self): - item = frappe.get_doc('Item', self.item) + item = frappe.get_doc("Item", self.item) if item: - item.update({ - 'item_name': self.lab_test_name, - 'item_group': self.lab_test_group, - 'disabled': 0, - 'standard_rate': self.lab_test_rate, - 'description': self.lab_test_description - }) + item.update( + { + "item_name": self.lab_test_name, + "item_group": self.lab_test_group, + "disabled": 0, + "standard_rate": self.lab_test_rate, + "description": self.lab_test_description, + } + ) item.flags.ignore_mandatory = True item.save(ignore_permissions=True) def item_price_exists(self): - item_price = frappe.db.exists({'doctype': 'Item Price', 'item_code': self.lab_test_code}) + item_price = frappe.db.exists({"doctype": "Item Price", "item_code": self.lab_test_code}) if item_price: return item_price[0][0] return False def validate_conversion_factor(self): - if self.lab_test_template_type == 'Single' and self.secondary_uom and not self.conversion_factor: - frappe.throw(_('Conversion Factor is mandatory')) - if self.lab_test_template_type == 'Compound': + if self.lab_test_template_type == "Single" and self.secondary_uom and not self.conversion_factor: + frappe.throw(_("Conversion Factor is mandatory")) + if self.lab_test_template_type == "Compound": for item in self.normal_test_templates: if item.secondary_uom and not item.conversion_factor: - frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(item.idx)) - if self.lab_test_template_type == 'Grouped': + frappe.throw(_("Row #{0}: Conversion Factor is mandatory").format(item.idx)) + if self.lab_test_template_type == "Grouped": for group in self.lab_test_groups: - if group.template_or_new_line == 'Add New Line' and group.secondary_uom and not group.conversion_factor: - frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(group.idx)) + if ( + group.template_or_new_line == "Add New Line" + and group.secondary_uom + and not group.conversion_factor + ): + frappe.throw(_("Row #{0}: Conversion Factor is mandatory").format(group.idx)) def create_item_from_template(doc): - uom = frappe.db.exists('UOM', 'Unit') or frappe.db.get_single_value('Stock Settings', 'stock_uom') + uom = frappe.db.exists("UOM", "Unit") or frappe.db.get_single_value("Stock Settings", "stock_uom") # Insert item - item = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': doc.lab_test_code, - 'item_name':doc.lab_test_name, - 'item_group': doc.lab_test_group, - 'description':doc.lab_test_description, - 'is_sales_item': 1, - 'is_service_item': 1, - 'is_purchase_item': 0, - 'is_stock_item': 0, - 'include_item_in_manufacturing': 0, - 'show_in_website': 0, - 'is_pro_applicable': 0, - 'disabled': 0 if doc.is_billable and not doc.disabled else doc.disabled, - 'stock_uom': uom - }).insert(ignore_permissions=True, ignore_mandatory=True) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": doc.lab_test_code, + "item_name": doc.lab_test_name, + "item_group": doc.lab_test_group, + "description": doc.lab_test_description, + "is_sales_item": 1, + "is_service_item": 1, + "is_purchase_item": 0, + "is_stock_item": 0, + "include_item_in_manufacturing": 0, + "show_in_website": 0, + "is_pro_applicable": 0, + "disabled": 0 if doc.is_billable and not doc.disabled else doc.disabled, + "stock_uom": uom, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) # Insert item price if doc.is_billable and doc.lab_test_rate != 0.0: - price_list_name = frappe.db.get_value('Selling Settings', None, 'selling_price_list') or frappe.db.get_value('Price List', {'selling': 1}) + price_list_name = frappe.db.get_value( + "Selling Settings", None, "selling_price_list" + ) or frappe.db.get_value("Price List", {"selling": 1}) if doc.lab_test_rate: make_item_price(item.name, price_list_name, doc.lab_test_rate) else: make_item_price(item.name, price_list_name, 0.0) # Set item in the template - frappe.db.set_value('Lab Test Template', doc.name, 'item', item.name) + frappe.db.set_value("Lab Test Template", doc.name, "item", item.name) doc.reload() + def make_item_price(item, price_list_name, item_price): - frappe.get_doc({ - 'doctype': 'Item Price', - 'price_list': price_list_name, - 'item_code': item, - 'price_list_rate': item_price - }).insert(ignore_permissions=True, ignore_mandatory=True) + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list_name, + "item_code": item, + "price_list_rate": item_price, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + @frappe.whitelist() def change_test_code_from_template(lab_test_code, doc): doc = frappe._dict(json.loads(doc)) - if frappe.db.exists({'doctype': 'Item', 'item_code': lab_test_code}): - frappe.throw(_('Lab Test Item {0} already exist').format(lab_test_code)) + if frappe.db.exists({"doctype": "Item", "item_code": lab_test_code}): + frappe.throw(_("Lab Test Item {0} already exist").format(lab_test_code)) else: - rename_doc('Item', doc.name, lab_test_code, ignore_permissions=True) - frappe.db.set_value('Lab Test Template', doc.name, 'lab_test_code', lab_test_code) - frappe.db.set_value('Lab Test Template', doc.name, 'lab_test_name', lab_test_code) - rename_doc('Lab Test Template', doc.name, lab_test_code, ignore_permissions=True) + rename_doc("Item", doc.name, lab_test_code, ignore_permissions=True) + frappe.db.set_value("Lab Test Template", doc.name, "lab_test_code", lab_test_code) + frappe.db.set_value("Lab Test Template", doc.name, "lab_test_name", lab_test_code) + rename_doc("Lab Test Template", doc.name, lab_test_code, ignore_permissions=True) return lab_test_code diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py index 8be84ff1ab1..fef381af0e6 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'template', - 'transactions': [ - { - 'label': _('Lab Tests'), - 'items': ['Lab Test'] - } - ] + "fieldname": "template", + "transactions": [{"label": _("Lab Tests"), "items": ["Lab Test"]}], } diff --git a/erpnext/healthcare/doctype/lab_test_template/test_lab_test_template.py b/erpnext/healthcare/doctype/lab_test_template/test_lab_test_template.py index c3b4d59e431..3848466339d 100644 --- a/erpnext/healthcare/doctype/lab_test_template/test_lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/test_lab_test_template.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Lab Test Template') + class TestLabTestTemplate(unittest.TestCase): pass diff --git a/erpnext/healthcare/doctype/lab_test_uom/test_lab_test_uom.py b/erpnext/healthcare/doctype/lab_test_uom/test_lab_test_uom.py index b9381713ada..747304bfc10 100644 --- a/erpnext/healthcare/doctype/lab_test_uom/test_lab_test_uom.py +++ b/erpnext/healthcare/doctype/lab_test_uom/test_lab_test_uom.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Lab Test UOM') + class TestLabTestUOM(unittest.TestCase): pass diff --git a/erpnext/healthcare/doctype/medical_code/medical_code.py b/erpnext/healthcare/doctype/medical_code/medical_code.py index 702bbd7e7e5..5909c7d44b7 100644 --- a/erpnext/healthcare/doctype/medical_code/medical_code.py +++ b/erpnext/healthcare/doctype/medical_code/medical_code.py @@ -7,4 +7,4 @@ from frappe.model.document import Document class MedicalCode(Document): def autoname(self): - self.name = self.medical_code_standard+" "+self.code + self.name = self.medical_code_standard + " " + self.code diff --git a/erpnext/healthcare/doctype/medical_department/test_medical_department.py b/erpnext/healthcare/doctype/medical_department/test_medical_department.py index 5091ba61b9e..3035bfd5ffb 100644 --- a/erpnext/healthcare/doctype/medical_department/test_medical_department.py +++ b/erpnext/healthcare/doctype/medical_department/test_medical_department.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Medical Department') + class TestMedicalDepartment(unittest.TestCase): pass diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index 46d63003cce..c5ed42656a1 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -23,7 +23,7 @@ from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import ( class Patient(Document): def onload(self): - '''Load address and contacts in `__onload`''' + """Load address and contacts in `__onload`""" load_address_and_contact(self) self.load_dashboard_info() @@ -34,16 +34,16 @@ class Patient(Document): self.set_missing_customer_details() def after_insert(self): - if frappe.db.get_single_value('Healthcare Settings', 'collect_registration_fee'): - frappe.db.set_value('Patient', self.name, 'status', 'Disabled') + if frappe.db.get_single_value("Healthcare Settings", "collect_registration_fee"): + frappe.db.set_value("Patient", self.name, "status", "Disabled") else: send_registration_sms(self) self.reload() def on_update(self): - if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'): + if frappe.db.get_single_value("Healthcare Settings", "link_customer_to_patient"): if self.customer: - customer = frappe.get_doc('Customer', self.customer) + customer = frappe.get_doc("Customer", self.customer) if self.customer_group: customer.customer_group = self.customer_group if self.territory: @@ -58,64 +58,83 @@ class Patient(Document): else: create_customer(self) - self.set_contact() # add or update contact + self.set_contact() # add or update contact if not self.user_id and self.email and self.invite_user: self.create_website_user() def load_dashboard_info(self): if self.customer: - info = get_dashboard_info('Customer', self.customer, None) - self.set_onload('dashboard_info', info) + info = get_dashboard_info("Customer", self.customer, None) + self.set_onload("dashboard_info", info) def set_full_name(self): if self.last_name: - self.patient_name = ' '.join(filter(None, [self.first_name, self.last_name])) + self.patient_name = " ".join(filter(None, [self.first_name, self.last_name])) else: self.patient_name = self.first_name def set_missing_customer_details(self): if not self.customer_group: - self.customer_group = frappe.db.get_single_value('Selling Settings', 'customer_group') or get_root_of('Customer Group') + self.customer_group = frappe.db.get_single_value( + "Selling Settings", "customer_group" + ) or get_root_of("Customer Group") if not self.territory: - self.territory = frappe.db.get_single_value('Selling Settings', 'territory') or get_root_of('Territory') + self.territory = frappe.db.get_single_value("Selling Settings", "territory") or get_root_of( + "Territory" + ) if not self.default_price_list: - self.default_price_list = frappe.db.get_single_value('Selling Settings', 'selling_price_list') + self.default_price_list = frappe.db.get_single_value("Selling Settings", "selling_price_list") if not self.customer_group or not self.territory or not self.default_price_list: - frappe.msgprint(_('Please set defaults for Customer Group, Territory and Selling Price List in Selling Settings'), alert=True) + frappe.msgprint( + _( + "Please set defaults for Customer Group, Territory and Selling Price List in Selling Settings" + ), + alert=True, + ) if not self.default_currency: self.default_currency = get_default_currency() if not self.language: - self.language = frappe.db.get_single_value('System Settings', 'language') + self.language = frappe.db.get_single_value("System Settings", "language") def create_website_user(self): - users = frappe.db.get_all('User', fields=['email', 'mobile_no'], or_filters={'email': self.email, 'mobile_no': self.mobile}) + users = frappe.db.get_all( + "User", + fields=["email", "mobile_no"], + or_filters={"email": self.email, "mobile_no": self.mobile}, + ) if users and users[0]: - frappe.throw(_("User exists with Email {}, Mobile {}
Please check email / mobile or disable 'Invite as User' to skip creating User") - .format(frappe.bold(users[0].email), frappe.bold(users[0].mobile_no)), frappe.DuplicateEntryError) + frappe.throw( + _( + "User exists with Email {}, Mobile {}
Please check email / mobile or disable 'Invite as User' to skip creating User" + ).format(frappe.bold(users[0].email), frappe.bold(users[0].mobile_no)), + frappe.DuplicateEntryError, + ) - user = frappe.get_doc({ - 'doctype': 'User', - 'first_name': self.first_name, - 'last_name': self.last_name, - 'email': self.email, - 'user_type': 'Website User', - 'gender': self.sex, - 'phone': self.phone, - 'mobile_no': self.mobile, - 'birth_date': self.dob - }) + user = frappe.get_doc( + { + "doctype": "User", + "first_name": self.first_name, + "last_name": self.last_name, + "email": self.email, + "user_type": "Website User", + "gender": self.sex, + "phone": self.phone, + "mobile_no": self.mobile, + "birth_date": self.dob, + } + ) user.flags.ignore_permissions = True user.enabled = True user.send_welcome_email = True - user.add_roles('Patient') - self.db_set('user_id', user.name) + user.add_roles("Patient") + self.db_set("user_id", user.name) def autoname(self): - patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by') - if patient_name_by == 'Patient Name': + patient_name_by = frappe.db.get_single_value("Healthcare Settings", "patient_name_by") + if patient_name_by == "Patient Name": self.name = self.get_patient_name() else: set_name_by_naming_series(self) @@ -123,9 +142,13 @@ class Patient(Document): def get_patient_name(self): self.set_full_name() name = self.patient_name - if frappe.db.get_value('Patient', name): - count = frappe.db.sql("""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabPatient - where name like %s""", "%{0} - %".format(name), as_list=1)[0][0] + if frappe.db.get_value("Patient", name): + count = frappe.db.sql( + """select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabPatient + where name like %s""", + "%{0} - %".format(name), + as_list=1, + )[0][0] count = cint(count) + 1 return "{0} - {1}".format(name, cstr(count)) @@ -143,22 +166,34 @@ class Patient(Document): age = self.age if not age: return - age_str = str(age.years) + ' ' + _("Year(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") + age_str = ( + str(age.years) + + " " + + _("Year(s)") + + " " + + str(age.months) + + " " + + _("Month(s)") + + " " + + str(age.days) + + " " + + _("Day(s)") + ) return age_str @frappe.whitelist() def invoice_patient_registration(self): - if frappe.db.get_single_value('Healthcare Settings', 'registration_fee'): - company = frappe.defaults.get_user_default('company') + if frappe.db.get_single_value("Healthcare Settings", "registration_fee"): + company = frappe.defaults.get_user_default("company") if not company: - company = frappe.db.get_single_value('Global Defaults', 'default_company') + company = frappe.db.get_single_value("Global Defaults", "default_company") sales_invoice = make_invoice(self.name, company) sales_invoice.save(ignore_permissions=True) - frappe.db.set_value('Patient', self.name, 'status', 'Active') + frappe.db.set_value("Patient", self.name, "status", "Active") send_registration_sms(self) - return {'invoice': sales_invoice.name} + return {"invoice": sales_invoice.name} def set_contact(self): contact = get_default_contact(self.doctype, self.name) @@ -173,33 +208,35 @@ class Patient(Document): else: if self.customer: # customer contact exists, link patient - contact = get_default_contact('Customer', self.customer) + contact = get_default_contact("Customer", self.customer) if contact: self.update_contact(contact) else: self.reload() if self.email or self.mobile or self.phone: - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': self.first_name, - 'middle_name': self.middle_name, - 'last_name': self.last_name, - 'gender': self.sex, - 'is_primary_contact': 1 - }) - contact.append('links', dict(link_doctype='Patient', link_name=self.name)) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": self.first_name, + "middle_name": self.middle_name, + "last_name": self.last_name, + "gender": self.sex, + "is_primary_contact": 1, + } + ) + contact.append("links", dict(link_doctype="Patient", link_name=self.name)) if self.customer: - contact.append('links', dict(link_doctype='Customer', link_name=self.customer)) + contact.append("links", dict(link_doctype="Customer", link_name=self.customer)) contact.insert(ignore_permissions=True) self.update_contact(contact.name) def update_contact(self, contact): - contact = frappe.get_doc('Contact', contact) + contact = frappe.get_doc("Contact", contact) if not contact.has_link(self.doctype, self.name): - contact.append('links', dict(link_doctype=self.doctype, link_name=self.name)) + contact.append("links", dict(link_doctype=self.doctype, link_name=self.name)) if self.email and self.email != contact.email_id: for email in contact.email_ids: @@ -211,73 +248,85 @@ class Patient(Document): for mobile in contact.phone_nos: mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False contact.add_phone(self.mobile, is_primary_mobile_no=True) - contact.set_primary('mobile_no') + contact.set_primary("mobile_no") if self.phone and self.phone != contact.phone: for phone in contact.phone_nos: phone.is_primary_phone = True if phone.phone == self.phone else False contact.add_phone(self.phone, is_primary_phone=True) - contact.set_primary('phone') + contact.set_primary("phone") contact.flags.skip_patient_update = True contact.save(ignore_permissions=True) def create_customer(doc): - customer = frappe.get_doc({ - 'doctype': 'Customer', - 'customer_name': doc.patient_name, - 'customer_group': doc.customer_group or frappe.db.get_single_value('Selling Settings', 'customer_group'), - 'territory' : doc.territory or frappe.db.get_single_value('Selling Settings', 'territory'), - 'customer_type': 'Individual', - 'default_currency': doc.default_currency, - 'default_price_list': doc.default_price_list, - 'language': doc.language - }).insert(ignore_permissions=True, ignore_mandatory=True) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": doc.patient_name, + "customer_group": doc.customer_group + or frappe.db.get_single_value("Selling Settings", "customer_group"), + "territory": doc.territory or frappe.db.get_single_value("Selling Settings", "territory"), + "customer_type": "Individual", + "default_currency": doc.default_currency, + "default_price_list": doc.default_price_list, + "language": doc.language, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + + frappe.db.set_value("Patient", doc.name, "customer", customer.name) + frappe.msgprint(_("Customer {0} is created.").format(customer.name), alert=True) - frappe.db.set_value('Patient', doc.name, 'customer', customer.name) - frappe.msgprint(_('Customer {0} is created.').format(customer.name), alert=True) def make_invoice(patient, company): - uom = frappe.db.exists('UOM', 'Nos') or frappe.db.get_single_value('Stock Settings', 'stock_uom') - sales_invoice = frappe.new_doc('Sales Invoice') - sales_invoice.customer = frappe.db.get_value('Patient', patient, 'customer') + uom = frappe.db.exists("UOM", "Nos") or frappe.db.get_single_value("Stock Settings", "stock_uom") + sales_invoice = frappe.new_doc("Sales Invoice") + sales_invoice.customer = frappe.db.get_value("Patient", patient, "customer") sales_invoice.due_date = getdate() sales_invoice.company = company sales_invoice.is_pos = 0 sales_invoice.debit_to = get_receivable_account(company) - item_line = sales_invoice.append('items') - item_line.item_name = 'Registration Fee' - item_line.description = 'Registration Fee' + item_line = sales_invoice.append("items") + item_line.item_name = "Registration Fee" + item_line.description = "Registration Fee" item_line.qty = 1 item_line.uom = uom item_line.conversion_factor = 1 item_line.income_account = get_income_account(None, company) - item_line.rate = frappe.db.get_single_value('Healthcare Settings', 'registration_fee') + item_line.rate = frappe.db.get_single_value("Healthcare Settings", "registration_fee") item_line.amount = item_line.rate sales_invoice.set_missing_values() return sales_invoice + @frappe.whitelist() def get_patient_detail(patient): patient_dict = frappe.db.sql("""select * from tabPatient where name=%s""", (patient), as_dict=1) if not patient_dict: - frappe.throw(_('Patient not found')) - vital_sign = frappe.db.sql("""select * from `tabVital Signs` where patient=%s - order by signs_date desc limit 1""", (patient), as_dict=1) + frappe.throw(_("Patient not found")) + vital_sign = frappe.db.sql( + """select * from `tabVital Signs` where patient=%s + order by signs_date desc limit 1""", + (patient), + as_dict=1, + ) details = patient_dict[0] if vital_sign: details.update(vital_sign[0]) return details + def get_timeline_data(doctype, name): - ''' + """ Return Patient's timeline data from medical records Also include the associated Customer timeline data - ''' - patient_timeline_data = dict(frappe.db.sql(''' + """ + patient_timeline_data = dict( + frappe.db.sql( + """ SELECT unix_timestamp(communication_date), count(*) FROM @@ -285,12 +334,16 @@ def get_timeline_data(doctype, name): WHERE patient=%s and `communication_date` > date_sub(curdate(), interval 1 year) - GROUP BY communication_date''', name)) + GROUP BY communication_date""", + name, + ) + ) - customer = frappe.db.get_value(doctype, name, 'customer') + customer = frappe.db.get_value(doctype, name, "customer") if customer: from erpnext.accounts.party import get_timeline_data - customer_timeline_data = get_timeline_data('Customer', customer) + + customer_timeline_data = get_timeline_data("Customer", customer) patient_timeline_data.update(customer_timeline_data) return patient_timeline_data diff --git a/erpnext/healthcare/doctype/patient/patient_dashboard.py b/erpnext/healthcare/doctype/patient/patient_dashboard.py index 532d3e55370..198c279cd7d 100644 --- a/erpnext/healthcare/doctype/patient/patient_dashboard.py +++ b/erpnext/healthcare/doctype/patient/patient_dashboard.py @@ -1,39 +1,26 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Patient. See timeline below for details'), - 'fieldname': 'patient', - 'non_standard_fieldnames': { - 'Payment Entry': 'party' - }, - 'transactions': [ + "heatmap": True, + "heatmap_message": _( + "This is based on transactions against this Patient. See timeline below for details" + ), + "fieldname": "patient", + "non_standard_fieldnames": {"Payment Entry": "party"}, + "transactions": [ { - 'label': _('Appointments and Encounters'), - 'items': ['Patient Appointment', 'Vital Signs', 'Patient Encounter'] + "label": _("Appointments and Encounters"), + "items": ["Patient Appointment", "Vital Signs", "Patient Encounter"], }, + {"label": _("Lab Tests and Vital Signs"), "items": ["Lab Test", "Sample Collection"]}, { - 'label': _('Lab Tests and Vital Signs'), - 'items': ['Lab Test', 'Sample Collection'] + "label": _("Rehab and Physiotherapy"), + "items": ["Patient Assessment", "Therapy Session", "Therapy Plan"], }, - { - 'label': _('Rehab and Physiotherapy'), - 'items': ['Patient Assessment', 'Therapy Session', 'Therapy Plan'] - }, - { - 'label': _('Surgery'), - 'items': ['Clinical Procedure'] - }, - { - 'label': _('Admissions'), - 'items': ['Inpatient Record', 'Inpatient Medication Order'] - }, - { - 'label': _('Billing and Payments'), - 'items': ['Sales Invoice', 'Payment Entry'] - } - ] + {"label": _("Surgery"), "items": ["Clinical Procedure"]}, + {"label": _("Admissions"), "items": ["Inpatient Record", "Inpatient Medication Order"]}, + {"label": _("Billing and Payments"), "items": ["Sales Invoice", "Payment Entry"]}, + ], } diff --git a/erpnext/healthcare/doctype/patient/test_patient.py b/erpnext/healthcare/doctype/patient/test_patient.py index e9bfff7116c..664fa9dea21 100644 --- a/erpnext/healthcare/doctype/patient/test_patient.py +++ b/erpnext/healthcare/doctype/patient/test_patient.py @@ -11,25 +11,25 @@ from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment imp class TestPatient(unittest.TestCase): def test_customer_created(self): frappe.db.sql("""delete from `tabPatient`""") - frappe.db.set_value('Healthcare Settings', None, 'link_customer_to_patient', 1) + frappe.db.set_value("Healthcare Settings", None, "link_customer_to_patient", 1) patient = create_patient() - self.assertTrue(frappe.db.get_value('Patient', patient, 'customer')) + self.assertTrue(frappe.db.get_value("Patient", patient, "customer")) def test_patient_registration(self): frappe.db.sql("""delete from `tabPatient`""") - settings = frappe.get_single('Healthcare Settings') + settings = frappe.get_single("Healthcare Settings") settings.collect_registration_fee = 1 settings.registration_fee = 500 settings.save() patient = create_patient() - patient = frappe.get_doc('Patient', patient) - self.assertEqual(patient.status, 'Disabled') + patient = frappe.get_doc("Patient", patient) + self.assertEqual(patient.status, "Disabled") # check sales invoice and patient status result = patient.invoice_patient_registration() - self.assertTrue(frappe.db.exists('Sales Invoice', result.get('invoice'))) - self.assertTrue(patient.status, 'Active') + self.assertTrue(frappe.db.exists("Sales Invoice", result.get("invoice"))) + self.assertTrue(patient.status, "Active") settings.collect_registration_fee = 0 settings.save() @@ -40,33 +40,60 @@ class TestPatient(unittest.TestCase): frappe.db.sql("""delete from `tabContact` where name like'_Test Patient%'""") frappe.db.sql("""delete from `tabDynamic Link` where parent like '_Test Patient%'""") - patient = create_patient(patient_name='_Test Patient Contact', email='test-patient@example.com', mobile='+91 0000000001') - customer = frappe.db.get_value('Patient', patient, 'customer') + patient = create_patient( + patient_name="_Test Patient Contact", email="test-patient@example.com", mobile="+91 0000000001" + ) + customer = frappe.db.get_value("Patient", patient, "customer") self.assertTrue(customer) - self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Patient', 'link_name': patient})) - self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Customer', 'link_name': customer})) + self.assertTrue( + frappe.db.exists( + "Dynamic Link", {"parenttype": "Contact", "link_doctype": "Patient", "link_name": patient} + ) + ) + self.assertTrue( + frappe.db.exists( + "Dynamic Link", {"parenttype": "Contact", "link_doctype": "Customer", "link_name": customer} + ) + ) # a second patient linking with same customer - new_patient = create_patient(email='test-patient@example.com', mobile='+91 0000000009', customer=customer) - self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Patient', 'link_name': new_patient})) - self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Customer', 'link_name': customer})) + new_patient = create_patient( + email="test-patient@example.com", mobile="+91 0000000009", customer=customer + ) + self.assertTrue( + frappe.db.exists( + "Dynamic Link", {"parenttype": "Contact", "link_doctype": "Patient", "link_name": new_patient} + ) + ) + self.assertTrue( + frappe.db.exists( + "Dynamic Link", {"parenttype": "Contact", "link_doctype": "Customer", "link_name": customer} + ) + ) def test_patient_user(self): frappe.db.sql("""delete from `tabUser` where email='test-patient-user@example.com'""") frappe.db.sql("""delete from `tabDynamic Link` where parent like '_Test Patient%'""") frappe.db.sql("""delete from `tabPatient` where name like '_Test Patient%'""") - patient = create_patient(patient_name='_Test Patient User', email='test-patient-user@example.com', mobile='+91 0000000009', create_user=True) - user = frappe.db.get_value('Patient', patient, 'user_id') - self.assertTrue(frappe.db.exists('User', user)) + patient = create_patient( + patient_name="_Test Patient User", + email="test-patient-user@example.com", + mobile="+91 0000000009", + create_user=True, + ) + user = frappe.db.get_value("Patient", patient, "user_id") + self.assertTrue(frappe.db.exists("User", user)) - new_patient = frappe.get_doc({ - 'doctype': 'Patient', - 'first_name': '_Test Patient Duplicate User', - 'sex': 'Male', - 'email': 'test-patient-user@example.com', - 'mobile': '+91 0000000009', - 'invite_user': 1 - }) + new_patient = frappe.get_doc( + { + "doctype": "Patient", + "first_name": "_Test Patient Duplicate User", + "sex": "Male", + "email": "test-patient-user@example.com", + "mobile": "+91 0000000009", + "invite_user": 1, + } + ) self.assertRaises(frappe.exceptions.DuplicateEntryError, new_patient.insert) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index c4f253a062f..b6e30060437 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -26,9 +26,12 @@ from erpnext.hr.doctype.employee.employee import is_holiday class MaximumCapacityError(frappe.ValidationError): pass + + class OverlapError(frappe.ValidationError): pass + class PatientAppointment(Document): def validate(self): self.validate_overlaps() @@ -46,8 +49,9 @@ class PatientAppointment(Document): send_confirmation_msg(self) def set_title(self): - self.title = _('{0} with {1}').format(self.patient_name or self.patient, - self.practitioner_name or self.practitioner) + self.title = _("{0} with {1}").format( + self.patient_name or self.patient, self.practitioner_name or self.practitioner + ) def set_status(self): today = getdate() @@ -55,16 +59,18 @@ class PatientAppointment(Document): # If appointment is created for today set status as Open else Scheduled if appointment_date == today: - self.status = 'Open' + self.status = "Open" elif appointment_date > today: - self.status = 'Scheduled' + self.status = "Scheduled" def validate_overlaps(self): - end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \ - + datetime.timedelta(minutes=flt(self.duration)) + end_time = datetime.datetime.combine( + getdate(self.appointment_date), get_time(self.appointment_time) + ) + datetime.timedelta(minutes=flt(self.duration)) # all appointments for both patient and practitioner overlapping the duration of this appointment - overlapping_appointments = frappe.db.sql(""" + overlapping_appointments = frappe.db.sql( + """ SELECT name, practitioner, patient, appointment_time, duration, service_unit FROM @@ -77,35 +83,52 @@ class PatientAppointment(Document): (appointment_time=%(appointment_time)s)) """, { - 'appointment_date': self.appointment_date, - 'name': self.name, - 'practitioner': self.practitioner, - 'patient': self.patient, - 'appointment_time': self.appointment_time, - 'end_time':end_time.time() + "appointment_date": self.appointment_date, + "name": self.name, + "practitioner": self.practitioner, + "patient": self.patient, + "appointment_time": self.appointment_time, + "end_time": end_time.time(), }, - as_dict = True + as_dict=True, ) if not overlapping_appointments: - return # No overlaps, nothing to validate! + return # No overlaps, nothing to validate! - if self.service_unit: # validate service unit capacity if overlap enabled - allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', self.service_unit, - ['overlap_appointments', 'service_unit_capacity']) + if self.service_unit: # validate service unit capacity if overlap enabled + allow_overlap, service_unit_capacity = frappe.get_value( + "Healthcare Service Unit", self.service_unit, ["overlap_appointments", "service_unit_capacity"] + ) if allow_overlap: - service_unit_appointments = list(filter(lambda appointment: appointment['service_unit'] == self.service_unit and - appointment['patient'] != self.patient, overlapping_appointments)) # if same patient already booked, it should be an overlap + service_unit_appointments = list( + filter( + lambda appointment: appointment["service_unit"] == self.service_unit + and appointment["patient"] != self.patient, + overlapping_appointments, + ) + ) # if same patient already booked, it should be an overlap if len(service_unit_appointments) >= (service_unit_capacity or 1): - frappe.throw(_("Not allowed, {} cannot exceed maximum capacity {}") - .format(frappe.bold(self.service_unit), frappe.bold(service_unit_capacity or 1)), MaximumCapacityError) - else: # service_unit_appointments within capacity, remove from overlapping_appointments - overlapping_appointments = [appointment for appointment in overlapping_appointments if appointment not in service_unit_appointments] + frappe.throw( + _("Not allowed, {} cannot exceed maximum capacity {}").format( + frappe.bold(self.service_unit), frappe.bold(service_unit_capacity or 1) + ), + MaximumCapacityError, + ) + else: # service_unit_appointments within capacity, remove from overlapping_appointments + overlapping_appointments = [ + appointment + for appointment in overlapping_appointments + if appointment not in service_unit_appointments + ] if overlapping_appointments: - frappe.throw(_("Not allowed, cannot overlap appointment {}") - .format(frappe.bold(', '.join([appointment['name'] for appointment in overlapping_appointments]))), OverlapError) - + frappe.throw( + _("Not allowed, cannot overlap appointment {}").format( + frappe.bold(", ".join([appointment["name"] for appointment in overlapping_appointments])) + ), + OverlapError, + ) def validate_service_unit(self): if self.inpatient_record and self.service_unit: @@ -113,48 +136,65 @@ class PatientAppointment(Document): get_current_healthcare_service_unit, ) - is_inpatient_occupancy_unit = frappe.db.get_value('Healthcare Service Unit', self.service_unit, - 'inpatient_occupancy') + is_inpatient_occupancy_unit = frappe.db.get_value( + "Healthcare Service Unit", self.service_unit, "inpatient_occupancy" + ) service_unit = get_current_healthcare_service_unit(self.inpatient_record) if is_inpatient_occupancy_unit and service_unit != self.service_unit: - msg = _('Patient {0} is not admitted in the service unit {1}').format(frappe.bold(self.patient), frappe.bold(self.service_unit)) + '
' - msg += _('Appointment for service units with Inpatient Occupancy can only be created against the unit where patient has been admitted.') - frappe.throw(msg, title=_('Invalid Healthcare Service Unit')) - + msg = ( + _("Patient {0} is not admitted in the service unit {1}").format( + frappe.bold(self.patient), frappe.bold(self.service_unit) + ) + + "
" + ) + msg += _( + "Appointment for service units with Inpatient Occupancy can only be created against the unit where patient has been admitted." + ) + frappe.throw(msg, title=_("Invalid Healthcare Service Unit")) def set_appointment_datetime(self): - self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00") + self.appointment_datetime = "%s %s" % ( + self.appointment_date, + self.appointment_time or "00:00:00", + ) def set_payment_details(self): - if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'): + if frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing"): details = get_service_item_and_practitioner_charge(self) - self.db_set('billing_item', details.get('service_item')) + self.db_set("billing_item", details.get("service_item")) if not self.paid_amount: - self.db_set('paid_amount', details.get('practitioner_charge')) + self.db_set("paid_amount", details.get("practitioner_charge")) def validate_customer_created(self): - if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'): - if not frappe.db.get_value('Patient', self.patient, 'customer'): + if frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing"): + if not frappe.db.get_value("Patient", self.patient, "customer"): msg = _("Please set a Customer linked to the Patient") - msg += " {0}".format(self.patient) - frappe.throw(msg, title=_('Customer Not Found')) + msg += " {0}".format(self.patient) + frappe.throw(msg, title=_("Customer Not Found")) def update_prescription_details(self): if self.procedure_prescription: - frappe.db.set_value('Procedure Prescription', self.procedure_prescription, 'appointment_booked', 1) + frappe.db.set_value( + "Procedure Prescription", self.procedure_prescription, "appointment_booked", 1 + ) if self.procedure_template: - comments = frappe.db.get_value('Procedure Prescription', self.procedure_prescription, 'comments') + comments = frappe.db.get_value( + "Procedure Prescription", self.procedure_prescription, "comments" + ) if comments: - frappe.db.set_value('Patient Appointment', self.name, 'notes', comments) + frappe.db.set_value("Patient Appointment", self.name, "notes", comments) def update_fee_validity(self): - if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): + if not frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups"): return fee_validity = manage_fee_validity(self) if fee_validity: - frappe.msgprint(_('{0}: {1} has fee validity till {2}').format(self.patient, - frappe.bold(self.patient_name), fee_validity.valid_till)) + frappe.msgprint( + _("{0}: {1} has fee validity till {2}").format( + self.patient, frappe.bold(self.patient_name), fee_validity.valid_till + ) + ) @frappe.whitelist() def get_therapy_types(self): @@ -162,7 +202,7 @@ class PatientAppointment(Document): return therapy_types = [] - doc = frappe.get_doc('Therapy Plan', self.therapy_plan) + doc = frappe.get_doc("Therapy Plan", self.therapy_plan) for entry in doc.therapy_plan_details: therapy_types.append(entry.therapy_type) @@ -171,26 +211,35 @@ class PatientAppointment(Document): @frappe.whitelist() def check_payment_fields_reqd(patient): - automate_invoicing = frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing') - free_follow_ups = frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups') + automate_invoicing = frappe.db.get_single_value( + "Healthcare Settings", "automate_appointment_invoicing" + ) + free_follow_ups = frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") if automate_invoicing: if free_follow_ups: - fee_validity = frappe.db.exists('Fee Validity', {'patient': patient, 'status': 'Pending'}) + fee_validity = frappe.db.exists("Fee Validity", {"patient": patient, "status": "Pending"}) if fee_validity: - return {'fee_validity': fee_validity} + return {"fee_validity": fee_validity} return True return False + def invoice_appointment(appointment_doc): - automate_invoicing = frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing') - appointment_invoiced = frappe.db.get_value('Patient Appointment', appointment_doc.name, 'invoiced') - enable_free_follow_ups = frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups') + automate_invoicing = frappe.db.get_single_value( + "Healthcare Settings", "automate_appointment_invoicing" + ) + appointment_invoiced = frappe.db.get_value( + "Patient Appointment", appointment_doc.name, "invoiced" + ) + enable_free_follow_ups = frappe.db.get_single_value( + "Healthcare Settings", "enable_free_follow_ups" + ) if enable_free_follow_ups: fee_validity = check_fee_validity(appointment_doc) - if fee_validity and fee_validity.status == 'Completed': + if fee_validity and fee_validity.status == "Completed": fee_validity = None elif not fee_validity: - if frappe.db.exists('Fee Validity Reference', {'appointment': appointment_doc.name}): + if frappe.db.exists("Fee Validity Reference", {"appointment": appointment_doc.name}): return else: fee_validity = None @@ -200,21 +249,21 @@ def invoice_appointment(appointment_doc): def create_sales_invoice(appointment_doc): - sales_invoice = frappe.new_doc('Sales Invoice') + sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = appointment_doc.patient - sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer') + sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company sales_invoice.debit_to = get_receivable_account(appointment_doc.company) - item = sales_invoice.append('items', {}) + item = sales_invoice.append("items", {}) item = get_appointment_item(appointment_doc, item) # Add payments if payment details are supplied else proceed to create invoice as Unpaid if appointment_doc.mode_of_payment and appointment_doc.paid_amount: sales_invoice.is_pos = 1 - payment = sales_invoice.append('payments', {}) + payment = sales_invoice.append("payments", {}) payment.mode_of_payment = appointment_doc.mode_of_payment payment.amount = appointment_doc.paid_amount @@ -222,56 +271,61 @@ def create_sales_invoice(appointment_doc): sales_invoice.flags.ignore_mandatory = True sales_invoice.save(ignore_permissions=True) sales_invoice.submit() - frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True) - frappe.db.set_value('Patient Appointment', appointment_doc.name, { - 'invoiced': 1, - 'ref_sales_invoice': sales_invoice.name - }) + frappe.msgprint(_("Sales Invoice {0} created").format(sales_invoice.name), alert=True) + frappe.db.set_value( + "Patient Appointment", + appointment_doc.name, + {"invoiced": 1, "ref_sales_invoice": sales_invoice.name}, + ) def check_is_new_patient(patient, name=None): - filters = {'patient': patient, 'status': ('!=','Cancelled')} + filters = {"patient": patient, "status": ("!=", "Cancelled")} if name: - filters['name'] = ('!=', name) + filters["name"] = ("!=", name) - has_previous_appointment = frappe.db.exists('Patient Appointment', filters) + has_previous_appointment = frappe.db.exists("Patient Appointment", filters) return not has_previous_appointment def get_appointment_item(appointment_doc, item): details = get_service_item_and_practitioner_charge(appointment_doc) - charge = appointment_doc.paid_amount or details.get('practitioner_charge') - item.item_code = details.get('service_item') - item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner) + charge = appointment_doc.paid_amount or details.get("practitioner_charge") + item.item_code = details.get("service_item") + item.description = _("Consulting Charges: {0}").format(appointment_doc.practitioner) item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company) - item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center') + item.cost_center = frappe.get_cached_value("Company", appointment_doc.company, "cost_center") item.rate = charge item.amount = charge item.qty = 1 - item.reference_dt = 'Patient Appointment' + item.reference_dt = "Patient Appointment" item.reference_dn = appointment_doc.name return item def cancel_appointment(appointment_id): - appointment = frappe.get_doc('Patient Appointment', appointment_id) + appointment = frappe.get_doc("Patient Appointment", appointment_id) if appointment.invoiced: sales_invoice = check_sales_invoice_exists(appointment) if sales_invoice and cancel_sales_invoice(sales_invoice): - msg = _('Appointment {0} and Sales Invoice {1} cancelled').format(appointment.name, sales_invoice.name) + msg = _("Appointment {0} and Sales Invoice {1} cancelled").format( + appointment.name, sales_invoice.name + ) else: - msg = _('Appointment Cancelled. Please review and cancel the invoice {0}').format(sales_invoice.name) + msg = _("Appointment Cancelled. Please review and cancel the invoice {0}").format( + sales_invoice.name + ) else: fee_validity = manage_fee_validity(appointment) - msg = _('Appointment Cancelled.') + msg = _("Appointment Cancelled.") if fee_validity: - msg += _('Fee Validity {0} updated.').format(fee_validity.name) + msg += _("Fee Validity {0} updated.").format(fee_validity.name) frappe.msgprint(msg) def cancel_sales_invoice(sales_invoice): - if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'): + if frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing"): if len(sales_invoice.items) == 1: sales_invoice.cancel() return True @@ -279,13 +333,14 @@ def cancel_sales_invoice(sales_invoice): def check_sales_invoice_exists(appointment): - sales_invoice = frappe.db.get_value('Sales Invoice Item', { - 'reference_dt': 'Patient Appointment', - 'reference_dn': appointment.name - }, 'parent') + sales_invoice = frappe.db.get_value( + "Sales Invoice Item", + {"reference_dt": "Patient Appointment", "reference_dn": appointment.name}, + "parent", + ) if sales_invoice: - sales_invoice = frappe.get_doc('Sales Invoice', sales_invoice) + sales_invoice = frappe.get_doc("Sales Invoice", sales_invoice) return sales_invoice return False @@ -300,24 +355,29 @@ def get_availability_data(date, practitioner): """ date = getdate(date) - weekday = date.strftime('%A') + weekday = date.strftime("%A") - practitioner_doc = frappe.get_doc('Healthcare Practitioner', practitioner) + practitioner_doc = frappe.get_doc("Healthcare Practitioner", practitioner) check_employee_wise_availability(date, practitioner_doc) if practitioner_doc.practitioner_schedules: slot_details = get_available_slots(practitioner_doc, date) else: - frappe.throw(_('{0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner master').format( - practitioner), title=_('Practitioner Schedule Not Found')) - + frappe.throw( + _( + "{0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner master" + ).format(practitioner), + title=_("Practitioner Schedule Not Found"), + ) if not slot_details: # TODO: return available slots in nearby dates - frappe.throw(_('Healthcare Practitioner not available on {0}').format(weekday), title=_('Not Available')) + frappe.throw( + _("Healthcare Practitioner not available on {0}").format(weekday), title=_("Not Available") + ) - return {'slot_details': slot_details} + return {"slot_details": slot_details} def check_employee_wise_availability(date, practitioner_doc): @@ -325,32 +385,41 @@ def check_employee_wise_availability(date, practitioner_doc): if practitioner_doc.employee: employee = practitioner_doc.employee elif practitioner_doc.user_id: - employee = frappe.db.get_value('Employee', {'user_id': practitioner_doc.user_id}, 'name') + employee = frappe.db.get_value("Employee", {"user_id": practitioner_doc.user_id}, "name") if employee: # check holiday if is_holiday(employee, date): - frappe.throw(_('{0} is a holiday'.format(date)), title=_('Not Available')) + frappe.throw(_("{0} is a holiday".format(date)), title=_("Not Available")) # check leave status - leave_record = frappe.db.sql("""select half_day from `tabLeave Application` + leave_record = frappe.db.sql( + """select half_day from `tabLeave Application` where employee = %s and %s between from_date and to_date - and docstatus = 1""", (employee, date), as_dict=True) + and docstatus = 1""", + (employee, date), + as_dict=True, + ) if leave_record: if leave_record[0].half_day: - frappe.throw(_('{0} is on a Half day Leave on {1}').format(practitioner_doc.name, date), title=_('Not Available')) + frappe.throw( + _("{0} is on a Half day Leave on {1}").format(practitioner_doc.name, date), + title=_("Not Available"), + ) else: - frappe.throw(_('{0} is on Leave on {1}').format(practitioner_doc.name, date), title=_('Not Available')) + frappe.throw( + _("{0} is on Leave on {1}").format(practitioner_doc.name, date), title=_("Not Available") + ) def get_available_slots(practitioner_doc, date): available_slots = slot_details = [] - weekday = date.strftime('%A') + weekday = date.strftime("%A") practitioner = practitioner_doc.name for schedule_entry in practitioner_doc.practitioner_schedules: validate_practitioner_schedules(schedule_entry, practitioner) - practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule) + practitioner_schedule = frappe.get_doc("Practitioner Schedule", schedule_entry.schedule) if practitioner_schedule and not practitioner_schedule.disabled: available_slots = [] @@ -364,32 +433,45 @@ def get_available_slots(practitioner_doc, date): service_unit_capacity = 0 # fetch all appointments to practitioner by service unit filters = { - 'practitioner': practitioner, - 'service_unit': schedule_entry.service_unit, - 'appointment_date': date, - 'status': ['not in',['Cancelled']] + "practitioner": practitioner, + "service_unit": schedule_entry.service_unit, + "appointment_date": date, + "status": ["not in", ["Cancelled"]], } if schedule_entry.service_unit: - slot_name = f'{schedule_entry.schedule}' - allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, ['overlap_appointments', 'service_unit_capacity']) + slot_name = f"{schedule_entry.schedule}" + allow_overlap, service_unit_capacity = frappe.get_value( + "Healthcare Service Unit", + schedule_entry.service_unit, + ["overlap_appointments", "service_unit_capacity"], + ) if not allow_overlap: # fetch all appointments to service unit - filters.pop('practitioner') + filters.pop("practitioner") else: slot_name = schedule_entry.schedule # fetch all appointments to practitioner without service unit - filters['practitioner'] = practitioner - filters.pop('service_unit') + filters["practitioner"] = practitioner + filters.pop("service_unit") appointments = frappe.get_all( - 'Patient Appointment', + "Patient Appointment", filters=filters, - fields=['name', 'appointment_time', 'duration', 'status']) + fields=["name", "appointment_time", "duration", "status"], + ) - slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots, - 'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity, - 'practitioner_name': practitioner_doc.practitioner_name}) + slot_details.append( + { + "slot_name": slot_name, + "service_unit": schedule_entry.service_unit, + "avail_slot": available_slots, + "appointments": appointments, + "allow_overlap": allow_overlap, + "service_unit_capacity": service_unit_capacity, + "practitioner_name": practitioner_doc.practitioner_name, + } + ) return slot_details @@ -397,82 +479,107 @@ def get_available_slots(practitioner_doc, date): def validate_practitioner_schedules(schedule_entry, practitioner): if schedule_entry.schedule: if not schedule_entry.service_unit: - frappe.throw(_('Practitioner {0} does not have a Service Unit set against the Practitioner Schedule {1}.').format( - get_link_to_form('Healthcare Practitioner', practitioner), frappe.bold(schedule_entry.schedule)), - title=_('Service Unit Not Found')) + frappe.throw( + _( + "Practitioner {0} does not have a Service Unit set against the Practitioner Schedule {1}." + ).format( + get_link_to_form("Healthcare Practitioner", practitioner), + frappe.bold(schedule_entry.schedule), + ), + title=_("Service Unit Not Found"), + ) else: - frappe.throw(_('Practitioner {0} does not have a Practitioner Schedule assigned.').format( - get_link_to_form('Healthcare Practitioner', practitioner)), - title=_('Practitioner Schedule Not Found')) + frappe.throw( + _("Practitioner {0} does not have a Practitioner Schedule assigned.").format( + get_link_to_form("Healthcare Practitioner", practitioner) + ), + title=_("Practitioner Schedule Not Found"), + ) @frappe.whitelist() def update_status(appointment_id, status): - frappe.db.set_value('Patient Appointment', appointment_id, 'status', status) + frappe.db.set_value("Patient Appointment", appointment_id, "status", status) appointment_booked = True - if status == 'Cancelled': + if status == "Cancelled": appointment_booked = False cancel_appointment(appointment_id) - procedure_prescription = frappe.db.get_value('Patient Appointment', appointment_id, 'procedure_prescription') + procedure_prescription = frappe.db.get_value( + "Patient Appointment", appointment_id, "procedure_prescription" + ) if procedure_prescription: - frappe.db.set_value('Procedure Prescription', procedure_prescription, 'appointment_booked', appointment_booked) + frappe.db.set_value( + "Procedure Prescription", procedure_prescription, "appointment_booked", appointment_booked + ) def send_confirmation_msg(doc): - if frappe.db.get_single_value('Healthcare Settings', 'send_appointment_confirmation'): - message = frappe.db.get_single_value('Healthcare Settings', 'appointment_confirmation_msg') + if frappe.db.get_single_value("Healthcare Settings", "send_appointment_confirmation"): + message = frappe.db.get_single_value("Healthcare Settings", "appointment_confirmation_msg") try: send_message(doc, message) except Exception: - frappe.log_error(frappe.get_traceback(), _('Appointment Confirmation Message Not Sent')) - frappe.msgprint(_('Appointment Confirmation Message Not Sent'), indicator='orange') + frappe.log_error(frappe.get_traceback(), _("Appointment Confirmation Message Not Sent")) + frappe.msgprint(_("Appointment Confirmation Message Not Sent"), indicator="orange") @frappe.whitelist() def make_encounter(source_name, target_doc=None): - doc = get_mapped_doc('Patient Appointment', source_name, { - 'Patient Appointment': { - 'doctype': 'Patient Encounter', - 'field_map': [ - ['appointment', 'name'], - ['patient', 'patient'], - ['practitioner', 'practitioner'], - ['medical_department', 'department'], - ['patient_sex', 'patient_sex'], - ['invoiced', 'invoiced'], - ['company', 'company'] - ] - } - }, target_doc) + doc = get_mapped_doc( + "Patient Appointment", + source_name, + { + "Patient Appointment": { + "doctype": "Patient Encounter", + "field_map": [ + ["appointment", "name"], + ["patient", "patient"], + ["practitioner", "practitioner"], + ["medical_department", "department"], + ["patient_sex", "patient_sex"], + ["invoiced", "invoiced"], + ["company", "company"], + ], + } + }, + target_doc, + ) return doc def send_appointment_reminder(): - if frappe.db.get_single_value('Healthcare Settings', 'send_appointment_reminder'): - remind_before = datetime.datetime.strptime(frappe.db.get_single_value('Healthcare Settings', 'remind_before'), '%H:%M:%S') + if frappe.db.get_single_value("Healthcare Settings", "send_appointment_reminder"): + remind_before = datetime.datetime.strptime( + frappe.db.get_single_value("Healthcare Settings", "remind_before"), "%H:%M:%S" + ) reminder_dt = datetime.datetime.now() + datetime.timedelta( - hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second) + hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second + ) - appointment_list = frappe.db.get_all('Patient Appointment', { - 'appointment_datetime': ['between', (datetime.datetime.now(), reminder_dt)], - 'reminded': 0, - 'status': ['!=', 'Cancelled'] - }) + appointment_list = frappe.db.get_all( + "Patient Appointment", + { + "appointment_datetime": ["between", (datetime.datetime.now(), reminder_dt)], + "reminded": 0, + "status": ["!=", "Cancelled"], + }, + ) for appointment in appointment_list: - doc = frappe.get_doc('Patient Appointment', appointment.name) - message = frappe.db.get_single_value('Healthcare Settings', 'appointment_reminder_msg') + doc = frappe.get_doc("Patient Appointment", appointment.name) + message = frappe.db.get_single_value("Healthcare Settings", "appointment_reminder_msg") send_message(doc, message) - frappe.db.set_value('Patient Appointment', doc.name, 'reminded', 1) + frappe.db.set_value("Patient Appointment", doc.name, "reminded", 1) + def send_message(doc, message): - patient_mobile = frappe.db.get_value('Patient', doc.patient, 'mobile') + patient_mobile = frappe.db.get_value("Patient", doc.patient, "mobile") if patient_mobile: - context = {'doc': doc, 'alert': doc, 'comments': None} - if doc.get('_comments'): - context['comments'] = json.loads(doc.get('_comments')) + context = {"doc": doc, "alert": doc, "comments": None} + if doc.get("_comments"): + context["comments"] = json.loads(doc.get("_comments")) # jinja to string convertion happens here message = frappe.render_template(message, context) @@ -480,7 +587,8 @@ def send_message(doc, message): try: send_sms(number, message) except Exception as e: - frappe.msgprint(_('SMS not sent, please check SMS Settings'), alert=True) + frappe.msgprint(_("SMS not sent, please check SMS Settings"), alert=True) + @frappe.whitelist() def get_events(start, end, filters=None): @@ -491,9 +599,11 @@ def get_events(start, end, filters=None): :param filters: Filters (JSON). """ from frappe.desk.calendar import get_event_conditions - conditions = get_event_conditions('Patient Appointment', filters) - data = frappe.db.sql(""" + conditions = get_event_conditions("Patient Appointment", filters) + + data = frappe.db.sql( + """ select `tabPatient Appointment`.name, `tabPatient Appointment`.patient, `tabPatient Appointment`.practitioner, `tabPatient Appointment`.status, @@ -505,11 +615,16 @@ def get_events(start, end, filters=None): left join `tabAppointment Type` on `tabPatient Appointment`.appointment_type=`tabAppointment Type`.name where (`tabPatient Appointment`.appointment_date between %(start)s and %(end)s) - and `tabPatient Appointment`.status != 'Cancelled' and `tabPatient Appointment`.docstatus < 2 {conditions}""".format(conditions=conditions), - {"start": start, "end": end}, as_dict=True, update={"allDay": 0}) + and `tabPatient Appointment`.status != 'Cancelled' and `tabPatient Appointment`.docstatus < 2 {conditions}""".format( + conditions=conditions + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) for item in data: - item.end = item.start + datetime.timedelta(minutes = item.duration) + item.end = item.start + datetime.timedelta(minutes=item.duration) return data @@ -527,7 +642,8 @@ def get_procedure_prescribed(patient): ct.patient=%(patient)s and pp.parent=ct.name and pp.appointment_booked=0 ORDER BY ct.creation desc - """, {'patient': patient} + """, + {"patient": patient}, ) @@ -544,15 +660,16 @@ def get_prescribed_therapies(patient): e.patient=%(patient)s and t.parent=e.name ORDER BY e.creation desc - """, {'patient': patient} + """, + {"patient": patient}, ) def update_appointment_status(): # update the status of appointments daily - appointments = frappe.get_all('Patient Appointment', { - 'status': ('not in', ['Closed', 'Cancelled']) - }, as_dict=1) + appointments = frappe.get_all( + "Patient Appointment", {"status": ("not in", ["Closed", "Cancelled"])}, as_dict=1 + ) for appointment in appointments: - frappe.get_doc('Patient Appointment', appointment.name).set_status() + frappe.get_doc("Patient Appointment", appointment.name).set_status() diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment_dashboard.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment_dashboard.py index 89349ee60f6..3eeb153055b 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment_dashboard.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment_dashboard.py @@ -1,17 +1,14 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'appointment', - 'non_standard_fieldnames': { - 'Patient Medical Record': 'reference_name' - }, - 'transactions': [ + "fieldname": "appointment", + "non_standard_fieldnames": {"Patient Medical Record": "reference_name"}, + "transactions": [ { - 'label': _('Consultations'), - 'items': ['Patient Encounter', 'Vital Signs', 'Patient Medical Record'] + "label": _("Consultations"), + "items": ["Patient Encounter", "Vital Signs", "Patient Medical Record"], } - ] + ], } diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 7afdba69eaa..048547a9322 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -22,24 +22,28 @@ class TestPatientAppointment(unittest.TestCase): frappe.db.sql("""delete from `tabPatient Encounter`""") make_pos_profile() frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test %'""") - frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test Service Unit Type%'""") + frappe.db.sql( + """delete from `tabHealthcare Service Unit` where name like '_Test Service Unit Type%'""" + ) def test_status(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 0) appointment = create_appointment(patient, practitioner, nowdate()) - self.assertEqual(appointment.status, 'Open') + self.assertEqual(appointment.status, "Open") appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) - self.assertEqual(appointment.status, 'Scheduled') + self.assertEqual(appointment.status, "Scheduled") encounter = create_encounter(appointment) - self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') + self.assertEqual( + frappe.db.get_value("Patient Appointment", appointment.name, "status"), "Closed" + ) encounter.cancel() - self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open') + self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "status"), "Open") def test_start_encounter(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice=1) appointment.reload() self.assertEqual(appointment.invoiced, 1) encounter = make_encounter(appointment.name) @@ -48,92 +52,115 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(encounter.practitioner, appointment.practitioner) self.assertEqual(encounter.patient, appointment.patient) # invoiced flag mapped from appointment - self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced')) + self.assertEqual( + encounter.invoiced, frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") + ) def test_auto_invoicing(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) + frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 0) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 0) appointment = create_appointment(patient, practitioner, nowdate()) - self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 0) + self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "invoiced"), 0) - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), invoice=1) - self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1) - sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') + self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "invoiced"), 1) + sales_invoice_name = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) self.assertTrue(sales_invoice_name) - self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'company'), appointment.company) - self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient) - self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "company"), appointment.company + ) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "patient"), appointment.patient + ) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "paid_amount"), appointment.paid_amount + ) def test_auto_invoicing_based_on_department(self): patient, practitioner = create_healthcare_docs() medical_department = create_medical_department() - frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment_type = create_appointment_type({'medical_department': medical_department}) + frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 0) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + appointment_type = create_appointment_type({"medical_department": medical_department}) - appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), - invoice=1, appointment_type=appointment_type.name, department=medical_department) + appointment = create_appointment( + patient, + practitioner, + add_days(nowdate(), 2), + invoice=1, + appointment_type=appointment_type.name, + department=medical_department, + ) appointment.reload() self.assertEqual(appointment.invoiced, 1) - self.assertEqual(appointment.billing_item, 'HLC-SI-001') + self.assertEqual(appointment.billing_item, "HLC-SI-001") self.assertEqual(appointment.paid_amount, 200) - sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') + sales_invoice_name = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) self.assertTrue(sales_invoice_name) - self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "paid_amount"), appointment.paid_amount + ) def test_auto_invoicing_according_to_appointment_type_charge(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 0) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) item = create_healthcare_service_items() - items = [{ - 'op_consulting_charge_item': item, - 'op_consulting_charge': 300 - }] - appointment_type = create_appointment_type(args={ - 'name': 'Generic Appointment Type charge', - 'items': items - }) + items = [{"op_consulting_charge_item": item, "op_consulting_charge": 300}] + appointment_type = create_appointment_type( + args={"name": "Generic Appointment Type charge", "items": items} + ) - appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), - invoice=1, appointment_type=appointment_type.name) + appointment = create_appointment( + patient, practitioner, add_days(nowdate(), 2), invoice=1, appointment_type=appointment_type.name + ) appointment.reload() self.assertEqual(appointment.invoiced, 1) self.assertEqual(appointment.billing_item, item) self.assertEqual(appointment.paid_amount, 300) - sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') + sales_invoice_name = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) self.assertTrue(sales_invoice_name) def test_appointment_cancel(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) + frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 1) appointment = create_appointment(patient, practitioner, nowdate()) - fee_validity = frappe.db.get_value('Fee Validity', {'patient': patient, 'practitioner': practitioner}) + fee_validity = frappe.db.get_value( + "Fee Validity", {"patient": patient, "practitioner": practitioner} + ) # fee validity created self.assertTrue(fee_validity) # first follow up appointment appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1)) - self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 1) + self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "visited"), 1) - update_status(appointment.name, 'Cancelled') + update_status(appointment.name, "Cancelled") # check fee validity updated - self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 0) + self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "visited"), 0) - frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 0) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1) - update_status(appointment.name, 'Cancelled') + update_status(appointment.name, "Cancelled") # check invoice cancelled - sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') - self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled') + sales_invoice_name = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) + self.assertEqual(frappe.db.get_value("Sales Invoice", sales_invoice_name, "status"), "Cancelled") def test_appointment_booking_for_admission_service_unit(self): from erpnext.healthcare.doctype.inpatient_record.inpatient_record import ( @@ -153,17 +180,19 @@ class TestPatientAppointment(unittest.TestCase): # Schedule Admission ip_record = create_inpatient(patient) ip_record.expected_length_of_stay = 0 - ip_record.save(ignore_permissions = True) + ip_record.save(ignore_permissions=True) # Admit - service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit("_Test Service Unit Ip Occupancy") admit_patient(ip_record, service_unit, now_datetime()) appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) self.assertEqual(appointment.service_unit, service_unit) # Discharge - schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) + schedule_discharge( + frappe.as_json({"patient": patient, "discharge_ordered_datetime": now_datetime()}) + ) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) discharge_patient(ip_record1, now_datetime()) @@ -186,27 +215,33 @@ class TestPatientAppointment(unittest.TestCase): # Schedule Admission ip_record = create_inpatient(patient) ip_record.expected_length_of_stay = 0 - ip_record.save(ignore_permissions = True) + ip_record.save(ignore_permissions=True) # Admit - service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit("_Test Service Unit Ip Occupancy") admit_patient(ip_record, service_unit, now_datetime()) - appointment_service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy for Appointment') - appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0) + appointment_service_unit = get_healthcare_service_unit( + "_Test Service Unit Ip Occupancy for Appointment" + ) + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0 + ) self.assertRaises(frappe.exceptions.ValidationError, appointment.save) # Discharge - schedule_discharge(frappe.as_json({'patient': patient, 'discharge_ordered_datetime': now_datetime()})) + schedule_discharge( + frappe.as_json({"patient": patient, "discharge_ordered_datetime": now_datetime()}) + ) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) mark_invoiced_inpatient_occupancy(ip_record1) discharge_patient(ip_record1, now_datetime()) def test_payment_should_be_mandatory_for_new_patient_appointment(self): - frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - frappe.db.set_value('Healthcare Settings', None, 'max_visits', 3) - frappe.db.set_value('Healthcare Settings', None, 'valid_days', 30) + frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 1) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + frappe.db.set_value("Healthcare Settings", None, "max_visits", 3) + frappe.db.set_value("Healthcare Settings", None, "valid_days", 30) patient = create_patient() assert check_is_new_patient(patient) @@ -215,43 +250,60 @@ class TestPatientAppointment(unittest.TestCase): def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - invoice_count = frappe.db.count('Sales Invoice') + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + invoice_count = frappe.db.count("Sales Invoice") assert check_is_new_patient(patient) create_appointment(patient, practitioner, nowdate()) - new_invoice_count = frappe.db.count('Sales Invoice') + new_invoice_count = frappe.db.count("Sales Invoice") assert new_invoice_count == invoice_count + 1 def test_overlap_appointment(self): from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError + patient, practitioner = create_healthcare_docs(id=1) patient_1, practitioner_1 = create_healthcare_docs(id=2) service_unit = create_service_unit(id=0) service_unit_1 = create_service_unit(id=1) - appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) # valid + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=service_unit + ) # valid # patient and practitioner cannot have overlapping appointments - appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit, save=0) + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=service_unit, save=0 + ) self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit_1, save=0) # diff service unit + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=service_unit_1, save=0 + ) # diff service unit self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment(patient, practitioner, nowdate(), save=0) # with no service unit link + appointment = create_appointment( + patient, practitioner, nowdate(), save=0 + ) # with no service unit link self.assertRaises(OverlapError, appointment.save) # patient cannot have overlapping appointments with other practitioners - appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit, save=0) + appointment = create_appointment( + patient, practitioner_1, nowdate(), service_unit=service_unit, save=0 + ) self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0) + appointment = create_appointment( + patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0 + ) self.assertRaises(OverlapError, appointment.save) appointment = create_appointment(patient, practitioner_1, nowdate(), save=0) self.assertRaises(OverlapError, appointment.save) # practitioner cannot have overlapping appointments with other patients - appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit, save=0) + appointment = create_appointment( + patient_1, practitioner, nowdate(), service_unit=service_unit, save=0 + ) self.assertRaises(OverlapError, appointment.save) - appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0) + appointment = create_appointment( + patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0 + ) self.assertRaises(OverlapError, appointment.save) appointment = create_appointment(patient_1, practitioner, nowdate(), save=0) self.assertRaises(OverlapError, appointment.save) @@ -261,19 +313,28 @@ class TestPatientAppointment(unittest.TestCase): MaximumCapacityError, OverlapError, ) + practitioner = create_practitioner() capacity = 3 - overlap_service_unit_type = create_service_unit_type(id=10, allow_appointments=1, overlap_appointments=1) - overlap_service_unit = create_service_unit(id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity) + overlap_service_unit_type = create_service_unit_type( + id=10, allow_appointments=1, overlap_appointments=1 + ) + overlap_service_unit = create_service_unit( + id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity + ) for i in range(0, capacity): patient = create_patient(id=i) - create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid - appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) # overlap + create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0 + ) # overlap self.assertRaises(OverlapError, appointment.save) patient = create_patient(id=capacity) - appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) + appointment = create_appointment( + patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0 + ) self.assertRaises(MaximumCapacityError, appointment.save) def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self): @@ -285,16 +346,16 @@ class TestPatientAppointment(unittest.TestCase): roles = [{"doctype": "Has Role", "role": "Physician"}] user = create_user(roles=roles) - new_practitioner = frappe.get_doc('Healthcare Practitioner', new_practitioner) + new_practitioner = frappe.get_doc("Healthcare Practitioner", new_practitioner) new_practitioner.user_id = user.email new_practitioner.save() frappe.set_user(user.name) - appointments = frappe.get_list('Patient Appointment') + appointments = frappe.get_list("Patient Appointment") assert len(appointments) == 1 frappe.set_user("Administrator") - appointments = frappe.get_list('Patient Appointment') + appointments = frappe.get_list("Patient Appointment") assert len(appointments) == 2 @@ -305,14 +366,16 @@ def create_healthcare_docs(id=0): return patient, practitioner -def create_patient(id=0, patient_name=None, email=None, mobile=None, customer=None, create_user=False): - if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}): - patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name']) +def create_patient( + id=0, patient_name=None, email=None, mobile=None, customer=None, create_user=False +): + if frappe.db.exists("Patient", {"firstname": f"_Test Patient {str(id)}"}): + patient = frappe.db.get_value("Patient", {"first_name": f"_Test Patient {str(id)}"}, ["name"]) return patient - patient = frappe.new_doc('Patient') - patient.first_name = patient_name if patient_name else f'_Test Patient {str(id)}' - patient.sex = 'Female' + patient = frappe.new_doc("Patient") + patient.first_name = patient_name if patient_name else f"_Test Patient {str(id)}" + patient.sex = "Female" patient.mobile = mobile patient.email = email patient.customer = customer @@ -323,24 +386,28 @@ def create_patient(id=0, patient_name=None, email=None, mobile=None, customer=No def create_medical_department(id=0): - if frappe.db.exists('Medical Department', f'_Test Medical Department {str(id)}'): - return f'_Test Medical Department {str(id)}' + if frappe.db.exists("Medical Department", f"_Test Medical Department {str(id)}"): + return f"_Test Medical Department {str(id)}" - medical_department = frappe.new_doc('Medical Department') - medical_department.department = f'_Test Medical Department {str(id)}' + medical_department = frappe.new_doc("Medical Department") + medical_department.department = f"_Test Medical Department {str(id)}" medical_department.save(ignore_permissions=True) return medical_department.name def create_practitioner(id=0, medical_department=None): - if frappe.db.exists('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}): - practitioner = frappe.db.get_value('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}, ['name']) + if frappe.db.exists( + "Healthcare Practitioner", {"firstname": f"_Test Healthcare Practitioner {str(id)}"} + ): + practitioner = frappe.db.get_value( + "Healthcare Practitioner", {"firstname": f"_Test Healthcare Practitioner {str(id)}"}, ["name"] + ) return practitioner - practitioner = frappe.new_doc('Healthcare Practitioner') - practitioner.first_name = f'_Test Healthcare Practitioner {str(id)}' - practitioner.gender = 'Female' + practitioner = frappe.new_doc("Healthcare Practitioner") + practitioner.first_name = f"_Test Healthcare Practitioner {str(id)}" + practitioner.gender = "Female" practitioner.department = medical_department or create_medical_department(id) practitioner.op_consulting_charge = 500 practitioner.inpatient_visit_charge = 500 @@ -348,9 +415,10 @@ def create_practitioner(id=0, medical_department=None): return practitioner.name + def create_encounter(appointment): if appointment: - encounter = frappe.new_doc('Patient Encounter') + encounter = frappe.new_doc("Patient Encounter") encounter.appointment = appointment.name encounter.patient = appointment.patient encounter.practitioner = appointment.practitioner @@ -362,27 +430,37 @@ def create_encounter(appointment): return encounter -def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, - service_unit=None, appointment_type=None, save=1, department=None): + +def create_appointment( + patient, + practitioner, + appointment_date, + invoice=0, + procedure_template=0, + service_unit=None, + appointment_type=None, + save=1, + department=None, +): item = create_healthcare_service_items() - frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item) - frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item) - appointment = frappe.new_doc('Patient Appointment') + frappe.db.set_value("Healthcare Settings", None, "inpatient_visit_charge_item", item) + frappe.db.set_value("Healthcare Settings", None, "op_consulting_charge_item", item) + appointment = frappe.new_doc("Patient Appointment") appointment.patient = patient appointment.practitioner = practitioner - appointment.department = department or '_Test Medical Department' + appointment.department = department or "_Test Medical Department" appointment.appointment_date = appointment_date - appointment.company = '_Test Company' + appointment.company = "_Test Company" appointment.duration = 15 if service_unit: appointment.service_unit = service_unit if invoice: - appointment.mode_of_payment = 'Cash' + appointment.mode_of_payment = "Cash" if appointment_type: appointment.appointment_type = appointment_type if procedure_template: - appointment.procedure_template = create_clinical_procedure_template().get('name') + appointment.procedure_template = create_clinical_procedure_template().get("name") if save: appointment.save(ignore_permissions=True) @@ -390,30 +468,30 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce def create_healthcare_service_items(): - if frappe.db.exists('Item', 'HLC-SI-001'): - return 'HLC-SI-001' + if frappe.db.exists("Item", "HLC-SI-001"): + return "HLC-SI-001" - item = frappe.new_doc('Item') - item.item_code = 'HLC-SI-001' - item.item_name = 'Consulting Charges' - item.item_group = 'Services' + item = frappe.new_doc("Item") + item.item_code = "HLC-SI-001" + item.item_name = "Consulting Charges" + item.item_group = "Services" item.is_stock_item = 0 - item.stock_uom = 'Nos' + item.stock_uom = "Nos" item.save() return item.name def create_clinical_procedure_template(): - if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'): - return frappe.get_doc('Clinical Procedure Template', 'Knee Surgery and Rehab') + if frappe.db.exists("Clinical Procedure Template", "Knee Surgery and Rehab"): + return frappe.get_doc("Clinical Procedure Template", "Knee Surgery and Rehab") - template = frappe.new_doc('Clinical Procedure Template') - template.template = 'Knee Surgery and Rehab' - template.item_code = 'Knee Surgery and Rehab' - template.item_group = 'Services' + template = frappe.new_doc("Clinical Procedure Template") + template.template = "Knee Surgery and Rehab" + template.item_code = "Knee Surgery and Rehab" + template.item_group = "Services" template.is_billable = 1 - template.description = 'Knee Surgery and Rehab' + template.description = "Knee Surgery and Rehab" template.rate = 50000 template.save() @@ -422,36 +500,40 @@ def create_clinical_procedure_template(): def create_appointment_type(args=None): if not args: - args = frappe.local.form_dict + args = frappe.local.form_dict - name = args.get('name') or 'Test Appointment Type wise Charge' + name = args.get("name") or "Test Appointment Type wise Charge" - if frappe.db.exists('Appointment Type', name): - return frappe.get_doc('Appointment Type', name) + if frappe.db.exists("Appointment Type", name): + return frappe.get_doc("Appointment Type", name) else: item = create_healthcare_service_items() - items = [{ - 'medical_department': args.get('medical_department') or '_Test Medical Department', - 'op_consulting_charge_item': item, - 'op_consulting_charge': 200 - }] - return frappe.get_doc({ - 'doctype': 'Appointment Type', - 'appointment_type': args.get('name') or 'Test Appointment Type wise Charge', - 'default_duration': args.get('default_duration') or 20, - 'color': args.get('color') or '#7575ff', - 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}), - 'items': args.get('items') or items - }).insert() + items = [ + { + "medical_department": args.get("medical_department") or "_Test Medical Department", + "op_consulting_charge_item": item, + "op_consulting_charge": 200, + } + ] + return frappe.get_doc( + { + "doctype": "Appointment Type", + "appointment_type": args.get("name") or "Test Appointment Type wise Charge", + "default_duration": args.get("default_duration") or 20, + "color": args.get("color") or "#7575ff", + "price_list": args.get("price_list") or frappe.db.get_value("Price List", {"selling": 1}), + "items": args.get("items") or items, + } + ).insert() def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0): - if frappe.db.exists('Healthcare Service Unit Type', f'_Test Service Unit Type {str(id)}'): - return f'_Test Service Unit Type {str(id)}' + if frappe.db.exists("Healthcare Service Unit Type", f"_Test Service Unit Type {str(id)}"): + return f"_Test Service Unit Type {str(id)}" - service_unit_type = frappe.new_doc('Healthcare Service Unit Type') - service_unit_type.service_unit_type = f'_Test Service Unit Type {str(id)}' + service_unit_type = frappe.new_doc("Healthcare Service Unit Type") + service_unit_type.service_unit_type = f"_Test Service Unit Type {str(id)}" service_unit_type.allow_appointments = allow_appointments service_unit_type.overlap_appointments = overlap_appointments service_unit_type.save(ignore_permissions=True) @@ -460,28 +542,31 @@ def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0) def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0): - if frappe.db.exists('Healthcare Service Unit', f'_Test Service Unit {str(id)}'): - return f'_Test service_unit {str(id)}' + if frappe.db.exists("Healthcare Service Unit", f"_Test Service Unit {str(id)}"): + return f"_Test service_unit {str(id)}" - service_unit = frappe.new_doc('Healthcare Service Unit') + service_unit = frappe.new_doc("Healthcare Service Unit") service_unit.is_group = 0 - service_unit.healthcare_service_unit_name= f'_Test Service Unit {str(id)}' + service_unit.healthcare_service_unit_name = f"_Test Service Unit {str(id)}" service_unit.service_unit_type = service_unit_type or create_service_unit_type(id) service_unit.service_unit_capacity = service_unit_capacity service_unit.save(ignore_permissions=True) return service_unit.name + def create_user(email=None, roles=None): if not email: - email = '{}@frappe.com'.format(frappe.utils.random_string(10)) - user = frappe.db.exists('User', email) + email = "{}@frappe.com".format(frappe.utils.random_string(10)) + user = frappe.db.exists("User", email) if not user: - user = frappe.get_doc({ - "doctype": "User", - "email": email, - "first_name": "test_user", - "password": "password", - "roles": roles, - }).insert() + user = frappe.get_doc( + { + "doctype": "User", + "email": email, + "first_name": "test_user", + "password": "password", + "roles": roles, + } + ).insert() return user diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py index fd6aac5e374..1d821a1e42e 100644 --- a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py +++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py @@ -17,17 +17,23 @@ class PatientAssessment(Document): total_score += int(entry.score) self.total_score_obtained = total_score + @frappe.whitelist() def create_patient_assessment(source_name, target_doc=None): - doc = get_mapped_doc('Therapy Session', source_name, { - 'Therapy Session': { - 'doctype': 'Patient Assessment', - 'field_map': [ - ['therapy_session', 'name'], - ['patient', 'patient'], - ['practitioner', 'practitioner'] - ] + doc = get_mapped_doc( + "Therapy Session", + source_name, + { + "Therapy Session": { + "doctype": "Patient Assessment", + "field_map": [ + ["therapy_session", "name"], + ["patient", "patient"], + ["practitioner", "practitioner"], + ], } - }, target_doc) + }, + target_doc, + ) return doc diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index b2fe2d5a589..ff804ead540 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -15,7 +15,7 @@ class PatientEncounter(Document): def on_update(self): if self.appointment: - frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') + frappe.db.set_value("Patient Appointment", self.appointment, "status", "Closed") def on_submit(self): if self.therapies: @@ -23,58 +23,59 @@ class PatientEncounter(Document): def on_cancel(self): if self.appointment: - frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open') + frappe.db.set_value("Patient Appointment", self.appointment, "status", "Open") if self.inpatient_record and self.drug_prescription: delete_ip_medication_order(self) def set_title(self): - self.title = _('{0} with {1}').format(self.patient_name or self.patient, - self.practitioner_name or self.practitioner)[:100] + self.title = _("{0} with {1}").format( + self.patient_name or self.patient, self.practitioner_name or self.practitioner + )[:100] @frappe.whitelist() @staticmethod def get_applicable_treatment_plans(encounter): - patient = frappe.get_doc('Patient', encounter['patient']) + patient = frappe.get_doc("Patient", encounter["patient"]) plan_filters = {} - plan_filters['name'] = ['in', []] + plan_filters["name"] = ["in", []] age = patient.age if age: - plan_filters['patient_age_from'] = ['<=', age.years] - plan_filters['patient_age_to'] = ['>=', age.years] + plan_filters["patient_age_from"] = ["<=", age.years] + plan_filters["patient_age_to"] = [">=", age.years] gender = patient.sex if gender: - plan_filters['gender'] = ['in', [gender, None]] + plan_filters["gender"] = ["in", [gender, None]] - diagnosis = encounter.get('diagnosis') + diagnosis = encounter.get("diagnosis") if diagnosis: - diagnosis = [_diagnosis['diagnosis'] for _diagnosis in encounter['diagnosis']] + diagnosis = [_diagnosis["diagnosis"] for _diagnosis in encounter["diagnosis"]] filters = [ - ['diagnosis', 'in', diagnosis], - ['parenttype', '=', 'Treatment Plan Template'], + ["diagnosis", "in", diagnosis], + ["parenttype", "=", "Treatment Plan Template"], ] - diagnosis = frappe.get_list('Patient Encounter Diagnosis', filters=filters, fields='*') - plan_names = [_diagnosis['parent'] for _diagnosis in diagnosis] - plan_filters['name'][1].extend(plan_names) + diagnosis = frappe.get_list("Patient Encounter Diagnosis", filters=filters, fields="*") + plan_names = [_diagnosis["parent"] for _diagnosis in diagnosis] + plan_filters["name"][1].extend(plan_names) - symptoms = encounter.get('symptoms') + symptoms = encounter.get("symptoms") if symptoms: - symptoms = [symptom['complaint'] for symptom in encounter['symptoms']] + symptoms = [symptom["complaint"] for symptom in encounter["symptoms"]] filters = [ - ['complaint', 'in', symptoms], - ['parenttype', '=', 'Treatment Plan Template'], + ["complaint", "in", symptoms], + ["parenttype", "=", "Treatment Plan Template"], ] - symptoms = frappe.get_list('Patient Encounter Symptom', filters=filters, fields='*') - plan_names = [symptom['parent'] for symptom in symptoms] - plan_filters['name'][1].extend(plan_names) + symptoms = frappe.get_list("Patient Encounter Symptom", filters=filters, fields="*") + plan_names = [symptom["parent"] for symptom in symptoms] + plan_filters["name"][1].extend(plan_names) - if not plan_filters['name'][1]: - plan_filters.pop('name') + if not plan_filters["name"][1]: + plan_filters.pop("name") - plans = frappe.get_list('Treatment Plan Template', fields='*', filters=plan_filters) + plans = frappe.get_list("Treatment Plan Template", fields="*", filters=plan_filters) return plans @@ -84,31 +85,27 @@ class PatientEncounter(Document): self.set_treatment_plan(treatment_plan) def set_treatment_plan(self, plan): - plan_items = frappe.get_list('Treatment Plan Template Item', filters={'parent': plan}, fields='*') + plan_items = frappe.get_list( + "Treatment Plan Template Item", filters={"parent": plan}, fields="*" + ) for plan_item in plan_items: self.set_treatment_plan_item(plan_item) - drugs = frappe.get_list('Drug Prescription', filters={'parent': plan}, fields='*') + drugs = frappe.get_list("Drug Prescription", filters={"parent": plan}, fields="*") for drug in drugs: - self.append('drug_prescription', drug) + self.append("drug_prescription", drug) self.save() def set_treatment_plan_item(self, plan_item): - if plan_item.type == 'Clinical Procedure Template': - self.append('procedure_prescription', { - 'procedure': plan_item.template - }) + if plan_item.type == "Clinical Procedure Template": + self.append("procedure_prescription", {"procedure": plan_item.template}) - if plan_item.type == 'Lab Test Template': - self.append('lab_test_prescription', { - 'lab_test_code': plan_item.template - }) + if plan_item.type == "Lab Test Template": + self.append("lab_test_prescription", {"lab_test_code": plan_item.template}) - if plan_item.type == 'Therapy Type': - self.append('therapies', { - 'therapy_type': plan_item.template - }) + if plan_item.type == "Therapy Type": + self.append("therapies", {"therapy_type": plan_item.template}) @frappe.whitelist() @@ -117,11 +114,11 @@ def make_ip_medication_order(source_name, target_doc=None): target.start_date = source.encounter_date for entry in source.drug_prescription: if entry.drug_code: - dosage = frappe.get_doc('Prescription Dosage', entry.dosage) + dosage = frappe.get_doc("Prescription Dosage", entry.dosage) dates = get_prescription_dates(entry.period, target.start_date) for date in dates: for dose in dosage.dosage_strength: - order = target.append('medication_orders') + order = target.append("medication_orders") order.drug = entry.drug_code order.drug_name = entry.drug_name order.dosage = dose.strength @@ -131,26 +128,32 @@ def make_ip_medication_order(source_name, target_doc=None): order.time = dose.strength_time target.end_date = dates[-1] - doc = get_mapped_doc('Patient Encounter', source_name, { - 'Patient Encounter': { - 'doctype': 'Inpatient Medication Order', - 'field_map': { - 'name': 'patient_encounter', - 'patient': 'patient', - 'patient_name': 'patient_name', - 'patient_age': 'patient_age', - 'inpatient_record': 'inpatient_record', - 'practitioner': 'practitioner', - 'start_date': 'encounter_date' + doc = get_mapped_doc( + "Patient Encounter", + source_name, + { + "Patient Encounter": { + "doctype": "Inpatient Medication Order", + "field_map": { + "name": "patient_encounter", + "patient": "patient", + "patient_name": "patient_name", + "patient_age": "patient_age", + "inpatient_record": "inpatient_record", + "practitioner": "practitioner", + "start_date": "encounter_date", }, } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return doc def get_prescription_dates(period, start_date): - prescription_duration = frappe.get_doc('Prescription Duration', period) + prescription_duration = frappe.get_doc("Prescription Duration", period) days = prescription_duration.get_days() dates = [start_date] for i in range(1, days): @@ -160,21 +163,23 @@ def get_prescription_dates(period, start_date): def create_therapy_plan(encounter): if len(encounter.therapies): - doc = frappe.new_doc('Therapy Plan') + doc = frappe.new_doc("Therapy Plan") doc.patient = encounter.patient doc.start_date = encounter.encounter_date for entry in encounter.therapies: - doc.append('therapy_plan_details', { - 'therapy_type': entry.therapy_type, - 'no_of_sessions': entry.no_of_sessions - }) + doc.append( + "therapy_plan_details", + {"therapy_type": entry.therapy_type, "no_of_sessions": entry.no_of_sessions}, + ) doc.save(ignore_permissions=True) - if doc.get('name'): - encounter.db_set('therapy_plan', doc.name) - frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True) + if doc.get("name"): + encounter.db_set("therapy_plan", doc.name) + frappe.msgprint( + _("Therapy Plan {0} created successfully.").format(frappe.bold(doc.name)), alert=True + ) def delete_ip_medication_order(encounter): - record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name}) + record = frappe.db.exists("Inpatient Medication Order", {"patient_encounter": encounter.name}) if record: - frappe.delete_doc('Inpatient Medication Order', record, force=1) + frappe.delete_doc("Inpatient Medication Order", record, force=1) diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py index 3db13be6b7d..f796e23f420 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py @@ -1,23 +1,16 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'encounter', - 'non_standard_fieldnames': { - 'Patient Medical Record': 'reference_name', - 'Inpatient Medication Order': 'patient_encounter' + "fieldname": "encounter", + "non_standard_fieldnames": { + "Patient Medical Record": "reference_name", + "Inpatient Medication Order": "patient_encounter", }, - 'transactions': [ - { - 'label': _('Records'), - 'items': ['Vital Signs', 'Patient Medical Record'] - }, - { - 'label': _('Orders'), - 'items': ['Inpatient Medication Order'] - } + "transactions": [ + {"label": _("Records"), "items": ["Vital Signs", "Patient Medical Record"]}, + {"label": _("Orders"), "items": ["Inpatient Medication Order"]}, ], - 'disable_create_buttons': ['Inpatient Medication Order'] + "disable_create_buttons": ["Inpatient Medication Order"], } diff --git a/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.py index 8260918c0d2..88f34d82dc0 100644 --- a/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.py @@ -11,75 +11,81 @@ from erpnext.healthcare.doctype.patient_encounter.patient_encounter import Patie class TestPatientEncounter(unittest.TestCase): def setUp(self): try: - gender_m = frappe.get_doc({ - 'doctype': 'Gender', - 'gender': 'MALE' - }).insert() - gender_f = frappe.get_doc({ - 'doctype': 'Gender', - 'gender': 'FEMALE' - }).insert() + gender_m = frappe.get_doc({"doctype": "Gender", "gender": "MALE"}).insert() + gender_f = frappe.get_doc({"doctype": "Gender", "gender": "FEMALE"}).insert() except frappe.exceptions.DuplicateEntryError: - gender_m = frappe.get_doc({ - 'doctype': 'Gender', - 'gender': 'MALE' - }) - gender_f = frappe.get_doc({ - 'doctype': 'Gender', - 'gender': 'FEMALE' - }) + gender_m = frappe.get_doc({"doctype": "Gender", "gender": "MALE"}) + gender_f = frappe.get_doc({"doctype": "Gender", "gender": "FEMALE"}) - self.patient_male = frappe.get_doc({ - 'doctype': 'Patient', - 'first_name': 'John', - 'sex': gender_m.gender, - }).insert() - self.patient_female = frappe.get_doc({ - 'doctype': 'Patient', - 'first_name': 'Curie', - 'sex': gender_f.gender, - }).insert() - self.practitioner = frappe.get_doc({ - 'doctype': 'Healthcare Practitioner', - 'first_name': 'Doc', - 'sex': 'MALE', - }).insert() + self.patient_male = frappe.get_doc( + { + "doctype": "Patient", + "first_name": "John", + "sex": gender_m.gender, + } + ).insert() + self.patient_female = frappe.get_doc( + { + "doctype": "Patient", + "first_name": "Curie", + "sex": gender_f.gender, + } + ).insert() + self.practitioner = frappe.get_doc( + { + "doctype": "Healthcare Practitioner", + "first_name": "Doc", + "sex": "MALE", + } + ).insert() try: - self.care_plan_male = frappe.get_doc({ - 'doctype': 'Treatment Plan Template', - 'template_name': 'test plan - m', - 'gender': gender_m.gender, - }).insert() - self.care_plan_female = frappe.get_doc({ - 'doctype': 'Treatment Plan Template', - 'template_name': 'test plan - f', - 'gender': gender_f.gender, - }).insert() + self.care_plan_male = frappe.get_doc( + { + "doctype": "Treatment Plan Template", + "template_name": "test plan - m", + "gender": gender_m.gender, + } + ).insert() + self.care_plan_female = frappe.get_doc( + { + "doctype": "Treatment Plan Template", + "template_name": "test plan - f", + "gender": gender_f.gender, + } + ).insert() except frappe.exceptions.DuplicateEntryError: - self.care_plan_male = frappe.get_doc({ - 'doctype': 'Treatment Plan Template', - 'template_name': 'test plan - m', - 'gender': gender_m.gender, - }) - self.care_plan_female = frappe.get_doc({ - 'doctype': 'Treatment Plan Template', - 'template_name': 'test plan - f', - 'gender': gender_f.gender, - }) + self.care_plan_male = frappe.get_doc( + { + "doctype": "Treatment Plan Template", + "template_name": "test plan - m", + "gender": gender_m.gender, + } + ) + self.care_plan_female = frappe.get_doc( + { + "doctype": "Treatment Plan Template", + "template_name": "test plan - f", + "gender": gender_f.gender, + } + ) def test_treatment_plan_template_filter(self): - encounter = frappe.get_doc({ - 'doctype': 'Patient Encounter', - 'patient': self.patient_male.name, - 'practitioner': self.practitioner.name, - }).insert() + encounter = frappe.get_doc( + { + "doctype": "Patient Encounter", + "patient": self.patient_male.name, + "practitioner": self.practitioner.name, + } + ).insert() plans = PatientEncounter.get_applicable_treatment_plans(encounter.as_dict()) - self.assertEqual(plans[0]['name'], self.care_plan_male.template_name) + self.assertEqual(plans[0]["name"], self.care_plan_male.template_name) - encounter = frappe.get_doc({ - 'doctype': 'Patient Encounter', - 'patient': self.patient_female.name, - 'practitioner': self.practitioner.name, - }).insert() + encounter = frappe.get_doc( + { + "doctype": "Patient Encounter", + "patient": self.patient_female.name, + "practitioner": self.practitioner.name, + } + ).insert() plans = PatientEncounter.get_applicable_treatment_plans(encounter.as_dict()) - self.assertEqual(plans[0]['name'], self.care_plan_female.template_name) + self.assertEqual(plans[0]["name"], self.care_plan_female.template_name) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 4c5d39f0335..4161f1dcaea 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -19,22 +19,29 @@ class PatientHistorySettings(Document): def validate_submittable_doctypes(self): for entry in self.custom_doctypes: - if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')): - msg = _('Row #{0}: Document Type {1} is not submittable.').format( - entry.idx, frappe.bold(entry.document_type)) - msg += _('Patient Medical Record can only be created for submittable document types.') + if not cint(frappe.db.get_value("DocType", entry.document_type, "is_submittable")): + msg = _("Row #{0}: Document Type {1} is not submittable.").format( + entry.idx, frappe.bold(entry.document_type) + ) + msg += _("Patient Medical Record can only be created for submittable document types.") frappe.throw(msg) def validate_date_fieldnames(self): for entry in self.custom_doctypes: field = frappe.get_meta(entry.document_type).get_field(entry.date_fieldname) if not field: - frappe.throw(_('Row #{0}: No such Field named {1} found in the Document Type {2}.').format( - entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) + frappe.throw( + _("Row #{0}: No such Field named {1} found in the Document Type {2}.").format( + entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type) + ) + ) - if field.fieldtype not in ['Date', 'Datetime']: - frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format( - entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) + if field.fieldtype not in ["Date", "Datetime"]: + frappe.throw( + _("Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.").format( + entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type) + ) + ) @frappe.whitelist() def get_doctype_fields(self, document_type, fields): @@ -42,40 +49,44 @@ class PatientHistorySettings(Document): doc_fields = frappe.get_meta(document_type).fields for field in doc_fields: - if field.fieldtype not in frappe.model.no_value_fields or \ - field.fieldtype in frappe.model.table_fields and not field.hidden: - multicheck_fields.append({ - 'label': field.label, - 'value': field.fieldname, - 'checked': 1 if field.fieldname in fields else 0 - }) + if ( + field.fieldtype not in frappe.model.no_value_fields + or field.fieldtype in frappe.model.table_fields + and not field.hidden + ): + multicheck_fields.append( + { + "label": field.label, + "value": field.fieldname, + "checked": 1 if field.fieldname in fields else 0, + } + ) return multicheck_fields @frappe.whitelist() def get_date_field_for_dt(self, document_type): meta = frappe.get_meta(document_type) - date_fields = meta.get('fields', { - 'fieldtype': ['in', ['Date', 'Datetime']] - }) + date_fields = meta.get("fields", {"fieldtype": ["in", ["Date", "Datetime"]]}) if date_fields: - return date_fields[0].get('fieldname') + return date_fields[0].get("fieldname") + def create_medical_record(doc, method=None): medical_record_required = validate_medical_record_required(doc) if not medical_record_required: return - if frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }): + if frappe.db.exists("Patient Medical Record", {"reference_name": doc.name}): return subject = set_subject_field(doc) date_field = get_date_field(doc.doctype) - medical_record = frappe.new_doc('Patient Medical Record') + medical_record = frappe.new_doc("Patient Medical Record") medical_record.patient = doc.patient medical_record.subject = subject - medical_record.status = 'Open' + medical_record.status = "Open" medical_record.communication_date = doc.get(date_field) medical_record.reference_doctype = doc.doctype medical_record.reference_name = doc.name @@ -88,11 +99,11 @@ def update_medical_record(doc, method=None): if not medical_record_required: return - medical_record_id = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }) + medical_record_id = frappe.db.exists("Patient Medical Record", {"reference_name": doc.name}) if medical_record_id: subject = set_subject_field(doc) - frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject) + frappe.db.set_value("Patient Medical Record", medical_record_id[0][0], "subject", subject) else: create_medical_record(doc) @@ -102,28 +113,30 @@ def delete_medical_record(doc, method=None): if not medical_record_required: return - record = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }) + record = frappe.db.exists("Patient Medical Record", {"reference_name": doc.name}) if record: - frappe.delete_doc('Patient Medical Record', record, force=1) + frappe.delete_doc("Patient Medical Record", record, force=1) def set_subject_field(doc): from frappe.utils.formatters import format_value meta = frappe.get_meta(doc.doctype) - subject = '' + subject = "" patient_history_fields = get_patient_history_fields(doc) for entry in patient_history_fields: - fieldname = entry.get('fieldname') - if entry.get('fieldtype') == 'Table' and doc.get(fieldname): - formatted_value = get_formatted_value_for_table_field(doc.get(fieldname), meta.get_field(fieldname)) - subject += frappe.bold(_(entry.get('label')) + ':') + '
' + cstr(formatted_value) + '
' + fieldname = entry.get("fieldname") + if entry.get("fieldtype") == "Table" and doc.get(fieldname): + formatted_value = get_formatted_value_for_table_field( + doc.get(fieldname), meta.get_field(fieldname) + ) + subject += frappe.bold(_(entry.get("label")) + ":") + "
" + cstr(formatted_value) + "
" else: if doc.get(fieldname): formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) - subject += frappe.bold(_(entry.get('label')) + ':') + cstr(formatted_value) + '
' + subject += frappe.bold(_(entry.get("label")) + ":") + cstr(formatted_value) + "
" return subject @@ -131,12 +144,14 @@ def set_subject_field(doc): def get_date_field(doctype): dt = get_patient_history_config_dt(doctype) - return frappe.db.get_value(dt, { 'document_type': doctype }, 'date_fieldname') + return frappe.db.get_value(dt, {"document_type": doctype}, "date_fieldname") def get_patient_history_fields(doc): dt = get_patient_history_config_dt(doc.doctype) - patient_history_fields = frappe.db.get_value(dt, { 'document_type': doc.doctype }, 'selected_fields') + patient_history_fields = frappe.db.get_value( + dt, {"document_type": doc.doctype}, "selected_fields" + ) if patient_history_fields: return json.loads(patient_history_fields) @@ -145,38 +160,44 @@ def get_patient_history_fields(doc): def get_formatted_value_for_table_field(items, df): child_meta = frappe.get_meta(df.options) - table_head = '' - table_row = '' - html = '' + table_head = "" + table_row = "" + html = "" create_head = True for item in items: - table_row += '' + table_row += "" for cdf in child_meta.fields: if cdf.in_list_view: if create_head: - table_head += '' + cdf.label + '' + table_head += "" + cdf.label + "" if item.get(cdf.fieldname): - table_row += '' + str(item.get(cdf.fieldname)) + '' + table_row += "" + str(item.get(cdf.fieldname)) + "" else: - table_row += '' + table_row += "" create_head = False - table_row += '' + table_row += "" - html += "" + table_head + table_row + "
" + html += ( + "" + table_head + table_row + "
" + ) return html def get_patient_history_config_dt(doctype): - if frappe.db.get_value('DocType', doctype, 'custom'): - return 'Patient History Custom Document Type' + if frappe.db.get_value("DocType", doctype, "custom"): + return "Patient History Custom Document Type" else: - return 'Patient History Standard Document Type' + return "Patient History Standard Document Type" def validate_medical_record_required(doc): - if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard \ - or get_module(doc) != 'Healthcare': + if ( + frappe.flags.in_patch + or frappe.flags.in_install + or frappe.flags.in_setup_wizard + or get_module(doc) != "Healthcare" + ): return False if doc.doctype not in get_patient_history_doctypes(): @@ -184,9 +205,10 @@ def validate_medical_record_required(doc): return True + def get_module(doc): module = doc.meta.module if not module: - module = frappe.db.get_value('DocType', doc.doctype, 'module') + module = frappe.db.get_value("DocType", doc.doctype, "module") return module diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py index 484955a7008..8248f5fc7ea 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -14,25 +14,20 @@ class TestPatientHistorySettings(unittest.TestCase): def setUp(self): dt = create_custom_doctype() settings = frappe.get_single("Patient History Settings") - settings.append("custom_doctypes", { - "document_type": dt.name, - "date_fieldname": "date", - "selected_fields": json.dumps([{ - "label": "Date", - "fieldname": "date", - "fieldtype": "Date" - }, + settings.append( + "custom_doctypes", { - "label": "Rating", - "fieldname": "rating", - "fieldtype": "Rating" + "document_type": dt.name, + "date_fieldname": "date", + "selected_fields": json.dumps( + [ + {"label": "Date", "fieldname": "date", "fieldtype": "Date"}, + {"label": "Rating", "fieldname": "rating", "fieldtype": "Rating"}, + {"label": "Feedback", "fieldname": "feedback", "fieldtype": "Small Text"}, + ] + ), }, - { - "label": "Feedback", - "fieldname": "feedback", - "fieldtype": "Small Text" - }]) - }) + ) settings.save() def test_custom_doctype_medical_record(self): @@ -40,12 +35,15 @@ class TestPatientHistorySettings(unittest.TestCase): patient = create_patient() doc = create_doc(patient) # check for medical record - medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name}) + medical_rec = frappe.db.exists( + "Patient Medical Record", {"status": "Open", "reference_name": doc.name} + ) self.assertTrue(medical_rec) medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) expected_subject = "Date:{0}Rating:3Feedback:Test Patient History Settings".format( - frappe.utils.format_date(getdate())) + frappe.utils.format_date(getdate()) + ) self.assertEqual(strip_html(medical_rec.subject), expected_subject) self.assertEqual(medical_rec.patient, patient) self.assertEqual(medical_rec.communication_date, getdate()) @@ -53,38 +51,22 @@ class TestPatientHistorySettings(unittest.TestCase): def create_custom_doctype(): if not frappe.db.exists("DocType", "Test Patient Feedback"): - doc = frappe.get_doc({ + doc = frappe.get_doc( + { "doctype": "DocType", "module": "Healthcare", "custom": 1, "is_submittable": 1, - "fields": [{ - "label": "Date", - "fieldname": "date", - "fieldtype": "Date" - }, - { - "label": "Patient", - "fieldname": "patient", - "fieldtype": "Link", - "options": "Patient" - }, - { - "label": "Rating", - "fieldname": "rating", - "fieldtype": "Rating" - }, - { - "label": "Feedback", - "fieldname": "feedback", - "fieldtype": "Small Text" - }], - "permissions": [{ - "role": "System Manager", - "read": 1 - }], + "fields": [ + {"label": "Date", "fieldname": "date", "fieldtype": "Date"}, + {"label": "Patient", "fieldname": "patient", "fieldtype": "Link", "options": "Patient"}, + {"label": "Rating", "fieldname": "rating", "fieldtype": "Rating"}, + {"label": "Feedback", "fieldname": "feedback", "fieldtype": "Small Text"}, + ], + "permissions": [{"role": "System Manager", "read": 1}], "name": "Test Patient Feedback", - }) + } + ) doc.insert() return doc else: @@ -92,13 +74,15 @@ def create_custom_doctype(): def create_doc(patient): - doc = frappe.get_doc({ - "doctype": "Test Patient Feedback", - "patient": patient, - "date": getdate(), - "rating": 3, - "feedback": "Test Patient History Settings" - }).insert() + doc = frappe.get_doc( + { + "doctype": "Test Patient Feedback", + "patient": patient, + "date": getdate(), + "rating": 3, + "feedback": "Test Patient History Settings", + } + ).insert() doc.submit() return doc diff --git a/erpnext/healthcare/doctype/patient_medical_record/patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/patient_medical_record.py index c243d16e575..45e4c02115e 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/patient_medical_record.py @@ -8,5 +8,5 @@ from frappe.model.document import Document class PatientMedicalRecord(Document): def after_insert(self): - if self.reference_doctype == "Patient Medical Record" : + if self.reference_doctype == "Patient Medical Record": frappe.db.set_value("Patient Medical Record", self.name, "reference_name", self.name) diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index 230011a890b..299a873e976 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -17,9 +17,9 @@ from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment imp class TestPatientMedicalRecord(unittest.TestCase): def setUp(self): - frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) - frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - frappe.db.sql('delete from `tabPatient Appointment`') + frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 0) + frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + frappe.db.sql("delete from `tabPatient Appointment`") make_pos_profile() def test_medical_record(self): @@ -29,32 +29,42 @@ class TestPatientMedicalRecord(unittest.TestCase): encounter = create_encounter(appointment) # check for encounter - medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': encounter.name}) + medical_rec = frappe.db.exists( + "Patient Medical Record", {"status": "Open", "reference_name": encounter.name} + ) self.assertTrue(medical_rec) vital_signs = create_vital_signs(appointment) # check for vital signs - medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': vital_signs.name}) + medical_rec = frappe.db.exists( + "Patient Medical Record", {"status": "Open", "reference_name": vital_signs.name} + ) self.assertTrue(medical_rec) - appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1, procedure_template=1) + appointment = create_appointment( + patient, practitioner, add_days(nowdate(), 1), invoice=1, procedure_template=1 + ) procedure = create_procedure(appointment) procedure.start_procedure() procedure.complete_procedure() # check for clinical procedure - medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': procedure.name}) + medical_rec = frappe.db.exists( + "Patient Medical Record", {"status": "Open", "reference_name": procedure.name} + ) self.assertTrue(medical_rec) template = create_lab_test_template(medical_department) lab_test = create_lab_test(template.name, patient) # check for lab test - medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': lab_test.name}) + medical_rec = frappe.db.exists( + "Patient Medical Record", {"status": "Open", "reference_name": lab_test.name} + ) self.assertTrue(medical_rec) def create_procedure(appointment): if appointment: - procedure = frappe.new_doc('Clinical Procedure') + procedure = frappe.new_doc("Clinical Procedure") procedure.procedure_template = appointment.procedure_template procedure.appointment = appointment.name procedure.patient = appointment.patient @@ -66,8 +76,9 @@ def create_procedure(appointment): procedure.submit() return procedure + def create_vital_signs(appointment): - vital_signs = frappe.new_doc('Vital Signs') + vital_signs = frappe.new_doc("Vital Signs") vital_signs.patient = appointment.patient vital_signs.signs_date = appointment.appointment_date vital_signs.signs_time = appointment.appointment_time @@ -76,24 +87,26 @@ def create_vital_signs(appointment): vital_signs.submit() return vital_signs -def create_lab_test_template(medical_department): - if frappe.db.exists('Lab Test Template', 'Blood Test'): - return frappe.get_doc('Lab Test Template', 'Blood Test') - template = frappe.new_doc('Lab Test Template') - template.lab_test_name = 'Blood Test' - template.lab_test_code = 'Blood Test' - template.lab_test_group = 'Services' +def create_lab_test_template(medical_department): + if frappe.db.exists("Lab Test Template", "Blood Test"): + return frappe.get_doc("Lab Test Template", "Blood Test") + + template = frappe.new_doc("Lab Test Template") + template.lab_test_name = "Blood Test" + template.lab_test_code = "Blood Test" + template.lab_test_group = "Services" template.department = medical_department template.is_billable = 1 template.lab_test_rate = 2000 template.save() return template + def create_lab_test(template, patient): - lab_test = frappe.new_doc('Lab Test') + lab_test = frappe.new_doc("Lab Test") lab_test.patient = patient - lab_test.patient_sex = frappe.db.get_value('Patient', patient, 'sex') + lab_test.patient_sex = frappe.db.get_value("Patient", patient, "sex") lab_test.template = template lab_test.save() lab_test.submit() diff --git a/erpnext/healthcare/doctype/prescription_duration/prescription_duration.py b/erpnext/healthcare/doctype/prescription_duration/prescription_duration.py index 4ca3b48cf07..ae464c23a79 100644 --- a/erpnext/healthcare/doctype/prescription_duration/prescription_duration.py +++ b/erpnext/healthcare/doctype/prescription_duration/prescription_duration.py @@ -8,65 +8,69 @@ from frappe.utils import cstr class PrescriptionDuration(Document): def autoname(self): - self.name = " ".join(filter(None, - [cstr(self.get(f)).strip() for f in ["number", "period"]])) + self.name = " ".join(filter(None, [cstr(self.get(f)).strip() for f in ["number", "period"]])) + def get_days(self): days = 0 duration = self - if(duration.period == 'Day'): + if duration.period == "Day": days = duration.number - if(duration.period == 'Hour'): - days = (duration.number)/24 - if(duration.period == 'Week'): - days = (duration.number*7) - if(duration.period == 'Month'): - days = (duration.number*30) + if duration.period == "Hour": + days = (duration.number) / 24 + if duration.period == "Week": + days = duration.number * 7 + if duration.period == "Month": + days = duration.number * 30 return days + def get_weeks(self): weeks = 0 duration = self - if(duration.period == 'Day'): - weeks = (duration.number)/7 - #if(duration.period == 'Hour'): - # weeks = (duration.number)/x - if(duration.period == 'Week'): + if duration.period == "Day": + weeks = (duration.number) / 7 + # if(duration.period == 'Hour'): + # weeks = (duration.number)/x + if duration.period == "Week": weeks = duration.number - if(duration.period == 'Month'): - weeks = duration.number*4 + if duration.period == "Month": + weeks = duration.number * 4 return weeks + def get_months(self): months = 0 duration = self - if(duration.period == 'Day'): - months = (duration.number)/30 - #if(duration.period == 'Hour'): - # months = (duration.number)/x - if(duration.period == 'Week'): - months = (duration.number)/4 - if(duration.period == 'Month'): + if duration.period == "Day": + months = (duration.number) / 30 + # if(duration.period == 'Hour'): + # months = (duration.number)/x + if duration.period == "Week": + months = (duration.number) / 4 + if duration.period == "Month": months = duration.number return months + def get_hours(self): hours = 0 duration = self - if(duration.period == 'Day'): - hours = (duration.number*24) - if(duration.period == 'Hour'): + if duration.period == "Day": + hours = duration.number * 24 + if duration.period == "Hour": hours = duration.number - if(duration.period == 'Week'): - hours = (duration.number*24)*7 - if(duration.period == 'Month'): - hours = (duration.number*24)*30 + if duration.period == "Week": + hours = (duration.number * 24) * 7 + if duration.period == "Month": + hours = (duration.number * 24) * 30 return hours + def get_minutes(self): minutes = 0 duration = self - if(duration.period == 'Day'): - minutes = (duration.number*1440) - if(duration.period == 'Hour'): - minutes = (duration.number*60) - if(duration.period == 'Week'): - minutes = (duration.number*10080) - if(duration.period == 'Month'): - minutes = (duration.number*43800) + if duration.period == "Day": + minutes = duration.number * 1440 + if duration.period == "Hour": + minutes = duration.number * 60 + if duration.period == "Week": + minutes = duration.number * 10080 + if duration.period == "Month": + minutes = duration.number * 43800 return minutes diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.py b/erpnext/healthcare/doctype/sample_collection/sample_collection.py index ba4ff81fdfe..b6ebc702653 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.py +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.py @@ -11,4 +11,4 @@ from frappe.utils import flt class SampleCollection(Document): def validate(self): if flt(self.sample_qty) <= 0: - frappe.throw(_('Sample Quantity cannot be negative or 0'), title=_('Invalid Quantity')) + frappe.throw(_("Sample Quantity cannot be negative or 0"), title=_("Invalid Quantity")) diff --git a/erpnext/healthcare/doctype/sample_collection/test_sample_collection.py b/erpnext/healthcare/doctype/sample_collection/test_sample_collection.py index 09bd0a5b677..61a0d009bb0 100644 --- a/erpnext/healthcare/doctype/sample_collection/test_sample_collection.py +++ b/erpnext/healthcare/doctype/sample_collection/test_sample_collection.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Sample Collection') + class TestSampleCollection(unittest.TestCase): pass diff --git a/erpnext/healthcare/doctype/sensitivity/test_sensitivity.py b/erpnext/healthcare/doctype/sensitivity/test_sensitivity.py index abf8e33f76b..726e304b7b0 100644 --- a/erpnext/healthcare/doctype/sensitivity/test_sensitivity.py +++ b/erpnext/healthcare/doctype/sensitivity/test_sensitivity.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Sensitivity') + class TestSensitivity(unittest.TestCase): pass diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py index 27fad0c7c56..098441bb0e2 100644 --- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py @@ -24,52 +24,58 @@ class TestTherapyPlan(unittest.TestCase): patient, practitioner = create_healthcare_docs() medical_department = create_medical_department() encounter = create_encounter(patient, medical_department, practitioner) - self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan)) + self.assertTrue(frappe.db.exists("Therapy Plan", encounter.therapy_plan)) def test_status(self): plan = create_therapy_plan() - self.assertEqual(plan.status, 'Not Started') + self.assertEqual(plan.status, "Not Started") - session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company') + session = make_therapy_session(plan.name, plan.patient, "Basic Rehab", "_Test Company") session.start_date = getdate() frappe.get_doc(session).submit() - self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress') + self.assertEqual(frappe.db.get_value("Therapy Plan", plan.name, "status"), "In Progress") - session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company') + session = make_therapy_session(plan.name, plan.patient, "Basic Rehab", "_Test Company") session.start_date = add_days(getdate(), 1) frappe.get_doc(session).submit() - self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') + self.assertEqual(frappe.db.get_value("Therapy Plan", plan.name, "status"), "Completed") patient, practitioner = create_healthcare_docs() appointment = create_appointment(patient, practitioner, nowdate()) - session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name) + session = make_therapy_session( + plan.name, plan.patient, "Basic Rehab", "_Test Company", appointment.name + ) session.start_date = add_days(getdate(), 2) session = frappe.get_doc(session) session.submit() - self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') + self.assertEqual( + frappe.db.get_value("Patient Appointment", appointment.name, "status"), "Closed" + ) session.cancel() - self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open') + self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "status"), "Open") def test_therapy_plan_from_template(self): patient = create_patient() template = create_therapy_plan_template() # check linked item - self.assertTrue(frappe.db.exists('Therapy Plan Template', {'linked_item': 'Complete Rehab'})) + self.assertTrue(frappe.db.exists("Therapy Plan Template", {"linked_item": "Complete Rehab"})) plan = create_therapy_plan(template) # invoice - si = make_sales_invoice(plan.name, patient, '_Test Company', template) + si = make_sales_invoice(plan.name, patient, "_Test Company", template) si.save() - therapy_plan_template_amt = frappe.db.get_value('Therapy Plan Template', template, 'total_amount') + therapy_plan_template_amt = frappe.db.get_value( + "Therapy Plan Template", template, "total_amount" + ) self.assertEqual(si.items[0].amount, therapy_plan_template_amt) def create_therapy_plan(template=None): patient = create_patient() therapy_type = create_therapy_type() - plan = frappe.new_doc('Therapy Plan') + plan = frappe.new_doc("Therapy Plan") plan.patient = patient plan.start_date = getdate() @@ -77,42 +83,36 @@ def create_therapy_plan(template=None): plan.therapy_plan_template = template plan = plan.set_therapy_details_from_template() else: - plan.append('therapy_plan_details', { - 'therapy_type': therapy_type.name, - 'no_of_sessions': 2 - }) + plan.append("therapy_plan_details", {"therapy_type": therapy_type.name, "no_of_sessions": 2}) plan.save() return plan + def create_encounter(patient, medical_department, practitioner): - encounter = frappe.new_doc('Patient Encounter') + encounter = frappe.new_doc("Patient Encounter") encounter.patient = patient encounter.practitioner = practitioner encounter.medical_department = medical_department therapy_type = create_therapy_type() - encounter.append('therapies', { - 'therapy_type': therapy_type.name, - 'no_of_sessions': 2 - }) + encounter.append("therapies", {"therapy_type": therapy_type.name, "no_of_sessions": 2}) encounter.save() encounter.submit() return encounter + def create_therapy_plan_template(): - template_name = frappe.db.exists('Therapy Plan Template', 'Complete Rehab') + template_name = frappe.db.exists("Therapy Plan Template", "Complete Rehab") if not template_name: therapy_type = create_therapy_type() - template = frappe.new_doc('Therapy Plan Template') - template.plan_name = template.item_code = template.item_name = 'Complete Rehab' - template.item_group = 'Services' - rate = frappe.db.get_value('Therapy Type', therapy_type.name, 'rate') - template.append('therapy_types', { - 'therapy_type': therapy_type.name, - 'no_of_sessions': 2, - 'rate': rate, - 'amount': 2 * flt(rate) - }) + template = frappe.new_doc("Therapy Plan Template") + template.plan_name = template.item_code = template.item_name = "Complete Rehab" + template.item_group = "Services" + rate = frappe.db.get_value("Therapy Type", therapy_type.name, "rate") + template.append( + "therapy_types", + {"therapy_type": therapy_type.name, "no_of_sessions": 2, "rate": rate, "amount": 2 * flt(rate)}, + ) template.save() template_name = template.name diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index a7bc9e90d51..44f0a9785c4 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -14,12 +14,12 @@ class TherapyPlan(Document): def set_status(self): if not self.total_sessions_completed: - self.status = 'Not Started' + self.status = "Not Started" else: if self.total_sessions_completed < self.total_sessions: - self.status = 'In Progress' + self.status = "In Progress" elif self.total_sessions_completed == self.total_sessions: - self.status = 'Completed' + self.status = "Completed" def set_totals(self): total_sessions = 0 @@ -30,28 +30,28 @@ class TherapyPlan(Document): if entry.sessions_completed: total_sessions_completed += entry.sessions_completed - self.db_set('total_sessions', total_sessions) - self.db_set('total_sessions_completed', total_sessions_completed) + self.db_set("total_sessions", total_sessions) + self.db_set("total_sessions_completed", total_sessions_completed) @frappe.whitelist() def set_therapy_details_from_template(self): # Add therapy types in the child table - self.set('therapy_plan_details', []) - therapy_plan_template = frappe.get_doc('Therapy Plan Template', self.therapy_plan_template) + self.set("therapy_plan_details", []) + therapy_plan_template = frappe.get_doc("Therapy Plan Template", self.therapy_plan_template) for data in therapy_plan_template.therapy_types: - self.append('therapy_plan_details', { - 'therapy_type': data.therapy_type, - 'no_of_sessions': data.no_of_sessions - }) + self.append( + "therapy_plan_details", + {"therapy_type": data.therapy_type, "no_of_sessions": data.no_of_sessions}, + ) return self @frappe.whitelist() def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None): - therapy_type = frappe.get_doc('Therapy Type', therapy_type) + therapy_type = frappe.get_doc("Therapy Type", therapy_type) - therapy_session = frappe.new_doc('Therapy Session') + therapy_session = frappe.new_doc("Therapy Session") therapy_session.therapy_plan = therapy_plan therapy_session.company = company therapy_session.patient = patient @@ -67,33 +67,36 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company, appointme @frappe.whitelist() def make_sales_invoice(reference_name, patient, company, therapy_plan_template): from erpnext.stock.get_item_details import get_item_details - si = frappe.new_doc('Sales Invoice') + + si = frappe.new_doc("Sales Invoice") si.company = company si.patient = patient - si.customer = frappe.db.get_value('Patient', patient, 'customer') + si.customer = frappe.db.get_value("Patient", patient, "customer") - item = frappe.db.get_value('Therapy Plan Template', therapy_plan_template, 'linked_item') - price_list, price_list_currency = frappe.db.get_values('Price List', {'selling': 1}, ['name', 'currency'])[0] + item = frappe.db.get_value("Therapy Plan Template", therapy_plan_template, "linked_item") + price_list, price_list_currency = frappe.db.get_values( + "Price List", {"selling": 1}, ["name", "currency"] + )[0] args = { - 'doctype': 'Sales Invoice', - 'item_code': item, - 'company': company, - 'customer': si.customer, - 'selling_price_list': price_list, - 'price_list_currency': price_list_currency, - 'plc_conversion_rate': 1.0, - 'conversion_rate': 1.0 + "doctype": "Sales Invoice", + "item_code": item, + "company": company, + "customer": si.customer, + "selling_price_list": price_list, + "price_list_currency": price_list_currency, + "plc_conversion_rate": 1.0, + "conversion_rate": 1.0, } - item_line = si.append('items', {}) + item_line = si.append("items", {}) item_details = get_item_details(args) item_line.item_code = item item_line.qty = 1 item_line.rate = item_details.price_list_rate item_line.amount = flt(item_line.rate) * flt(item_line.qty) - item_line.reference_dt = 'Therapy Plan' + item_line.reference_dt = "Therapy Plan" item_line.reference_dn = reference_name item_line.description = item_details.description - si.set_missing_values(for_validate = True) + si.set_missing_values(for_validate=True) return si diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py index b52a23b625a..65eb787a147 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py @@ -1,22 +1,13 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'therapy_plan', - 'non_standard_fieldnames': { - 'Sales Invoice': 'reference_dn' - }, - 'transactions': [ - { - 'label': _('Therapy Sessions'), - 'items': ['Therapy Session'] - }, - { - 'label': _('Billing'), - 'items': ['Sales Invoice'] - } + "fieldname": "therapy_plan", + "non_standard_fieldnames": {"Sales Invoice": "reference_dn"}, + "transactions": [ + {"label": _("Therapy Sessions"), "items": ["Therapy Session"]}, + {"label": _("Billing"), "items": ["Sales Invoice"]}, ], - 'disable_create_buttons': ['Sales Invoice'] + "disable_create_buttons": ["Sales Invoice"], } diff --git a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py index 96ce86252b8..d1c64685565 100644 --- a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py +++ b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template.py @@ -18,9 +18,13 @@ class TherapyPlanTemplate(Document): def on_update(self): doc_before_save = self.get_doc_before_save() - if not doc_before_save: return - if doc_before_save.item_name != self.item_name or doc_before_save.item_group != self.item_group \ - or doc_before_save.description != self.description: + if not doc_before_save: + return + if ( + doc_before_save.item_name != self.item_name + or doc_before_save.item_group != self.item_group + or doc_before_save.description != self.description + ): self.update_item() if doc_before_save.therapy_types != self.therapy_types: @@ -38,28 +42,30 @@ class TherapyPlanTemplate(Document): self.total_amount = total_amount def create_item_from_template(self): - uom = frappe.db.exists('UOM', 'Nos') or frappe.db.get_single_value('Stock Settings', 'stock_uom') + uom = frappe.db.exists("UOM", "Nos") or frappe.db.get_single_value("Stock Settings", "stock_uom") - item = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': self.item_code, - 'item_name': self.item_name, - 'item_group': self.item_group, - 'description': self.description, - 'is_sales_item': 1, - 'is_service_item': 1, - 'is_purchase_item': 0, - 'is_stock_item': 0, - 'show_in_website': 0, - 'is_pro_applicable': 0, - 'stock_uom': uom - }).insert(ignore_permissions=True, ignore_mandatory=True) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": self.item_code, + "item_name": self.item_name, + "item_group": self.item_group, + "description": self.description, + "is_sales_item": 1, + "is_service_item": 1, + "is_purchase_item": 0, + "is_stock_item": 0, + "show_in_website": 0, + "is_pro_applicable": 0, + "stock_uom": uom, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) make_item_price(item.name, self.total_amount) - self.db_set('linked_item', item.name) + self.db_set("linked_item", item.name) def update_item(self): - item_doc = frappe.get_doc('Item', {'item_code': self.linked_item}) + item_doc = frappe.get_doc("Item", {"item_code": self.linked_item}) item_doc.item_name = self.item_name item_doc.item_group = self.item_group item_doc.description = self.description @@ -67,7 +73,7 @@ class TherapyPlanTemplate(Document): item_doc.save(ignore_permissions=True) def update_item_price(self): - item_price = frappe.get_doc('Item Price', {'item_code': self.linked_item}) + item_price = frappe.get_doc("Item Price", {"item_code": self.linked_item}) item_price.item_name = self.item_name item_price.price_list_rate = self.total_amount item_price.ignore_mandatory = True diff --git a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template_dashboard.py b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template_dashboard.py index b186598d3cc..7c23ae845d3 100644 --- a/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template_dashboard.py +++ b/erpnext/healthcare/doctype/therapy_plan_template/therapy_plan_template_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'therapy_plan_template', - 'transactions': [ - { - 'label': _('Therapy Plans'), - 'items': ['Therapy Plan'] - } - ] + "fieldname": "therapy_plan_template", + "transactions": [{"label": _("Therapy Plans"), "items": ["Therapy Plan"]}], } diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py index 71eb4ced837..6cbcdcb2d82 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py @@ -22,10 +22,12 @@ class TherapySession(Document): self.set_total_counts() def validate_duplicate(self): - end_time = datetime.datetime.combine(getdate(self.start_date), get_time(self.start_time)) \ - + datetime.timedelta(minutes=flt(self.duration)) + end_time = datetime.datetime.combine( + getdate(self.start_date), get_time(self.start_time) + ) + datetime.timedelta(minutes=flt(self.duration)) - overlaps = frappe.db.sql(""" + overlaps = frappe.db.sql( + """ select name from @@ -36,28 +38,41 @@ class TherapySession(Document): ((start_time<%s and start_time + INTERVAL duration MINUTE>%s) or (start_time>%s and start_time<%s) or (start_time=%s)) - """, (self.start_date, self.name, self.practitioner, self.patient, - self.start_time, end_time.time(), self.start_time, end_time.time(), self.start_time)) + """, + ( + self.start_date, + self.name, + self.practitioner, + self.patient, + self.start_time, + end_time.time(), + self.start_time, + end_time.time(), + self.start_time, + ), + ) if overlaps: - overlapping_details = _('Therapy Session overlaps with {0}').format(get_link_to_form('Therapy Session', overlaps[0][0])) - frappe.throw(overlapping_details, title=_('Therapy Sessions Overlapping')) + overlapping_details = _("Therapy Session overlaps with {0}").format( + get_link_to_form("Therapy Session", overlaps[0][0]) + ) + frappe.throw(overlapping_details, title=_("Therapy Sessions Overlapping")) def on_submit(self): self.update_sessions_count_in_therapy_plan() def on_update(self): if self.appointment: - frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') + frappe.db.set_value("Patient Appointment", self.appointment, "status", "Closed") def on_cancel(self): if self.appointment: - frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open') + frappe.db.set_value("Patient Appointment", self.appointment, "status", "Open") self.update_sessions_count_in_therapy_plan(on_cancel=True) def update_sessions_count_in_therapy_plan(self, on_cancel=False): - therapy_plan = frappe.get_doc('Therapy Plan', self.therapy_plan) + therapy_plan = frappe.get_doc("Therapy Plan", self.therapy_plan) for entry in therapy_plan.therapy_plan_details: if entry.therapy_type == self.therapy_type: if on_cancel: @@ -75,36 +90,42 @@ class TherapySession(Document): if entry.counts_completed: counts_completed += entry.counts_completed - self.db_set('total_counts_targeted', target_total) - self.db_set('total_counts_completed', counts_completed) + self.db_set("total_counts_targeted", target_total) + self.db_set("total_counts_completed", counts_completed) @frappe.whitelist() def create_therapy_session(source_name, target_doc=None): def set_missing_values(source, target): - therapy_type = frappe.get_doc('Therapy Type', source.therapy_type) + therapy_type = frappe.get_doc("Therapy Type", source.therapy_type) target.exercises = therapy_type.exercises - doc = get_mapped_doc('Patient Appointment', source_name, { - 'Patient Appointment': { - 'doctype': 'Therapy Session', - 'field_map': [ - ['appointment', 'name'], - ['patient', 'patient'], - ['patient_age', 'patient_age'], - ['gender', 'patient_sex'], - ['therapy_type', 'therapy_type'], - ['therapy_plan', 'therapy_plan'], - ['practitioner', 'practitioner'], - ['department', 'department'], - ['start_date', 'appointment_date'], - ['start_time', 'appointment_time'], - ['service_unit', 'service_unit'], - ['company', 'company'], - ['invoiced', 'invoiced'] - ] + doc = get_mapped_doc( + "Patient Appointment", + source_name, + { + "Patient Appointment": { + "doctype": "Therapy Session", + "field_map": [ + ["appointment", "name"], + ["patient", "patient"], + ["patient_age", "patient_age"], + ["gender", "patient_sex"], + ["therapy_type", "therapy_type"], + ["therapy_plan", "therapy_plan"], + ["practitioner", "practitioner"], + ["department", "department"], + ["start_date", "appointment_date"], + ["start_time", "appointment_time"], + ["service_unit", "service_unit"], + ["company", "company"], + ["invoiced", "invoiced"], + ], } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return doc @@ -112,36 +133,42 @@ def create_therapy_session(source_name, target_doc=None): @frappe.whitelist() def invoice_therapy_session(source_name, target_doc=None): def set_missing_values(source, target): - target.customer = frappe.db.get_value('Patient', source.patient, 'customer') + target.customer = frappe.db.get_value("Patient", source.patient, "customer") target.due_date = getdate() target.debit_to = get_receivable_account(source.company) - item = target.append('items', {}) + item = target.append("items", {}) item = get_therapy_item(source, item) target.set_missing_values(for_validate=True) - doc = get_mapped_doc('Therapy Session', source_name, { - 'Therapy Session': { - 'doctype': 'Sales Invoice', - 'field_map': [ - ['patient', 'patient'], - ['referring_practitioner', 'practitioner'], - ['company', 'company'], - ['due_date', 'start_date'] - ] + doc = get_mapped_doc( + "Therapy Session", + source_name, + { + "Therapy Session": { + "doctype": "Sales Invoice", + "field_map": [ + ["patient", "patient"], + ["referring_practitioner", "practitioner"], + ["company", "company"], + ["due_date", "start_date"], + ], } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return doc def get_therapy_item(therapy, item): - item.item_code = frappe.db.get_value('Therapy Type', therapy.therapy_type, 'item') - item.description = _('Therapy Session Charges: {0}').format(therapy.practitioner) + item.item_code = frappe.db.get_value("Therapy Type", therapy.therapy_type, "item") + item.description = _("Therapy Session Charges: {0}").format(therapy.practitioner) item.income_account = get_income_account(therapy.practitioner, therapy.company) - item.cost_center = frappe.get_cached_value('Company', therapy.company, 'cost_center') + item.cost_center = frappe.get_cached_value("Company", therapy.company, "cost_center") item.rate = therapy.rate item.amount = therapy.rate item.qty = 1 - item.reference_dt = 'Therapy Session' + item.reference_dt = "Therapy Session" item.reference_dn = therapy.name return item diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py b/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py index 4a37ec44a47..9ceb0d7207e 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'therapy_session', - 'transactions': [ - { - 'label': _('Assessments'), - 'items': ['Patient Assessment'] - } - ] + "fieldname": "therapy_session", + "transactions": [{"label": _("Assessments"), "items": ["Patient Assessment"]}], } diff --git a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py index 468b424c933..e350be6493a 100644 --- a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py +++ b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py @@ -9,46 +9,44 @@ import frappe class TestTherapyType(unittest.TestCase): def test_therapy_type_item(self): therapy_type = create_therapy_type() - self.assertTrue(frappe.db.exists('Item', therapy_type.item)) + self.assertTrue(frappe.db.exists("Item", therapy_type.item)) therapy_type.disabled = 1 therapy_type.save() - self.assertEqual(frappe.db.get_value('Item', therapy_type.item, 'disabled'), 1) + self.assertEqual(frappe.db.get_value("Item", therapy_type.item, "disabled"), 1) + def create_therapy_type(): exercise = create_exercise_type() - therapy_type = frappe.db.exists('Therapy Type', 'Basic Rehab') + therapy_type = frappe.db.exists("Therapy Type", "Basic Rehab") if not therapy_type: - therapy_type = frappe.new_doc('Therapy Type') - therapy_type.therapy_type = 'Basic Rehab' + therapy_type = frappe.new_doc("Therapy Type") + therapy_type.therapy_type = "Basic Rehab" therapy_type.default_duration = 30 therapy_type.is_billable = 1 therapy_type.rate = 5000 - therapy_type.item_code = 'Basic Rehab' - therapy_type.item_name = 'Basic Rehab' - therapy_type.item_group = 'Services' - therapy_type.append('exercises', { - 'exercise_type': exercise.name, - 'counts_target': 10, - 'assistance_level': 'Passive' - }) + therapy_type.item_code = "Basic Rehab" + therapy_type.item_name = "Basic Rehab" + therapy_type.item_group = "Services" + therapy_type.append( + "exercises", + {"exercise_type": exercise.name, "counts_target": 10, "assistance_level": "Passive"}, + ) therapy_type.save() else: - therapy_type = frappe.get_doc('Therapy Type', therapy_type) + therapy_type = frappe.get_doc("Therapy Type", therapy_type) return therapy_type + def create_exercise_type(): - exercise_type = frappe.db.exists('Exercise Type', 'Sit to Stand') + exercise_type = frappe.db.exists("Exercise Type", "Sit to Stand") if not exercise_type: - exercise_type = frappe.new_doc('Exercise Type') - exercise_type.exercise_name = 'Sit to Stand' - exercise_type.append('steps_table', { - 'title': 'Step 1', - 'description': 'Squat and Rise' - }) + exercise_type = frappe.new_doc("Exercise Type") + exercise_type.exercise_name = "Sit to Stand" + exercise_type.append("steps_table", {"title": "Step 1", "description": "Squat and Rise"}) exercise_type.save() else: - exercise_type = frappe.get_doc('Exercise Type', exercise_type) + exercise_type = frappe.get_doc("Exercise Type", exercise_type) return exercise_type diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.py b/erpnext/healthcare/doctype/therapy_type/therapy_type.py index 3f65352d5cd..64e51960072 100644 --- a/erpnext/healthcare/doctype/therapy_type/therapy_type.py +++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.py @@ -25,13 +25,13 @@ class TherapyType(Document): def enable_disable_item(self): if self.is_billable: if self.disabled: - frappe.db.set_value('Item', self.item, 'disabled', 1) + frappe.db.set_value("Item", self.item, "disabled", 1) else: - frappe.db.set_value('Item', self.item, 'disabled', 0) + frappe.db.set_value("Item", self.item, "disabled", 0) def update_item_and_item_price(self): if self.is_billable and self.item: - item_doc = frappe.get_doc('Item', {'item_code': self.item}) + item_doc = frappe.get_doc("Item", {"item_code": self.item}) item_doc.item_name = self.item_name item_doc.item_group = self.item_group item_doc.description = self.description @@ -40,23 +40,28 @@ class TherapyType(Document): item_doc.save(ignore_permissions=True) if self.rate: - item_price = frappe.get_doc('Item Price', {'item_code': self.item}) + item_price = frappe.get_doc("Item Price", {"item_code": self.item}) item_price.item_name = self.item_name item_price.price_list_rate = self.rate item_price.ignore_mandatory = True item_price.save() elif not self.is_billable and self.item: - frappe.db.set_value('Item', self.item, 'disabled', 1) + frappe.db.set_value("Item", self.item, "disabled", 1) - self.db_set('change_in_item', 0) + self.db_set("change_in_item", 0) @frappe.whitelist() def add_exercises(self): exercises = self.get_exercises_for_body_parts() - last_idx = max([cint(d.idx) for d in self.get('exercises')] or [0,]) + last_idx = max( + [cint(d.idx) for d in self.get("exercises")] + or [ + 0, + ] + ) for i, d in enumerate(exercises): - ch = self.append('exercises', {}) + ch = self.append("exercises", {}) ch.exercise_type = d.parent ch.idx = last_idx + i + 1 @@ -71,7 +76,10 @@ class TherapyType(Document): `tabExercise Type` e, `tabBody Part Link` b WHERE b.body_part IN %(body_parts)s AND b.parent=e.name - """, {'body_parts': body_parts}, as_dict=1) + """, + {"body_parts": body_parts}, + as_dict=1, + ) return exercises @@ -81,44 +89,49 @@ def create_item_from_therapy(doc): if doc.is_billable and not doc.disabled: disabled = 0 - uom = frappe.db.exists('UOM', 'Unit') or frappe.db.get_single_value('Stock Settings', 'stock_uom') + uom = frappe.db.exists("UOM", "Unit") or frappe.db.get_single_value("Stock Settings", "stock_uom") - item = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': doc.item_code, - 'item_name': doc.item_name, - 'item_group': doc.item_group, - 'description': doc.description, - 'is_sales_item': 1, - 'is_service_item': 1, - 'is_purchase_item': 0, - 'is_stock_item': 0, - 'show_in_website': 0, - 'is_pro_applicable': 0, - 'disabled': disabled, - 'stock_uom': uom - }).insert(ignore_permissions=True, ignore_mandatory=True) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": doc.item_code, + "item_name": doc.item_name, + "item_group": doc.item_group, + "description": doc.description, + "is_sales_item": 1, + "is_service_item": 1, + "is_purchase_item": 0, + "is_stock_item": 0, + "show_in_website": 0, + "is_pro_applicable": 0, + "disabled": disabled, + "stock_uom": uom, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) make_item_price(item.name, doc.rate) - doc.db_set('item', item.name) + doc.db_set("item", item.name) def make_item_price(item, item_price): - price_list_name = frappe.db.get_value('Price List', {'selling': 1}) - frappe.get_doc({ - 'doctype': 'Item Price', - 'price_list': price_list_name, - 'item_code': item, - 'price_list_rate': item_price - }).insert(ignore_permissions=True, ignore_mandatory=True) + price_list_name = frappe.db.get_value("Price List", {"selling": 1}) + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list_name, + "item_code": item, + "price_list_rate": item_price, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + @frappe.whitelist() def change_item_code_from_therapy(item_code, doc): doc = frappe._dict(json.loads(doc)) - if frappe.db.exists('Item', {'item_code': item_code}): - frappe.throw(_('Item with Item Code {0} already exists').format(item_code)) + if frappe.db.exists("Item", {"item_code": item_code}): + frappe.throw(_("Item with Item Code {0} already exists").format(item_code)) else: - rename_doc('Item', doc.item, item_code, ignore_permissions=True) - frappe.db.set_value('Therapy Type', doc.name, 'item_code', item_code) + rename_doc("Item", doc.item, item_code, ignore_permissions=True) + frappe.db.set_value("Therapy Type", doc.name, "item_code", item_code) return diff --git a/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.py b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.py index dbe0e9ae5f4..3283178a597 100644 --- a/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.py +++ b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.py @@ -12,9 +12,8 @@ class TreatmentPlanTemplate(Document): def validate_age(self): if self.patient_age_from and self.patient_age_from < 0: - frappe.throw(_('Patient Age From cannot be less than 0')) + frappe.throw(_("Patient Age From cannot be less than 0")) if self.patient_age_to and self.patient_age_to < 0: - frappe.throw(_('Patient Age To cannot be less than 0')) - if self.patient_age_to and self.patient_age_from and \ - self.patient_age_to < self.patient_age_from: - frappe.throw(_('Patient Age To cannot be less than Patient Age From')) + frappe.throw(_("Patient Age To cannot be less than 0")) + if self.patient_age_to and self.patient_age_from and self.patient_age_to < self.patient_age_from: + frappe.throw(_("Patient Age To cannot be less than Patient Age From")) diff --git a/erpnext/healthcare/doctype/vital_signs/test_vital_signs.py b/erpnext/healthcare/doctype/vital_signs/test_vital_signs.py index f2f58c2b58f..bd79fb6e5a7 100644 --- a/erpnext/healthcare/doctype/vital_signs/test_vital_signs.py +++ b/erpnext/healthcare/doctype/vital_signs/test_vital_signs.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Vital Signs') + class TestVitalSigns(unittest.TestCase): pass diff --git a/erpnext/healthcare/doctype/vital_signs/vital_signs.json b/erpnext/healthcare/doctype/vital_signs/vital_signs.json index a945032c7e0..6d6c351ec35 100644 --- a/erpnext/healthcare/doctype/vital_signs/vital_signs.json +++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.json @@ -72,7 +72,6 @@ "fieldtype": "Link", "in_filter": 1, "label": "Patient Appointment", - "no_copy": 1, "options": "Patient Appointment", "print_hide": 1, "read_only": 1 @@ -82,7 +81,6 @@ "fieldtype": "Link", "in_filter": 1, "label": "Patient Encounter", - "no_copy": 1, "options": "Patient Encounter", "print_hide": 1, "read_only": 1 @@ -258,7 +256,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2022-01-20 12:30:07.515185", + "modified": "2022-02-19 11:48:16.347334", "modified_by": "Administrator", "module": "Healthcare", "name": "Vital Signs", diff --git a/erpnext/healthcare/doctype/vital_signs/vital_signs.py b/erpnext/healthcare/doctype/vital_signs/vital_signs.py index ba54427f7b4..d3a3cb57334 100644 --- a/erpnext/healthcare/doctype/vital_signs/vital_signs.py +++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.py @@ -12,5 +12,6 @@ class VitalSigns(Document): self.set_title() def set_title(self): - self.title = _('{0} on {1}').format(self.patient_name or self.patient, - frappe.utils.format_date(self.signs_date))[:100] + self.title = _("{0} on {1}").format( + self.patient_name or self.patient, frappe.utils.format_date(self.signs_date) + )[:100] diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py index f5cf736495e..8829daed4e7 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.py +++ b/erpnext/healthcare/page/patient_history/patient_history.py @@ -13,30 +13,30 @@ def get_feed(name, document_types=None, date_range=None, start=0, page_length=20 """get feed""" filters = get_filters(name, document_types, date_range) - result = frappe.db.get_all('Patient Medical Record', - fields=['name', 'owner', 'communication_date', - 'reference_doctype', 'reference_name', 'subject'], + result = frappe.db.get_all( + "Patient Medical Record", + fields=["name", "owner", "communication_date", "reference_doctype", "reference_name", "subject"], filters=filters, - order_by='communication_date DESC', + order_by="communication_date DESC", limit=cint(page_length), - start=cint(start) + start=cint(start), ) return result def get_filters(name, document_types=None, date_range=None): - filters = {'patient': name} + filters = {"patient": name} if document_types: document_types = json.loads(document_types) if len(document_types): - filters['reference_doctype'] = ['IN', document_types] + filters["reference_doctype"] = ["IN", document_types] if date_range: try: date_range = json.loads(date_range) if date_range: - filters['communication_date'] = ['between', [date_range[0], date_range[1]]] + filters["communication_date"] = ["between", [date_range[0], date_range[1]]] except json.decoder.JSONDecodeError: pass @@ -46,14 +46,11 @@ def get_filters(name, document_types=None, date_range=None): @frappe.whitelist() def get_feed_for_dt(doctype, docname): """get feed""" - result = frappe.db.get_all('Patient Medical Record', - fields=['name', 'owner', 'communication_date', - 'reference_doctype', 'reference_name', 'subject'], - filters={ - 'reference_doctype': doctype, - 'reference_name': docname - }, - order_by='communication_date DESC' + result = frappe.db.get_all( + "Patient Medical Record", + fields=["name", "owner", "communication_date", "reference_doctype", "reference_name", "subject"], + filters={"reference_doctype": doctype, "reference_name": docname}, + order_by="communication_date DESC", ) return result diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.py b/erpnext/healthcare/page/patient_progress/patient_progress.py index c17f10574a9..8d27c86a060 100644 --- a/erpnext/healthcare/page/patient_progress/patient_progress.py +++ b/erpnext/healthcare/page/patient_progress/patient_progress.py @@ -8,27 +8,21 @@ from frappe.utils import get_timespan_date_range, getdate @frappe.whitelist() def get_therapy_sessions_count(patient): - total = frappe.db.count('Therapy Session', filters={ - 'docstatus': 1, - 'patient': patient - }) + total = frappe.db.count("Therapy Session", filters={"docstatus": 1, "patient": patient}) month_start = datetime.today().replace(day=1) - this_month = frappe.db.count('Therapy Session', filters={ - 'creation': ['>', month_start], - 'docstatus': 1, - 'patient': patient - }) + this_month = frappe.db.count( + "Therapy Session", filters={"creation": [">", month_start], "docstatus": 1, "patient": patient} + ) - return { - 'total_therapy_sessions': total, - 'therapy_sessions_this_month': this_month - } + return {"total_therapy_sessions": total, "therapy_sessions_this_month": this_month} @frappe.whitelist() def get_patient_heatmap_data(patient, date): - return dict(frappe.db.sql(""" + return dict( + frappe.db.sql( + """ SELECT unix_timestamp(communication_date), count(*) FROM @@ -38,50 +32,56 @@ def get_patient_heatmap_data(patient, date): communication_date < subdate(%(date)s, interval -1 year) and patient = %(patient)s GROUP BY communication_date - ORDER BY communication_date asc""", {'date': date, 'patient': patient})) + ORDER BY communication_date asc""", + {"date": date, "patient": patient}, + ) + ) @frappe.whitelist() def get_therapy_sessions_distribution_data(patient, field): - if field == 'therapy_type': - result = frappe.db.get_all('Therapy Session', - filters = {'patient': patient, 'docstatus': 1}, - group_by = field, - order_by = field, - fields = [field, 'count(*)'], - as_list = True) + if field == "therapy_type": + result = frappe.db.get_all( + "Therapy Session", + filters={"patient": patient, "docstatus": 1}, + group_by=field, + order_by=field, + fields=[field, "count(*)"], + as_list=True, + ) - elif field == 'exercise_type': - data = frappe.db.get_all('Therapy Session', filters={ - 'docstatus': 1, - 'patient': patient - }, as_list=True) + elif field == "exercise_type": + data = frappe.db.get_all( + "Therapy Session", filters={"docstatus": 1, "patient": patient}, as_list=True + ) therapy_sessions = [entry[0] for entry in data] - result = frappe.db.get_all('Exercise', - filters = { - 'parenttype': 'Therapy Session', - 'parent': ['in', therapy_sessions], - 'docstatus': 1 - }, - group_by = field, - order_by = field, - fields = [field, 'count(*)'], - as_list = True) + result = frappe.db.get_all( + "Exercise", + filters={"parenttype": "Therapy Session", "parent": ["in", therapy_sessions], "docstatus": 1}, + group_by=field, + order_by=field, + fields=[field, "count(*)"], + as_list=True, + ) return { - 'labels': [r[0] for r in result if r[0] != None], - 'datasets': [{ - 'values': [r[1] for r in result] - }] + "labels": [r[0] for r in result if r[0] != None], + "datasets": [{"values": [r[1] for r in result]}], } @frappe.whitelist() def get_therapy_progress_data(patient, therapy_type, time_span): date_range = get_date_range(time_span) - query_values = {'from_date': date_range[0], 'to_date': date_range[1], 'therapy_type': therapy_type, 'patient': patient} - result = frappe.db.sql(""" + query_values = { + "from_date": date_range[0], + "to_date": date_range[1], + "therapy_type": therapy_type, + "patient": patient, + } + result = frappe.db.sql( + """ SELECT start_date, total_counts_targeted, total_counts_completed FROM @@ -91,21 +91,31 @@ def get_therapy_progress_data(patient, therapy_type, time_span): docstatus = 1 and therapy_type = %(therapy_type)s and patient = %(patient)s - ORDER BY start_date""", query_values, as_list=1) + ORDER BY start_date""", + query_values, + as_list=1, + ) return { - 'labels': [r[0] for r in result if r[0] != None], - 'datasets': [ - { 'name': _('Targetted'), 'values': [r[1] for r in result if r[0] != None] }, - { 'name': _('Completed'), 'values': [r[2] for r in result if r[0] != None] } - ] + "labels": [r[0] for r in result if r[0] != None], + "datasets": [ + {"name": _("Targetted"), "values": [r[1] for r in result if r[0] != None]}, + {"name": _("Completed"), "values": [r[2] for r in result if r[0] != None]}, + ], } + @frappe.whitelist() def get_patient_assessment_data(patient, assessment_template, time_span): date_range = get_date_range(time_span) - query_values = {'from_date': date_range[0], 'to_date': date_range[1], 'assessment_template': assessment_template, 'patient': patient} - result = frappe.db.sql(""" + query_values = { + "from_date": date_range[0], + "to_date": date_range[1], + "assessment_template": assessment_template, + "patient": patient, + } + result = frappe.db.sql( + """ SELECT assessment_datetime, total_score, total_score_obtained FROM @@ -115,21 +125,29 @@ def get_patient_assessment_data(patient, assessment_template, time_span): docstatus = 1 and assessment_template = %(assessment_template)s and patient = %(patient)s - ORDER BY assessment_datetime""", query_values, as_list=1) + ORDER BY assessment_datetime""", + query_values, + as_list=1, + ) return { - 'labels': [getdate(r[0]) for r in result if r[0] != None], - 'datasets': [ - { 'name': _('Score Obtained'), 'values': [r[2] for r in result if r[0] != None] } - ], - 'max_score': result[0][1] if result else None + "labels": [getdate(r[0]) for r in result if r[0] != None], + "datasets": [{"name": _("Score Obtained"), "values": [r[2] for r in result if r[0] != None]}], + "max_score": result[0][1] if result else None, } + @frappe.whitelist() def get_therapy_assessment_correlation_data(patient, assessment_template, time_span): date_range = get_date_range(time_span) - query_values = {'from_date': date_range[0], 'to_date': date_range[1], 'assessment': assessment_template, 'patient': patient} - result = frappe.db.sql(""" + query_values = { + "from_date": date_range[0], + "to_date": date_range[1], + "assessment": assessment_template, + "patient": patient, + } + result = frappe.db.sql( + """ SELECT therapy.therapy_type, count(*), avg(assessment.total_score_obtained), total_score FROM @@ -142,22 +160,36 @@ def get_therapy_assessment_correlation_data(patient, assessment_template, time_s assessment.patient = %(patient)s and assessment.assessment_template = %(assessment)s GROUP BY therapy.therapy_type - """, query_values, as_list=1) + """, + query_values, + as_list=1, + ) return { - 'labels': [r[0] for r in result if r[0] != None], - 'datasets': [ - { 'name': _('Sessions'), 'chartType': 'bar', 'values': [r[1] for r in result if r[0] != None] }, - { 'name': _('Average Score'), 'chartType': 'line', 'values': [round(r[2], 2) for r in result if r[0] != None] } + "labels": [r[0] for r in result if r[0] != None], + "datasets": [ + {"name": _("Sessions"), "chartType": "bar", "values": [r[1] for r in result if r[0] != None]}, + { + "name": _("Average Score"), + "chartType": "line", + "values": [round(r[2], 2) for r in result if r[0] != None], + }, ], - 'max_score': result[0][1] if result else None + "max_score": result[0][1] if result else None, } + @frappe.whitelist() def get_assessment_parameter_data(patient, parameter, time_span): date_range = get_date_range(time_span) - query_values = {'from_date': date_range[0], 'to_date': date_range[1], 'parameter': parameter, 'patient': patient} - results = frappe.db.sql(""" + query_values = { + "from_date": date_range[0], + "to_date": date_range[1], + "parameter": parameter, + "patient": patient, + } + results = frappe.db.sql( + """ SELECT assessment.assessment_datetime, sheet.score, @@ -175,7 +207,10 @@ def get_assessment_parameter_data(patient, parameter, time_span): assessment.patient = %(patient)s ORDER BY assessment.assessment_datetime asc - """, query_values, as_list=1) + """, + query_values, + as_list=1, + ) score_percentages = [] for r in results: @@ -184,12 +219,11 @@ def get_assessment_parameter_data(patient, parameter, time_span): score_percentages.append(score) return { - 'labels': [getdate(r[0]) for r in results if r[0] != None], - 'datasets': [ - { 'name': _('Score'), 'values': score_percentages } - ] + "labels": [getdate(r[0]) for r in results if r[0] != None], + "datasets": [{"name": _("Score"), "values": score_percentages}], } + def get_date_range(time_span): try: time_span = json.loads(time_span) diff --git a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py index 99941997e98..20cc606e9c7 100644 --- a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py +++ b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py @@ -16,6 +16,7 @@ def execute(filters=None): return columns, data, None, chart + def get_columns(): return [ { @@ -23,87 +24,69 @@ def get_columns(): "fieldtype": "Link", "label": "Patient", "options": "Patient", - "width": 200 + "width": 200, }, { "fieldname": "healthcare_service_unit", "fieldtype": "Link", "label": "Healthcare Service Unit", "options": "Healthcare Service Unit", - "width": 150 + "width": 150, }, { "fieldname": "drug", "fieldtype": "Link", "label": "Drug Code", "options": "Item", - "width": 150 - }, - { - "fieldname": "drug_name", - "fieldtype": "Data", - "label": "Drug Name", - "width": 150 + "width": 150, }, + {"fieldname": "drug_name", "fieldtype": "Data", "label": "Drug Name", "width": 150}, { "fieldname": "dosage", "fieldtype": "Link", "label": "Dosage", "options": "Prescription Dosage", - "width": 80 + "width": 80, }, { "fieldname": "dosage_form", "fieldtype": "Link", "label": "Dosage Form", "options": "Dosage Form", - "width": 100 - }, - { - "fieldname": "date", - "fieldtype": "Date", - "label": "Date", - "width": 100 - }, - { - "fieldname": "time", - "fieldtype": "Time", - "label": "Time", - "width": 100 - }, - { - "fieldname": "is_completed", - "fieldtype": "Check", - "label": "Is Order Completed", - "width": 100 + "width": 100, }, + {"fieldname": "date", "fieldtype": "Date", "label": "Date", "width": 100}, + {"fieldname": "time", "fieldtype": "Time", "label": "Time", "width": 100}, + {"fieldname": "is_completed", "fieldtype": "Check", "label": "Is Order Completed", "width": 100}, { "fieldname": "healthcare_practitioner", "fieldtype": "Link", "label": "Healthcare Practitioner", "options": "Healthcare Practitioner", - "width": 200 + "width": 200, }, { "fieldname": "inpatient_medication_entry", "fieldtype": "Link", "label": "Inpatient Medication Entry", "options": "Inpatient Medication Entry", - "width": 200 + "width": 200, }, { "fieldname": "inpatient_record", "fieldtype": "Link", "label": "Inpatient Record", "options": "Inpatient Record", - "width": 200 - } + "width": 200, + }, ] + def get_data(filters): conditions, values = get_conditions(filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT parent.patient, parent.inpatient_record, parent.practitioner, child.drug, child.drug_name, child.dosage, child.dosage_form, @@ -115,12 +98,18 @@ def get_data(filters): parent.docstatus = 1 {conditions} ORDER BY date, time - """.format(conditions=conditions), values, as_dict=1) + """.format( + conditions=conditions + ), + values, + as_dict=1, + ) data = get_inpatient_details(data, filters.get("service_unit")) return data + def get_conditions(filters): conditions = "" values = dict() @@ -152,7 +141,9 @@ def get_inpatient_details(data, service_unit): if entry.is_completed: entry["inpatient_medication_entry"] = get_inpatient_medication_entry(entry.name) - if service_unit and entry.healthcare_service_unit and service_unit != entry.healthcare_service_unit: + if ( + service_unit and entry.healthcare_service_unit and service_unit != entry.healthcare_service_unit + ): service_unit_filtered_data.append(entry) entry.pop("name", None) @@ -162,8 +153,12 @@ def get_inpatient_details(data, service_unit): return data + def get_inpatient_medication_entry(order_entry): - return frappe.db.get_value("Inpatient Medication Entry Detail", {"against_imoe": order_entry}, "parent") + return frappe.db.get_value( + "Inpatient Medication Entry Detail", {"against_imoe": order_entry}, "parent" + ) + def get_chart_data(data): if not data: @@ -172,10 +167,7 @@ def get_chart_data(data): labels = ["Pending", "Completed"] datasets = [] - status_wise_data = { - "Pending": 0, - "Completed": 0 - } + status_wise_data = {"Pending": 0, "Completed": 0} for d in data: if d.is_completed: @@ -183,19 +175,14 @@ def get_chart_data(data): else: status_wise_data["Pending"] += 1 - datasets.append({ - "name": "Inpatient Medication Order Status", - "values": [status_wise_data.get("Pending"), status_wise_data.get("Completed")] - }) + datasets.append( + { + "name": "Inpatient Medication Order Status", + "values": [status_wise_data.get("Pending"), status_wise_data.get("Completed")], + } + ) - chart = { - "data": { - "labels": labels, - "datasets": datasets - }, - "type": "donut", - "height": 300 - } + chart = {"data": {"labels": labels, "datasets": datasets}, "type": "donut", "height": 300} chart["fieldtype"] = "Data" diff --git a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py index 86eb7f7f852..12d81bb6659 100644 --- a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py +++ b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py @@ -38,92 +38,94 @@ class TestInpatientMedicationOrders(unittest.TestCase): def test_inpatient_medication_orders_report(self): filters = { - 'company': '_Test Company', - 'from_date': getdate(), - 'to_date': getdate(), - 'patient': '_Test IPD Patient', - 'service_unit': '_Test Service Unit Ip Occupancy - _TC' + "company": "_Test Company", + "from_date": getdate(), + "to_date": getdate(), + "patient": "_Test IPD Patient", + "service_unit": "_Test Service Unit Ip Occupancy - _TC", } report = execute(filters) expected_data = [ { - 'patient': '_Test IPD Patient', - 'inpatient_record': self.ip_record.name, - 'practitioner': None, - 'drug': 'Dextromethorphan', - 'drug_name': 'Dextromethorphan', - 'dosage': 1.0, - 'dosage_form': 'Tablet', - 'date': getdate(), - 'time': datetime.timedelta(seconds=32400), - 'is_completed': 0, - 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' + "patient": "_Test IPD Patient", + "inpatient_record": self.ip_record.name, + "practitioner": None, + "drug": "Dextromethorphan", + "drug_name": "Dextromethorphan", + "dosage": 1.0, + "dosage_form": "Tablet", + "date": getdate(), + "time": datetime.timedelta(seconds=32400), + "is_completed": 0, + "healthcare_service_unit": "_Test Service Unit Ip Occupancy - _TC", }, { - 'patient': '_Test IPD Patient', - 'inpatient_record': self.ip_record.name, - 'practitioner': None, - 'drug': 'Dextromethorphan', - 'drug_name': 'Dextromethorphan', - 'dosage': 1.0, - 'dosage_form': 'Tablet', - 'date': getdate(), - 'time': datetime.timedelta(seconds=50400), - 'is_completed': 0, - 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' + "patient": "_Test IPD Patient", + "inpatient_record": self.ip_record.name, + "practitioner": None, + "drug": "Dextromethorphan", + "drug_name": "Dextromethorphan", + "dosage": 1.0, + "dosage_form": "Tablet", + "date": getdate(), + "time": datetime.timedelta(seconds=50400), + "is_completed": 0, + "healthcare_service_unit": "_Test Service Unit Ip Occupancy - _TC", }, { - 'patient': '_Test IPD Patient', - 'inpatient_record': self.ip_record.name, - 'practitioner': None, - 'drug': 'Dextromethorphan', - 'drug_name': 'Dextromethorphan', - 'dosage': 1.0, - 'dosage_form': 'Tablet', - 'date': getdate(), - 'time': datetime.timedelta(seconds=75600), - 'is_completed': 0, - 'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC' - } + "patient": "_Test IPD Patient", + "inpatient_record": self.ip_record.name, + "practitioner": None, + "drug": "Dextromethorphan", + "drug_name": "Dextromethorphan", + "dosage": 1.0, + "dosage_form": "Tablet", + "date": getdate(), + "time": datetime.timedelta(seconds=75600), + "is_completed": 0, + "healthcare_service_unit": "_Test Service Unit Ip Occupancy - _TC", + }, ] self.assertEqual(expected_data, report[1]) - filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='') + filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time="", to_time="") ipme = create_ipme(filters) ipme.submit() filters = { - 'company': '_Test Company', - 'from_date': getdate(), - 'to_date': getdate(), - 'patient': '_Test IPD Patient', - 'service_unit': '_Test Service Unit Ip Occupancy - _TC', - 'show_completed_orders': 0 + "company": "_Test Company", + "from_date": getdate(), + "to_date": getdate(), + "patient": "_Test IPD Patient", + "service_unit": "_Test Service Unit Ip Occupancy - _TC", + "show_completed_orders": 0, } report = execute(filters) self.assertEqual(len(report[1]), 0) def tearDown(self): - if frappe.db.get_value('Patient', self.patient, 'inpatient_record'): + if frappe.db.get_value("Patient", self.patient, "inpatient_record"): # cleanup - Discharge - schedule_discharge(frappe.as_json({'patient': self.patient, 'discharge_ordered_datetime': now_datetime()})) + schedule_discharge( + frappe.as_json({"patient": self.patient, "discharge_ordered_datetime": now_datetime()}) + ) self.ip_record.reload() mark_invoiced_inpatient_occupancy(self.ip_record) self.ip_record.reload() discharge_patient(self.ip_record, now_datetime()) - for entry in frappe.get_all('Inpatient Medication Entry'): - doc = frappe.get_doc('Inpatient Medication Entry', entry.name) + for entry in frappe.get_all("Inpatient Medication Entry"): + doc = frappe.get_doc("Inpatient Medication Entry", entry.name) doc.cancel() doc.delete() - for entry in frappe.get_all('Inpatient Medication Order'): - doc = frappe.get_doc('Inpatient Medication Order', entry.name) + for entry in frappe.get_all("Inpatient Medication Order"): + doc = frappe.get_doc("Inpatient Medication Order", entry.name) doc.cancel() doc.delete() @@ -136,7 +138,7 @@ def create_records(patient): ip_record.expected_length_of_stay = 0 ip_record.save() ip_record.reload() - service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit("_Test Service Unit Ip Occupancy") admit_patient(ip_record, service_unit, now_datetime()) ipmo = create_ipmo(patient) diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.py b/erpnext/healthcare/report/lab_test_report/lab_test_report.py index f51c713aeba..0d149201fa2 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.py +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.py @@ -7,7 +7,8 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} data, columns = [], [] @@ -15,24 +16,26 @@ def execute(filters=None): lab_test_list = get_lab_tests(filters) if not lab_test_list: - msgprint(_('No records found')) + msgprint(_("No records found")) return columns, lab_test_list data = [] for lab_test in lab_test_list: - row = frappe._dict({ - 'test': lab_test.name, - 'template': lab_test.template, - 'company': lab_test.company, - 'patient': lab_test.patient, - 'patient_name': lab_test.patient_name, - 'practitioner': lab_test.practitioner, - 'employee': lab_test.employee, - 'status': lab_test.status, - 'invoiced': lab_test.invoiced, - 'result_date': lab_test.result_date, - 'department': lab_test.department - }) + row = frappe._dict( + { + "test": lab_test.name, + "template": lab_test.template, + "company": lab_test.company, + "patient": lab_test.patient, + "patient_name": lab_test.patient_name, + "practitioner": lab_test.practitioner, + "employee": lab_test.employee, + "status": lab_test.status, + "invoiced": lab_test.invoiced, + "result_date": lab_test.result_date, + "department": lab_test.department, + } + ) data.append(row) chart = get_chart_data(data) @@ -43,99 +46,91 @@ def execute(filters=None): def get_columns(): return [ { - 'fieldname': 'test', - 'label': _('Lab Test'), - 'fieldtype': 'Link', - 'options': 'Lab Test', - 'width': '120' + "fieldname": "test", + "label": _("Lab Test"), + "fieldtype": "Link", + "options": "Lab Test", + "width": "120", }, { - 'fieldname': 'template', - 'label': _('Lab Test Template'), - 'fieldtype': 'Link', - 'options': 'Lab Test Template', - 'width': '120' + "fieldname": "template", + "label": _("Lab Test Template"), + "fieldtype": "Link", + "options": "Lab Test Template", + "width": "120", }, { - 'fieldname': 'company', - 'label': _('Company'), - 'fieldtype': 'Link', - 'options': 'Company', - 'width': '120' + "fieldname": "company", + "label": _("Company"), + "fieldtype": "Link", + "options": "Company", + "width": "120", }, { - 'fieldname': 'patient', - 'label': _('Patient'), - 'fieldtype': 'Link', - 'options': 'Patient', - 'width': '120' + "fieldname": "patient", + "label": _("Patient"), + "fieldtype": "Link", + "options": "Patient", + "width": "120", + }, + {"fieldname": "patient_name", "label": _("Patient Name"), "fieldtype": "Data", "width": "120"}, + { + "fieldname": "employee", + "label": _("Lab Technician"), + "fieldtype": "Link", + "options": "Employee", + "width": "120", + }, + {"fieldname": "status", "label": _("Status"), "fieldtype": "Data", "width": "100"}, + {"fieldname": "invoiced", "label": _("Invoiced"), "fieldtype": "Check", "width": "100"}, + {"fieldname": "result_date", "label": _("Result Date"), "fieldtype": "Date", "width": "100"}, + { + "fieldname": "practitioner", + "label": _("Requesting Practitioner"), + "fieldtype": "Link", + "options": "Healthcare Practitioner", + "width": "120", }, { - 'fieldname': 'patient_name', - 'label': _('Patient Name'), - 'fieldtype': 'Data', - 'width': '120' + "fieldname": "department", + "label": _("Medical Department"), + "fieldtype": "Link", + "options": "Medical Department", + "width": "100", }, - { - 'fieldname': 'employee', - 'label': _('Lab Technician'), - 'fieldtype': 'Link', - 'options': 'Employee', - 'width': '120' - }, - { - 'fieldname': 'status', - 'label': _('Status'), - 'fieldtype': 'Data', - 'width': '100' - }, - { - 'fieldname': 'invoiced', - 'label': _('Invoiced'), - 'fieldtype': 'Check', - 'width': '100' - }, - { - 'fieldname': 'result_date', - 'label': _('Result Date'), - 'fieldtype': 'Date', - 'width': '100' - }, - { - 'fieldname': 'practitioner', - 'label': _('Requesting Practitioner'), - 'fieldtype': 'Link', - 'options': 'Healthcare Practitioner', - 'width': '120' - }, - { - 'fieldname': 'department', - 'label': _('Medical Department'), - 'fieldtype': 'Link', - 'options': 'Medical Department', - 'width': '100' - } ] + def get_lab_tests(filters): conditions = get_conditions(filters) data = frappe.get_all( - doctype='Lab Test', - fields=['name', 'template', 'company', 'patient', 'patient_name', 'practitioner', 'employee', 'status', 'invoiced', 'result_date', 'department'], + doctype="Lab Test", + fields=[ + "name", + "template", + "company", + "patient", + "patient_name", + "practitioner", + "employee", + "status", + "invoiced", + "result_date", + "department", + ], filters=conditions, - order_by='submitted_date desc' + order_by="submitted_date desc", ) return data -def get_conditions(filters): - conditions = { - 'docstatus': ('=', 1) - } - if filters.get('from_date') and filters.get('to_date'): - conditions['result_date'] = ('between', (filters.get('from_date'), filters.get('to_date'))) - filters.pop('from_date') - filters.pop('to_date') +def get_conditions(filters): + conditions = {"docstatus": ("=", 1)} + + if filters.get("from_date") and filters.get("to_date"): + conditions["result_date"] = ("between", (filters.get("from_date"), filters.get("to_date"))) + filters.pop("from_date") + filters.pop("to_date") for key, value in filters.items(): if filters.get(key): @@ -143,35 +138,35 @@ def get_conditions(filters): return conditions + def get_chart_data(data): if not data: return None - labels = ['Completed', 'Approved', 'Rejected'] + labels = ["Completed", "Approved", "Rejected"] - status_wise_data = { - 'Completed': 0, - 'Approved': 0, - 'Rejected': 0 - } + status_wise_data = {"Completed": 0, "Approved": 0, "Rejected": 0} datasets = [] for entry in data: status_wise_data[entry.status] += 1 - datasets.append({ - 'name': 'Lab Test Status', - 'values': [status_wise_data.get('Completed'), status_wise_data.get('Approved'), status_wise_data.get('Rejected')] - }) + datasets.append( + { + "name": "Lab Test Status", + "values": [ + status_wise_data.get("Completed"), + status_wise_data.get("Approved"), + status_wise_data.get("Rejected"), + ], + } + ) chart = { - 'data': { - 'labels': labels, - 'datasets': datasets - }, - 'type': 'bar', - 'height': 300, + "data": {"labels": labels, "datasets": datasets}, + "type": "bar", + "height": 300, } return chart @@ -192,21 +187,21 @@ def get_report_summary(data): return [ { - 'value': total_lab_tests, - 'indicator': 'Blue', - 'label': 'Total Lab Tests', - 'datatype': 'Int', + "value": total_lab_tests, + "indicator": "Blue", + "label": "Total Lab Tests", + "datatype": "Int", }, { - 'value': invoiced_lab_tests, - 'indicator': 'Green', - 'label': 'Invoiced Lab Tests', - 'datatype': 'Int', + "value": invoiced_lab_tests, + "indicator": "Green", + "label": "Invoiced Lab Tests", + "datatype": "Int", }, { - 'value': unbilled_lab_tests, - 'indicator': 'Red', - 'label': 'Unbilled Lab Tests', - 'datatype': 'Int', - } + "value": unbilled_lab_tests, + "indicator": "Red", + "label": "Unbilled Lab Tests", + "datatype": "Int", + }, ] diff --git a/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py index b128a1ed487..a4eede39feb 100644 --- a/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py +++ b/erpnext/healthcare/report/patient_appointment_analytics/patient_appointment_analytics.py @@ -13,11 +13,25 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): return Analytics(filters).run() + class Analytics(object): def __init__(self, filters=None): """Patient Appointment Analytics Report.""" self.filters = frappe._dict(filters or {}) - self.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + self.months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] self.get_period_date_ranges() def run(self): @@ -29,25 +43,23 @@ class Analytics(object): def get_period_date_ranges(self): from dateutil.relativedelta import MO, relativedelta + from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) - increment = { - 'Monthly': 1, - 'Quarterly': 3, - 'Half-Yearly': 6, - 'Yearly': 12 - }.get(self.filters.range, 1) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( + self.filters.range, 1 + ) - if self.filters.range in ['Monthly', 'Quarterly']: + if self.filters.range in ["Monthly", "Quarterly"]: from_date = from_date.replace(day=1) - elif self.filters.range == 'Yearly': + elif self.filters.range == "Yearly": from_date = get_fiscal_year(from_date)[1] else: from_date = from_date + relativedelta(from_date, weekday=MO(-1)) self.periodic_daterange = [] for dummy in range(1, 53): - if self.filters.range == 'Weekly': + if self.filters.range == "Weekly": period_end_date = add_days(from_date, 6) else: period_end_date = add_to_date(from_date, months=increment, days=-1) @@ -64,87 +76,87 @@ class Analytics(object): def get_columns(self): self.columns = [] - if self.filters.tree_type == 'Healthcare Practitioner': - self.columns.append({ - 'label': _('Healthcare Practitioner'), - 'options': 'Healthcare Practitioner', - 'fieldname': 'practitioner', - 'fieldtype': 'Link', - 'width': 200 - }) + if self.filters.tree_type == "Healthcare Practitioner": + self.columns.append( + { + "label": _("Healthcare Practitioner"), + "options": "Healthcare Practitioner", + "fieldname": "practitioner", + "fieldtype": "Link", + "width": 200, + } + ) - elif self.filters.tree_type == 'Medical Department': - self.columns.append({ - 'label': _('Medical Department'), - 'fieldname': 'department', - 'fieldtype': 'Link', - 'options': 'Medical Department', - 'width': 150 - }) + elif self.filters.tree_type == "Medical Department": + self.columns.append( + { + "label": _("Medical Department"), + "fieldname": "department", + "fieldtype": "Link", + "options": "Medical Department", + "width": 150, + } + ) for end_date in self.periodic_daterange: period = self.get_period(end_date) - self.columns.append({ - 'label': _(period), - 'fieldname': scrub(period), - 'fieldtype': 'Int', - 'width': 120 - }) + self.columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Int", "width": 120} + ) - self.columns.append({ - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Int', - 'width': 120 - }) + self.columns.append( + {"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "width": 120} + ) def get_data(self): - if self.filters.tree_type == 'Healthcare Practitioner': + if self.filters.tree_type == "Healthcare Practitioner": self.get_appointments_based_on_healthcare_practitioner() self.get_rows() - elif self.filters.tree_type == 'Medical Department': + elif self.filters.tree_type == "Medical Department": self.get_appointments_based_on_medical_department() self.get_rows() def get_period(self, appointment_date): - if self.filters.range == 'Weekly': - period = 'Week ' + str(appointment_date.isocalendar()[1]) - elif self.filters.range == 'Monthly': + if self.filters.range == "Weekly": + period = "Week " + str(appointment_date.isocalendar()[1]) + elif self.filters.range == "Monthly": period = str(self.months[appointment_date.month - 1]) - elif self.filters.range == 'Quarterly': - period = 'Quarter ' + str(((appointment_date.month - 1) // 3) + 1) + elif self.filters.range == "Quarterly": + period = "Quarter " + str(((appointment_date.month - 1) // 3) + 1) else: year = get_fiscal_year(appointment_date, company=self.filters.company) period = str(year[0]) if getdate(self.filters.from_date).year != getdate(self.filters.to_date).year: - period += ' ' + str(appointment_date.year) + period += " " + str(appointment_date.year) return period def get_appointments_based_on_healthcare_practitioner(self): filters = self.get_common_filters() - self.entries = frappe.db.get_all('Patient Appointment', - fields=['appointment_date', 'name', 'patient', 'practitioner'], - filters=filters + self.entries = frappe.db.get_all( + "Patient Appointment", + fields=["appointment_date", "name", "patient", "practitioner"], + filters=filters, ) def get_appointments_based_on_medical_department(self): filters = self.get_common_filters() - if not filters.get('department'): - filters['department'] = ('!=', '') + if not filters.get("department"): + filters["department"] = ("!=", "") - self.entries = frappe.db.get_all('Patient Appointment', - fields=['appointment_date', 'name', 'patient', 'practitioner', 'department'], - filters=filters + self.entries = frappe.db.get_all( + "Patient Appointment", + fields=["appointment_date", "name", "patient", "practitioner", "department"], + filters=filters, ) def get_common_filters(self): filters = {} - filters['appointment_date'] = ('between', [self.filters.from_date, self.filters.to_date]) - for entry in ['appointment_type', 'practitioner', 'department', 'status']: + filters["appointment_date"] = ("between", [self.filters.from_date, self.filters.to_date]) + for entry in ["appointment_type", "practitioner", "department", "status"]: if self.filters.get(entry): filters[entry] = self.filters.get(entry) @@ -155,10 +167,10 @@ class Analytics(object): self.get_periodic_data() for entity, period_data in iteritems(self.appointment_periodic_data): - if self.filters.tree_type == 'Healthcare Practitioner': - row = {'practitioner': entity} - elif self.filters.tree_type == 'Medical Department': - row = {'department': entity} + if self.filters.tree_type == "Healthcare Practitioner": + row = {"practitioner": entity} + elif self.filters.tree_type == "Medical Department": + row = {"department": entity} total = 0 for end_date in self.periodic_daterange: @@ -167,7 +179,7 @@ class Analytics(object): row[scrub(period)] = amount total += amount - row['total'] = total + row["total"] = total self.data.append(row) @@ -175,22 +187,18 @@ class Analytics(object): self.appointment_periodic_data = frappe._dict() for d in self.entries: - period = self.get_period(d.get('appointment_date')) - if self.filters.tree_type == 'Healthcare Practitioner': - self.appointment_periodic_data.setdefault(d.practitioner, frappe._dict()).setdefault(period, 0.0) + period = self.get_period(d.get("appointment_date")) + if self.filters.tree_type == "Healthcare Practitioner": + self.appointment_periodic_data.setdefault(d.practitioner, frappe._dict()).setdefault( + period, 0.0 + ) self.appointment_periodic_data[d.practitioner][period] += 1 - elif self.filters.tree_type == 'Medical Department': + elif self.filters.tree_type == "Medical Department": self.appointment_periodic_data.setdefault(d.department, frappe._dict()).setdefault(period, 0.0) self.appointment_periodic_data[d.department][period] += 1 def get_chart_data(self): length = len(self.columns) - labels = [d.get("label") for d in self.columns[1:length - 1]] - self.chart = { - "data": { - 'labels': labels, - 'datasets': [] - }, - "type": "line" - } + labels = [d.get("label") for d in self.columns[1 : length - 1]] + self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} diff --git a/erpnext/healthcare/setup.py b/erpnext/healthcare/setup.py index 02295bb5858..b537ba39de3 100644 --- a/erpnext/healthcare/setup.py +++ b/erpnext/healthcare/setup.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ @@ -6,7 +5,7 @@ from erpnext.setup.utils import insert_record def setup_healthcare(): - if frappe.db.exists('Medical Department', 'Cardiology'): + if frappe.db.exists("Medical Department", "Cardiology"): # already setup return create_medical_departments() @@ -19,12 +18,31 @@ def setup_healthcare(): add_healthcare_service_unit_tree_root() setup_patient_history_settings() + def create_medical_departments(): departments = [ - "Accident And Emergency Care" ,"Anaesthetics", "Biochemistry", "Cardiology", "Dermatology", - "Diagnostic Imaging", "ENT", "Gastroenterology", "General Surgery", "Gynaecology", - "Haematology", "Maternity", "Microbiology", "Nephrology", "Neurology", "Oncology", - "Orthopaedics", "Pathology", "Physiotherapy", "Rheumatology", "Serology", "Urology" + "Accident And Emergency Care", + "Anaesthetics", + "Biochemistry", + "Cardiology", + "Dermatology", + "Diagnostic Imaging", + "ENT", + "Gastroenterology", + "General Surgery", + "Gynaecology", + "Haematology", + "Maternity", + "Microbiology", + "Nephrology", + "Neurology", + "Oncology", + "Orthopaedics", + "Pathology", + "Physiotherapy", + "Rheumatology", + "Serology", + "Urology", ] for department in departments: mediacal_department = frappe.new_doc("Medical Department") @@ -34,37 +52,175 @@ def create_medical_departments(): except frappe.DuplicateEntryError: pass + def create_antibiotics(): abt = [ - "Amoxicillin", "Ampicillin", "Bacampicillin", "Carbenicillin", "Cloxacillin", "Dicloxacillin", - "Flucloxacillin", "Mezlocillin", "Nafcillin", "Oxacillin", "Penicillin G", "Penicillin V", - "Piperacillin", "Pivampicillin", "Pivmecillinam", "Ticarcillin", "Cefacetrile (cephacetrile)", - "Cefadroxil (cefadroxyl)", "Cefalexin (cephalexin)", "Cefaloglycin (cephaloglycin)", - "Cefalonium (cephalonium)", "Cefaloridine (cephaloradine)", "Cefalotin (cephalothin)", - "Cefapirin (cephapirin)", "Cefatrizine", "Cefazaflur", "Cefazedone", "Cefazolin (cephazolin)", - "Cefradine (cephradine)", "Cefroxadine", "Ceftezole", "Cefaclor", "Cefamandole", "Cefmetazole", - "Cefonicid", "Cefotetan", "Cefoxitin", "Cefprozil (cefproxil)", "Cefuroxime", "Cefuzonam", - "Cefcapene", "Cefdaloxime", "Cefdinir", "Cefditoren", "Cefetamet", "Cefixime", "Cefmenoxime", - "Cefodizime", "Cefotaxime", "Cefpimizole", "Cefpodoxime", "Cefteram", "Ceftibuten", "Ceftiofur", - "Ceftiolene", "Ceftizoxime", "Ceftriaxone", "Cefoperazone", "Ceftazidime", "Cefclidine", "Cefepime", - "Cefluprenam", "Cefoselis", "Cefozopran", "Cefpirome", "Cefquinome", "Ceftobiprole", "Ceftaroline", - "Cefaclomezine","Cefaloram", "Cefaparole", "Cefcanel", "Cefedrolor", "Cefempidone", "Cefetrizole", - "Cefivitril", "Cefmatilen", "Cefmepidium", "Cefovecin", "Cefoxazole", "Cefrotil", "Cefsumide", - "Cefuracetime", "Ceftioxide", "Ceftazidime/Avibactam", "Ceftolozane/Tazobactam", "Aztreonam", - "Imipenem", "Imipenem/cilastatin", "Doripenem", "Meropenem", "Ertapenem", "Azithromycin", - "Erythromycin", "Clarithromycin", "Dirithromycin", "Roxithromycin", "Telithromycin", "Clindamycin", - "Lincomycin", "Pristinamycin", "Quinupristin/dalfopristin", "Amikacin", "Gentamicin", "Kanamycin", - "Neomycin", "Netilmicin", "Paromomycin", "Streptomycin", "Tobramycin", "Flumequine", "Nalidixic acid", - "Oxolinic acid", "Piromidic acid", "Pipemidic acid", "Rosoxacin", "Ciprofloxacin", "Enoxacin", - "Lomefloxacin", "Nadifloxacin", "Norfloxacin", "Ofloxacin", "Pefloxacin", "Rufloxacin", "Balofloxacin", - "Gatifloxacin", "Grepafloxacin", "Levofloxacin", "Moxifloxacin", "Pazufloxacin", "Sparfloxacin", - "Temafloxacin", "Tosufloxacin", "Besifloxacin", "Clinafloxacin", "Gemifloxacin", - "Sitafloxacin", "Trovafloxacin", "Prulifloxacin", "Sulfamethizole", "Sulfamethoxazole", - "Sulfisoxazole", "Trimethoprim-Sulfamethoxazole", "Demeclocycline", "Doxycycline", "Minocycline", - "Oxytetracycline", "Tetracycline", "Tigecycline", "Chloramphenicol", "Metronidazole", - "Tinidazole", "Nitrofurantoin", "Vancomycin", "Teicoplanin", "Telavancin", "Linezolid", - "Cycloserine 2", "Rifampin", "Rifabutin", "Rifapentine", "Rifalazil", "Bacitracin", "Polymyxin B", - "Viomycin", "Capreomycin" + "Amoxicillin", + "Ampicillin", + "Bacampicillin", + "Carbenicillin", + "Cloxacillin", + "Dicloxacillin", + "Flucloxacillin", + "Mezlocillin", + "Nafcillin", + "Oxacillin", + "Penicillin G", + "Penicillin V", + "Piperacillin", + "Pivampicillin", + "Pivmecillinam", + "Ticarcillin", + "Cefacetrile (cephacetrile)", + "Cefadroxil (cefadroxyl)", + "Cefalexin (cephalexin)", + "Cefaloglycin (cephaloglycin)", + "Cefalonium (cephalonium)", + "Cefaloridine (cephaloradine)", + "Cefalotin (cephalothin)", + "Cefapirin (cephapirin)", + "Cefatrizine", + "Cefazaflur", + "Cefazedone", + "Cefazolin (cephazolin)", + "Cefradine (cephradine)", + "Cefroxadine", + "Ceftezole", + "Cefaclor", + "Cefamandole", + "Cefmetazole", + "Cefonicid", + "Cefotetan", + "Cefoxitin", + "Cefprozil (cefproxil)", + "Cefuroxime", + "Cefuzonam", + "Cefcapene", + "Cefdaloxime", + "Cefdinir", + "Cefditoren", + "Cefetamet", + "Cefixime", + "Cefmenoxime", + "Cefodizime", + "Cefotaxime", + "Cefpimizole", + "Cefpodoxime", + "Cefteram", + "Ceftibuten", + "Ceftiofur", + "Ceftiolene", + "Ceftizoxime", + "Ceftriaxone", + "Cefoperazone", + "Ceftazidime", + "Cefclidine", + "Cefepime", + "Cefluprenam", + "Cefoselis", + "Cefozopran", + "Cefpirome", + "Cefquinome", + "Ceftobiprole", + "Ceftaroline", + "Cefaclomezine", + "Cefaloram", + "Cefaparole", + "Cefcanel", + "Cefedrolor", + "Cefempidone", + "Cefetrizole", + "Cefivitril", + "Cefmatilen", + "Cefmepidium", + "Cefovecin", + "Cefoxazole", + "Cefrotil", + "Cefsumide", + "Cefuracetime", + "Ceftioxide", + "Ceftazidime/Avibactam", + "Ceftolozane/Tazobactam", + "Aztreonam", + "Imipenem", + "Imipenem/cilastatin", + "Doripenem", + "Meropenem", + "Ertapenem", + "Azithromycin", + "Erythromycin", + "Clarithromycin", + "Dirithromycin", + "Roxithromycin", + "Telithromycin", + "Clindamycin", + "Lincomycin", + "Pristinamycin", + "Quinupristin/dalfopristin", + "Amikacin", + "Gentamicin", + "Kanamycin", + "Neomycin", + "Netilmicin", + "Paromomycin", + "Streptomycin", + "Tobramycin", + "Flumequine", + "Nalidixic acid", + "Oxolinic acid", + "Piromidic acid", + "Pipemidic acid", + "Rosoxacin", + "Ciprofloxacin", + "Enoxacin", + "Lomefloxacin", + "Nadifloxacin", + "Norfloxacin", + "Ofloxacin", + "Pefloxacin", + "Rufloxacin", + "Balofloxacin", + "Gatifloxacin", + "Grepafloxacin", + "Levofloxacin", + "Moxifloxacin", + "Pazufloxacin", + "Sparfloxacin", + "Temafloxacin", + "Tosufloxacin", + "Besifloxacin", + "Clinafloxacin", + "Gemifloxacin", + "Sitafloxacin", + "Trovafloxacin", + "Prulifloxacin", + "Sulfamethizole", + "Sulfamethoxazole", + "Sulfisoxazole", + "Trimethoprim-Sulfamethoxazole", + "Demeclocycline", + "Doxycycline", + "Minocycline", + "Oxytetracycline", + "Tetracycline", + "Tigecycline", + "Chloramphenicol", + "Metronidazole", + "Tinidazole", + "Nitrofurantoin", + "Vancomycin", + "Teicoplanin", + "Telavancin", + "Linezolid", + "Cycloserine 2", + "Rifampin", + "Rifabutin", + "Rifapentine", + "Rifalazil", + "Bacitracin", + "Polymyxin B", + "Viomycin", + "Capreomycin", ] for a in abt: @@ -75,115 +231,259 @@ def create_antibiotics(): except frappe.DuplicateEntryError: pass + def create_lab_test_uom(): records = [ - {"doctype": "Lab Test UOM", "name": "umol/L", "lab_test_uom": "umol/L", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "mg/L", "lab_test_uom": "mg/L", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "mg / dl", "lab_test_uom": "mg / dl", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "pg / ml", "lab_test_uom": "pg / ml", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "U/ml", "lab_test_uom": "U/ml", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "/HPF", "lab_test_uom": "/HPF", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "Million Cells / cumm", "lab_test_uom": "Million Cells / cumm", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "Lakhs Cells / cumm", "lab_test_uom": "Lakhs Cells / cumm", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "U / L", "lab_test_uom": "U / L", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "g / L", "lab_test_uom": "g / L", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "IU / ml", "lab_test_uom": "IU / ml", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "gm %", "lab_test_uom": "gm %", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "Microgram", "lab_test_uom": "Microgram", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "Micron", "lab_test_uom": "Micron", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "Cells / cumm", "lab_test_uom": "Cells / cumm", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "%", "lab_test_uom": "%", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "mm / dl", "lab_test_uom": "mm / dl", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "mm / hr", "lab_test_uom": "mm / hr", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "ulU / ml", "lab_test_uom": "ulU / ml", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "ng / ml", "lab_test_uom": "ng / ml", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "ng / dl", "lab_test_uom": "ng / dl", "uom_description": None }, - {"doctype": "Lab Test UOM", "name": "ug / dl", "lab_test_uom": "ug / dl", "uom_description": None } + {"doctype": "Lab Test UOM", "name": "umol/L", "lab_test_uom": "umol/L", "uom_description": None}, + {"doctype": "Lab Test UOM", "name": "mg/L", "lab_test_uom": "mg/L", "uom_description": None}, + { + "doctype": "Lab Test UOM", + "name": "mg / dl", + "lab_test_uom": "mg / dl", + "uom_description": None, + }, + { + "doctype": "Lab Test UOM", + "name": "pg / ml", + "lab_test_uom": "pg / ml", + "uom_description": None, + }, + {"doctype": "Lab Test UOM", "name": "U/ml", "lab_test_uom": "U/ml", "uom_description": None}, + {"doctype": "Lab Test UOM", "name": "/HPF", "lab_test_uom": "/HPF", "uom_description": None}, + { + "doctype": "Lab Test UOM", + "name": "Million Cells / cumm", + "lab_test_uom": "Million Cells / cumm", + "uom_description": None, + }, + { + "doctype": "Lab Test UOM", + "name": "Lakhs Cells / cumm", + "lab_test_uom": "Lakhs Cells / cumm", + "uom_description": None, + }, + {"doctype": "Lab Test UOM", "name": "U / L", "lab_test_uom": "U / L", "uom_description": None}, + {"doctype": "Lab Test UOM", "name": "g / L", "lab_test_uom": "g / L", "uom_description": None}, + { + "doctype": "Lab Test UOM", + "name": "IU / ml", + "lab_test_uom": "IU / ml", + "uom_description": None, + }, + {"doctype": "Lab Test UOM", "name": "gm %", "lab_test_uom": "gm %", "uom_description": None}, + { + "doctype": "Lab Test UOM", + "name": "Microgram", + "lab_test_uom": "Microgram", + "uom_description": None, + }, + {"doctype": "Lab Test UOM", "name": "Micron", "lab_test_uom": "Micron", "uom_description": None}, + { + "doctype": "Lab Test UOM", + "name": "Cells / cumm", + "lab_test_uom": "Cells / cumm", + "uom_description": None, + }, + {"doctype": "Lab Test UOM", "name": "%", "lab_test_uom": "%", "uom_description": None}, + { + "doctype": "Lab Test UOM", + "name": "mm / dl", + "lab_test_uom": "mm / dl", + "uom_description": None, + }, + { + "doctype": "Lab Test UOM", + "name": "mm / hr", + "lab_test_uom": "mm / hr", + "uom_description": None, + }, + { + "doctype": "Lab Test UOM", + "name": "ulU / ml", + "lab_test_uom": "ulU / ml", + "uom_description": None, + }, + { + "doctype": "Lab Test UOM", + "name": "ng / ml", + "lab_test_uom": "ng / ml", + "uom_description": None, + }, + { + "doctype": "Lab Test UOM", + "name": "ng / dl", + "lab_test_uom": "ng / dl", + "uom_description": None, + }, + { + "doctype": "Lab Test UOM", + "name": "ug / dl", + "lab_test_uom": "ug / dl", + "uom_description": None, + }, ] insert_record(records) + def create_duration(): records = [ - {"doctype": "Prescription Duration", "name": "3 Month", "number": "3", "period": "Month" }, - {"doctype": "Prescription Duration", "name": "2 Month", "number": "2", "period": "Month" }, - {"doctype": "Prescription Duration", "name": "1 Month", "number": "1", "period": "Month" }, - {"doctype": "Prescription Duration", "name": "12 Hour", "number": "12", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "11 Hour", "number": "11", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "10 Hour", "number": "10", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "9 Hour", "number": "9", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "8 Hour", "number": "8", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "7 Hour", "number": "7", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "6 Hour", "number": "6", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "5 Hour", "number": "5", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "4 Hour", "number": "4", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "3 Hour", "number": "3", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "2 Hour", "number": "2", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "1 Hour", "number": "1", "period": "Hour" }, - {"doctype": "Prescription Duration", "name": "5 Week", "number": "5", "period": "Week" }, - {"doctype": "Prescription Duration", "name": "4 Week", "number": "4", "period": "Week" }, - {"doctype": "Prescription Duration", "name": "3 Week", "number": "3", "period": "Week" }, - {"doctype": "Prescription Duration", "name": "2 Week", "number": "2", "period": "Week" }, - {"doctype": "Prescription Duration", "name": "1 Week", "number": "1", "period": "Week" }, - {"doctype": "Prescription Duration", "name": "6 Day", "number": "6", "period": "Day" }, - {"doctype": "Prescription Duration", "name": "5 Day", "number": "5", "period": "Day" }, - {"doctype": "Prescription Duration", "name": "4 Day", "number": "4", "period": "Day" }, - {"doctype": "Prescription Duration", "name": "3 Day", "number": "3", "period": "Day" }, - {"doctype": "Prescription Duration", "name": "2 Day", "number": "2", "period": "Day" }, - {"doctype": "Prescription Duration", "name": "1 Day", "number": "1", "period": "Day" } + {"doctype": "Prescription Duration", "name": "3 Month", "number": "3", "period": "Month"}, + {"doctype": "Prescription Duration", "name": "2 Month", "number": "2", "period": "Month"}, + {"doctype": "Prescription Duration", "name": "1 Month", "number": "1", "period": "Month"}, + {"doctype": "Prescription Duration", "name": "12 Hour", "number": "12", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "11 Hour", "number": "11", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "10 Hour", "number": "10", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "9 Hour", "number": "9", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "8 Hour", "number": "8", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "7 Hour", "number": "7", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "6 Hour", "number": "6", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "5 Hour", "number": "5", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "4 Hour", "number": "4", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "3 Hour", "number": "3", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "2 Hour", "number": "2", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "1 Hour", "number": "1", "period": "Hour"}, + {"doctype": "Prescription Duration", "name": "5 Week", "number": "5", "period": "Week"}, + {"doctype": "Prescription Duration", "name": "4 Week", "number": "4", "period": "Week"}, + {"doctype": "Prescription Duration", "name": "3 Week", "number": "3", "period": "Week"}, + {"doctype": "Prescription Duration", "name": "2 Week", "number": "2", "period": "Week"}, + {"doctype": "Prescription Duration", "name": "1 Week", "number": "1", "period": "Week"}, + {"doctype": "Prescription Duration", "name": "6 Day", "number": "6", "period": "Day"}, + {"doctype": "Prescription Duration", "name": "5 Day", "number": "5", "period": "Day"}, + {"doctype": "Prescription Duration", "name": "4 Day", "number": "4", "period": "Day"}, + {"doctype": "Prescription Duration", "name": "3 Day", "number": "3", "period": "Day"}, + {"doctype": "Prescription Duration", "name": "2 Day", "number": "2", "period": "Day"}, + {"doctype": "Prescription Duration", "name": "1 Day", "number": "1", "period": "Day"}, ] insert_record(records) + def create_dosage(): records = [ - {"doctype": "Prescription Dosage", "name": "1-1-1-1", "dosage": "1-1-1-1","dosage_strength": - [{"strength": "1.0","strength_time": "9:00:00"}, {"strength": "1.0","strength_time": "13:00:00"},{"strength": "1.0","strength_time": "17:00:00"},{"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "1-1-1-1", + "dosage": "1-1-1-1", + "dosage_strength": [ + {"strength": "1.0", "strength_time": "9:00:00"}, + {"strength": "1.0", "strength_time": "13:00:00"}, + {"strength": "1.0", "strength_time": "17:00:00"}, + {"strength": "1.0", "strength_time": "21:00:00"}, + ], }, - {"doctype": "Prescription Dosage", "name": "0-0-1", "dosage": "0-0-1","dosage_strength": - [{"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "0-0-1", + "dosage": "0-0-1", + "dosage_strength": [{"strength": "1.0", "strength_time": "21:00:00"}], }, - {"doctype": "Prescription Dosage", "name": "1-0-0", "dosage": "1-0-0","dosage_strength": - [{"strength": "1.0","strength_time": "9:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "1-0-0", + "dosage": "1-0-0", + "dosage_strength": [{"strength": "1.0", "strength_time": "9:00:00"}], }, - {"doctype": "Prescription Dosage", "name": "0-1-0", "dosage": "0-1-0","dosage_strength": - [{"strength": "1.0","strength_time": "14:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "0-1-0", + "dosage": "0-1-0", + "dosage_strength": [{"strength": "1.0", "strength_time": "14:00:00"}], }, - {"doctype": "Prescription Dosage", "name": "1-1-1", "dosage": "1-1-1","dosage_strength": - [{"strength": "1.0","strength_time": "9:00:00"}, {"strength": "1.0","strength_time": "14:00:00"},{"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "1-1-1", + "dosage": "1-1-1", + "dosage_strength": [ + {"strength": "1.0", "strength_time": "9:00:00"}, + {"strength": "1.0", "strength_time": "14:00:00"}, + {"strength": "1.0", "strength_time": "21:00:00"}, + ], }, - {"doctype": "Prescription Dosage", "name": "1-0-1", "dosage": "1-0-1","dosage_strength": - [{"strength": "1.0","strength_time": "9:00:00"}, {"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "1-0-1", + "dosage": "1-0-1", + "dosage_strength": [ + {"strength": "1.0", "strength_time": "9:00:00"}, + {"strength": "1.0", "strength_time": "21:00:00"}, + ], }, - {"doctype": "Prescription Dosage", "name": "Once Bedtime", "dosage": "Once Bedtime","dosage_strength": - [{"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "Once Bedtime", + "dosage": "Once Bedtime", + "dosage_strength": [{"strength": "1.0", "strength_time": "21:00:00"}], }, - {"doctype": "Prescription Dosage", "name": "5 times a day", "dosage": "5 times a day","dosage_strength": - [{"strength": "1.0","strength_time": "5:00:00"}, {"strength": "1.0","strength_time": "9:00:00"}, {"strength": "1.0","strength_time": "13:00:00"},{"strength": "1.0","strength_time": "17:00:00"},{"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "5 times a day", + "dosage": "5 times a day", + "dosage_strength": [ + {"strength": "1.0", "strength_time": "5:00:00"}, + {"strength": "1.0", "strength_time": "9:00:00"}, + {"strength": "1.0", "strength_time": "13:00:00"}, + {"strength": "1.0", "strength_time": "17:00:00"}, + {"strength": "1.0", "strength_time": "21:00:00"}, + ], }, - {"doctype": "Prescription Dosage", "name": "QID", "dosage": "QID","dosage_strength": - [{"strength": "1.0","strength_time": "9:00:00"}, {"strength": "1.0","strength_time": "13:00:00"},{"strength": "1.0","strength_time": "17:00:00"},{"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "QID", + "dosage": "QID", + "dosage_strength": [ + {"strength": "1.0", "strength_time": "9:00:00"}, + {"strength": "1.0", "strength_time": "13:00:00"}, + {"strength": "1.0", "strength_time": "17:00:00"}, + {"strength": "1.0", "strength_time": "21:00:00"}, + ], }, - {"doctype": "Prescription Dosage", "name": "TID", "dosage": "TID","dosage_strength": - [{"strength": "1.0","strength_time": "9:00:00"}, {"strength": "1.0","strength_time": "14:00:00"},{"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "TID", + "dosage": "TID", + "dosage_strength": [ + {"strength": "1.0", "strength_time": "9:00:00"}, + {"strength": "1.0", "strength_time": "14:00:00"}, + {"strength": "1.0", "strength_time": "21:00:00"}, + ], }, - {"doctype": "Prescription Dosage", "name": "BID", "dosage": "BID","dosage_strength": - [{"strength": "1.0","strength_time": "9:00:00"}, {"strength": "1.0","strength_time": "21:00:00"}] + { + "doctype": "Prescription Dosage", + "name": "BID", + "dosage": "BID", + "dosage_strength": [ + {"strength": "1.0", "strength_time": "9:00:00"}, + {"strength": "1.0", "strength_time": "21:00:00"}, + ], + }, + { + "doctype": "Prescription Dosage", + "name": "Once Daily", + "dosage": "Once Daily", + "dosage_strength": [{"strength": "1.0", "strength_time": "9:00:00"}], }, - {"doctype": "Prescription Dosage", "name": "Once Daily", "dosage": "Once Daily","dosage_strength": - [{"strength": "1.0","strength_time": "9:00:00"}] - } ] insert_record(records) + def create_healthcare_item_groups(): records = [ - {'doctype': 'Item Group', 'item_group_name': _('Laboratory'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Drug'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') } + { + "doctype": "Item Group", + "item_group_name": _("Laboratory"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Drug"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, ] insert_record(records) + def create_sensitivity(): records = [ {"doctype": "Sensitivity", "sensitivity": _("Low Sensitivity")}, @@ -191,21 +491,23 @@ def create_sensitivity(): {"doctype": "Sensitivity", "sensitivity": _("Moderate Sensitivity")}, {"doctype": "Sensitivity", "sensitivity": _("Susceptible")}, {"doctype": "Sensitivity", "sensitivity": _("Resistant")}, - {"doctype": "Sensitivity", "sensitivity": _("Intermediate")} + {"doctype": "Sensitivity", "sensitivity": _("Intermediate")}, ] insert_record(records) + def add_healthcare_service_unit_tree_root(): record = [ { "doctype": "Healthcare Service Unit", "healthcare_service_unit_name": "All Healthcare Service Units", "is_group": 1, - "company": get_company() - } + "company": get_company(), + } ] insert_record(record) + def get_company(): company = frappe.defaults.get_defaults().company if company: @@ -216,81 +518,108 @@ def get_company(): return company[0].name return None + def setup_patient_history_settings(): import json - settings = frappe.get_single('Patient History Settings') + settings = frappe.get_single("Patient History Settings") configuration = get_patient_history_config() for dt, config in configuration.items(): - settings.append("standard_doctypes", { - "document_type": dt, - "date_fieldname": config[0], - "selected_fields": json.dumps(config[1]) - }) + settings.append( + "standard_doctypes", + {"document_type": dt, "date_fieldname": config[0], "selected_fields": json.dumps(config[1])}, + ) settings.save() + def get_patient_history_config(): return { - "Patient Encounter": ("encounter_date", [ - {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, - {"label": "Symptoms", "fieldname": "symptoms", "fieldtype": "Table Multiselect"}, - {"label": "Diagnosis", "fieldname": "diagnosis", "fieldtype": "Table Multiselect"}, - {"label": "Drug Prescription", "fieldname": "drug_prescription", "fieldtype": "Table"}, - {"label": "Lab Tests", "fieldname": "lab_test_prescription", "fieldtype": "Table"}, - {"label": "Clinical Procedures", "fieldname": "procedure_prescription", "fieldtype": "Table"}, - {"label": "Therapies", "fieldname": "therapies", "fieldtype": "Table"}, - {"label": "Review Details", "fieldname": "encounter_comment", "fieldtype": "Small Text"} - ]), - "Clinical Procedure": ("start_date", [ - {"label": "Procedure Template", "fieldname": "procedure_template", "fieldtype": "Link"}, - {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, - {"label": "Notes", "fieldname": "notes", "fieldtype": "Small Text"}, - {"label": "Service Unit", "fieldname": "service_unit", "fieldtype": "Healthcare Service Unit"}, - {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"}, - {"label": "Sample", "fieldname": "sample", "fieldtype": "Link"} - ]), - "Lab Test": ("result_date", [ - {"label": "Test Template", "fieldname": "template", "fieldtype": "Link"}, - {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, - {"label": "Test Name", "fieldname": "lab_test_name", "fieldtype": "Data"}, - {"label": "Lab Technician Name", "fieldname": "employee_name", "fieldtype": "Data"}, - {"label": "Sample ID", "fieldname": "sample", "fieldtype": "Link"}, - {"label": "Normal Test Result", "fieldname": "normal_test_items", "fieldtype": "Table"}, - {"label": "Descriptive Test Result", "fieldname": "descriptive_test_items", "fieldtype": "Table"}, - {"label": "Organism Test Result", "fieldname": "organism_test_items", "fieldtype": "Table"}, - {"label": "Sensitivity Test Result", "fieldname": "sensitivity_test_items", "fieldtype": "Table"}, - {"label": "Comments", "fieldname": "lab_test_comment", "fieldtype": "Table"} - ]), - "Therapy Session": ("start_date", [ - {"label": "Therapy Type", "fieldname": "therapy_type", "fieldtype": "Link"}, - {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, - {"label": "Therapy Plan", "fieldname": "therapy_plan", "fieldtype": "Link"}, - {"label": "Duration", "fieldname": "duration", "fieldtype": "Int"}, - {"label": "Location", "fieldname": "location", "fieldtype": "Link"}, - {"label": "Healthcare Service Unit", "fieldname": "service_unit", "fieldtype": "Link"}, - {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"}, - {"label": "Exercises", "fieldname": "exercises", "fieldtype": "Table"}, - {"label": "Total Counts Targeted", "fieldname": "total_counts_targeted", "fieldtype": "Int"}, - {"label": "Total Counts Completed", "fieldname": "total_counts_completed", "fieldtype": "Int"} - ]), - "Vital Signs": ("signs_date", [ - {"label": "Body Temperature", "fieldname": "temperature", "fieldtype": "Data"}, - {"label": "Heart Rate / Pulse", "fieldname": "pulse", "fieldtype": "Data"}, - {"label": "Respiratory rate", "fieldname": "respiratory_rate", "fieldtype": "Data"}, - {"label": "Tongue", "fieldname": "tongue", "fieldtype": "Select"}, - {"label": "Abdomen", "fieldname": "abdomen", "fieldtype": "Select"}, - {"label": "Reflexes", "fieldname": "reflexes", "fieldtype": "Select"}, - {"label": "Blood Pressure", "fieldname": "bp", "fieldtype": "Data"}, - {"label": "Notes", "fieldname": "vital_signs_note", "fieldtype": "Small Text"}, - {"label": "Height (In Meter)", "fieldname": "height", "fieldtype": "Float"}, - {"label": "Weight (In Kilogram)", "fieldname": "weight", "fieldtype": "Float"}, - {"label": "BMI", "fieldname": "bmi", "fieldtype": "Float"} - ]), - "Inpatient Medication Order": ("start_date", [ - {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, - {"label": "Start Date", "fieldname": "start_date", "fieldtype": "Date"}, - {"label": "End Date", "fieldname": "end_date", "fieldtype": "Date"}, - {"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"}, - {"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"} - ]) + "Patient Encounter": ( + "encounter_date", + [ + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Symptoms", "fieldname": "symptoms", "fieldtype": "Table Multiselect"}, + {"label": "Diagnosis", "fieldname": "diagnosis", "fieldtype": "Table Multiselect"}, + {"label": "Drug Prescription", "fieldname": "drug_prescription", "fieldtype": "Table"}, + {"label": "Lab Tests", "fieldname": "lab_test_prescription", "fieldtype": "Table"}, + {"label": "Clinical Procedures", "fieldname": "procedure_prescription", "fieldtype": "Table"}, + {"label": "Therapies", "fieldname": "therapies", "fieldtype": "Table"}, + {"label": "Review Details", "fieldname": "encounter_comment", "fieldtype": "Small Text"}, + ], + ), + "Clinical Procedure": ( + "start_date", + [ + {"label": "Procedure Template", "fieldname": "procedure_template", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Notes", "fieldname": "notes", "fieldtype": "Small Text"}, + {"label": "Service Unit", "fieldname": "service_unit", "fieldtype": "Healthcare Service Unit"}, + {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"}, + {"label": "Sample", "fieldname": "sample", "fieldtype": "Link"}, + ], + ), + "Lab Test": ( + "result_date", + [ + {"label": "Test Template", "fieldname": "template", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Test Name", "fieldname": "lab_test_name", "fieldtype": "Data"}, + {"label": "Lab Technician Name", "fieldname": "employee_name", "fieldtype": "Data"}, + {"label": "Sample ID", "fieldname": "sample", "fieldtype": "Link"}, + {"label": "Normal Test Result", "fieldname": "normal_test_items", "fieldtype": "Table"}, + { + "label": "Descriptive Test Result", + "fieldname": "descriptive_test_items", + "fieldtype": "Table", + }, + {"label": "Organism Test Result", "fieldname": "organism_test_items", "fieldtype": "Table"}, + { + "label": "Sensitivity Test Result", + "fieldname": "sensitivity_test_items", + "fieldtype": "Table", + }, + {"label": "Comments", "fieldname": "lab_test_comment", "fieldtype": "Table"}, + ], + ), + "Therapy Session": ( + "start_date", + [ + {"label": "Therapy Type", "fieldname": "therapy_type", "fieldtype": "Link"}, + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Therapy Plan", "fieldname": "therapy_plan", "fieldtype": "Link"}, + {"label": "Duration", "fieldname": "duration", "fieldtype": "Int"}, + {"label": "Location", "fieldname": "location", "fieldtype": "Link"}, + {"label": "Healthcare Service Unit", "fieldname": "service_unit", "fieldtype": "Link"}, + {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"}, + {"label": "Exercises", "fieldname": "exercises", "fieldtype": "Table"}, + {"label": "Total Counts Targeted", "fieldname": "total_counts_targeted", "fieldtype": "Int"}, + {"label": "Total Counts Completed", "fieldname": "total_counts_completed", "fieldtype": "Int"}, + ], + ), + "Vital Signs": ( + "signs_date", + [ + {"label": "Body Temperature", "fieldname": "temperature", "fieldtype": "Data"}, + {"label": "Heart Rate / Pulse", "fieldname": "pulse", "fieldtype": "Data"}, + {"label": "Respiratory rate", "fieldname": "respiratory_rate", "fieldtype": "Data"}, + {"label": "Tongue", "fieldname": "tongue", "fieldtype": "Select"}, + {"label": "Abdomen", "fieldname": "abdomen", "fieldtype": "Select"}, + {"label": "Reflexes", "fieldname": "reflexes", "fieldtype": "Select"}, + {"label": "Blood Pressure", "fieldname": "bp", "fieldtype": "Data"}, + {"label": "Notes", "fieldname": "vital_signs_note", "fieldtype": "Small Text"}, + {"label": "Height (In Meter)", "fieldname": "height", "fieldtype": "Float"}, + {"label": "Weight (In Kilogram)", "fieldname": "weight", "fieldtype": "Float"}, + {"label": "BMI", "fieldname": "bmi", "fieldtype": "Float"}, + ], + ), + "Inpatient Medication Order": ( + "start_date", + [ + {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"}, + {"label": "Start Date", "fieldname": "start_date", "fieldtype": "Date"}, + {"label": "End Date", "fieldname": "end_date", "fieldtype": "Date"}, + {"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"}, + {"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"}, + ], + ), } diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 99f06bf6c81..52f31cddb0b 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -17,7 +17,7 @@ from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple @frappe.whitelist() def get_healthcare_services_to_invoice(patient, company): - patient = frappe.get_doc('Patient', patient) + patient = frappe.get_doc("Patient", patient) items_to_invoice = [] if patient: validate_customer_created(patient) @@ -34,50 +34,62 @@ def get_healthcare_services_to_invoice(patient, company): def validate_customer_created(patient): - if not frappe.db.get_value('Patient', patient.name, 'customer'): + if not frappe.db.get_value("Patient", patient.name, "customer"): msg = _("Please set a Customer linked to the Patient") - msg += " {0}".format(patient.name) - frappe.throw(msg, title=_('Customer Not Found')) + msg += " {0}".format(patient.name) + frappe.throw(msg, title=_("Customer Not Found")) def get_appointments_to_invoice(patient, company): appointments_to_invoice = [] patient_appointments = frappe.get_list( - 'Patient Appointment', - fields = '*', - filters = {'patient': patient.name, 'company': company, 'invoiced': 0, 'status': ['not in', 'Cancelled']}, - order_by = 'appointment_date' - ) + "Patient Appointment", + fields="*", + filters={ + "patient": patient.name, + "company": company, + "invoiced": 0, + "status": ["not in", "Cancelled"], + }, + order_by="appointment_date", + ) for appointment in patient_appointments: # Procedure Appointments if appointment.procedure_template: - if frappe.db.get_value('Clinical Procedure Template', appointment.procedure_template, 'is_billable'): - appointments_to_invoice.append({ - 'reference_type': 'Patient Appointment', - 'reference_name': appointment.name, - 'service': appointment.procedure_template - }) + if frappe.db.get_value( + "Clinical Procedure Template", appointment.procedure_template, "is_billable" + ): + appointments_to_invoice.append( + { + "reference_type": "Patient Appointment", + "reference_name": appointment.name, + "service": appointment.procedure_template, + } + ) # Consultation Appointments, should check fee validity else: - if frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups') and \ - frappe.db.exists('Fee Validity Reference', {'appointment': appointment.name}): - continue # Skip invoicing, fee validty present + if frappe.db.get_single_value( + "Healthcare Settings", "enable_free_follow_ups" + ) and frappe.db.exists("Fee Validity Reference", {"appointment": appointment.name}): + continue # Skip invoicing, fee validty present practitioner_charge = 0 income_account = None service_item = None if appointment.practitioner: details = get_service_item_and_practitioner_charge(appointment) - service_item = details.get('service_item') - practitioner_charge = details.get('practitioner_charge') + service_item = details.get("service_item") + practitioner_charge = details.get("practitioner_charge") income_account = get_income_account(appointment.practitioner, appointment.company) - appointments_to_invoice.append({ - 'reference_type': 'Patient Appointment', - 'reference_name': appointment.name, - 'service': service_item, - 'rate': practitioner_charge, - 'income_account': income_account - }) + appointments_to_invoice.append( + { + "reference_type": "Patient Appointment", + "reference_name": appointment.name, + "service": service_item, + "rate": practitioner_charge, + "income_account": income_account, + } + ) return appointments_to_invoice @@ -87,9 +99,9 @@ def get_encounters_to_invoice(patient, company): patient = patient.name encounters_to_invoice = [] encounters = frappe.get_list( - 'Patient Encounter', - fields=['*'], - filters={'patient': patient, 'company': company, 'invoiced': False, 'docstatus': 1} + "Patient Encounter", + fields=["*"], + filters={"patient": patient, "company": company, "invoiced": False, "docstatus": 1}, ) if encounters: for encounter in encounters: @@ -98,22 +110,25 @@ def get_encounters_to_invoice(patient, company): income_account = None service_item = None if encounter.practitioner: - if encounter.inpatient_record and \ - frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'): + if encounter.inpatient_record and frappe.db.get_single_value( + "Healthcare Settings", "do_not_bill_inpatient_encounters" + ): continue details = get_service_item_and_practitioner_charge(encounter) - service_item = details.get('service_item') - practitioner_charge = details.get('practitioner_charge') + service_item = details.get("service_item") + practitioner_charge = details.get("practitioner_charge") income_account = get_income_account(encounter.practitioner, encounter.company) - encounters_to_invoice.append({ - 'reference_type': 'Patient Encounter', - 'reference_name': encounter.name, - 'service': service_item, - 'rate': practitioner_charge, - 'income_account': income_account - }) + encounters_to_invoice.append( + { + "reference_type": "Patient Encounter", + "reference_name": encounter.name, + "service": service_item, + "rate": practitioner_charge, + "income_account": income_account, + } + ) return encounters_to_invoice @@ -121,21 +136,21 @@ def get_encounters_to_invoice(patient, company): def get_lab_tests_to_invoice(patient, company): lab_tests_to_invoice = [] lab_tests = frappe.get_list( - 'Lab Test', - fields=['name', 'template'], - filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1} + "Lab Test", + fields=["name", "template"], + filters={"patient": patient.name, "company": company, "invoiced": False, "docstatus": 1}, ) for lab_test in lab_tests: - item, is_billable = frappe.get_cached_value('Lab Test Template', lab_test.template, ['item', 'is_billable']) + item, is_billable = frappe.get_cached_value( + "Lab Test Template", lab_test.template, ["item", "is_billable"] + ) if is_billable: - lab_tests_to_invoice.append({ - 'reference_type': 'Lab Test', - 'reference_name': lab_test.name, - 'service': item - }) + lab_tests_to_invoice.append( + {"reference_type": "Lab Test", "reference_name": lab_test.name, "service": item} + ) lab_prescriptions = frappe.db.sql( - ''' + """ SELECT lp.name, lp.lab_test_code FROM @@ -145,16 +160,19 @@ def get_lab_tests_to_invoice(patient, company): and lp.parent=et.name and lp.lab_test_created=0 and lp.invoiced=0 - ''', (patient.name), as_dict=1) + """, + (patient.name), + as_dict=1, + ) for prescription in lab_prescriptions: - item, is_billable = frappe.get_cached_value('Lab Test Template', prescription.lab_test_code, ['item', 'is_billable']) + item, is_billable = frappe.get_cached_value( + "Lab Test Template", prescription.lab_test_code, ["item", "is_billable"] + ) if prescription.lab_test_code and is_billable: - lab_tests_to_invoice.append({ - 'reference_type': 'Lab Prescription', - 'reference_name': prescription.name, - 'service': item - }) + lab_tests_to_invoice.append( + {"reference_type": "Lab Prescription", "reference_name": prescription.name, "service": item} + ) return lab_tests_to_invoice @@ -162,40 +180,51 @@ def get_lab_tests_to_invoice(patient, company): def get_clinical_procedures_to_invoice(patient, company): clinical_procedures_to_invoice = [] procedures = frappe.get_list( - 'Clinical Procedure', - fields='*', - filters={'patient': patient.name, 'company': company, 'invoiced': False} + "Clinical Procedure", + fields="*", + filters={"patient": patient.name, "company": company, "invoiced": False}, ) for procedure in procedures: if not procedure.appointment: - item, is_billable = frappe.get_cached_value('Clinical Procedure Template', procedure.procedure_template, ['item', 'is_billable']) + item, is_billable = frappe.get_cached_value( + "Clinical Procedure Template", procedure.procedure_template, ["item", "is_billable"] + ) if procedure.procedure_template and is_billable: - clinical_procedures_to_invoice.append({ - 'reference_type': 'Clinical Procedure', - 'reference_name': procedure.name, - 'service': item - }) + clinical_procedures_to_invoice.append( + {"reference_type": "Clinical Procedure", "reference_name": procedure.name, "service": item} + ) # consumables - if procedure.invoice_separately_as_consumables and procedure.consume_stock \ - and procedure.status == 'Completed' and not procedure.consumption_invoiced: + if ( + procedure.invoice_separately_as_consumables + and procedure.consume_stock + and procedure.status == "Completed" + and not procedure.consumption_invoiced + ): - service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') + service_item = frappe.db.get_single_value( + "Healthcare Settings", "clinical_procedure_consumable_item" + ) if not service_item: - frappe.throw(_('Please configure Clinical Procedure Consumable Item in {0}').format( - frappe.utils.get_link_to_form('Healthcare Settings', 'Healthcare Settings')), - title=_('Missing Configuration')) + frappe.throw( + _("Please configure Clinical Procedure Consumable Item in {0}").format( + frappe.utils.get_link_to_form("Healthcare Settings", "Healthcare Settings") + ), + title=_("Missing Configuration"), + ) - clinical_procedures_to_invoice.append({ - 'reference_type': 'Clinical Procedure', - 'reference_name': procedure.name, - 'service': service_item, - 'rate': procedure.consumable_total_amount, - 'description': procedure.consumption_details - }) + clinical_procedures_to_invoice.append( + { + "reference_type": "Clinical Procedure", + "reference_name": procedure.name, + "service": service_item, + "rate": procedure.consumable_total_amount, + "description": procedure.consumption_details, + } + ) procedure_prescriptions = frappe.db.sql( - ''' + """ SELECT pp.name, pp.procedure FROM @@ -206,16 +235,23 @@ def get_clinical_procedures_to_invoice(patient, company): and pp.procedure_created=0 and pp.invoiced=0 and pp.appointment_booked=0 - ''', (patient.name), as_dict=1) + """, + (patient.name), + as_dict=1, + ) for prescription in procedure_prescriptions: - item, is_billable = frappe.get_cached_value('Clinical Procedure Template', prescription.procedure, ['item', 'is_billable']) + item, is_billable = frappe.get_cached_value( + "Clinical Procedure Template", prescription.procedure, ["item", "is_billable"] + ) if is_billable: - clinical_procedures_to_invoice.append({ - 'reference_type': 'Procedure Prescription', - 'reference_name': prescription.name, - 'service': item - }) + clinical_procedures_to_invoice.append( + { + "reference_type": "Procedure Prescription", + "reference_name": prescription.name, + "service": item, + } + ) return clinical_procedures_to_invoice @@ -223,7 +259,7 @@ def get_clinical_procedures_to_invoice(patient, company): def get_inpatient_services_to_invoice(patient, company): services_to_invoice = [] inpatient_services = frappe.db.sql( - ''' + """ SELECT io.* FROM @@ -234,11 +270,16 @@ def get_inpatient_services_to_invoice(patient, company): and io.parent=ip.name and io.left=1 and io.invoiced=0 - ''', (patient.name, company), as_dict=1) + """, + (patient.name, company), + as_dict=1, + ) for inpatient_occupancy in inpatient_services: - service_unit_type = frappe.db.get_value('Healthcare Service Unit', inpatient_occupancy.service_unit, 'service_unit_type') - service_unit_type = frappe.get_cached_doc('Healthcare Service Unit Type', service_unit_type) + service_unit_type = frappe.db.get_value( + "Healthcare Service Unit", inpatient_occupancy.service_unit, "service_unit_type" + ) + service_unit_type = frappe.get_cached_doc("Healthcare Service Unit Type", service_unit_type) if service_unit_type and service_unit_type.is_billable: hours_occupied = time_diff_in_hours(inpatient_occupancy.check_out, inpatient_occupancy.check_in) qty = 0.5 @@ -252,11 +293,14 @@ def get_inpatient_services_to_invoice(patient, company): qty = rounded(floor + 0.5, 1) if qty <= 0: qty = 0.5 - services_to_invoice.append({ - 'reference_type': 'Inpatient Occupancy', - 'reference_name': inpatient_occupancy.name, - 'service': service_unit_type.item, 'qty': qty - }) + services_to_invoice.append( + { + "reference_type": "Inpatient Occupancy", + "reference_name": inpatient_occupancy.name, + "service": service_unit_type.item, + "qty": qty, + } + ) return services_to_invoice @@ -264,53 +308,62 @@ def get_inpatient_services_to_invoice(patient, company): def get_therapy_plans_to_invoice(patient, company): therapy_plans_to_invoice = [] therapy_plans = frappe.get_list( - 'Therapy Plan', - fields=['therapy_plan_template', 'name'], + "Therapy Plan", + fields=["therapy_plan_template", "name"], filters={ - 'patient': patient.name, - 'invoiced': 0, - 'company': company, - 'therapy_plan_template': ('!=', '') - } + "patient": patient.name, + "invoiced": 0, + "company": company, + "therapy_plan_template": ("!=", ""), + }, ) for plan in therapy_plans: - therapy_plans_to_invoice.append({ - 'reference_type': 'Therapy Plan', - 'reference_name': plan.name, - 'service': frappe.db.get_value('Therapy Plan Template', plan.therapy_plan_template, 'linked_item') - }) + therapy_plans_to_invoice.append( + { + "reference_type": "Therapy Plan", + "reference_name": plan.name, + "service": frappe.db.get_value( + "Therapy Plan Template", plan.therapy_plan_template, "linked_item" + ), + } + ) return therapy_plans_to_invoice def get_therapy_sessions_to_invoice(patient, company): therapy_sessions_to_invoice = [] - therapy_plans = frappe.db.get_all('Therapy Plan', {'therapy_plan_template': ('!=', '')}) + therapy_plans = frappe.db.get_all("Therapy Plan", {"therapy_plan_template": ("!=", "")}) therapy_plans_created_from_template = [] for entry in therapy_plans: therapy_plans_created_from_template.append(entry.name) therapy_sessions = frappe.get_list( - 'Therapy Session', - fields='*', + "Therapy Session", + fields="*", filters={ - 'patient': patient.name, - 'invoiced': 0, - 'company': company, - 'therapy_plan': ('not in', therapy_plans_created_from_template) - } + "patient": patient.name, + "invoiced": 0, + "company": company, + "therapy_plan": ("not in", therapy_plans_created_from_template), + }, ) for therapy in therapy_sessions: if not therapy.appointment: - if therapy.therapy_type and frappe.db.get_value('Therapy Type', therapy.therapy_type, 'is_billable'): - therapy_sessions_to_invoice.append({ - 'reference_type': 'Therapy Session', - 'reference_name': therapy.name, - 'service': frappe.db.get_value('Therapy Type', therapy.therapy_type, 'item') - }) + if therapy.therapy_type and frappe.db.get_value( + "Therapy Type", therapy.therapy_type, "is_billable" + ): + therapy_sessions_to_invoice.append( + { + "reference_type": "Therapy Session", + "reference_name": therapy.name, + "service": frappe.db.get_value("Therapy Type", therapy.therapy_type, "item"), + } + ) return therapy_sessions_to_invoice + @frappe.whitelist() def get_service_item_and_practitioner_charge(doc): if isinstance(doc, str): @@ -319,12 +372,14 @@ def get_service_item_and_practitioner_charge(doc): service_item = None practitioner_charge = None - department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department + department = doc.medical_department if doc.doctype == "Patient Encounter" else doc.department is_inpatient = doc.inpatient_record - if doc.get('appointment_type'): - service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient) + if doc.get("appointment_type"): + service_item, practitioner_charge = get_appointment_type_service_item( + doc.appointment_type, department, is_inpatient + ) if not service_item and not practitioner_charge: service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient) @@ -337,7 +392,7 @@ def get_service_item_and_practitioner_charge(doc): if not practitioner_charge: throw_config_practitioner_charge(is_inpatient, doc.practitioner) - return {'service_item': service_item, 'practitioner_charge': practitioner_charge} + return {"service_item": service_item, "practitioner_charge": practitioner_charge} def get_appointment_type_service_item(appointment_type, department, is_inpatient): @@ -351,33 +406,37 @@ def get_appointment_type_service_item(appointment_type, department, is_inpatient if item_list: if is_inpatient: - service_item = item_list.get('inpatient_visit_charge_item') - practitioner_charge = item_list.get('inpatient_visit_charge') + service_item = item_list.get("inpatient_visit_charge_item") + practitioner_charge = item_list.get("inpatient_visit_charge") else: - service_item = item_list.get('op_consulting_charge_item') - practitioner_charge = item_list.get('op_consulting_charge') + service_item = item_list.get("op_consulting_charge_item") + practitioner_charge = item_list.get("op_consulting_charge") return service_item, practitioner_charge def throw_config_service_item(is_inpatient): - service_item_label = _('Out Patient Consulting Charge Item') + service_item_label = _("Out Patient Consulting Charge Item") if is_inpatient: - service_item_label = _('Inpatient Visit Charge Item') + service_item_label = _("Inpatient Visit Charge Item") - msg = _(('Please Configure {0} in ').format(service_item_label) \ - + '''Healthcare Settings''') - frappe.throw(msg, title=_('Missing Configuration')) + msg = _( + ("Please Configure {0} in ").format(service_item_label) + + """Healthcare Settings""" + ) + frappe.throw(msg, title=_("Missing Configuration")) def throw_config_practitioner_charge(is_inpatient, practitioner): - charge_name = _('OP Consulting Charge') + charge_name = _("OP Consulting Charge") if is_inpatient: - charge_name = _('Inpatient Visit Charge') + charge_name = _("Inpatient Visit Charge") - msg = _(('Please Configure {0} for Healthcare Practitioner').format(charge_name) \ - + ''' {0}'''.format(practitioner)) - frappe.throw(msg, title=_('Missing Configuration')) + msg = _( + ("Please Configure {0} for Healthcare Practitioner").format(charge_name) + + """ {0}""".format(practitioner) + ) + frappe.throw(msg, title=_("Missing Configuration")) def get_practitioner_service_item(practitioner, is_inpatient): @@ -385,9 +444,15 @@ def get_practitioner_service_item(practitioner, is_inpatient): practitioner_charge = None if is_inpatient: - service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge']) + service_item, practitioner_charge = frappe.db.get_value( + "Healthcare Practitioner", + practitioner, + ["inpatient_visit_charge_item", "inpatient_visit_charge"], + ) else: - service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge']) + service_item, practitioner_charge = frappe.db.get_value( + "Healthcare Practitioner", practitioner, ["op_consulting_charge_item", "op_consulting_charge"] + ) return service_item, practitioner_charge @@ -396,18 +461,22 @@ def get_healthcare_service_item(is_inpatient): service_item = None if is_inpatient: - service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item') + service_item = frappe.db.get_single_value("Healthcare Settings", "inpatient_visit_charge_item") else: - service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item') + service_item = frappe.db.get_single_value("Healthcare Settings", "op_consulting_charge_item") return service_item def get_practitioner_charge(practitioner, is_inpatient): if is_inpatient: - practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, 'inpatient_visit_charge') + practitioner_charge = frappe.db.get_value( + "Healthcare Practitioner", practitioner, "inpatient_visit_charge" + ) else: - practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, 'op_consulting_charge') + practitioner_charge = frappe.db.get_value( + "Healthcare Practitioner", practitioner, "op_consulting_charge" + ) if practitioner_charge: return practitioner_charge return False @@ -416,75 +485,92 @@ def get_practitioner_charge(practitioner, is_inpatient): def manage_invoice_submit_cancel(doc, method): if doc.items: for item in doc.items: - if item.get('reference_dt') and item.get('reference_dn'): - if frappe.get_meta(item.reference_dt).has_field('invoiced'): + if item.get("reference_dt") and item.get("reference_dn"): + if frappe.get_meta(item.reference_dt).has_field("invoiced"): set_invoiced(item, method, doc.name) - if method=='on_submit' and frappe.db.get_single_value('Healthcare Settings', 'create_lab_test_on_si_submit'): - create_multiple('Sales Invoice', doc.name) + if method == "on_submit" and frappe.db.get_single_value( + "Healthcare Settings", "create_lab_test_on_si_submit" + ): + create_multiple("Sales Invoice", doc.name) def set_invoiced(item, method, ref_invoice=None): invoiced = False - if method=='on_submit': + if method == "on_submit": validate_invoiced_on_submit(item) invoiced = True - if item.reference_dt == 'Clinical Procedure': - service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') + if item.reference_dt == "Clinical Procedure": + service_item = frappe.db.get_single_value( + "Healthcare Settings", "clinical_procedure_consumable_item" + ) if service_item == item.item_code: - frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced) + frappe.db.set_value(item.reference_dt, item.reference_dn, "consumption_invoiced", invoiced) else: - frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced) + frappe.db.set_value(item.reference_dt, item.reference_dn, "invoiced", invoiced) else: - frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced) + frappe.db.set_value(item.reference_dt, item.reference_dn, "invoiced", invoiced) - if item.reference_dt == 'Patient Appointment': - if frappe.db.get_value('Patient Appointment', item.reference_dn, 'procedure_template'): - dt_from_appointment = 'Clinical Procedure' + if item.reference_dt == "Patient Appointment": + if frappe.db.get_value("Patient Appointment", item.reference_dn, "procedure_template"): + dt_from_appointment = "Clinical Procedure" else: - dt_from_appointment = 'Patient Encounter' + dt_from_appointment = "Patient Encounter" manage_doc_for_appointment(dt_from_appointment, item.reference_dn, invoiced) - elif item.reference_dt == 'Lab Prescription': - manage_prescriptions(invoiced, item.reference_dt, item.reference_dn, 'Lab Test', 'lab_test_created') + elif item.reference_dt == "Lab Prescription": + manage_prescriptions( + invoiced, item.reference_dt, item.reference_dn, "Lab Test", "lab_test_created" + ) - elif item.reference_dt == 'Procedure Prescription': - manage_prescriptions(invoiced, item.reference_dt, item.reference_dn, 'Clinical Procedure', 'procedure_created') + elif item.reference_dt == "Procedure Prescription": + manage_prescriptions( + invoiced, item.reference_dt, item.reference_dn, "Clinical Procedure", "procedure_created" + ) def validate_invoiced_on_submit(item): - if item.reference_dt == 'Clinical Procedure' and \ - frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code: - is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced') + if ( + item.reference_dt == "Clinical Procedure" + and frappe.db.get_single_value("Healthcare Settings", "clinical_procedure_consumable_item") + == item.item_code + ): + is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, "consumption_invoiced") else: - is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced') + is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, "invoiced") if is_invoiced: - frappe.throw(_('The item referenced by {0} - {1} is already invoiced').format( - item.reference_dt, item.reference_dn)) + frappe.throw( + _("The item referenced by {0} - {1} is already invoiced").format( + item.reference_dt, item.reference_dn + ) + ) def manage_prescriptions(invoiced, ref_dt, ref_dn, dt, created_check_field): created = frappe.db.get_value(ref_dt, ref_dn, created_check_field) if created: # Fetch the doc created for the prescription - doc_created = frappe.db.get_value(dt, {'prescription': ref_dn}) - frappe.db.set_value(dt, doc_created, 'invoiced', invoiced) + doc_created = frappe.db.get_value(dt, {"prescription": ref_dn}) + frappe.db.set_value(dt, doc_created, "invoiced", invoiced) def check_fee_validity(appointment): - if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): + if not frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups"): return - validity = frappe.db.exists('Fee Validity', { - 'practitioner': appointment.practitioner, - 'patient': appointment.patient, - 'valid_till': ('>=', appointment.appointment_date) - }) + validity = frappe.db.exists( + "Fee Validity", + { + "practitioner": appointment.practitioner, + "patient": appointment.patient, + "valid_till": (">=", appointment.appointment_date), + }, + ) if not validity: return - validity = frappe.get_doc('Fee Validity', validity) + validity = frappe.get_doc("Fee Validity", validity) return validity @@ -492,16 +578,14 @@ def manage_fee_validity(appointment): fee_validity = check_fee_validity(appointment) if fee_validity: - if appointment.status == 'Cancelled' and fee_validity.visited > 0: + if appointment.status == "Cancelled" and fee_validity.visited > 0: fee_validity.visited -= 1 - frappe.db.delete('Fee Validity Reference', {'appointment': appointment.name}) - elif fee_validity.status == 'Completed': + frappe.db.delete("Fee Validity Reference", {"appointment": appointment.name}) + elif fee_validity.status == "Completed": return else: fee_validity.visited += 1 - fee_validity.append('ref_appointments', { - 'appointment': appointment.name - }) + fee_validity.append("ref_appointments", {"appointment": appointment.name}) fee_validity.save(ignore_permissions=True) else: fee_validity = create_fee_validity(appointment) @@ -510,36 +594,33 @@ def manage_fee_validity(appointment): def manage_doc_for_appointment(dt_from_appointment, appointment, invoiced): dn_from_appointment = frappe.db.get_value( - dt_from_appointment, - filters={'appointment': appointment} + dt_from_appointment, filters={"appointment": appointment} ) if dn_from_appointment: - frappe.db.set_value(dt_from_appointment, dn_from_appointment, 'invoiced', invoiced) + frappe.db.set_value(dt_from_appointment, dn_from_appointment, "invoiced", invoiced) @frappe.whitelist() def get_drugs_to_invoice(encounter): - encounter = frappe.get_doc('Patient Encounter', encounter) + encounter = frappe.get_doc("Patient Encounter", encounter) if encounter: - patient = frappe.get_doc('Patient', encounter.patient) + patient = frappe.get_doc("Patient", encounter.patient) if patient: if patient.customer: items_to_invoice = [] for drug_line in encounter.drug_prescription: if drug_line.drug_code: qty = 1 - if frappe.db.get_value('Item', drug_line.drug_code, 'stock_uom') == 'Nos': + if frappe.db.get_value("Item", drug_line.drug_code, "stock_uom") == "Nos": qty = drug_line.get_quantity() - description = '' + description = "" if drug_line.dosage and drug_line.period: - description = _('{0} for {1}').format(drug_line.dosage, drug_line.period) + description = _("{0} for {1}").format(drug_line.dosage, drug_line.period) - items_to_invoice.append({ - 'drug_code': drug_line.drug_code, - 'quantity': qty, - 'description': description - }) + items_to_invoice.append( + {"drug_code": drug_line.drug_code, "quantity": qty, "description": description} + ) return items_to_invoice else: validate_customer_created(patient) @@ -547,52 +628,56 @@ def get_drugs_to_invoice(encounter): @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): - parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_') - fields = [ - 'name as value', - 'is_group as expandable', - 'lft', - 'rgt' - ] + parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") + fields = ["name as value", "is_group as expandable", "lft", "rgt"] - filters = [["ifnull(`{0}`,'')".format(parent_fieldname), - '=', '' if is_root else parent]] + filters = [["ifnull(`{0}`,'')".format(parent_fieldname), "=", "" if is_root else parent]] if is_root: - fields += ['service_unit_type'] if doctype == 'Healthcare Service Unit' else [] - filters.append(['company', '=', company]) + fields += ["service_unit_type"] if doctype == "Healthcare Service Unit" else [] + filters.append(["company", "=", company]) else: - fields += ['service_unit_type', 'allow_appointments', 'inpatient_occupancy', - 'occupancy_status'] if doctype == 'Healthcare Service Unit' else [] - fields += [parent_fieldname + ' as parent'] + fields += ( + ["service_unit_type", "allow_appointments", "inpatient_occupancy", "occupancy_status"] + if doctype == "Healthcare Service Unit" + else [] + ) + fields += [parent_fieldname + " as parent"] service_units = frappe.get_list(doctype, fields=fields, filters=filters) for each in service_units: - if each['expandable'] == 1: # group node - available_count = frappe.db.count('Healthcare Service Unit', filters={ - 'parent_healthcare_service_unit': each['value'], - 'inpatient_occupancy': 1}) + if each["expandable"] == 1: # group node + available_count = frappe.db.count( + "Healthcare Service Unit", + filters={"parent_healthcare_service_unit": each["value"], "inpatient_occupancy": 1}, + ) if available_count > 0: - occupied_count = frappe.db.count('Healthcare Service Unit', { - 'parent_healthcare_service_unit': each['value'], - 'inpatient_occupancy': 1, - 'occupancy_status': 'Occupied'}) + occupied_count = frappe.db.count( + "Healthcare Service Unit", + { + "parent_healthcare_service_unit": each["value"], + "inpatient_occupancy": 1, + "occupancy_status": "Occupied", + }, + ) # set occupancy status of group node - each['occupied_of_available'] = str( - occupied_count) + ' Occupied of ' + str(available_count) + each["occupied_of_available"] = str(occupied_count) + " Occupied of " + str(available_count) return service_units @frappe.whitelist() def get_patient_vitals(patient, from_date=None, to_date=None): - if not patient: return + if not patient: + return - vitals = frappe.db.get_all('Vital Signs', filters={ - 'docstatus': 1, - 'patient': patient - }, order_by='signs_date, signs_time', fields=['*']) + vitals = frappe.db.get_all( + "Vital Signs", + filters={"docstatus": 1, "patient": patient}, + order_by="signs_date, signs_time", + fields=["*"], + ) if len(vitals): return vitals @@ -604,14 +689,14 @@ def render_docs_as_html(docs): # docs key value pair {doctype: docname} docs_html = "
" for doc in docs: - docs_html += render_doc_as_html(doc['doctype'], doc['docname'])['html'] + '
' - return {'html': docs_html} + docs_html += render_doc_as_html(doc["doctype"], doc["docname"])["html"] + "
" + return {"html": docs_html} @frappe.whitelist() -def render_doc_as_html(doctype, docname, exclude_fields = None): +def render_doc_as_html(doctype, docname, exclude_fields=None): """ - Render document as HTML + Render document as HTML """ doc = frappe.get_doc(doctype, docname) @@ -642,7 +727,9 @@ def render_doc_as_html(doctype, docname, exclude_fields = None): {1} {2}
- """.format(section_label, section_html, html) + """.format( + section_label, section_html, html + ) # close divs for columns while col_on: @@ -672,7 +759,9 @@ def render_doc_as_html(doctype, docname, exclude_fields = None):
{1}
- """.format(section_label, html) + """.format( + section_label, html + ) elif col_on == 1 and has_data: section_html += "
" + html + "
" elif col_on > 1 and has_data: @@ -684,7 +773,9 @@ def render_doc_as_html(doctype, docname, exclude_fields = None): {0}
- """.format(html) + """.format( + html + ) html = "" col_on += 1 @@ -726,21 +817,31 @@ def render_doc_as_html(doctype, docname, exclude_fields = None): {0} {1}
- """.format(table_head, table_row) + """.format( + table_head, table_row + ) else: html += """ {0} {1}
- """.format(table_head, table_row) + """.format( + table_head, table_row + ) continue # on any other field type add label and value to html - if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields: + if ( + not df.hidden + and not df.print_hide + and doc.get(df.fieldname) + and df.fieldname not in exclude_fields + ): formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc) html += "
{0} : {1}".format(df.label or df.fieldname, formatted_value) - if not has_data : has_data = True + if not has_data: + has_data = True if sec_on and col_on and has_data: doc_html += section_html + html + "" @@ -751,49 +852,53 @@ def render_doc_as_html(doctype, docname, exclude_fields = None): {0} {1} - """.format(section_html, html) + """.format( + section_html, html + ) return {"html": doc_html} def update_address_links(address, method): - ''' + """ Hook validate Address If Patient is linked in Address, also link the associated Customer - ''' - if 'Healthcare' not in frappe.get_active_domains(): + """ + if "Healthcare" not in frappe.get_active_domains(): return - patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', address.links)) + patient_links = list(filter(lambda link: link.get("link_doctype") == "Patient", address.links)) for link in patient_links: - customer = frappe.db.get_value('Patient', link.get('link_name'), 'customer') - if customer and not address.has_link('Customer', customer): - address.append('links', dict(link_doctype = 'Customer', link_name = customer)) + customer = frappe.db.get_value("Patient", link.get("link_name"), "customer") + if customer and not address.has_link("Customer", customer): + address.append("links", dict(link_doctype="Customer", link_name=customer)) def update_patient_email_and_phone_numbers(contact, method): - ''' + """ Hook validate Contact Update linked Patients' primary mobile and phone numbers - ''' - if 'Healthcare' not in frappe.get_active_domains() or contact.flags.skip_patient_update: + """ + if "Healthcare" not in frappe.get_active_domains() or contact.flags.skip_patient_update: return if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone): - patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', contact.links)) + patient_links = list(filter(lambda link: link.get("link_doctype") == "Patient", contact.links)) for link in patient_links: - contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1) + contact_details = frappe.db.get_value( + "Patient", link.get("link_name"), ["email", "mobile", "phone"], as_dict=1 + ) new_contact_details = {} - if contact.email_id and contact.email_id != contact_details.get('email'): - new_contact_details.update({'email': contact.email_id}) - if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'): - new_contact_details.update({'mobile': contact.mobile_no}) - if contact.phone and contact.phone != contact_details.get('phone'): - new_contact_details.update({'phone': contact.phone}) + if contact.email_id and contact.email_id != contact_details.get("email"): + new_contact_details.update({"email": contact.email_id}) + if contact.mobile_no and contact.mobile_no != contact_details.get("mobile"): + new_contact_details.update({"mobile": contact.mobile_no}) + if contact.phone and contact.phone != contact_details.get("phone"): + new_contact_details.update({"phone": contact.phone}) if new_contact_details: - frappe.db.set_value('Patient', link.get('link_name'), new_contact_details) + frappe.db.set_value("Patient", link.get("link_name"), new_contact_details) diff --git a/erpnext/healthcare/web_form/lab_test/lab_test.py b/erpnext/healthcare/web_form/lab_test/lab_test.py index 94ffbcfa2f2..1bccbd4f4e7 100644 --- a/erpnext/healthcare/web_form/lab_test/lab_test.py +++ b/erpnext/healthcare/web_form/lab_test/lab_test.py @@ -1,22 +1,31 @@ - import frappe def get_context(context): context.read_only = 1 + def get_list_context(context): context.row_template = "erpnext/templates/includes/healthcare/lab_test_row_template.html" context.get_list = get_lab_test_list -def get_lab_test_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'): + +def get_lab_test_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc" +): patient = get_patient() - lab_tests = frappe.db.sql("""select * from `tabLab Test` - where patient = %s order by result_date""", patient, as_dict = True) + lab_tests = frappe.db.sql( + """select * from `tabLab Test` + where patient = %s order by result_date""", + patient, + as_dict=True, + ) return lab_tests + def get_patient(): - return frappe.get_value("Patient",{"email": frappe.session.user}, "name") + return frappe.get_value("Patient", {"email": frappe.session.user}, "name") + def has_website_permission(doc, ptype, user, verbose=False): if doc.patient == get_patient(): diff --git a/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py b/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py index 9f0903cece7..89e8eb168f3 100644 --- a/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py +++ b/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py @@ -1,22 +1,31 @@ - import frappe def get_context(context): context.read_only = 1 + def get_list_context(context): context.row_template = "erpnext/templates/includes/healthcare/appointment_row_template.html" context.get_list = get_appointment_list -def get_appointment_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'): + +def get_appointment_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc" +): patient = get_patient() - lab_tests = frappe.db.sql("""select * from `tabPatient Appointment` - where patient = %s and (status = 'Open' or status = 'Scheduled') order by appointment_date""", patient, as_dict = True) + lab_tests = frappe.db.sql( + """select * from `tabPatient Appointment` + where patient = %s and (status = 'Open' or status = 'Scheduled') order by appointment_date""", + patient, + as_dict=True, + ) return lab_tests + def get_patient(): - return frappe.get_value("Patient",{"email": frappe.session.user}, "name") + return frappe.get_value("Patient", {"email": frappe.session.user}, "name") + def has_website_permission(doc, ptype, user, verbose=False): if doc.patient == get_patient(): diff --git a/erpnext/healthcare/web_form/patient_registration/patient_registration.py b/erpnext/healthcare/web_form/patient_registration/patient_registration.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/healthcare/web_form/patient_registration/patient_registration.py +++ b/erpnext/healthcare/web_form/patient_registration/patient_registration.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/healthcare/web_form/personal_details/personal_details.py b/erpnext/healthcare/web_form/personal_details/personal_details.py index fc8e8c0a5b7..dfbd73eeec1 100644 --- a/erpnext/healthcare/web_form/personal_details/personal_details.py +++ b/erpnext/healthcare/web_form/personal_details/personal_details.py @@ -1,23 +1,25 @@ - import frappe from frappe import _ no_cache = 1 + def get_context(context): - if frappe.session.user=='Guest': + if frappe.session.user == "Guest": frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError) - context.show_sidebar=True + context.show_sidebar = True - if frappe.db.exists("Patient", {'email': frappe.session.user}): - patient = frappe.get_doc("Patient", {'email': frappe.session.user}) + if frappe.db.exists("Patient", {"email": frappe.session.user}): + patient = frappe.get_doc("Patient", {"email": frappe.session.user}) context.doc = patient frappe.form_dict.new = 0 frappe.form_dict.name = patient.name + def get_patient(): - return frappe.get_value("Patient",{"email": frappe.session.user}, "name") + return frappe.get_value("Patient", {"email": frappe.session.user}, "name") + def has_website_permission(doc, ptype, user, verbose=False): if doc.name == get_patient(): diff --git a/erpnext/healthcare/web_form/prescription/prescription.py b/erpnext/healthcare/web_form/prescription/prescription.py index 0e1bc3d5dd6..44caeb00b59 100644 --- a/erpnext/healthcare/web_form/prescription/prescription.py +++ b/erpnext/healthcare/web_form/prescription/prescription.py @@ -1,22 +1,31 @@ - import frappe def get_context(context): context.read_only = 1 + def get_list_context(context): context.row_template = "erpnext/templates/includes/healthcare/prescription_row_template.html" context.get_list = get_encounter_list -def get_encounter_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'): + +def get_encounter_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc" +): patient = get_patient() - encounters = frappe.db.sql("""select * from `tabPatient Encounter` - where patient = %s order by creation desc""", patient, as_dict = True) + encounters = frappe.db.sql( + """select * from `tabPatient Encounter` + where patient = %s order by creation desc""", + patient, + as_dict=True, + ) return encounters + def get_patient(): - return frappe.get_value("Patient",{"email": frappe.session.user}, "name") + return frappe.get_value("Patient", {"email": frappe.session.user}, "name") + def has_website_permission(doc, ptype, user, verbose=False): if doc.patient == get_patient(): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 00e46393590..ff120a52926 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -1,4 +1,3 @@ - from frappe import _ app_name = "erpnext" @@ -13,7 +12,7 @@ source_link = "https://github.com/frappe/erpnext" app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" -develop_version = '13.x.x-develop' +develop_version = "13.x.x-develop" app_include_js = "/assets/js/erpnext.min.js" app_include_css = "/assets/css/erpnext.css" @@ -25,12 +24,10 @@ doctype_js = { "Communication": "public/js/communication.js", "Event": "public/js/event.js", "Newsletter": "public/js/newsletter.js", - "Contact": "public/js/contact.js" + "Contact": "public/js/contact.js", } -override_doctype_class = { - 'Address': 'erpnext.accounts.custom.address.ERPNextAddress' -} +override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} welcome_email = "erpnext.setup.utils.welcome_email" @@ -51,146 +48,266 @@ additional_print_settings = "erpnext.controllers.print_settings.get_print_settin on_session_creation = [ "erpnext.portal.utils.create_customer_or_supplier", - "erpnext.e_commerce.shopping_cart.utils.set_cart_count" + "erpnext.e_commerce.shopping_cart.utils.set_cart_count", ] on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count" -treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Group', 'Sales Person', 'Territory', 'Assessment Group', 'Department'] +treeviews = [ + "Account", + "Cost Center", + "Warehouse", + "Item Group", + "Customer Group", + "Sales Person", + "Territory", + "Assessment Group", + "Department", +] # website -update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"] +update_website_context = [ + "erpnext.e_commerce.shopping_cart.utils.update_website_context", + "erpnext.education.doctype.education_settings.education_settings.update_website_context", +] my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context" webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context" -calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"] +calendars = [ + "Task", + "Work Order", + "Leave Application", + "Sales Order", + "Holiday List", + "Course Schedule", +] domains = { - 'Agriculture': 'erpnext.domains.agriculture', - 'Distribution': 'erpnext.domains.distribution', - 'Education': 'erpnext.domains.education', - 'Healthcare': 'erpnext.domains.healthcare', - 'Hospitality': 'erpnext.domains.hospitality', - 'Manufacturing': 'erpnext.domains.manufacturing', - 'Non Profit': 'erpnext.domains.non_profit', - 'Retail': 'erpnext.domains.retail', - 'Services': 'erpnext.domains.services', + "Agriculture": "erpnext.domains.agriculture", + "Distribution": "erpnext.domains.distribution", + "Education": "erpnext.domains.education", + "Healthcare": "erpnext.domains.healthcare", + "Hospitality": "erpnext.domains.hospitality", + "Manufacturing": "erpnext.domains.manufacturing", + "Non Profit": "erpnext.domains.non_profit", + "Retail": "erpnext.domains.retail", + "Services": "erpnext.domains.services", } -website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner", - "Job Opening", "Student Admission"] +website_generators = [ + "Item Group", + "Website Item", + "BOM", + "Sales Partner", + "Job Opening", + "Student Admission", +] website_context = { - "favicon": "/assets/erpnext/images/erpnext-favicon.svg", - "splash_image": "/assets/erpnext/images/erpnext-logo.svg" + "favicon": "/assets/erpnext/images/erpnext-favicon.svg", + "splash_image": "/assets/erpnext/images/erpnext-logo.svg", } website_route_rules = [ {"from_route": "/orders", "to_route": "Sales Order"}, - {"from_route": "/orders/", "to_route": "order", - "defaults": { - "doctype": "Sales Order", - "parents": [{"label": _("Orders"), "route": "orders"}] - } + { + "from_route": "/orders/", + "to_route": "order", + "defaults": {"doctype": "Sales Order", "parents": [{"label": _("Orders"), "route": "orders"}]}, }, {"from_route": "/invoices", "to_route": "Sales Invoice"}, - {"from_route": "/invoices/", "to_route": "order", + { + "from_route": "/invoices/", + "to_route": "order", "defaults": { "doctype": "Sales Invoice", - "parents": [{"label": _("Invoices"), "route": "invoices"}] - } + "parents": [{"label": _("Invoices"), "route": "invoices"}], + }, }, {"from_route": "/supplier-quotations", "to_route": "Supplier Quotation"}, - {"from_route": "/supplier-quotations/", "to_route": "order", + { + "from_route": "/supplier-quotations/", + "to_route": "order", "defaults": { "doctype": "Supplier Quotation", - "parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}] - } + "parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}], + }, }, {"from_route": "/purchase-orders", "to_route": "Purchase Order"}, - {"from_route": "/purchase-orders/", "to_route": "order", + { + "from_route": "/purchase-orders/", + "to_route": "order", "defaults": { "doctype": "Purchase Order", - "parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}] - } + "parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}], + }, }, {"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"}, - {"from_route": "/purchase-invoices/", "to_route": "order", + { + "from_route": "/purchase-invoices/", + "to_route": "order", "defaults": { "doctype": "Purchase Invoice", - "parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}] - } + "parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}], + }, }, {"from_route": "/quotations", "to_route": "Quotation"}, - {"from_route": "/quotations/", "to_route": "order", + { + "from_route": "/quotations/", + "to_route": "order", "defaults": { "doctype": "Quotation", - "parents": [{"label": _("Quotations"), "route": "quotations"}] - } + "parents": [{"label": _("Quotations"), "route": "quotations"}], + }, }, {"from_route": "/shipments", "to_route": "Delivery Note"}, - {"from_route": "/shipments/", "to_route": "order", + { + "from_route": "/shipments/", + "to_route": "order", "defaults": { "doctype": "Delivery Note", - "parents": [{"label": _("Shipments"), "route": "shipments"}] - } + "parents": [{"label": _("Shipments"), "route": "shipments"}], + }, }, {"from_route": "/rfq", "to_route": "Request for Quotation"}, - {"from_route": "/rfq/", "to_route": "rfq", + { + "from_route": "/rfq/", + "to_route": "rfq", "defaults": { "doctype": "Request for Quotation", - "parents": [{"label": _("Request for Quotation"), "route": "rfq"}] - } + "parents": [{"label": _("Request for Quotation"), "route": "rfq"}], + }, }, {"from_route": "/addresses", "to_route": "Address"}, - {"from_route": "/addresses/", "to_route": "addresses", - "defaults": { - "doctype": "Address", - "parents": [{"label": _("Addresses"), "route": "addresses"}] - } + { + "from_route": "/addresses/", + "to_route": "addresses", + "defaults": {"doctype": "Address", "parents": [{"label": _("Addresses"), "route": "addresses"}]}, }, {"from_route": "/jobs", "to_route": "Job Opening"}, {"from_route": "/admissions", "to_route": "Student Admission"}, {"from_route": "/boms", "to_route": "BOM"}, {"from_route": "/timesheets", "to_route": "Timesheet"}, {"from_route": "/material-requests", "to_route": "Material Request"}, - {"from_route": "/material-requests/", "to_route": "material_request_info", + { + "from_route": "/material-requests/", + "to_route": "material_request_info", "defaults": { "doctype": "Material Request", - "parents": [{"label": _("Material Request"), "route": "material-requests"}] - } + "parents": [{"label": _("Material Request"), "route": "material-requests"}], + }, }, - {"from_route": "/project", "to_route": "Project"} + {"from_route": "/project", "to_route": "Project"}, ] standard_portal_menu_items = [ - {"title": _("Personal Details"), "route": "/personal-details", "reference_doctype": "Patient", "role": "Patient"}, + { + "title": _("Personal Details"), + "route": "/personal-details", + "reference_doctype": "Patient", + "role": "Patient", + }, {"title": _("Projects"), "route": "/project", "reference_doctype": "Project"}, - {"title": _("Request for Quotations"), "route": "/rfq", "reference_doctype": "Request for Quotation", "role": "Supplier"}, - {"title": _("Supplier Quotation"), "route": "/supplier-quotations", "reference_doctype": "Supplier Quotation", "role": "Supplier"}, - {"title": _("Purchase Orders"), "route": "/purchase-orders", "reference_doctype": "Purchase Order", "role": "Supplier"}, - {"title": _("Purchase Invoices"), "route": "/purchase-invoices", "reference_doctype": "Purchase Invoice", "role": "Supplier"}, - {"title": _("Quotations"), "route": "/quotations", "reference_doctype": "Quotation", "role":"Customer"}, - {"title": _("Orders"), "route": "/orders", "reference_doctype": "Sales Order", "role":"Customer"}, - {"title": _("Invoices"), "route": "/invoices", "reference_doctype": "Sales Invoice", "role":"Customer"}, - {"title": _("Shipments"), "route": "/shipments", "reference_doctype": "Delivery Note", "role":"Customer"}, - {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role":"Customer"}, + { + "title": _("Request for Quotations"), + "route": "/rfq", + "reference_doctype": "Request for Quotation", + "role": "Supplier", + }, + { + "title": _("Supplier Quotation"), + "route": "/supplier-quotations", + "reference_doctype": "Supplier Quotation", + "role": "Supplier", + }, + { + "title": _("Purchase Orders"), + "route": "/purchase-orders", + "reference_doctype": "Purchase Order", + "role": "Supplier", + }, + { + "title": _("Purchase Invoices"), + "route": "/purchase-invoices", + "reference_doctype": "Purchase Invoice", + "role": "Supplier", + }, + { + "title": _("Quotations"), + "route": "/quotations", + "reference_doctype": "Quotation", + "role": "Customer", + }, + { + "title": _("Orders"), + "route": "/orders", + "reference_doctype": "Sales Order", + "role": "Customer", + }, + { + "title": _("Invoices"), + "route": "/invoices", + "reference_doctype": "Sales Invoice", + "role": "Customer", + }, + { + "title": _("Shipments"), + "route": "/shipments", + "reference_doctype": "Delivery Note", + "role": "Customer", + }, + {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role": "Customer"}, {"title": _("Addresses"), "route": "/addresses", "reference_doctype": "Address"}, - {"title": _("Timesheets"), "route": "/timesheets", "reference_doctype": "Timesheet", "role":"Customer"}, - {"title": _("Lab Test"), "route": "/lab-test", "reference_doctype": "Lab Test", "role":"Patient"}, - {"title": _("Prescription"), "route": "/prescription", "reference_doctype": "Patient Encounter", "role":"Patient"}, - {"title": _("Patient Appointment"), "route": "/patient-appointments", "reference_doctype": "Patient Appointment", "role":"Patient"}, - {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role":"Student"}, + { + "title": _("Timesheets"), + "route": "/timesheets", + "reference_doctype": "Timesheet", + "role": "Customer", + }, + { + "title": _("Lab Test"), + "route": "/lab-test", + "reference_doctype": "Lab Test", + "role": "Patient", + }, + { + "title": _("Prescription"), + "route": "/prescription", + "reference_doctype": "Patient Encounter", + "role": "Patient", + }, + { + "title": _("Patient Appointment"), + "route": "/patient-appointments", + "reference_doctype": "Patient Appointment", + "role": "Patient", + }, + {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role": "Student"}, {"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"}, - {"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"}, - {"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"}, - {"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"}, + { + "title": _("Admission"), + "route": "/admissions", + "reference_doctype": "Student Admission", + "role": "Student", + }, + { + "title": _("Certification"), + "route": "/certification", + "reference_doctype": "Certification Application", + "role": "Non Profit Portal User", + }, + { + "title": _("Material Request"), + "route": "/material-requests", + "reference_doctype": "Material Request", + "role": "Customer", + }, {"title": _("Appointment Booking"), "route": "/book_appointment"}, ] default_roles = [ - {'role': 'Customer', 'doctype':'Contact', 'email_field': 'email_id'}, - {'role': 'Supplier', 'doctype':'Contact', 'email_field': 'email_id'}, - {'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'}, + {"role": "Customer", "doctype": "Contact", "email_field": "email_id"}, + {"role": "Supplier", "doctype": "Contact", "email_field": "email_id"}, + {"role": "Student", "doctype": "Student", "email_field": "student_email_id"}, ] sounds = [ @@ -198,9 +315,7 @@ sounds = [ {"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2}, ] -has_upload_permission = { - "Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission" -} +has_upload_permission = {"Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission"} has_website_permission = { "Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission", @@ -216,7 +331,7 @@ has_website_permission = { "Lab Test": "erpnext.healthcare.web_form.lab_test.lab_test.has_website_permission", "Patient Encounter": "erpnext.healthcare.web_form.prescription.prescription.has_website_permission", "Patient Appointment": "erpnext.healthcare.web_form.patient_appointments.patient_appointments.has_website_permission", - "Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission" + "Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission", } dump_report_map = "erpnext.startup.report_data_map.data_map" @@ -225,112 +340,115 @@ before_tests = "erpnext.setup.utils.before_tests" standard_queries = { "Customer": "erpnext.selling.doctype.customer.customer.get_customer_list", - "Healthcare Practitioner": "erpnext.healthcare.doctype.healthcare_practitioner.healthcare_practitioner.get_practitioner_list" + "Healthcare Practitioner": "erpnext.healthcare.doctype.healthcare_practitioner.healthcare_practitioner.get_practitioner_list", } doc_events = { "*": { "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", "on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record", - "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record" + "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record", }, "Stock Entry": { "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", - "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty" + "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", }, "User": { "after_insert": "frappe.contacts.doctype.contact.contact.update_contact", "validate": "erpnext.hr.doctype.employee.employee.validate_employee_role", - "on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions", - "erpnext.portal.utils.set_default_role"] - }, - "Communication": { "on_update": [ - "erpnext.support.doctype.issue.issue.set_first_response_time" - ] + "erpnext.hr.doctype.employee.employee.update_user_permissions", + "erpnext.portal.utils.set_default_role", + ], }, + "Communication": {"on_update": ["erpnext.support.doctype.issue.issue.set_first_response_time"]}, "Sales Taxes and Charges Template": { "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings" }, - "Tax Category": { - "validate": "erpnext.regional.india.utils.validate_tax_category" - }, + "Tax Category": {"validate": "erpnext.regional.india.utils.validate_tax_category"}, "Sales Invoice": { "on_submit": [ "erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit", "erpnext.regional.saudi_arabia.utils.create_qr_code", - "erpnext.erpnext_integrations.taxjar_integration.create_transaction" + "erpnext.erpnext_integrations.taxjar_integration.create_transaction", ], "on_cancel": [ "erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.erpnext_integrations.taxjar_integration.delete_transaction", - "erpnext.regional.saudi_arabia.utils.delete_qr_code_file" + "erpnext.regional.saudi_arabia.utils.delete_qr_code_file", ], "on_trash": "erpnext.regional.check_deletion_permission", "validate": [ "erpnext.regional.india.utils.validate_document_name", - "erpnext.regional.india.utils.update_taxable_values" - ] - }, - "POS Invoice": { - "on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"] + "erpnext.regional.india.utils.update_taxable_values", + ], }, + "POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]}, "Purchase Invoice": { "validate": [ "erpnext.regional.india.utils.validate_reverse_charge_transaction", "erpnext.regional.india.utils.update_itc_availed_fields", "erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm", "erpnext.regional.united_arab_emirates.utils.validate_returns", - "erpnext.regional.india.utils.update_taxable_values" + "erpnext.regional.india.utils.update_taxable_values", ] }, "Payment Entry": { "validate": "erpnext.regional.india.utils.update_place_of_supply", - "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], - "on_trash": "erpnext.regional.check_deletion_permission" + "on_submit": [ + "erpnext.regional.create_transaction_log", + "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", + "erpnext.accounts.doctype.dunning.dunning.resolve_dunning", + ], + "on_trash": "erpnext.regional.check_deletion_permission", }, - 'Address': { - 'validate': [ - 'erpnext.regional.india.utils.validate_gstin_for_india', - 'erpnext.regional.italy.utils.set_state_code', - 'erpnext.regional.india.utils.update_gst_category', - 'erpnext.healthcare.utils.update_address_links' + "Address": { + "validate": [ + "erpnext.regional.india.utils.validate_gstin_for_india", + "erpnext.regional.italy.utils.set_state_code", + "erpnext.regional.india.utils.update_gst_category", + "erpnext.healthcare.utils.update_address_links", ], }, - 'Supplier': { - 'validate': 'erpnext.regional.india.utils.validate_pan_for_india' - }, - ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { - 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] - }, + "Supplier": {"validate": "erpnext.regional.india.utils.validate_pan_for_india"}, + ( + "Sales Invoice", + "Sales Order", + "Delivery Note", + "Purchase Invoice", + "Purchase Order", + "Purchase Receipt", + ): {"validate": ["erpnext.regional.india.utils.set_place_of_supply"]}, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations", - "validate": ["erpnext.crm.utils.update_lead_phone_numbers", "erpnext.healthcare.utils.update_patient_email_and_phone_numbers"] + "validate": [ + "erpnext.crm.utils.update_lead_phone_numbers", + "erpnext.healthcare.utils.update_patient_email_and_phone_numbers", + ], }, "Email Unsubscribe": { "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" }, - ('Quotation', 'Sales Order', 'Sales Invoice'): { - 'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"] + ("Quotation", "Sales Order", "Sales Invoice"): { + "validate": ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"] }, "Company": { - "on_trash": ["erpnext.regional.india.utils.delete_gst_settings_for_company", - "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"] + "on_trash": [ + "erpnext.regional.india.utils.delete_gst_settings_for_company", + "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company", + ] }, "Integration Request": { "validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment" - } + }, } # On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled. # to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled. # if payment entry not in auto cancel exempted doctypes it will cancel payment entry. -auto_cancel_exempted_doctypes= [ - "Payment Entry", - "Inpatient Medication Entry" -] +auto_cancel_exempted_doctypes = ["Payment Entry", "Inpatient Medication Entry"] after_migrate = ["erpnext.setup.install.update_select_perm_after_install"] @@ -344,10 +462,10 @@ scheduler_events = { "erpnext.projects.doctype.project.project.project_status_update_reminder", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder", "erpnext.hr.doctype.interview.interview.send_interview_reminder", - "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts" + "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts", ], "hourly": [ - 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', + "erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails", "erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details", "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", @@ -356,7 +474,7 @@ 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" @@ -389,28 +507,24 @@ scheduler_events = { "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email", "erpnext.non_profit.doctype.membership.membership.set_expired_status", - "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder" + "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder", ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", - "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", + "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", "erpnext.hr.utils.generate_leave_encashment", "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", - "erpnext.crm.doctype.lead.lead.daily_open_lead" - ], - "weekly": [ - "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly" - ], - "monthly": [ - "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly" + "erpnext.crm.doctype.lead.lead.daily_open_lead", ], + "weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"], + "monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", - "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" - ] + "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans", + ], } email_brand_image = "assets/erpnext/images/erpnext-logo.jpg" @@ -429,63 +543,99 @@ get_translated_dict = { } bot_parsers = [ - 'erpnext.utilities.bot.FindItemBot', + "erpnext.utilities.bot.FindItemBot", ] -get_site_info = 'erpnext.utilities.get_site_info' +get_site_info = "erpnext.utilities.get_site_info" payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account" communication_doctypes = ["Customer", "Supplier"] -accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", - "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", - "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", - "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", - "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", - "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", - "Subscription Plan", "POS Invoice", "POS Invoice Item" +accounting_dimension_doctypes = [ + "GL Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Asset", + "Expense Claim", + "Expense Claim Detail", + "Expense Taxes and Charges", + "Stock Entry", + "Budget", + "Payroll Entry", + "Delivery Note", + "Sales Invoice Item", + "Purchase Invoice Item", + "Purchase Order Item", + "Journal Entry Account", + "Material Request Item", + "Delivery Note Item", + "Purchase Receipt Item", + "Stock Entry Detail", + "Payment Entry Deduction", + "Sales Taxes and Charges", + "Purchase Taxes and Charges", + "Shipping Rule", + "Landed Cost Item", + "Asset Value Adjustment", + "Loyalty Program", + "Fee Schedule", + "Fee Structure", + "Stock Reconciliation", + "Travel Request", + "Fees", + "POS Profile", + "Opening Invoice Creation Tool", + "Opening Invoice Creation Tool Item", + "Subscription", + "Subscription Plan", + "POS Invoice", + "POS Invoice Item", + "Purchase Order", + "Purchase Receipt", + "Sales Order", ] regional_overrides = { - 'France': { - 'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method' + "France": { + "erpnext.tests.test_regional.test_method": "erpnext.regional.france.utils.test_method" }, - 'India': { - 'erpnext.tests.test_regional.test_method': 'erpnext.regional.india.utils.test_method', - 'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header', - 'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data', - 'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details', - 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts', - 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', - 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', - 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', - 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', - 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' + "India": { + "erpnext.tests.test_regional.test_method": "erpnext.regional.india.utils.test_method", + "erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header": "erpnext.regional.india.utils.get_itemised_tax_breakup_header", + "erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data": "erpnext.regional.india.utils.get_itemised_tax_breakup_data", + "erpnext.accounts.party.get_regional_address_details": "erpnext.regional.india.utils.get_regional_address_details", + "erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts": "erpnext.regional.india.utils.get_regional_round_off_accounts", + "erpnext.hr.utils.calculate_annual_eligible_hra_exemption": "erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption", + "erpnext.hr.utils.calculate_hra_exemption_for_period": "erpnext.regional.india.utils.calculate_hra_exemption_for_period", + "erpnext.controllers.accounts_controller.validate_einvoice_fields": "erpnext.regional.india.e_invoice.utils.validate_einvoice_fields", + "erpnext.assets.doctype.asset.asset.get_depreciation_amount": "erpnext.regional.india.utils.get_depreciation_amount", + "erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code": "erpnext.regional.india.utils.set_item_tax_from_hsn_code", }, - 'United Arab Emirates': { - 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', - 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries', + "United Arab Emirates": { + "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data", + "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries": "erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries", }, - 'Saudi Arabia': { - 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' + "Saudi Arabia": { + "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data" + }, + "Italy": { + "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.italy.utils.update_itemised_tax_data", + "erpnext.controllers.accounts_controller.validate_regional": "erpnext.regional.italy.utils.sales_invoice_validate", }, - 'Italy': { - 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', - 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate', - } } user_privacy_documents = [ { - 'doctype': 'Lead', - 'match_field': 'email_id', - 'personal_fields': ['phone', 'mobile_no', 'fax', 'website', 'lead_name'], + "doctype": "Lead", + "match_field": "email_id", + "personal_fields": ["phone", "mobile_no", "fax", "website", "lead_name"], }, { - 'doctype': 'Opportunity', - 'match_field': 'contact_email', - 'personal_fields': ['contact_mobile', 'contact_display', 'customer_name'], - } + "doctype": "Opportunity", + "match_field": "contact_email", + "personal_fields": ["contact_mobile", "contact_display", "customer_name"], + }, ] # ERPNext doctypes for Global Search @@ -541,107 +691,107 @@ global_search_doctypes = { {"doctype": "Warranty Claim", "index": 47}, ], "Healthcare": [ - {'doctype': 'Patient', 'index': 1}, - {'doctype': 'Medical Department', 'index': 2}, - {'doctype': 'Vital Signs', 'index': 3}, - {'doctype': 'Healthcare Practitioner', 'index': 4}, - {'doctype': 'Patient Appointment', 'index': 5}, - {'doctype': 'Healthcare Service Unit', 'index': 6}, - {'doctype': 'Patient Encounter', 'index': 7}, - {'doctype': 'Antibiotic', 'index': 8}, - {'doctype': 'Diagnosis', 'index': 9}, - {'doctype': 'Lab Test', 'index': 10}, - {'doctype': 'Clinical Procedure', 'index': 11}, - {'doctype': 'Inpatient Record', 'index': 12}, - {'doctype': 'Sample Collection', 'index': 13}, - {'doctype': 'Patient Medical Record', 'index': 14}, - {'doctype': 'Appointment Type', 'index': 15}, - {'doctype': 'Fee Validity', 'index': 16}, - {'doctype': 'Practitioner Schedule', 'index': 17}, - {'doctype': 'Dosage Form', 'index': 18}, - {'doctype': 'Lab Test Sample', 'index': 19}, - {'doctype': 'Prescription Duration', 'index': 20}, - {'doctype': 'Prescription Dosage', 'index': 21}, - {'doctype': 'Sensitivity', 'index': 22}, - {'doctype': 'Complaint', 'index': 23}, - {'doctype': 'Medical Code', 'index': 24}, + {"doctype": "Patient", "index": 1}, + {"doctype": "Medical Department", "index": 2}, + {"doctype": "Vital Signs", "index": 3}, + {"doctype": "Healthcare Practitioner", "index": 4}, + {"doctype": "Patient Appointment", "index": 5}, + {"doctype": "Healthcare Service Unit", "index": 6}, + {"doctype": "Patient Encounter", "index": 7}, + {"doctype": "Antibiotic", "index": 8}, + {"doctype": "Diagnosis", "index": 9}, + {"doctype": "Lab Test", "index": 10}, + {"doctype": "Clinical Procedure", "index": 11}, + {"doctype": "Inpatient Record", "index": 12}, + {"doctype": "Sample Collection", "index": 13}, + {"doctype": "Patient Medical Record", "index": 14}, + {"doctype": "Appointment Type", "index": 15}, + {"doctype": "Fee Validity", "index": 16}, + {"doctype": "Practitioner Schedule", "index": 17}, + {"doctype": "Dosage Form", "index": 18}, + {"doctype": "Lab Test Sample", "index": 19}, + {"doctype": "Prescription Duration", "index": 20}, + {"doctype": "Prescription Dosage", "index": 21}, + {"doctype": "Sensitivity", "index": 22}, + {"doctype": "Complaint", "index": 23}, + {"doctype": "Medical Code", "index": 24}, ], "Education": [ - {'doctype': 'Article', 'index': 1}, - {'doctype': 'Video', 'index': 2}, - {'doctype': 'Topic', 'index': 3}, - {'doctype': 'Course', 'index': 4}, - {'doctype': 'Program', 'index': 5}, - {'doctype': 'Quiz', 'index': 6}, - {'doctype': 'Question', 'index': 7}, - {'doctype': 'Fee Schedule', 'index': 8}, - {'doctype': 'Fee Structure', 'index': 9}, - {'doctype': 'Fees', 'index': 10}, - {'doctype': 'Student Group', 'index': 11}, - {'doctype': 'Student', 'index': 12}, - {'doctype': 'Instructor', 'index': 13}, - {'doctype': 'Course Activity', 'index': 14}, - {'doctype': 'Quiz Activity', 'index': 15}, - {'doctype': 'Course Enrollment', 'index': 16}, - {'doctype': 'Program Enrollment', 'index': 17}, - {'doctype': 'Student Language', 'index': 18}, - {'doctype': 'Student Applicant', 'index': 19}, - {'doctype': 'Assessment Result', 'index': 20}, - {'doctype': 'Assessment Plan', 'index': 21}, - {'doctype': 'Grading Scale', 'index': 22}, - {'doctype': 'Guardian', 'index': 23}, - {'doctype': 'Student Leave Application', 'index': 24}, - {'doctype': 'Student Log', 'index': 25}, - {'doctype': 'Room', 'index': 26}, - {'doctype': 'Course Schedule', 'index': 27}, - {'doctype': 'Student Attendance', 'index': 28}, - {'doctype': 'Announcement', 'index': 29}, - {'doctype': 'Student Category', 'index': 30}, - {'doctype': 'Assessment Group', 'index': 31}, - {'doctype': 'Student Batch Name', 'index': 32}, - {'doctype': 'Assessment Criteria', 'index': 33}, - {'doctype': 'Academic Year', 'index': 34}, - {'doctype': 'Academic Term', 'index': 35}, - {'doctype': 'School House', 'index': 36}, - {'doctype': 'Student Admission', 'index': 37}, - {'doctype': 'Fee Category', 'index': 38}, - {'doctype': 'Assessment Code', 'index': 39}, - {'doctype': 'Discussion', 'index': 40}, + {"doctype": "Article", "index": 1}, + {"doctype": "Video", "index": 2}, + {"doctype": "Topic", "index": 3}, + {"doctype": "Course", "index": 4}, + {"doctype": "Program", "index": 5}, + {"doctype": "Quiz", "index": 6}, + {"doctype": "Question", "index": 7}, + {"doctype": "Fee Schedule", "index": 8}, + {"doctype": "Fee Structure", "index": 9}, + {"doctype": "Fees", "index": 10}, + {"doctype": "Student Group", "index": 11}, + {"doctype": "Student", "index": 12}, + {"doctype": "Instructor", "index": 13}, + {"doctype": "Course Activity", "index": 14}, + {"doctype": "Quiz Activity", "index": 15}, + {"doctype": "Course Enrollment", "index": 16}, + {"doctype": "Program Enrollment", "index": 17}, + {"doctype": "Student Language", "index": 18}, + {"doctype": "Student Applicant", "index": 19}, + {"doctype": "Assessment Result", "index": 20}, + {"doctype": "Assessment Plan", "index": 21}, + {"doctype": "Grading Scale", "index": 22}, + {"doctype": "Guardian", "index": 23}, + {"doctype": "Student Leave Application", "index": 24}, + {"doctype": "Student Log", "index": 25}, + {"doctype": "Room", "index": 26}, + {"doctype": "Course Schedule", "index": 27}, + {"doctype": "Student Attendance", "index": 28}, + {"doctype": "Announcement", "index": 29}, + {"doctype": "Student Category", "index": 30}, + {"doctype": "Assessment Group", "index": 31}, + {"doctype": "Student Batch Name", "index": 32}, + {"doctype": "Assessment Criteria", "index": 33}, + {"doctype": "Academic Year", "index": 34}, + {"doctype": "Academic Term", "index": 35}, + {"doctype": "School House", "index": 36}, + {"doctype": "Student Admission", "index": 37}, + {"doctype": "Fee Category", "index": 38}, + {"doctype": "Assessment Code", "index": 39}, + {"doctype": "Discussion", "index": 40}, ], "Agriculture": [ - {'doctype': 'Weather', 'index': 1}, - {'doctype': 'Soil Texture', 'index': 2}, - {'doctype': 'Water Analysis', 'index': 3}, - {'doctype': 'Soil Analysis', 'index': 4}, - {'doctype': 'Plant Analysis', 'index': 5}, - {'doctype': 'Agriculture Analysis Criteria', 'index': 6}, - {'doctype': 'Disease', 'index': 7}, - {'doctype': 'Crop', 'index': 8}, - {'doctype': 'Fertilizer', 'index': 9}, - {'doctype': 'Crop Cycle', 'index': 10} + {"doctype": "Weather", "index": 1}, + {"doctype": "Soil Texture", "index": 2}, + {"doctype": "Water Analysis", "index": 3}, + {"doctype": "Soil Analysis", "index": 4}, + {"doctype": "Plant Analysis", "index": 5}, + {"doctype": "Agriculture Analysis Criteria", "index": 6}, + {"doctype": "Disease", "index": 7}, + {"doctype": "Crop", "index": 8}, + {"doctype": "Fertilizer", "index": 9}, + {"doctype": "Crop Cycle", "index": 10}, ], "Non Profit": [ - {'doctype': 'Certified Consultant', 'index': 1}, - {'doctype': 'Certification Application', 'index': 2}, - {'doctype': 'Volunteer', 'index': 3}, - {'doctype': 'Membership', 'index': 4}, - {'doctype': 'Member', 'index': 5}, - {'doctype': 'Donor', 'index': 6}, - {'doctype': 'Chapter', 'index': 7}, - {'doctype': 'Grant Application', 'index': 8}, - {'doctype': 'Volunteer Type', 'index': 9}, - {'doctype': 'Donor Type', 'index': 10}, - {'doctype': 'Membership Type', 'index': 11} + {"doctype": "Certified Consultant", "index": 1}, + {"doctype": "Certification Application", "index": 2}, + {"doctype": "Volunteer", "index": 3}, + {"doctype": "Membership", "index": 4}, + {"doctype": "Member", "index": 5}, + {"doctype": "Donor", "index": 6}, + {"doctype": "Chapter", "index": 7}, + {"doctype": "Grant Application", "index": 8}, + {"doctype": "Volunteer Type", "index": 9}, + {"doctype": "Donor Type", "index": 10}, + {"doctype": "Membership Type", "index": 11}, ], "Hospitality": [ - {'doctype': 'Hotel Room', 'index': 0}, - {'doctype': 'Hotel Room Reservation', 'index': 1}, - {'doctype': 'Hotel Room Pricing', 'index': 2}, - {'doctype': 'Hotel Room Package', 'index': 3}, - {'doctype': 'Hotel Room Type', 'index': 4} - ] + {"doctype": "Hotel Room", "index": 0}, + {"doctype": "Hotel Room Reservation", "index": 1}, + {"doctype": "Hotel Room Pricing", "index": 2}, + {"doctype": "Hotel Room Package", "index": 3}, + {"doctype": "Hotel Room Type", "index": 4}, + ], } additional_timeline_content = { - '*': ['erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs'] + "*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"] } diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.py b/erpnext/hotels/doctype/hotel_room/hotel_room.py index e4bd1c88462..b4f826f6487 100644 --- a/erpnext/hotels/doctype/hotel_room/hotel_room.py +++ b/erpnext/hotels/doctype/hotel_room/hotel_room.py @@ -9,5 +9,6 @@ from frappe.model.document import Document class HotelRoom(Document): def validate(self): if not self.capacity: - self.capacity, self.extra_bed_capacity = frappe.db.get_value('Hotel Room Type', - self.hotel_room_type, ['capacity', 'extra_bed_capacity']) + self.capacity, self.extra_bed_capacity = frappe.db.get_value( + "Hotel Room Type", self.hotel_room_type, ["capacity", "extra_bed_capacity"] + ) diff --git a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py b/erpnext/hotels/doctype/hotel_room/test_hotel_room.py index 95efe2c6068..0fa211c40cc 100644 --- a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py +++ b/erpnext/hotels/doctype/hotel_room/test_hotel_room.py @@ -5,19 +5,14 @@ import unittest test_dependencies = ["Hotel Room Package"] test_records = [ - dict(doctype="Hotel Room", name="1001", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1002", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1003", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1004", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1005", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1006", - hotel_room_type="Basic Room") + dict(doctype="Hotel Room", name="1001", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1002", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1003", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1004", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1005", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1006", hotel_room_type="Basic Room"), ] + class TestHotelRoom(unittest.TestCase): pass diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py index aedc83a8468..160a1d368ad 100644 --- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py +++ b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py @@ -9,12 +9,10 @@ from frappe.model.document import Document class HotelRoomPackage(Document): def validate(self): if not self.item: - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = self.name, - item_group = 'Products', - is_stock_item = 0, - stock_uom = 'Unit' - )) + item = frappe.get_doc( + dict( + doctype="Item", item_code=self.name, item_group="Products", is_stock_item=0, stock_uom="Unit" + ) + ) item.insert() self.item = item.name diff --git a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py index 749731f4918..06f992106f3 100644 --- a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py +++ b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py @@ -4,44 +4,44 @@ import unittest test_records = [ - dict(doctype='Item', item_code='Breakfast', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Lunch', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Dinner', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='WiFi', - item_group='Products', is_stock_item=0), - dict(doctype='Hotel Room Type', name="Delux Room", + dict(doctype="Item", item_code="Breakfast", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Lunch", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Dinner", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="WiFi", item_group="Products", is_stock_item=0), + dict( + doctype="Hotel Room Type", + name="Delux Room", capacity=4, extra_bed_capacity=2, - amenities = [ - dict(item='WiFi', billable=0) - ]), - dict(doctype='Hotel Room Type', name="Basic Room", + amenities=[dict(item="WiFi", billable=0)], + ), + dict( + doctype="Hotel Room Type", + name="Basic Room", capacity=4, extra_bed_capacity=2, - amenities = [ - dict(item='Breakfast', billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Breakfast", + amenities=[dict(item="Breakfast", billable=0)], + ), + dict( + doctype="Hotel Room Package", + name="Basic Room with Breakfast", hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Lunch", + amenities=[dict(item="Breakfast", billable=0)], + ), + dict( + doctype="Hotel Room Package", + name="Basic Room with Lunch", hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0), - dict(item="Lunch", billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Dinner", + amenities=[dict(item="Breakfast", billable=0), dict(item="Lunch", billable=0)], + ), + dict( + doctype="Hotel Room Package", + name="Basic Room with Dinner", hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0), - dict(item="Dinner", billable=0) - ]) + amenities=[dict(item="Breakfast", billable=0), dict(item="Dinner", billable=0)], + ), ] + class TestHotelRoomPackage(unittest.TestCase): pass diff --git a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py index 34550096dd9..15752b5b1af 100644 --- a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py +++ b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py @@ -5,15 +5,20 @@ import unittest test_dependencies = ["Hotel Room Package"] test_records = [ - dict(doctype="Hotel Room Pricing", enabled=1, + dict( + doctype="Hotel Room Pricing", + enabled=1, name="Winter 2017", - from_date="2017-01-01", to_date="2017-01-10", - items = [ + from_date="2017-01-01", + to_date="2017-01-10", + items=[ dict(item="Basic Room with Breakfast", rate=10000), dict(item="Basic Room with Lunch", rate=11000), - dict(item="Basic Room with Dinner", rate=12000) - ]) + dict(item="Basic Room with Dinner", rate=12000), + ], + ) ] + class TestHotelRoomPricing(unittest.TestCase): pass diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py index 7725955396b..eaad6898a09 100644 --- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py +++ b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py @@ -10,8 +10,13 @@ from frappe.model.document import Document from frappe.utils import add_days, date_diff, flt -class HotelRoomUnavailableError(frappe.ValidationError): pass -class HotelRoomPricingNotSetError(frappe.ValidationError): pass +class HotelRoomUnavailableError(frappe.ValidationError): + pass + + +class HotelRoomPricingNotSetError(frappe.ValidationError): + pass + class HotelRoomReservation(Document): def validate(self): @@ -28,27 +33,39 @@ class HotelRoomReservation(Document): if not d.item in self.rooms_booked: self.rooms_booked[d.item] = 0 - room_type = frappe.db.get_value("Hotel Room Package", - d.item, 'hotel_room_type') - rooms_booked = get_rooms_booked(room_type, day, exclude_reservation=self.name) \ - + d.qty + self.rooms_booked.get(d.item) + room_type = frappe.db.get_value("Hotel Room Package", d.item, "hotel_room_type") + rooms_booked = ( + get_rooms_booked(room_type, day, exclude_reservation=self.name) + + d.qty + + self.rooms_booked.get(d.item) + ) total_rooms = self.get_total_rooms(d.item) if total_rooms < rooms_booked: - frappe.throw(_("Hotel Rooms of type {0} are unavailable on {1}").format(d.item, - frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomUnavailableError) + frappe.throw( + _("Hotel Rooms of type {0} are unavailable on {1}").format( + d.item, frappe.format(day, dict(fieldtype="Date")) + ), + exc=HotelRoomUnavailableError, + ) self.rooms_booked[d.item] += rooms_booked def get_total_rooms(self, item): if not item in self.total_rooms: - self.total_rooms[item] = frappe.db.sql(""" + self.total_rooms[item] = ( + frappe.db.sql( + """ select count(*) from `tabHotel Room Package` package inner join `tabHotel Room` room on package.hotel_room_type = room.hotel_room_type where - package.item = %s""", item)[0][0] or 0 + package.item = %s""", + item, + )[0][0] + or 0 + ) return self.total_rooms[item] @@ -60,7 +77,8 @@ class HotelRoomReservation(Document): day = add_days(self.from_date, i) if not d.item: continue - day_rate = frappe.db.sql(""" + day_rate = frappe.db.sql( + """ select item.rate from @@ -70,18 +88,22 @@ class HotelRoomReservation(Document): item.parent = pricing.name and item.item = %s and %s between pricing.from_date - and pricing.to_date""", (d.item, day)) + and pricing.to_date""", + (d.item, day), + ) if day_rate: net_rate += day_rate[0][0] else: frappe.throw( - _("Please set Hotel Room Rate on {}").format( - frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomPricingNotSetError) + _("Please set Hotel Room Rate on {}").format(frappe.format(day, dict(fieldtype="Date"))), + exc=HotelRoomPricingNotSetError, + ) d.rate = net_rate d.amount = net_rate * flt(d.qty) self.net_total += d.amount + @frappe.whitelist() def get_room_rate(hotel_room_reservation): """Calculate rate for each day as it may belong to different Hotel Room Pricing Item""" @@ -89,12 +111,15 @@ def get_room_rate(hotel_room_reservation): doc.set_rates() return doc.as_dict() -def get_rooms_booked(room_type, day, exclude_reservation=None): - exclude_condition = '' - if exclude_reservation: - exclude_condition = 'and reservation.name != {0}'.format(frappe.db.escape(exclude_reservation)) - return frappe.db.sql(""" +def get_rooms_booked(room_type, day, exclude_reservation=None): + exclude_condition = "" + if exclude_reservation: + exclude_condition = "and reservation.name != {0}".format(frappe.db.escape(exclude_reservation)) + + return ( + frappe.db.sql( + """ select sum(item.qty) from `tabHotel Room Package` room_package, @@ -107,5 +132,10 @@ def get_rooms_booked(room_type, day, exclude_reservation=None): and reservation.docstatus = 1 {exclude_condition} and %s between reservation.from_date - and reservation.to_date""".format(exclude_condition=exclude_condition), - (room_type, day))[0][0] or 0 + and reservation.to_date""".format( + exclude_condition=exclude_condition + ), + (room_type, day), + )[0][0] + or 0 + ) diff --git a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py index bb32a27fa7c..52e7d69f79f 100644 --- a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py +++ b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py @@ -12,6 +12,7 @@ from erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation import test_dependencies = ["Hotel Room Package", "Hotel Room Pricing", "Hotel Room"] + class TestHotelRoomReservation(unittest.TestCase): def setUp(self): frappe.db.sql("delete from `tabHotel Room Reservation`") @@ -19,22 +20,14 @@ class TestHotelRoomReservation(unittest.TestCase): def test_reservation(self): reservation = make_reservation( - from_date="2017-01-01", - to_date="2017-01-03", - items=[ - dict(item="Basic Room with Dinner", qty=2) - ] + from_date="2017-01-01", to_date="2017-01-03", items=[dict(item="Basic Room with Dinner", qty=2)] ) reservation.insert() self.assertEqual(reservation.net_total, 48000) def test_price_not_set(self): reservation = make_reservation( - from_date="2016-01-01", - to_date="2016-01-03", - items=[ - dict(item="Basic Room with Dinner", qty=2) - ] + from_date="2016-01-01", to_date="2016-01-03", items=[dict(item="Basic Room with Dinner", qty=2)] ) self.assertRaises(HotelRoomPricingNotSetError, reservation.insert) @@ -44,7 +37,7 @@ class TestHotelRoomReservation(unittest.TestCase): to_date="2017-01-03", items=[ dict(item="Basic Room with Dinner", qty=2), - ] + ], ) reservation.insert() @@ -53,10 +46,11 @@ class TestHotelRoomReservation(unittest.TestCase): to_date="2017-01-03", items=[ dict(item="Basic Room with Dinner", qty=20), - ] + ], ) self.assertRaises(HotelRoomUnavailableError, reservation.insert) + def make_reservation(**kwargs): kwargs["doctype"] = "Hotel Room Reservation" if not "guest_name" in kwargs: diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py index c43589d2a8d..ada5332bce0 100644 --- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py +++ b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py @@ -14,16 +14,18 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): columns = [ dict(label=_("Room Type"), fieldname="room_type"), - dict(label=_("Rooms Booked"), fieldtype="Int") + dict(label=_("Rooms Booked"), fieldtype="Int"), ] return columns + def get_data(filters): out = [] - for room_type in frappe.get_all('Hotel Room Type'): + for room_type in frappe.get_all("Hotel Room Type"): total_booked = 0 for i in range(date_diff(filters.to_date, filters.from_date)): day = add_days(filters.from_date, i) diff --git a/erpnext/hr/doctype/appointment_letter/appointment_letter.py b/erpnext/hr/doctype/appointment_letter/appointment_letter.py index 71327bf1b01..a58589af210 100644 --- a/erpnext/hr/doctype/appointment_letter/appointment_letter.py +++ b/erpnext/hr/doctype/appointment_letter/appointment_letter.py @@ -9,18 +9,21 @@ from frappe.model.document import Document class AppointmentLetter(Document): pass + @frappe.whitelist() def get_appointment_letter_details(template): body = [] - intro = frappe.get_list('Appointment Letter Template', - fields=['introduction', 'closing_notes'], - filters={'name': template} + intro = frappe.get_list( + "Appointment Letter Template", + fields=["introduction", "closing_notes"], + filters={"name": template}, )[0] - content = frappe.get_all('Appointment Letter content', - fields=['title', 'description'], - filters={'parent': template}, - order_by='idx' + content = frappe.get_all( + "Appointment Letter content", + fields=["title", "description"], + filters={"parent": template}, + order_by="idx", ) body.append(intro) - body.append({'description': content}) + body.append({"description": content}) return body diff --git a/erpnext/hr/doctype/appraisal/appraisal.py b/erpnext/hr/doctype/appraisal/appraisal.py index 83273f86544..382c643abae 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.py +++ b/erpnext/hr/doctype/appraisal/appraisal.py @@ -34,46 +34,61 @@ class Appraisal(Document): frappe.throw(_("End Date can not be less than Start Date")) def validate_existing_appraisal(self): - chk = frappe.db.sql("""select name from `tabAppraisal` where employee=%s + chk = frappe.db.sql( + """select name from `tabAppraisal` where employee=%s and (status='Submitted' or status='Completed') and ((start_date>=%s and start_date<=%s) or (end_date>=%s and end_date<=%s))""", - (self.employee,self.start_date,self.end_date,self.start_date,self.end_date)) + (self.employee, self.start_date, self.end_date, self.start_date, self.end_date), + ) if chk: - frappe.throw(_("Appraisal {0} created for Employee {1} in the given date range").format(chk[0][0], self.employee_name)) + frappe.throw( + _("Appraisal {0} created for Employee {1} in the given date range").format( + chk[0][0], self.employee_name + ) + ) def calculate_total(self): - total, total_w = 0, 0 - for d in self.get('goals'): + total, total_w = 0, 0 + for d in self.get("goals"): if d.score: d.score_earned = flt(d.score) * flt(d.per_weightage) / 100 total = total + d.score_earned total_w += flt(d.per_weightage) if int(total_w) != 100: - frappe.throw(_("Total weightage assigned should be 100%.
It is {0}").format(str(total_w) + "%")) + frappe.throw( + _("Total weightage assigned should be 100%.
It is {0}").format(str(total_w) + "%") + ) - if frappe.db.get_value("Employee", self.employee, "user_id") != \ - frappe.session.user and total == 0: + if ( + frappe.db.get_value("Employee", self.employee, "user_id") != frappe.session.user and total == 0 + ): frappe.throw(_("Total cannot be zero")) self.total_score = total def on_submit(self): - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") def on_cancel(self): - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") + @frappe.whitelist() def fetch_appraisal_template(source_name, target_doc=None): - target_doc = get_mapped_doc("Appraisal Template", source_name, { - "Appraisal Template": { - "doctype": "Appraisal", + target_doc = get_mapped_doc( + "Appraisal Template", + source_name, + { + "Appraisal Template": { + "doctype": "Appraisal", + }, + "Appraisal Template Goal": { + "doctype": "Appraisal Goal", + }, }, - "Appraisal Template Goal": { - "doctype": "Appraisal Goal", - } - }, target_doc) + target_doc, + ) return target_doc diff --git a/erpnext/hr/doctype/appraisal/test_appraisal.py b/erpnext/hr/doctype/appraisal/test_appraisal.py index 90c30ef3475..13a39f38200 100644 --- a/erpnext/hr/doctype/appraisal/test_appraisal.py +++ b/erpnext/hr/doctype/appraisal/test_appraisal.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Appraisal') + class TestAppraisal(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py b/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py index f52e2b027ca..476de4f51b4 100644 --- a/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py +++ b/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'kra_template', - 'transactions': [ - { - 'items': ['Appraisal'] - }, - ], - } + return { + "fieldname": "kra_template", + "transactions": [ + {"items": ["Appraisal"]}, + ], + } diff --git a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py index d0e81a7dc5b..560e992e8a1 100644 --- a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py +++ b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Appraisal Template') + class TestAppraisalTemplate(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index b1eaaf8b587..7f4bd836854 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -13,6 +13,7 @@ from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_emp class Attendance(Document): def validate(self): from erpnext.controllers.status_updater import validate_status + validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) validate_active_employee(self.employee) self.validate_attendance_date() @@ -24,62 +25,84 @@ class Attendance(Document): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") # leaves can be marked for future dates - if self.status != 'On Leave' and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()): + if ( + self.status != "On Leave" + and not self.leave_application + and getdate(self.attendance_date) > getdate(nowdate()) + ): frappe.throw(_("Attendance can not be marked for future dates")) elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining): frappe.throw(_("Attendance date can not be less than employee's joining date")) def validate_duplicate_record(self): - res = frappe.db.sql(""" + res = frappe.db.sql( + """ select name from `tabAttendance` where employee = %s and attendance_date = %s and name != %s and docstatus != 2 - """, (self.employee, getdate(self.attendance_date), self.name)) + """, + (self.employee, getdate(self.attendance_date), self.name), + ) if res: - frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( - frappe.bold(self.employee), frappe.bold(self.attendance_date))) + frappe.throw( + _("Attendance for employee {0} is already marked for the date {1}").format( + frappe.bold(self.employee), frappe.bold(self.attendance_date) + ) + ) def validate_employee_status(self): if frappe.db.get_value("Employee", self.employee, "status") == "Inactive": frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee)) def check_leave_record(self): - leave_record = frappe.db.sql(""" + leave_record = frappe.db.sql( + """ select leave_type, half_day, half_day_date from `tabLeave Application` where employee = %s and %s between from_date and to_date and status = 'Approved' and docstatus = 1 - """, (self.employee, self.attendance_date), as_dict=True) + """, + (self.employee, self.attendance_date), + as_dict=True, + ) if leave_record: for d in leave_record: self.leave_type = d.leave_type if d.half_day_date == getdate(self.attendance_date): - self.status = 'Half Day' - frappe.msgprint(_("Employee {0} on Half day on {1}") - .format(self.employee, formatdate(self.attendance_date))) + self.status = "Half Day" + frappe.msgprint( + _("Employee {0} on Half day on {1}").format(self.employee, formatdate(self.attendance_date)) + ) else: - self.status = 'On Leave' - frappe.msgprint(_("Employee {0} is on Leave on {1}") - .format(self.employee, formatdate(self.attendance_date))) + self.status = "On Leave" + frappe.msgprint( + _("Employee {0} is on Leave on {1}").format(self.employee, formatdate(self.attendance_date)) + ) if self.status in ("On Leave", "Half Day"): if not leave_record: - frappe.msgprint(_("No leave record found for employee {0} on {1}") - .format(self.employee, formatdate(self.attendance_date)), alert=1) + frappe.msgprint( + _("No leave record found for employee {0} on {1}").format( + self.employee, formatdate(self.attendance_date) + ), + alert=1, + ) elif self.leave_type: self.leave_type = None self.leave_application = None def validate_employee(self): - emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'", - self.employee) + emp = frappe.db.sql( + "select name from `tabEmployee` where name = %s and status = 'Active'", self.employee + ) if not emp: frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) + @frappe.whitelist() def get_events(start, end, filters=None): events = [] @@ -90,10 +113,12 @@ def get_events(start, end, filters=None): return events from frappe.desk.reportview import get_filters_cond + conditions = get_filters_cond("Attendance", filters, []) add_attendance(events, start, end, conditions=conditions) return events + def add_attendance(events, start, end, conditions=None): query = """select name, attendance_date, status from `tabAttendance` where @@ -102,93 +127,121 @@ def add_attendance(events, start, end, conditions=None): if conditions: query += conditions - for d in frappe.db.sql(query, {"from_date":start, "to_date":end}, as_dict=True): + for d in frappe.db.sql(query, {"from_date": start, "to_date": end}, as_dict=True): e = { "name": d.name, "doctype": "Attendance", "start": d.attendance_date, "end": d.attendance_date, "title": cstr(d.status), - "docstatus": d.docstatus + "docstatus": d.docstatus, } if e not in events: events.append(e) -def mark_attendance(employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False): - if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): - company = frappe.db.get_value('Employee', employee, 'company') - attendance = frappe.get_doc({ - 'doctype': 'Attendance', - 'employee': employee, - 'attendance_date': attendance_date, - 'status': status, - 'company': company, - 'shift': shift, - 'leave_type': leave_type - }) + +def mark_attendance( + employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False +): + if not frappe.db.exists( + "Attendance", + {"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")}, + ): + company = frappe.db.get_value("Employee", employee, "company") + attendance = frappe.get_doc( + { + "doctype": "Attendance", + "employee": employee, + "attendance_date": attendance_date, + "status": status, + "company": company, + "shift": shift, + "leave_type": leave_type, + } + ) attendance.flags.ignore_validate = ignore_validate attendance.insert() attendance.submit() return attendance.name + @frappe.whitelist() def mark_bulk_attendance(data): import json + if isinstance(data, str): data = json.loads(data) data = frappe._dict(data) - company = frappe.get_value('Employee', data.employee, 'company') + company = frappe.get_value("Employee", data.employee, "company") if not data.unmarked_days: frappe.throw(_("Please select a date.")) return for date in data.unmarked_days: doc_dict = { - 'doctype': 'Attendance', - 'employee': data.employee, - 'attendance_date': get_datetime(date), - 'status': data.status, - 'company': company, + "doctype": "Attendance", + "employee": data.employee, + "attendance_date": get_datetime(date), + "status": data.status, + "company": company, } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() def get_month_map(): - return frappe._dict({ - "January": 1, - "February": 2, - "March": 3, - "April": 4, - "May": 5, - "June": 6, - "July": 7, - "August": 8, - "September": 9, - "October": 10, - "November": 11, - "December": 12 - }) + return frappe._dict( + { + "January": 1, + "February": 2, + "March": 3, + "April": 4, + "May": 5, + "June": 6, + "July": 7, + "August": 8, + "September": 9, + "October": 10, + "November": 11, + "December": 12, + } + ) + @frappe.whitelist() def get_unmarked_days(employee, month, exclude_holidays=0): import calendar - month_map = get_month_map() + month_map = get_month_map() today = get_datetime() - dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)] + joining_date, relieving_date = frappe.get_cached_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) + start_day = 1 + end_day = calendar.monthrange(today.year, month_map[month])[1] + 1 - length = len(dates_of_month) - month_start, month_end = dates_of_month[0], dates_of_month[length-1] + if joining_date and joining_date.month == month_map[month]: + start_day = joining_date.day + if relieving_date and relieving_date.month == month_map[month]: + end_day = relieving_date.day + 1 - records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [ - ["attendance_date", ">=", month_start], - ["attendance_date", "<=", month_end], - ["employee", "=", employee], - ["docstatus", "!=", 2] - ]) + dates_of_month = [ + "{}-{}-{}".format(today.year, month_map[month], r) for r in range(start_day, end_day) + ] + month_start, month_end = dates_of_month[0], dates_of_month[-1] + + records = frappe.get_all( + "Attendance", + fields=["attendance_date", "employee"], + filters=[ + ["attendance_date", ">=", month_start], + ["attendance_date", "<=", month_end], + ["employee", "=", employee], + ["docstatus", "!=", 2], + ], + ) marked_days = [get_datetime(record.attendance_date) for record in records] if cint(exclude_holidays): @@ -200,7 +253,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0): for date in dates_of_month: date_time = get_datetime(date) - if today.day == date_time.day and today.month == date_time.month: + if today.day <= date_time.day and today.month <= date_time.month: break if date_time not in marked_days: unmarked_days.append(date) diff --git a/erpnext/hr/doctype/attendance/attendance_dashboard.py b/erpnext/hr/doctype/attendance/attendance_dashboard.py index f466534d2c7..abed78fbbbe 100644 --- a/erpnext/hr/doctype/attendance/attendance_dashboard.py +++ b/erpnext/hr/doctype/attendance/attendance_dashboard.py @@ -1,12 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'attendance', - 'transactions': [ - { - 'label': '', - 'items': ['Employee Checkin'] - } - ] - } + return {"fieldname": "attendance", "transactions": [{"label": "", "items": ["Employee Checkin"]}]} diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index a770d70ffa9..058bc93d72a 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -1,20 +1,124 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt -import unittest - import frappe -from frappe.utils import nowdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate -test_records = frappe.get_test_records('Attendance') +from erpnext.hr.doctype.attendance.attendance import ( + get_month_map, + get_unmarked_days, + mark_attendance, +) +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday + +test_records = frappe.get_test_records("Attendance") + + +class TestAttendance(FrappeTestCase): + def setUp(self): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + + from_date = get_year_start(getdate()) + to_date = get_year_ending(getdate()) + self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date) -class TestAttendance(unittest.TestCase): def test_mark_absent(self): - from erpnext.hr.doctype.employee.test_employee import make_employee employee = make_employee("test_mark_absent@example.com") date = nowdate() - frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date}) - from erpnext.hr.doctype.attendance.attendance import mark_attendance - attendance = mark_attendance(employee, date, 'Absent') - fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'}) + frappe.db.delete("Attendance", {"employee": employee, "attendance_date": date}) + attendance = mark_attendance(employee, date, "Absent") + fetch_attendance = frappe.get_value( + "Attendance", {"employee": employee, "attendance_date": date, "status": "Absent"} + ) self.assertEqual(attendance, fetch_attendance) + + def test_unmarked_days(self): + now = now_datetime() + previous_month = now.month - 1 + first_day = now.replace(day=1).replace(month=previous_month).date() + + employee = make_employee( + "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1) + ) + frappe.db.delete("Attendance", {"employee": employee}) + frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) + + first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) + mark_attendance(employee, first_day, "Present") + month_name = get_month_name(first_day) + + unmarked_days = get_unmarked_days(employee, month_name) + unmarked_days = [getdate(date) for date in unmarked_days] + + # attendance already marked for the day + self.assertNotIn(first_day, unmarked_days) + # attendance unmarked + self.assertIn(getdate(add_days(first_day, 1)), unmarked_days) + # holiday considered in unmarked days + self.assertIn(first_sunday, unmarked_days) + + def test_unmarked_days_excluding_holidays(self): + now = now_datetime() + previous_month = now.month - 1 + first_day = now.replace(day=1).replace(month=previous_month).date() + + employee = make_employee( + "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1) + ) + frappe.db.delete("Attendance", {"employee": employee}) + + frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) + + first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) + mark_attendance(employee, first_day, "Present") + month_name = get_month_name(first_day) + + unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True) + unmarked_days = [getdate(date) for date in unmarked_days] + + # attendance already marked for the day + self.assertNotIn(first_day, unmarked_days) + # attendance unmarked + self.assertIn(getdate(add_days(first_day, 1)), unmarked_days) + # holidays not considered in unmarked days + self.assertNotIn(first_sunday, unmarked_days) + + def test_unmarked_days_as_per_joining_and_relieving_dates(self): + now = now_datetime() + previous_month = now.month - 1 + first_day = now.replace(day=1).replace(month=previous_month).date() + + doj = add_days(first_day, 1) + relieving_date = add_days(first_day, 5) + employee = make_employee( + "test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date + ) + frappe.db.delete("Attendance", {"employee": employee}) + + frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) + + attendance_date = add_days(first_day, 2) + mark_attendance(employee, attendance_date, "Present") + month_name = get_month_name(first_day) + + unmarked_days = get_unmarked_days(employee, month_name) + unmarked_days = [getdate(date) for date in unmarked_days] + + # attendance already marked for the day + self.assertNotIn(attendance_date, unmarked_days) + # date before doj not in unmarked days + self.assertNotIn(add_days(doj, -1), unmarked_days) + # date after relieving not in unmarked days + self.assertNotIn(add_days(relieving_date, 1), unmarked_days) + + def tearDown(self): + frappe.db.rollback() + + +def get_month_name(date): + month_number = date.month + for month, number in get_month_map().items(): + if number == month_number: + return month diff --git a/erpnext/hr/doctype/attendance_request/attendance_request.py b/erpnext/hr/doctype/attendance_request/attendance_request.py index 8fbe7c7a9ad..78652f669d7 100644 --- a/erpnext/hr/doctype/attendance_request/attendance_request.py +++ b/erpnext/hr/doctype/attendance_request/attendance_request.py @@ -16,17 +16,19 @@ class AttendanceRequest(Document): validate_active_employee(self.employee) validate_dates(self, self.from_date, self.to_date) if self.half_day: - if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date): + if not getdate(self.from_date) <= getdate(self.half_day_date) <= getdate(self.to_date): frappe.throw(_("Half day date should be in between from date and to date")) def on_submit(self): self.create_attendance() def on_cancel(self): - attendance_list = frappe.get_list("Attendance", {'employee': self.employee, 'attendance_request': self.name}) + attendance_list = frappe.get_list( + "Attendance", {"employee": self.employee, "attendance_request": self.name} + ) if attendance_list: for attendance in attendance_list: - attendance_obj = frappe.get_doc("Attendance", attendance['name']) + attendance_obj = frappe.get_doc("Attendance", attendance["name"]) attendance_obj.cancel() def create_attendance(self): @@ -53,15 +55,24 @@ class AttendanceRequest(Document): def validate_if_attendance_not_applicable(self, attendance_date): # Check if attendance_date is a Holiday if is_holiday(self.employee, attendance_date): - frappe.msgprint(_("Attendance not submitted for {0} as it is a Holiday.").format(attendance_date), alert=1) + frappe.msgprint( + _("Attendance not submitted for {0} as it is a Holiday.").format(attendance_date), alert=1 + ) return True # Check if employee on Leave - leave_record = frappe.db.sql("""select half_day from `tabLeave Application` + leave_record = frappe.db.sql( + """select half_day from `tabLeave Application` where employee = %s and %s between from_date and to_date - and docstatus = 1""", (self.employee, attendance_date), as_dict=True) + and docstatus = 1""", + (self.employee, attendance_date), + as_dict=True, + ) if leave_record: - frappe.msgprint(_("Attendance not submitted for {0} as {1} on leave.").format(attendance_date, self.employee), alert=1) + frappe.msgprint( + _("Attendance not submitted for {0} as {1} on leave.").format(attendance_date, self.employee), + alert=1, + ) return True return False diff --git a/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py b/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py index b23e0fd93a2..059725cb44a 100644 --- a/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py +++ b/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'attendance_request', - 'transactions': [ - { - 'items': ['Attendance'] - } - ] - } + return {"fieldname": "attendance_request", "transactions": [{"items": ["Attendance"]}]} diff --git a/erpnext/hr/doctype/attendance_request/test_attendance_request.py b/erpnext/hr/doctype/attendance_request/test_attendance_request.py index 3f0442c7d69..ee436f50687 100644 --- a/erpnext/hr/doctype/attendance_request/test_attendance_request.py +++ b/erpnext/hr/doctype/attendance_request/test_attendance_request.py @@ -9,6 +9,7 @@ from frappe.utils import nowdate test_dependencies = ["Employee"] + class TestAttendanceRequest(unittest.TestCase): def setUp(self): for doctype in ["Attendance Request", "Attendance"]: @@ -34,10 +35,10 @@ class TestAttendanceRequest(unittest.TestCase): "Attendance", filters={ "attendance_request": attendance_request.name, - "attendance_date": date(date.today().year, 1, 1) + "attendance_date": date(date.today().year, 1, 1), }, fieldname=["status", "docstatus"], - as_dict=True + as_dict=True, ) self.assertEqual(attendance.status, "Present") self.assertEqual(attendance.docstatus, 1) @@ -51,9 +52,9 @@ class TestAttendanceRequest(unittest.TestCase): "Attendance", filters={ "attendance_request": attendance_request.name, - "attendance_date": date(date.today().year, 1, 1) + "attendance_date": date(date.today().year, 1, 1), }, - fieldname="docstatus" + fieldname="docstatus", ) self.assertEqual(attendance_docstatus, 2) @@ -74,11 +75,11 @@ class TestAttendanceRequest(unittest.TestCase): "Attendance", filters={ "attendance_request": attendance_request.name, - "attendance_date": date(date.today().year, 1, 1) + "attendance_date": date(date.today().year, 1, 1), }, - fieldname="status" + fieldname="status", ) - self.assertEqual(attendance_status, 'Work From Home') + self.assertEqual(attendance_status, "Work From Home") attendance_request.cancel() @@ -88,11 +89,12 @@ class TestAttendanceRequest(unittest.TestCase): "Attendance", filters={ "attendance_request": attendance_request.name, - "attendance_date": date(date.today().year, 1, 1) + "attendance_date": date(date.today().year, 1, 1), }, - fieldname="docstatus" + fieldname="docstatus", ) self.assertEqual(attendance_docstatus, 2) + def get_employee(): return frappe.get_doc("Employee", "_T-Employee-00001") diff --git a/erpnext/hr/doctype/branch/test_branch.py b/erpnext/hr/doctype/branch/test_branch.py index e84c6e4c60e..c14d4aad690 100644 --- a/erpnext/hr/doctype/branch/test_branch.py +++ b/erpnext/hr/doctype/branch/test_branch.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Branch') +test_records = frappe.get_test_records("Branch") diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index 7d6051508ad..d233226e663 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -18,14 +18,15 @@ from erpnext.hr.utils import ( class CompensatoryLeaveRequest(Document): - def validate(self): validate_active_employee(self.employee) validate_dates(self, self.work_from_date, self.work_end_date) if self.half_day: if not self.half_day_date: frappe.throw(_("Half Day Date is mandatory")) - if not getdate(self.work_from_date)<=getdate(self.half_day_date)<=getdate(self.work_end_date): + if ( + not getdate(self.work_from_date) <= getdate(self.half_day_date) <= getdate(self.work_end_date) + ): frappe.throw(_("Half Day Date should be in between Work From Date and Work End Date")) validate_overlap(self, self.work_from_date, self.work_end_date) self.validate_holidays() @@ -34,13 +35,16 @@ class CompensatoryLeaveRequest(Document): frappe.throw(_("Leave Type is madatory")) def validate_attendance(self): - attendance = frappe.get_all('Attendance', + attendance = frappe.get_all( + "Attendance", filters={ - 'attendance_date': ['between', (self.work_from_date, self.work_end_date)], - 'status': 'Present', - 'docstatus': 1, - 'employee': self.employee - }, fields=['attendance_date', 'status']) + "attendance_date": ["between", (self.work_from_date, self.work_end_date)], + "status": "Present", + "docstatus": 1, + "employee": self.employee, + }, + fields=["attendance_date", "status"], + ) if len(attendance) < date_diff(self.work_end_date, self.work_from_date) + 1: frappe.throw(_("You are not present all day(s) between compensatory leave request days")) @@ -49,7 +53,9 @@ class CompensatoryLeaveRequest(Document): holidays = get_holiday_dates_for_employee(self.employee, self.work_from_date, self.work_end_date) if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1: if date_diff(self.work_end_date, self.work_from_date): - msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date))) + msg = _("The days between {0} to {1} are not valid holidays.").format( + frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)) + ) else: msg = _("{0} is not a holiday.").format(frappe.bold(format_date(self.work_from_date))) @@ -70,13 +76,19 @@ class CompensatoryLeaveRequest(Document): leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) # generate additional ledger entry for the new compensatory leaves off - create_additional_leave_ledger_entry(leave_allocation, date_difference, add_days(self.work_end_date, 1)) + create_additional_leave_ledger_entry( + leave_allocation, date_difference, add_days(self.work_end_date, 1) + ) else: leave_allocation = self.create_leave_allocation(leave_period, date_difference) self.db_set("leave_allocation", leave_allocation.name) else: - frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date))) + frappe.throw( + _("There is no leave period in between {0} and {1}").format( + format_date(self.work_from_date), format_date(self.work_end_date) + ) + ) def on_cancel(self): if self.leave_allocation: @@ -93,10 +105,13 @@ class CompensatoryLeaveRequest(Document): leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) # create reverse entry on cancelation - create_additional_leave_ledger_entry(leave_allocation, date_difference * -1, add_days(self.work_end_date, 1)) + create_additional_leave_ledger_entry( + leave_allocation, date_difference * -1, add_days(self.work_end_date, 1) + ) def get_existing_allocation_for_period(self, leave_period): - leave_allocation = frappe.db.sql(""" + leave_allocation = frappe.db.sql( + """ select name from `tabLeave Allocation` where employee=%(employee)s and leave_type=%(leave_type)s @@ -104,12 +119,15 @@ class CompensatoryLeaveRequest(Document): and (from_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s or (from_date < %(from_date)s and to_date > %(to_date)s)) - """, { - "from_date": leave_period[0].from_date, - "to_date": leave_period[0].to_date, - "employee": self.employee, - "leave_type": self.leave_type - }, as_dict=1) + """, + { + "from_date": leave_period[0].from_date, + "to_date": leave_period[0].to_date, + "employee": self.employee, + "leave_type": self.leave_type, + }, + as_dict=1, + ) if leave_allocation: return frappe.get_doc("Leave Allocation", leave_allocation[0].name) @@ -118,18 +136,20 @@ class CompensatoryLeaveRequest(Document): def create_leave_allocation(self, leave_period, date_difference): is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward") - allocation = frappe.get_doc(dict( - doctype="Leave Allocation", - employee=self.employee, - employee_name=self.employee_name, - leave_type=self.leave_type, - from_date=add_days(self.work_end_date, 1), - to_date=leave_period[0].to_date, - carry_forward=cint(is_carry_forward), - new_leaves_allocated=date_difference, - total_leaves_allocated=date_difference, - description=self.reason - )) + allocation = frappe.get_doc( + dict( + doctype="Leave Allocation", + employee=self.employee, + employee_name=self.employee_name, + leave_type=self.leave_type, + from_date=add_days(self.work_end_date, 1), + to_date=leave_period[0].to_date, + carry_forward=cint(is_carry_forward), + new_leaves_allocated=date_difference, + total_leaves_allocated=date_difference, + description=self.reason, + ) + ) allocation.insert(ignore_permissions=True) allocation.submit() return allocation diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py index 5e51879328b..7bbec293f41 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py @@ -12,12 +12,17 @@ from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_perio test_dependencies = ["Employee"] + class TestCompensatoryLeaveRequest(unittest.TestCase): def setUp(self): - frappe.db.sql(''' delete from `tabCompensatory Leave Request`''') - frappe.db.sql(''' delete from `tabLeave Ledger Entry`''') - frappe.db.sql(''' delete from `tabLeave Allocation`''') - frappe.db.sql(''' delete from `tabAttendance` where attendance_date in {0} '''.format((today(), add_days(today(), -1)))) #nosec + frappe.db.sql(""" delete from `tabCompensatory Leave Request`""") + frappe.db.sql(""" delete from `tabLeave Ledger Entry`""") + frappe.db.sql(""" delete from `tabLeave Allocation`""") + frappe.db.sql( + """ delete from `tabAttendance` where attendance_date in {0} """.format( + (today(), add_days(today(), -1)) + ) + ) # nosec create_leave_period(add_months(today(), -3), add_months(today(), 3), "_Test Company") create_holiday_list() @@ -26,7 +31,7 @@ class TestCompensatoryLeaveRequest(unittest.TestCase): employee.save() def test_leave_balance_on_submit(self): - ''' check creation of leave allocation on submission of compensatory leave request ''' + """check creation of leave allocation on submission of compensatory leave request""" employee = get_employee() mark_attendance(employee) compensatory_leave_request = get_compensatory_leave_request(employee.name) @@ -34,18 +39,27 @@ class TestCompensatoryLeaveRequest(unittest.TestCase): before = get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, today()) compensatory_leave_request.submit() - self.assertEqual(get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, add_days(today(), 1)), before + 1) + self.assertEqual( + get_leave_balance_on( + employee.name, compensatory_leave_request.leave_type, add_days(today(), 1) + ), + before + 1, + ) def test_leave_allocation_update_on_submit(self): employee = get_employee() mark_attendance(employee, date=add_days(today(), -1)) - compensatory_leave_request = get_compensatory_leave_request(employee.name, leave_date=add_days(today(), -1)) + compensatory_leave_request = get_compensatory_leave_request( + employee.name, leave_date=add_days(today(), -1) + ) compensatory_leave_request.submit() # leave allocation creation on submit - leaves_allocated = frappe.db.get_value('Leave Allocation', { - 'name': compensatory_leave_request.leave_allocation - }, ['total_leaves_allocated']) + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"name": compensatory_leave_request.leave_allocation}, + ["total_leaves_allocated"], + ) self.assertEqual(leaves_allocated, 1) mark_attendance(employee) @@ -53,20 +67,22 @@ class TestCompensatoryLeaveRequest(unittest.TestCase): compensatory_leave_request.submit() # leave allocation updates on submission of second compensatory leave request - leaves_allocated = frappe.db.get_value('Leave Allocation', { - 'name': compensatory_leave_request.leave_allocation - }, ['total_leaves_allocated']) + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"name": compensatory_leave_request.leave_allocation}, + ["total_leaves_allocated"], + ) self.assertEqual(leaves_allocated, 2) def test_creation_of_leave_ledger_entry_on_submit(self): - ''' check creation of leave ledger entry on submission of leave request ''' + """check creation of leave ledger entry on submission of leave request""" employee = get_employee() mark_attendance(employee) compensatory_leave_request = get_compensatory_leave_request(employee.name) compensatory_leave_request.submit() filters = dict(transaction_name=compensatory_leave_request.leave_allocation) - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters) + leave_ledger_entry = frappe.get_all("Leave Ledger Entry", fields="*", filters=filters) self.assertEqual(len(leave_ledger_entry), 1) self.assertEqual(leave_ledger_entry[0].employee, compensatory_leave_request.employee) @@ -75,60 +91,67 @@ class TestCompensatoryLeaveRequest(unittest.TestCase): # check reverse leave ledger entry on cancellation compensatory_leave_request.cancel() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters, order_by = 'creation desc') + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", fields="*", filters=filters, order_by="creation desc" + ) self.assertEqual(len(leave_ledger_entry), 2) self.assertEqual(leave_ledger_entry[0].employee, compensatory_leave_request.employee) self.assertEqual(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type) self.assertEqual(leave_ledger_entry[0].leaves, -1) + def get_compensatory_leave_request(employee, leave_date=today()): - prev_comp_leave_req = frappe.db.get_value('Compensatory Leave Request', - dict(leave_type='Compensatory Off', + prev_comp_leave_req = frappe.db.get_value( + "Compensatory Leave Request", + dict( + leave_type="Compensatory Off", work_from_date=leave_date, work_end_date=leave_date, - employee=employee), 'name') - if prev_comp_leave_req: - return frappe.get_doc('Compensatory Leave Request', prev_comp_leave_req) - - return frappe.get_doc(dict( - doctype='Compensatory Leave Request', employee=employee, - leave_type='Compensatory Off', + ), + "name", + ) + if prev_comp_leave_req: + return frappe.get_doc("Compensatory Leave Request", prev_comp_leave_req) + + return frappe.get_doc( + dict( + doctype="Compensatory Leave Request", + employee=employee, + leave_type="Compensatory Off", work_from_date=leave_date, work_end_date=leave_date, - reason='test' - )).insert() + reason="test", + ) + ).insert() -def mark_attendance(employee, date=today(), status='Present'): - if not frappe.db.exists(dict(doctype='Attendance', employee=employee.name, attendance_date=date, status='Present')): - attendance = frappe.get_doc({ - "doctype": "Attendance", - "employee": employee.name, - "attendance_date": date, - "status": status - }) + +def mark_attendance(employee, date=today(), status="Present"): + if not frappe.db.exists( + dict(doctype="Attendance", employee=employee.name, attendance_date=date, status="Present") + ): + attendance = frappe.get_doc( + {"doctype": "Attendance", "employee": employee.name, "attendance_date": date, "status": status} + ) attendance.save() attendance.submit() + def create_holiday_list(): if frappe.db.exists("Holiday List", "_Test Compensatory Leave"): return - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "from_date": add_months(today(), -3), - "to_date": add_months(today(), 3), - "holidays": [ - { - "description": "Test Holiday", - "holiday_date": today() - }, - { - "description": "Test Holiday 1", - "holiday_date": add_days(today(), -1) - } - ], - "holiday_list_name": "_Test Compensatory Leave" - }) + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "from_date": add_months(today(), -3), + "to_date": add_months(today(), 3), + "holidays": [ + {"description": "Test Holiday", "holiday_date": today()}, + {"description": "Test Holiday 1", "holiday_date": add_days(today(), -1)}, + ], + "holiday_list_name": "_Test Compensatory Leave", + } + ) holiday_list.save() diff --git a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py index 38e1f54ba96..d311188090d 100644 --- a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py +++ b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py @@ -12,53 +12,59 @@ from six import string_types class DailyWorkSummary(Document): def send_mails(self, dws_group, emails): - '''Send emails to get daily work summary to all users \ - in selected daily work summary group''' - incoming_email_account = frappe.db.get_value('Email Account', - dict(enable_incoming=1, default_incoming=1), - 'email_id') + """Send emails to get daily work summary to all users \ + in selected daily work summary group""" + incoming_email_account = frappe.db.get_value( + "Email Account", dict(enable_incoming=1, default_incoming=1), "email_id" + ) - self.db_set('email_sent_to', '\n'.join(emails)) - frappe.sendmail(recipients=emails, + self.db_set("email_sent_to", "\n".join(emails)) + frappe.sendmail( + recipients=emails, message=dws_group.message, subject=dws_group.subject, reference_doctype=self.doctype, reference_name=self.name, - reply_to=incoming_email_account) + reply_to=incoming_email_account, + ) def send_summary(self): - '''Send summary of all replies. Called at midnight''' + """Send summary of all replies. Called at midnight""" args = self.get_message_details() emails = get_user_emails_from_group(self.daily_work_summary_group) - frappe.sendmail(recipients=emails, - template='daily_work_summary', + frappe.sendmail( + recipients=emails, + template="daily_work_summary", args=args, subject=_(self.daily_work_summary_group), reference_doctype=self.doctype, - reference_name=self.name) + reference_name=self.name, + ) - self.db_set('status', 'Sent') + self.db_set("status", "Sent") def get_message_details(self): - '''Return args for template''' - dws_group = frappe.get_doc('Daily Work Summary Group', - self.daily_work_summary_group) + """Return args for template""" + dws_group = frappe.get_doc("Daily Work Summary Group", self.daily_work_summary_group) - replies = frappe.get_all('Communication', - fields=['content', 'text_content', 'sender'], - filters=dict(reference_doctype=self.doctype, + replies = frappe.get_all( + "Communication", + fields=["content", "text_content", "sender"], + filters=dict( + reference_doctype=self.doctype, reference_name=self.name, - communication_type='Communication', - sent_or_received='Received'), - order_by='creation asc') + communication_type="Communication", + sent_or_received="Received", + ), + order_by="creation asc", + ) did_not_reply = self.email_sent_to.split() for d in replies: - user = frappe.db.get_values("User", - {"email": d.sender}, - ["full_name", "user_image"], - as_dict=True) + user = frappe.db.get_values( + "User", {"email": d.sender}, ["full_name", "user_image"], as_dict=True + ) d.sender_name = user[0].full_name if user else d.sender d.image = user[0].image if user and user[0].image else None @@ -67,17 +73,13 @@ class DailyWorkSummary(Document): # make thumbnail image try: if original_image: - file_name = frappe.get_list('File', - {'file_url': original_image}) + file_name = frappe.get_list("File", {"file_url": original_image}) if file_name: file_name = file_name[0].name - file_doc = frappe.get_doc('File', file_name) + file_doc = frappe.get_doc("File", file_name) thumbnail_image = file_doc.make_thumbnail( - set_as_thumbnail=False, - width=100, - height=100, - crop=True + set_as_thumbnail=False, width=100, height=100, crop=True ) d.image = thumbnail_image except Exception: @@ -86,34 +88,33 @@ class DailyWorkSummary(Document): if d.sender in did_not_reply: did_not_reply.remove(d.sender) if d.text_content: - d.content = frappe.utils.md_to_html( - EmailReplyParser.parse_reply(d.text_content) - ) + d.content = frappe.utils.md_to_html(EmailReplyParser.parse_reply(d.text_content)) - did_not_reply = [(frappe.db.get_value("User", {"email": email}, "full_name") or email) - for email in did_not_reply] + did_not_reply = [ + (frappe.db.get_value("User", {"email": email}, "full_name") or email) for email in did_not_reply + ] - return dict(replies=replies, + return dict( + replies=replies, original_message=dws_group.message, - title=_('Work Summary for {0}').format( - global_date_format(self.creation) - ), - did_not_reply=', '.join(did_not_reply) or '', - did_not_reply_title=_('No replies from')) + title=_("Work Summary for {0}").format(global_date_format(self.creation)), + did_not_reply=", ".join(did_not_reply) or "", + did_not_reply_title=_("No replies from"), + ) def get_user_emails_from_group(group): - '''Returns list of email of enabled users from the given group + """Returns list of email of enabled users from the given group - :param group: Daily Work Summary Group `name`''' + :param group: Daily Work Summary Group `name`""" group_doc = group if isinstance(group_doc, string_types): - group_doc = frappe.get_doc('Daily Work Summary Group', group) + group_doc = frappe.get_doc("Daily Work Summary Group", group) emails = get_users_email(group_doc) return emails + def get_users_email(doc): - return [d.email for d in doc.users - if frappe.db.get_value("User", d.user, "enabled")] + return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")] diff --git a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py index 5edfb315564..703436529d0 100644 --- a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py +++ b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py @@ -9,82 +9,96 @@ import frappe.utils # test_records = frappe.get_test_records('Daily Work Summary') + class TestDailyWorkSummary(unittest.TestCase): def test_email_trigger(self): self.setup_and_prepare_test() for d in self.users: # check that email is sent to users if d.message: - self.assertTrue(d.email in [d.recipient for d in self.emails - if self.groups.subject in d.message]) + self.assertTrue( + d.email in [d.recipient for d in self.emails if self.groups.subject in d.message] + ) def test_email_trigger_failed(self): - hour = '00:00' - if frappe.utils.nowtime().split(':')[0] == '00': - hour = '01:00' + hour = "00:00" + if frappe.utils.nowtime().split(":")[0] == "00": + hour = "01:00" self.setup_and_prepare_test(hour) for d in self.users: # check that email is not sent to users - self.assertFalse(d.email in [d.recipient for d in self.emails - if self.groups.subject in d.message]) + self.assertFalse( + d.email in [d.recipient for d in self.emails if self.groups.subject in d.message] + ) def test_incoming(self): # get test mail with message-id as in-reply-to self.setup_and_prepare_test() with open(os.path.join(os.path.dirname(__file__), "test_data", "test-reply.raw"), "r") as f: - if not self.emails: return - test_mails = [f.read().replace('{{ sender }}', - self.users[-1].email).replace('{{ message_id }}', - self.emails[-1].message_id)] + if not self.emails: + return + test_mails = [ + f.read() + .replace("{{ sender }}", self.users[-1].email) + .replace("{{ message_id }}", self.emails[-1].message_id) + ] # pull the mail email_account = frappe.get_doc("Email Account", "_Test Email Account 1") - email_account.db_set('enable_incoming', 1) + email_account.db_set("enable_incoming", 1) email_account.receive(test_mails=test_mails) - daily_work_summary = frappe.get_doc('Daily Work Summary', - frappe.get_all('Daily Work Summary')[0].name) + daily_work_summary = frappe.get_doc( + "Daily Work Summary", frappe.get_all("Daily Work Summary")[0].name + ) args = daily_work_summary.get_message_details() - self.assertTrue('I built Daily Work Summary!' in args.get('replies')[0].content) + self.assertTrue("I built Daily Work Summary!" in args.get("replies")[0].content) def setup_and_prepare_test(self, hour=None): - frappe.db.sql('delete from `tabDaily Work Summary`') - frappe.db.sql('delete from `tabEmail Queue`') - frappe.db.sql('delete from `tabEmail Queue Recipient`') - frappe.db.sql('delete from `tabCommunication`') - frappe.db.sql('delete from `tabDaily Work Summary Group`') + frappe.db.sql("delete from `tabDaily Work Summary`") + frappe.db.sql("delete from `tabEmail Queue`") + frappe.db.sql("delete from `tabEmail Queue Recipient`") + frappe.db.sql("delete from `tabCommunication`") + frappe.db.sql("delete from `tabDaily Work Summary Group`") - self.users = frappe.get_all('User', - fields=['email'], - filters=dict(email=('!=', 'test@example.com'))) + self.users = frappe.get_all( + "User", fields=["email"], filters=dict(email=("!=", "test@example.com")) + ) self.setup_groups(hour) from erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group import trigger_emails + trigger_emails() # check if emails are created - self.emails = frappe.db.sql("""select r.recipient, q.message, q.message_id \ + self.emails = frappe.db.sql( + """select r.recipient, q.message, q.message_id \ from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \ - where q.name = r.parent""", as_dict=1) - + where q.name = r.parent""", + as_dict=1, + ) def setup_groups(self, hour=None): # setup email to trigger at this hour if not hour: - hour = frappe.utils.nowtime().split(':')[0] - hour = hour+':00' + hour = frappe.utils.nowtime().split(":")[0] + hour = hour + ":00" - groups = frappe.get_doc(dict(doctype="Daily Work Summary Group", - name="Daily Work Summary", - users=self.users, - send_emails_at=hour, - subject="this is a subject for testing summary emails", - message='this is a message for testing summary emails')) + groups = frappe.get_doc( + dict( + doctype="Daily Work Summary Group", + name="Daily Work Summary", + users=self.users, + send_emails_at=hour, + subject="this is a subject for testing summary emails", + message="this is a message for testing summary emails", + ) + ) groups.insert() self.groups = groups diff --git a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py index 306f43a418f..e8ec265464e 100644 --- a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py +++ b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py @@ -16,37 +16,41 @@ class DailyWorkSummaryGroup(Document): def validate(self): if self.users: if not frappe.flags.in_test and not is_incoming_account_enabled(): - frappe.throw(_('Please enable default incoming account before creating Daily Work Summary Group')) + frappe.throw( + _("Please enable default incoming account before creating Daily Work Summary Group") + ) def trigger_emails(): - '''Send emails to Employees at the given hour asking - them what did they work on today''' + """Send emails to Employees at the given hour asking + them what did they work on today""" groups = frappe.get_all("Daily Work Summary Group") for d in groups: group_doc = frappe.get_doc("Daily Work Summary Group", d) - if (is_current_hour(group_doc.send_emails_at) + if ( + is_current_hour(group_doc.send_emails_at) and not is_holiday(group_doc.holiday_list) - and group_doc.enabled): + and group_doc.enabled + ): emails = get_user_emails_from_group(group_doc) # find emails relating to a company if emails: daily_work_summary = frappe.get_doc( - dict(doctype='Daily Work Summary', daily_work_summary_group=group_doc.name) + dict(doctype="Daily Work Summary", daily_work_summary_group=group_doc.name) ).insert() daily_work_summary.send_mails(group_doc, emails) def is_current_hour(hour): - return frappe.utils.nowtime().split(':')[0] == hour.split(':')[0] + return frappe.utils.nowtime().split(":")[0] == hour.split(":")[0] def send_summary(): - '''Send summary to everyone''' - for d in frappe.get_all('Daily Work Summary', dict(status='Open')): - daily_work_summary = frappe.get_doc('Daily Work Summary', d.name) + """Send summary to everyone""" + for d in frappe.get_all("Daily Work Summary", dict(status="Open")): + daily_work_summary = frappe.get_doc("Daily Work Summary", d.name) daily_work_summary.send_summary() def is_incoming_account_enabled(): - return frappe.db.get_value('Email Account', dict(enable_incoming=1, default_incoming=1)) + return frappe.db.get_value("Email Account", dict(enable_incoming=1, default_incoming=1)) diff --git a/erpnext/hr/doctype/department/department.py b/erpnext/hr/doctype/department/department.py index ed0bfcf0d5a..a9806c529f6 100644 --- a/erpnext/hr/doctype/department/department.py +++ b/erpnext/hr/doctype/department/department.py @@ -9,7 +9,7 @@ from erpnext.utilities.transaction_base import delete_events class Department(NestedSet): - nsm_parent_field = 'parent_department' + nsm_parent_field = "parent_department" def autoname(self): root = get_root_of("Department") @@ -26,7 +26,7 @@ class Department(NestedSet): def before_rename(self, old, new, merge=False): # renaming consistency with abbreviation - if not frappe.get_cached_value('Company', self.company, 'abbr') in new: + if not frappe.get_cached_value("Company", self.company, "abbr") in new: new = get_abbreviated_name(new, self.company) return new @@ -39,17 +39,20 @@ class Department(NestedSet): super(Department, self).on_trash() delete_events(self.doctype, self.name) + def on_doctype_update(): frappe.db.add_index("Department", ["lft", "rgt"]) + def get_abbreviated_name(name, company): - abbr = frappe.get_cached_value('Company', company, 'abbr') - new_name = '{0} - {1}'.format(name, abbr) + abbr = frappe.get_cached_value("Company", company, "abbr") + new_name = "{0} - {1}".format(name, abbr) return new_name + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): - condition = '' + condition = "" var_dict = { "name": get_root_of("Department"), "parent": parent, @@ -62,18 +65,26 @@ def get_children(doctype, parent=None, company=None, is_root=False): else: condition = "parent_department = %(parent)s" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name as value, is_group as expandable from `tab{doctype}` where {condition} - order by name""".format(doctype=doctype, condition=condition), var_dict, as_dict=1) + order by name""".format( + doctype=doctype, condition=condition + ), + var_dict, + as_dict=1, + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = frappe.form_dict args = make_tree_args(**args) diff --git a/erpnext/hr/doctype/department/test_department.py b/erpnext/hr/doctype/department/test_department.py index 95bf6635011..b8c043f0b2d 100644 --- a/erpnext/hr/doctype/department/test_department.py +++ b/erpnext/hr/doctype/department/test_department.py @@ -6,20 +6,26 @@ import unittest import frappe test_ignore = ["Leave Block List"] + + class TestDepartment(unittest.TestCase): - def test_remove_department_data(self): - doc = create_department("Test Department") - frappe.delete_doc('Department', doc.name) + def test_remove_department_data(self): + doc = create_department("Test Department") + frappe.delete_doc("Department", doc.name) + def create_department(department_name, parent_department=None): - doc = frappe.get_doc({ - 'doctype': 'Department', - 'is_group': 0, - 'parent_department': parent_department, - 'department_name': department_name, - 'company': frappe.defaults.get_defaults().company - }).insert() + doc = frappe.get_doc( + { + "doctype": "Department", + "is_group": 0, + "parent_department": parent_department, + "department_name": department_name, + "company": frappe.defaults.get_defaults().company, + } + ).insert() - return doc + return doc -test_records = frappe.get_test_records('Department') + +test_records = frappe.get_test_records("Department") diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index 375ae7c70ae..d849900ef2d 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -10,6 +10,7 @@ from frappe.model.document import Document class DepartmentApprover(Document): pass + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_approvers(doctype, txt, searchfield, start, page_len, filters): @@ -20,25 +21,44 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): approvers = [] department_details = {} department_list = [] - employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True) + employee = frappe.get_value( + "Employee", + filters.get("employee"), + ["employee_name", "department", "leave_approver", "expense_approver", "shift_request_approver"], + as_dict=True, + ) employee_department = filters.get("department") or employee.department if employee_department: - department_details = frappe.db.get_value("Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True) + department_details = frappe.db.get_value( + "Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True + ) if department_details: - department_list = frappe.db.sql("""select name from `tabDepartment` where lft <= %s + department_list = frappe.db.sql( + """select name from `tabDepartment` where lft <= %s and rgt >= %s and disabled=0 - order by lft desc""", (department_details.lft, department_details.rgt), as_list=True) + order by lft desc""", + (department_details.lft, department_details.rgt), + as_list=True, + ) if filters.get("doctype") == "Leave Application" and employee.leave_approver: - approvers.append(frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name'])) + approvers.append( + frappe.db.get_value("User", employee.leave_approver, ["name", "first_name", "last_name"]) + ) if filters.get("doctype") == "Expense Claim" and employee.expense_approver: - approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name'])) + approvers.append( + frappe.db.get_value("User", employee.expense_approver, ["name", "first_name", "last_name"]) + ) if filters.get("doctype") == "Shift Request" and employee.shift_request_approver: - approvers.append(frappe.db.get_value("User", employee.shift_request_approver, ['name', 'first_name', 'last_name'])) + approvers.append( + frappe.db.get_value( + "User", employee.shift_request_approver, ["name", "first_name", "last_name"] + ) + ) if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" @@ -51,15 +71,21 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): field_name = "Shift Request Approver" if department_list: for d in department_list: - approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from + approvers += frappe.db.sql( + """select user.name, user.first_name, user.last_name from tabUser user, `tabDepartment Approver` approver where approver.parent = %s and user.name like %s and approver.parentfield = %s - and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) + and approver.approver=user.name""", + (d, "%" + txt + "%", parentfield), + as_list=True, + ) if len(approvers) == 0: - error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name)) + error_msg = _("Please set {0} for the Employee: {1}").format( + field_name, frappe.bold(employee.employee_name) + ) if department_list: error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department)) frappe.throw(error_msg, title=_(field_name + " Missing")) diff --git a/erpnext/hr/doctype/designation/test_designation.py b/erpnext/hr/doctype/designation/test_designation.py index f2d6d36ff88..0840d13d1f3 100644 --- a/erpnext/hr/doctype/designation/test_designation.py +++ b/erpnext/hr/doctype/designation/test_designation.py @@ -5,15 +5,18 @@ import frappe # test_records = frappe.get_test_records('Designation') -def create_designation(**args): - args = frappe._dict(args) - if frappe.db.exists("Designation", args.designation_name or "_Test designation"): - return frappe.get_doc("Designation", args.designation_name or "_Test designation") - designation = frappe.get_doc({ - "doctype": "Designation", - "designation_name": args.designation_name or "_Test designation", - "description": args.description or "_Test description" - }) - designation.save() - return designation +def create_designation(**args): + args = frappe._dict(args) + if frappe.db.exists("Designation", args.designation_name or "_Test designation"): + return frappe.get_doc("Designation", args.designation_name or "_Test designation") + + designation = frappe.get_doc( + { + "doctype": "Designation", + "designation_name": args.designation_name or "_Test designation", + "description": args.description or "_Test description", + } + ) + designation.save() + return designation diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 8a2950696af..d24e7038422 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -18,22 +18,25 @@ from erpnext.utilities.transaction_base import delete_events class EmployeeUserDisabledError(frappe.ValidationError): pass + + class InactiveEmployeeStatusError(frappe.ValidationError): pass + class Employee(NestedSet): - nsm_parent_field = 'reports_to' + nsm_parent_field = "reports_to" def autoname(self): naming_method = frappe.db.get_value("HR Settings", None, "emp_created_by") if not naming_method: throw(_("Please setup Employee Naming System in Human Resource > HR Settings")) else: - if naming_method == 'Naming Series': + if naming_method == "Naming Series": set_name_by_naming_series(self) - elif naming_method == 'Employee Number': + elif naming_method == "Employee Number": self.name = self.employee_number - elif naming_method == 'Full Name': + elif naming_method == "Full Name": self.set_employee_name() self.name = self.employee_name @@ -41,6 +44,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status + validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"]) self.employee = self.name @@ -58,19 +62,19 @@ class Employee(NestedSet): else: existing_user_id = frappe.db.get_value("Employee", self.name, "user_id") if existing_user_id: - remove_user_permission( - "Employee", self.name, existing_user_id) + remove_user_permission("Employee", self.name, existing_user_id) def after_rename(self, old, new, merge): self.db_set("employee", new) def set_employee_name(self): - self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name])) + self.employee_name = " ".join( + filter(lambda x: x, [self.first_name, self.middle_name, self.last_name]) + ) def validate_user_details(self): if self.user_id: - data = frappe.db.get_value("User", - self.user_id, ["enabled", "user_image"], as_dict=1) + data = frappe.db.get_value("User", self.user_id, ["enabled", "user_image"], as_dict=1) if not data: self.user_id = None @@ -93,14 +97,14 @@ class Employee(NestedSet): self.update_approver_role() def update_user_permissions(self): - if not self.create_user_permission: return - if not has_permission('User Permission', ptype='write', raise_exception=False): return + if not self.create_user_permission: + return + if not has_permission("User Permission", ptype="write", raise_exception=False): + return - employee_user_permission_exists = frappe.db.exists('User Permission', { - 'allow': 'Employee', - 'for_value': self.name, - 'user': self.user_id - }) + employee_user_permission_exists = frappe.db.exists( + "User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id} + ) if employee_user_permission_exists: return @@ -137,12 +141,14 @@ class Employee(NestedSet): if not user.user_image: user.user_image = self.image try: - frappe.get_doc({ - "doctype": "File", - "file_url": self.image, - "attached_to_doctype": "User", - "attached_to_name": self.user_id - }).insert() + frappe.get_doc( + { + "doctype": "File", + "file_url": self.image, + "attached_to_doctype": "User", + "attached_to_name": self.user_id, + } + ).insert() except frappe.DuplicateEntryError: # already exists pass @@ -164,16 +170,32 @@ class Employee(NestedSet): if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()): throw(_("Date of Birth cannot be greater than today.")) - if self.date_of_birth and self.date_of_joining and getdate(self.date_of_birth) >= getdate(self.date_of_joining): + if ( + self.date_of_birth + and self.date_of_joining + and getdate(self.date_of_birth) >= getdate(self.date_of_joining) + ): throw(_("Date of Joining must be greater than Date of Birth")) - elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)): + elif ( + self.date_of_retirement + and self.date_of_joining + and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)) + ): throw(_("Date Of Retirement must be greater than Date of Joining")) - elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) < getdate(self.date_of_joining)): + elif ( + self.relieving_date + and self.date_of_joining + and (getdate(self.relieving_date) < getdate(self.date_of_joining)) + ): throw(_("Relieving Date must be greater than or equal to Date of Joining")) - elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)): + elif ( + self.contract_end_date + and self.date_of_joining + and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)) + ): throw(_("Contract End Date must be greater than Date of Joining")) def validate_email(self): @@ -189,14 +211,20 @@ class Employee(NestedSet): self.prefered_email = preferred_email def validate_status(self): - if self.status == 'Left': - reports_to = frappe.db.get_all('Employee', - filters={'reports_to': self.name, 'status': "Active"}, - fields=['name','employee_name'] + if self.status == "Left": + reports_to = frappe.db.get_all( + "Employee", + filters={"reports_to": self.name, "status": "Active"}, + fields=["name", "employee_name"], ) if reports_to: - link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to] - message = _("The following employees are currently still reporting to {0}:").format(frappe.bold(self.employee_name)) + link_to_employees = [ + frappe.utils.get_link_to_form("Employee", employee.name, label=employee.employee_name) + for employee in reports_to + ] + message = _("The following employees are currently still reporting to {0}:").format( + frappe.bold(self.employee_name) + ) message += "

  • " + "
  • ".join(link_to_employees) message += "

" message += _("Please make sure the employees above report to another Active employee.") @@ -205,7 +233,7 @@ class Employee(NestedSet): throw(_("Please enter relieving date.")) def validate_for_enabled_user_id(self, enabled): - if not self.status == 'Active': + if not self.status == "Active": return if enabled is None: @@ -214,11 +242,16 @@ class Employee(NestedSet): frappe.throw(_("User {0} is disabled").format(self.user_id), EmployeeUserDisabledError) def validate_duplicate_user_id(self): - employee = frappe.db.sql_list("""select name from `tabEmployee` where - user_id=%s and status='Active' and name!=%s""", (self.user_id, self.name)) + employee = frappe.db.sql_list( + """select name from `tabEmployee` where + user_id=%s and status='Active' and name!=%s""", + (self.user_id, self.name), + ) if employee: - throw(_("User {0} is already assigned to Employee {1}").format( - self.user_id, employee[0]), frappe.DuplicateEntryError) + throw( + _("User {0} is already assigned to Employee {1}").format(self.user_id, employee[0]), + frappe.DuplicateEntryError, + ) def validate_reports_to(self): if self.reports_to == self.name: @@ -227,17 +260,25 @@ class Employee(NestedSet): def on_trash(self): self.update_nsm_model() delete_events(self.doctype, self.name) - if frappe.db.exists("Employee Transfer", {'new_employee_id': self.name, 'docstatus': 1}): - emp_transfer = frappe.get_doc("Employee Transfer", {'new_employee_id': self.name, 'docstatus': 1}) - emp_transfer.db_set("new_employee_id", '') + if frappe.db.exists("Employee Transfer", {"new_employee_id": self.name, "docstatus": 1}): + emp_transfer = frappe.get_doc( + "Employee Transfer", {"new_employee_id": self.name, "docstatus": 1} + ) + emp_transfer.db_set("new_employee_id", "") def validate_preferred_email(self): if self.prefered_contact_email and not self.get(scrub(self.prefered_contact_email)): frappe.msgprint(_("Please enter {0}").format(self.prefered_contact_email)) def validate_onboarding_process(self): - employee_onboarding = frappe.get_all("Employee Onboarding", - filters={"job_applicant": self.job_applicant, "docstatus": 1, "boarding_status": ("!=", "Completed")}) + employee_onboarding = frappe.get_all( + "Employee Onboarding", + filters={ + "job_applicant": self.job_applicant, + "docstatus": 1, + "boarding_status": ("!=", "Completed"), + }, + ) if employee_onboarding: doc = frappe.get_doc("Employee Onboarding", employee_onboarding[0].name) doc.validate_employee_creation() @@ -245,20 +286,26 @@ class Employee(NestedSet): def reset_employee_emails_cache(self): prev_doc = self.get_doc_before_save() or {} - cell_number = cstr(self.get('cell_number')) - prev_number = cstr(prev_doc.get('cell_number')) - if (cell_number != prev_number or - self.get('user_id') != prev_doc.get('user_id')): - frappe.cache().hdel('employees_with_number', cell_number) - frappe.cache().hdel('employees_with_number', prev_number) + cell_number = cstr(self.get("cell_number")) + prev_number = cstr(prev_doc.get("cell_number")) + if cell_number != prev_number or self.get("user_id") != prev_doc.get("user_id"): + frappe.cache().hdel("employees_with_number", cell_number) + frappe.cache().hdel("employees_with_number", prev_number) + def get_timeline_data(doctype, name): - '''Return timeline for attendance''' - return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*) + """Return timeline for attendance""" + return dict( + frappe.db.sql( + """select unix_timestamp(attendance_date), count(*) from `tabAttendance` where employee=%s and attendance_date > date_sub(curdate(), interval 1 year) and status in ('Present', 'Half Day') - group by attendance_date''', name)) + group by attendance_date""", + name, + ) + ) + @frappe.whitelist() def get_retirement_date(date_of_birth=None): @@ -266,14 +313,15 @@ def get_retirement_date(date_of_birth=None): if date_of_birth: try: retirement_age = int(frappe.db.get_single_value("HR Settings", "retirement_age") or 60) - dt = add_years(getdate(date_of_birth),retirement_age) - ret = {'date_of_retirement': dt.strftime('%Y-%m-%d')} + dt = add_years(getdate(date_of_birth), retirement_age) + ret = {"date_of_retirement": dt.strftime("%Y-%m-%d")} except ValueError: # invalid date ret = {} return ret + def validate_employee_role(doc, method): # called via User hook if "Employee" in [d.role for d in doc.get("roles")]: @@ -281,39 +329,52 @@ def validate_employee_role(doc, method): frappe.msgprint(_("Please set User ID field in an Employee record to set Employee Role")) doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0]) + def update_user_permissions(doc, method): # called via User hook if "Employee" in [d.role for d in doc.get("roles")]: - if not has_permission('User Permission', ptype='write', raise_exception=False): return + if not has_permission("User Permission", ptype="write", raise_exception=False): + return employee = frappe.get_doc("Employee", {"user_id": doc.name}) employee.update_user_permissions() + def get_employee_email(employee_doc): - return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email") + return ( + employee_doc.get("user_id") + or employee_doc.get("personal_email") + or employee_doc.get("company_email") + ) + def get_holiday_list_for_employee(employee, raise_exception=True): if employee: holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"]) else: - holiday_list='' - company=frappe.db.get_value("Global Defaults", None, "default_company") + holiday_list = "" + company = frappe.db.get_value("Global Defaults", None, "default_company") if not holiday_list: - holiday_list = frappe.get_cached_value('Company', company, "default_holiday_list") + holiday_list = frappe.get_cached_value("Company", company, "default_holiday_list") if not holiday_list and raise_exception: - frappe.throw(_('Please set a default Holiday List for Employee {0} or Company {1}').format(employee, company)) + frappe.throw( + _("Please set a default Holiday List for Employee {0} or Company {1}").format(employee, company) + ) return holiday_list -def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False): - ''' + +def is_holiday( + employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False +): + """ Returns True if given Employee has an holiday on the given date - :param employee: Employee `name` - :param date: Date to check. Will check for today if None - :param raise_exception: Raise an exception if no holiday list found, default is True - :param only_non_weekly: Check only non-weekly holidays, default is False - ''' + :param employee: Employee `name` + :param date: Date to check. Will check for today if None + :param raise_exception: Raise an exception if no holiday list found, default is True + :param only_non_weekly: Check only non-weekly holidays, default is False + """ holiday_list = get_holiday_list_for_employee(employee, raise_exception) if not date: @@ -322,34 +383,28 @@ def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, if not holiday_list: return False - filters = { - 'parent': holiday_list, - 'holiday_date': date - } + filters = {"parent": holiday_list, "holiday_date": date} if only_non_weekly: - filters['weekly_off'] = False + filters["weekly_off"] = False - holidays = frappe.get_all( - 'Holiday', - fields=['description'], - filters=filters, - pluck='description' - ) + holidays = frappe.get_all("Holiday", fields=["description"], filters=filters, pluck="description") if with_description: return len(holidays) > 0, holidays return len(holidays) > 0 + @frappe.whitelist() -def deactivate_sales_person(status = None, employee = None): +def deactivate_sales_person(status=None, employee=None): if status == "Left": sales_person = frappe.db.get_value("Sales Person", {"Employee": employee}) if sales_person: frappe.db.set_value("Sales Person", sales_person, "enabled", 0) + @frappe.whitelist() -def create_user(employee, user = None, email=None): +def create_user(employee, user=None, email=None): emp = frappe.get_doc("Employee", employee) employee_name = emp.employee_name.split(" ") @@ -367,93 +422,98 @@ def create_user(employee, user = None, email=None): emp.prefered_email = email user = frappe.new_doc("User") - user.update({ - "name": emp.employee_name, - "email": emp.prefered_email, - "enabled": 1, - "first_name": first_name, - "middle_name": middle_name, - "last_name": last_name, - "gender": emp.gender, - "birth_date": emp.date_of_birth, - "phone": emp.cell_number, - "bio": emp.bio - }) + user.update( + { + "name": emp.employee_name, + "email": emp.prefered_email, + "enabled": 1, + "first_name": first_name, + "middle_name": middle_name, + "last_name": last_name, + "gender": emp.gender, + "birth_date": emp.date_of_birth, + "phone": emp.cell_number, + "bio": emp.bio, + } + ) user.insert() return user.name + def get_all_employee_emails(company): - '''Returns list of employee emails either based on user_id or company_email''' - employee_list = frappe.get_all('Employee', - fields=['name','employee_name'], - filters={ - 'status': 'Active', - 'company': company - } + """Returns list of employee emails either based on user_id or company_email""" + employee_list = frappe.get_all( + "Employee", fields=["name", "employee_name"], filters={"status": "Active", "company": company} ) employee_emails = [] for employee in employee_list: if not employee: continue - user, company_email, personal_email = frappe.db.get_value('Employee', - employee, ['user_id', 'company_email', 'personal_email']) + user, company_email, personal_email = frappe.db.get_value( + "Employee", employee, ["user_id", "company_email", "personal_email"] + ) email = user or company_email or personal_email if email: employee_emails.append(email) return employee_emails + def get_employee_emails(employee_list): - '''Returns list of employee emails either based on user_id or company_email''' + """Returns list of employee emails either based on user_id or company_email""" employee_emails = [] for employee in employee_list: if not employee: continue - user, company_email, personal_email = frappe.db.get_value('Employee', employee, - ['user_id', 'company_email', 'personal_email']) + user, company_email, personal_email = frappe.db.get_value( + "Employee", employee, ["user_id", "company_email", "personal_email"] + ) email = user or company_email or personal_email if email: employee_emails.append(email) return employee_emails + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): - filters = [['status', '=', 'Active']] - if company and company != 'All Companies': - filters.append(['company', '=', company]) + filters = [["status", "=", "Active"]] + if company and company != "All Companies": + filters.append(["company", "=", company]) - fields = ['name as value', 'employee_name as title'] + fields = ["name as value", "employee_name as title"] if is_root: - parent = '' - if parent and company and parent!=company: - filters.append(['reports_to', '=', parent]) + parent = "" + if parent and company and parent != company: + filters.append(["reports_to", "=", parent]) else: - filters.append(['reports_to', '=', '']) + filters.append(["reports_to", "=", ""]) - employees = frappe.get_list(doctype, fields=fields, - filters=filters, order_by='name') + employees = frappe.get_list(doctype, fields=fields, filters=filters, order_by="name") for employee in employees: - is_expandable = frappe.get_all(doctype, filters=[ - ['reports_to', '=', employee.get('value')] - ]) + is_expandable = frappe.get_all(doctype, filters=[["reports_to", "=", employee.get("value")]]) employee.expandable = 1 if is_expandable else 0 return employees + def on_doctype_update(): frappe.db.add_index("Employee", ["lft", "rgt"]) -def has_user_permission_for_employee(user_name, employee_name): - return frappe.db.exists({ - 'doctype': 'User Permission', - 'user': user_name, - 'allow': 'Employee', - 'for_value': employee_name - }) -def has_upload_permission(doc, ptype='read', user=None): +def has_user_permission_for_employee(user_name, employee_name): + return frappe.db.exists( + { + "doctype": "User Permission", + "user": user_name, + "allow": "Employee", + "for_value": employee_name, + } + ) + + +def has_upload_permission(doc, ptype="read", user=None): if not user: user = frappe.session.user if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype): diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 0aaff52ee28..d7235418a23 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -1,52 +1,46 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on the attendance of this Employee'), - 'fieldname': 'employee', - 'non_standard_fieldnames': { - 'Bank Account': 'party', - 'Employee Grievance': 'raised_by' - }, - 'transactions': [ + "heatmap": True, + "heatmap_message": _("This is based on the attendance of this Employee"), + "fieldname": "employee", + "non_standard_fieldnames": {"Bank Account": "party", "Employee Grievance": "raised_by"}, + "transactions": [ + {"label": _("Attendance"), "items": ["Attendance", "Attendance Request", "Employee Checkin"]}, { - 'label': _('Attendance'), - 'items': ['Attendance', 'Attendance Request', 'Employee Checkin'] + "label": _("Leave"), + "items": ["Leave Application", "Leave Allocation", "Leave Policy Assignment"], }, { - 'label': _('Leave'), - 'items': ['Leave Application', 'Leave Allocation', 'Leave Policy Assignment'] + "label": _("Lifecycle"), + "items": [ + "Employee Transfer", + "Employee Promotion", + "Employee Separation", + "Employee Grievance", + ], + }, + {"label": _("Shift"), "items": ["Shift Request", "Shift Assignment"]}, + {"label": _("Expense"), "items": ["Expense Claim", "Travel Request", "Employee Advance"]}, + {"label": _("Benefit"), "items": ["Employee Benefit Application", "Employee Benefit Claim"]}, + { + "label": _("Payroll"), + "items": [ + "Salary Structure Assignment", + "Salary Slip", + "Additional Salary", + "Timesheet", + "Employee Incentive", + "Retention Bonus", + "Bank Account", + ], }, { - 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] + "label": _("Training"), + "items": ["Training Event", "Training Result", "Training Feedback", "Employee Skill Map"], }, - { - 'label': _('Shift'), - 'items': ['Shift Request', 'Shift Assignment'] - }, - { - 'label': _('Expense'), - 'items': ['Expense Claim', 'Travel Request', 'Employee Advance'] - }, - { - 'label': _('Benefit'), - 'items': ['Employee Benefit Application', 'Employee Benefit Claim'] - }, - { - 'label': _('Payroll'), - 'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account'] - }, - { - 'label': _('Training'), - 'items': ['Training Event', 'Training Result', 'Training Feedback', 'Employee Skill Map'] - }, - { - 'label': _('Evaluation'), - 'items': ['Appraisal'] - }, - ] + {"label": _("Evaluation"), "items": ["Appraisal"]}, + ], } diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py index 0bb66374d1e..1829bc4f2fc 100644 --- a/erpnext/hr/doctype/employee/employee_reminders.py +++ b/erpnext/hr/doctype/employee/employee_reminders.py @@ -44,13 +44,10 @@ def send_advance_holiday_reminders(frequency): else: return - employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name') + employees = frappe.db.get_all("Employee", filters={"status": "Active"}, pluck="name") for employee in employees: holidays = get_holidays_for_employee( - employee, - start_date, end_date, - only_non_weekly=True, - raise_exception=False + employee, start_date, end_date, only_non_weekly=True, raise_exception=False ) send_holidays_reminder_in_advance(employee, holidays) @@ -60,7 +57,7 @@ def send_holidays_reminder_in_advance(employee, holidays): if not holidays: return - employee_doc = frappe.get_doc('Employee', employee) + employee_doc = frappe.get_doc("Employee", employee) employee_email = get_employee_email(employee_doc) frequency = frappe.db.get_single_value("HR Settings", "frequency") @@ -70,15 +67,18 @@ def send_holidays_reminder_in_advance(employee, holidays): subject=_("Upcoming Holidays Reminder"), template="holiday_reminder", args=dict( - reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(employee_doc.get('first_name')), + reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format( + employee_doc.get("first_name") + ), message=_("Below is the list of upcoming holidays for you:"), advance_holiday_reminder=True, holidays=holidays, - frequency=frequency[:-2] + frequency=frequency[:-2], ), - header=email_header + header=email_header, ) + # ------------------ # BIRTHDAY REMINDERS # ------------------ @@ -109,10 +109,10 @@ def send_birthday_reminders(): def get_birthday_reminder_text_and_message(birthday_persons): if len(birthday_persons) == 1: - birthday_person_text = birthday_persons[0]['name'] + birthday_person_text = birthday_persons[0]["name"] else: # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim - person_names = [d['name'] for d in birthday_persons] + person_names = [d["name"] for d in birthday_persons] birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False) reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text) @@ -133,7 +133,7 @@ def send_birthday_reminder(recipients, reminder_text, birthday_persons, message) birthday_persons=birthday_persons, message=message, ), - header=_("Birthday Reminder 🎂") + header=_("Birthday Reminder 🎂"), ) @@ -150,15 +150,16 @@ def get_employees_having_an_event_today(event_type): from collections import defaultdict # Set column based on event type - if event_type == 'birthday': - condition_column = 'date_of_birth' - elif event_type == 'work_anniversary': - condition_column = 'date_of_joining' + if event_type == "birthday": + condition_column = "date_of_birth" + elif event_type == "work_anniversary": + condition_column = "date_of_joining" else: return - employees_born_today = frappe.db.multisql({ - "mariadb": f""" + employees_born_today = frappe.db.multisql( + { + "mariadb": f""" SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining` FROM `tabEmployee` WHERE @@ -170,7 +171,7 @@ def get_employees_having_an_event_today(event_type): AND `status` = 'Active' """, - "postgres": f""" + "postgres": f""" SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image" FROM "tabEmployee" WHERE @@ -182,12 +183,15 @@ def get_employees_having_an_event_today(event_type): AND "status" = 'Active' """, - }, dict(today=today(), condition_column=condition_column), as_dict=1) + }, + dict(today=today(), condition_column=condition_column), + as_dict=1, + ) grouped_employees = defaultdict(lambda: []) for employee_doc in employees_born_today: - grouped_employees[employee_doc.get('company')].append(employee_doc) + grouped_employees[employee_doc.get("company")].append(employee_doc) return grouped_employees @@ -222,19 +226,19 @@ def send_work_anniversary_reminders(): def get_work_anniversary_reminder_text_and_message(anniversary_persons): if len(anniversary_persons) == 1: - anniversary_person = anniversary_persons[0]['name'] + anniversary_person = anniversary_persons[0]["name"] persons_name = anniversary_person # Number of years completed at the company - completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year + completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year anniversary_person += f" completed {completed_years} year(s)" else: person_names_with_years = [] names = [] for person in anniversary_persons: - person_text = person['name'] + person_text = person["name"] names.append(person_text) # Number of years completed at the company - completed_years = getdate().year - person['date_of_joining'].year + completed_years = getdate().year - person["date_of_joining"].year person_text += f" completed {completed_years} year(s)" person_names_with_years.append(person_text) @@ -260,5 +264,5 @@ def send_work_anniversary_reminder(recipients, reminder_text, anniversary_person anniversary_persons=anniversary_persons, message=message, ), - header=_("Work Anniversary Reminder") + header=_("Work Anniversary Reminder"), ) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 67cbea67e1f..50894b61716 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -9,7 +9,8 @@ import frappe.utils import erpnext from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError -test_records = frappe.get_test_records('Employee') +test_records = frappe.get_test_records("Employee") + class TestEmployee(unittest.TestCase): def test_employee_status_left(self): @@ -21,7 +22,7 @@ class TestEmployee(unittest.TestCase): employee2_doc.reports_to = employee1_doc.name employee2_doc.save() employee1_doc.reload() - employee1_doc.status = 'Left' + employee1_doc.status = "Left" self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save) def test_employee_status_inactive(self): @@ -36,11 +37,19 @@ class TestEmployee(unittest.TestCase): employee_doc.reload() make_holiday_list() - frappe.db.set_value("Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value( + "Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List" + ) - frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""") - salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly", - employee=employee_doc.name, company=employee_doc.company) + frappe.db.sql( + """delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""" + ) + salary_structure = make_salary_structure( + "Test Inactive Employee Salary Slip", + "Monthly", + employee=employee_doc.name, + company=employee_doc.company, + ) salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name) self.assertRaises(InactiveEmployeeStatusError, salary_slip.save) @@ -48,38 +57,43 @@ class TestEmployee(unittest.TestCase): def tearDown(self): frappe.db.rollback() + def make_employee(user, company=None, **kwargs): if not frappe.db.get_value("User", user): - frappe.get_doc({ - "doctype": "User", - "email": user, - "first_name": user, - "new_password": "password", - "send_welcome_email": 0, - "roles": [{"doctype": "Has Role", "role": "Employee"}] - }).insert() + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": user, + "new_password": "password", + "send_welcome_email": 0, + "roles": [{"doctype": "Has Role", "role": "Employee"}], + } + ).insert() if not frappe.db.get_value("Employee", {"user_id": user}): - employee = frappe.get_doc({ - "doctype": "Employee", - "naming_series": "EMP-", - "first_name": user, - "company": company or erpnext.get_default_company(), - "user_id": user, - "date_of_birth": "1990-05-08", - "date_of_joining": "2013-01-01", - "department": frappe.get_all("Department", fields="name")[0].name, - "gender": "Female", - "company_email": user, - "prefered_contact_email": "Company Email", - "prefered_email": user, - "status": "Active", - "employment_type": "Intern" - }) + employee = frappe.get_doc( + { + "doctype": "Employee", + "naming_series": "EMP-", + "first_name": user, + "company": company or erpnext.get_default_company(), + "user_id": user, + "date_of_birth": "1990-05-08", + "date_of_joining": "2013-01-01", + "department": frappe.get_all("Department", fields="name")[0].name, + "gender": "Female", + "company_email": user, + "prefered_contact_email": "Company Email", + "prefered_email": user, + "status": "Active", + "employment_type": "Intern", + } + ) if kwargs: employee.update(kwargs) employee.insert() return employee.name else: - frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active") - return frappe.get_value("Employee", {"employee_name":user}, "name") + frappe.db.set_value("Employee", {"employee_name": user}, "status", "Active") + return frappe.get_value("Employee", {"employee_name": user}, "name") diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py index a4097ab9d19..9bde77c9569 100644 --- a/erpnext/hr/doctype/employee/test_employee_reminders.py +++ b/erpnext/hr/doctype/employee/test_employee_reminders.py @@ -21,23 +21,22 @@ class TestEmployeeReminders(unittest.TestCase): # Create a test holiday list test_holiday_dates = cls.get_test_holiday_dates() test_holiday_list = make_holiday_list( - 'TestHolidayRemindersList', + "TestHolidayRemindersList", holiday_dates=[ - {'holiday_date': test_holiday_dates[0], 'description': 'test holiday1'}, - {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'}, - {'holiday_date': test_holiday_dates[2], 'description': 'test holiday3', 'weekly_off': 1}, - {'holiday_date': test_holiday_dates[3], 'description': 'test holiday4'}, - {'holiday_date': test_holiday_dates[4], 'description': 'test holiday5'}, - {'holiday_date': test_holiday_dates[5], 'description': 'test holiday6'}, + {"holiday_date": test_holiday_dates[0], "description": "test holiday1"}, + {"holiday_date": test_holiday_dates[1], "description": "test holiday2"}, + {"holiday_date": test_holiday_dates[2], "description": "test holiday3", "weekly_off": 1}, + {"holiday_date": test_holiday_dates[3], "description": "test holiday4"}, + {"holiday_date": test_holiday_dates[4], "description": "test holiday5"}, + {"holiday_date": test_holiday_dates[5], "description": "test holiday6"}, ], - from_date=getdate()-timedelta(days=10), - to_date=getdate()+timedelta(weeks=5) + from_date=getdate() - timedelta(days=10), + to_date=getdate() + timedelta(weeks=5), ) # Create a test employee test_employee = frappe.get_doc( - 'Employee', - make_employee('test@gopher.io', company="_Test Company") + "Employee", make_employee("test@gopher.io", company="_Test Company") ) # Attach the holiday list to employee @@ -49,16 +48,16 @@ class TestEmployeeReminders(unittest.TestCase): cls.test_holiday_dates = test_holiday_dates # Employee without holidays in this month/week - test_employee_2 = make_employee('test@empwithoutholiday.io', company="_Test Company") - test_employee_2 = frappe.get_doc('Employee', test_employee_2) + test_employee_2 = make_employee("test@empwithoutholiday.io", company="_Test Company") + test_employee_2 = frappe.get_doc("Employee", test_employee_2) test_holiday_list = make_holiday_list( - 'TestHolidayRemindersList2', + "TestHolidayRemindersList2", holiday_dates=[ - {'holiday_date': add_months(getdate(), 1), 'description': 'test holiday1'}, + {"holiday_date": add_months(getdate(), 1), "description": "test holiday1"}, ], from_date=add_months(getdate(), -2), - to_date=add_months(getdate(), 2) + to_date=add_months(getdate(), 2), ) test_employee_2.holiday_list = test_holiday_list.name test_employee_2.save() @@ -71,11 +70,11 @@ class TestEmployeeReminders(unittest.TestCase): today_date = getdate() return [ today_date, - today_date-timedelta(days=4), - today_date-timedelta(days=3), - today_date+timedelta(days=1), - today_date+timedelta(days=3), - today_date+timedelta(weeks=3) + today_date - timedelta(days=4), + today_date - timedelta(days=3), + today_date + timedelta(days=1), + today_date + timedelta(days=3), + today_date + timedelta(weeks=3), ] def setUp(self): @@ -88,19 +87,23 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue(is_holiday(self.test_employee.name)) self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1])) - self.assertFalse(is_holiday(self.test_employee.name, date=getdate()-timedelta(days=1))) + self.assertFalse(is_holiday(self.test_employee.name, date=getdate() - timedelta(days=1))) # Test weekly_off holidays self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2])) - self.assertFalse(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True)) + self.assertFalse( + is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True) + ) # Test with descriptions has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True) self.assertTrue(has_holiday) - self.assertTrue('test holiday1' in descriptions) + self.assertTrue("test holiday1" in descriptions) def test_birthday_reminders(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + employee = frappe.get_doc( + "Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0] + ) employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:] employee.company_email = "test@example.com" employee.company = "_Test Company" @@ -124,7 +127,8 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) def test_work_anniversary_reminders(self): - make_employee("test_work_anniversary@gmail.com", + make_employee( + "test_work_anniversary@gmail.com", date_of_joining="1998" + frappe.utils.nowdate()[4:], company="_Test Company", ) @@ -134,7 +138,7 @@ class TestEmployeeReminders(unittest.TestCase): send_work_anniversary_reminders, ) - employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') + employees_having_work_anniversary = get_employees_having_an_event_today("work_anniversary") employees = employees_having_work_anniversary.get("_Test Company") or [] user_ids = [] for entry in employees: @@ -152,14 +156,15 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message) def test_work_anniversary_reminder_not_sent_for_0_years(self): - make_employee("test_work_anniversary_2@gmail.com", + make_employee( + "test_work_anniversary_2@gmail.com", date_of_joining=getdate(), company="_Test Company", ) from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today - employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') + employees_having_work_anniversary = get_employees_having_an_event_today("work_anniversary") employees = employees_having_work_anniversary.get("_Test Company") or [] user_ids = [] for entry in employees: @@ -168,20 +173,18 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids) def test_send_holidays_reminder_in_advance(self): - setup_hr_settings('Weekly') + setup_hr_settings("Weekly") holidays = get_holidays_for_employee( - self.test_employee.get('name'), - getdate(), getdate() + timedelta(days=3), - only_non_weekly=True, - raise_exception=False - ) - - send_holidays_reminder_in_advance( - self.test_employee.get('name'), - holidays + self.test_employee.get("name"), + getdate(), + getdate() + timedelta(days=3), + only_non_weekly=True, + raise_exception=False, ) + send_holidays_reminder_in_advance(self.test_employee.get("name"), holidays) + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertEqual(len(email_queue), 1) self.assertTrue("Holidays this Week." in email_queue[0].message) @@ -189,67 +192,69 @@ class TestEmployeeReminders(unittest.TestCase): def test_advance_holiday_reminders_monthly(self): from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly - setup_hr_settings('Monthly') + setup_hr_settings("Monthly") # disable emp 2, set same holiday list - frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Left', - 'holiday_list': self.test_employee.holiday_list - }) + frappe.db.set_value( + "Employee", + self.test_employee_2.name, + {"status": "Left", "holiday_list": self.test_employee.holiday_list}, + ) send_reminders_in_advance_monthly() email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue(len(email_queue) > 0) # even though emp 2 has holiday, non-active employees should not be recipients - recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient') + recipients = frappe.db.get_all("Email Queue Recipient", pluck="recipient") self.assertTrue(self.test_employee_2.user_id not in recipients) # teardown: enable emp 2 - frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Active', - 'holiday_list': self.holiday_list_2.name - }) + frappe.db.set_value( + "Employee", + self.test_employee_2.name, + {"status": "Active", "holiday_list": self.holiday_list_2.name}, + ) def test_advance_holiday_reminders_weekly(self): from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly - setup_hr_settings('Weekly') + setup_hr_settings("Weekly") # disable emp 2, set same holiday list - frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Left', - 'holiday_list': self.test_employee.holiday_list - }) + frappe.db.set_value( + "Employee", + self.test_employee_2.name, + {"status": "Left", "holiday_list": self.test_employee.holiday_list}, + ) send_reminders_in_advance_weekly() email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue(len(email_queue) > 0) # even though emp 2 has holiday, non-active employees should not be recipients - recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient') + recipients = frappe.db.get_all("Email Queue Recipient", pluck="recipient") self.assertTrue(self.test_employee_2.user_id not in recipients) # teardown: enable emp 2 - frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Active', - 'holiday_list': self.holiday_list_2.name - }) + frappe.db.set_value( + "Employee", + self.test_employee_2.name, + {"status": "Active", "holiday_list": self.holiday_list_2.name}, + ) def test_reminder_not_sent_if_no_holdays(self): - setup_hr_settings('Monthly') + setup_hr_settings("Monthly") # reminder not sent if there are no holidays holidays = get_holidays_for_employee( - self.test_employee_2.get('name'), - getdate(), getdate() + timedelta(days=3), + self.test_employee_2.get("name"), + getdate(), + getdate() + timedelta(days=3), only_non_weekly=True, - raise_exception=False - ) - send_holidays_reminder_in_advance( - self.test_employee_2.get('name'), - holidays + raise_exception=False, ) + send_holidays_reminder_in_advance(self.test_employee_2.get("name"), holidays) email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertEqual(len(email_queue), 0) @@ -259,5 +264,5 @@ def setup_hr_settings(frequency=None): hr_settings = frappe.get_doc("HR Settings", "HR Settings") hr_settings.send_holiday_reminders = 1 set_proceed_with_frequency_change() - hr_settings.frequency = frequency or 'Weekly' - hr_settings.save() \ No newline at end of file + hr_settings.frequency = frequency or "Weekly" + hr_settings.save() diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 7aac2b63ed3..3d4023d3195 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -16,17 +16,19 @@ from erpnext.hr.utils import validate_active_employee class EmployeeAdvanceOverPayment(frappe.ValidationError): pass + class EmployeeAdvance(Document): def onload(self): - self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value('Accounts Settings', - 'make_payment_via_journal_entry') + self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value( + "Accounts Settings", "make_payment_via_journal_entry" + ) def validate(self): validate_active_employee(self.employee) self.set_status() def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry') + self.ignore_linked_doctypes = "GL Entry" def set_status(self): if self.docstatus == 0: @@ -46,30 +48,30 @@ class EmployeeAdvance(Document): paid_amount = ( frappe.qb.from_(gle) - .select(Sum(gle.debit).as_("paid_amount")) - .where( - (gle.against_voucher_type == 'Employee Advance') - & (gle.against_voucher == self.name) - & (gle.party_type == 'Employee') - & (gle.party == self.employee) - & (gle.docstatus == 1) - & (gle.is_cancelled == 0) - ) - ).run(as_dict=True)[0].paid_amount or 0 + .select(Sum(gle.debit).as_("paid_amount")) + .where( + (gle.against_voucher_type == "Employee Advance") + & (gle.against_voucher == self.name) + & (gle.party_type == "Employee") + & (gle.party == self.employee) + & (gle.docstatus == 1) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True)[0].paid_amount or 0 return_amount = ( frappe.qb.from_(gle) - .select(Sum(gle.credit).as_("return_amount")) - .where( - (gle.against_voucher_type == 'Employee Advance') - & (gle.voucher_type != 'Expense Claim') - & (gle.against_voucher == self.name) - & (gle.party_type == 'Employee') - & (gle.party == self.employee) - & (gle.docstatus == 1) - & (gle.is_cancelled == 0) - ) - ).run(as_dict=True)[0].return_amount or 0 + .select(Sum(gle.credit).as_("return_amount")) + .where( + (gle.against_voucher_type == "Employee Advance") + & (gle.voucher_type != "Expense Claim") + & (gle.against_voucher == self.name) + & (gle.party_type == "Employee") + & (gle.party == self.employee) + & (gle.docstatus == 1) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True)[0].return_amount or 0 if paid_amount != 0: paid_amount = flt(paid_amount) / flt(self.exchange_rate) @@ -77,8 +79,10 @@ class EmployeeAdvance(Document): return_amount = flt(return_amount) / flt(self.exchange_rate) if flt(paid_amount) > self.advance_amount: - frappe.throw(_("Row {0}# Paid Amount cannot be greater than requested advance amount"), - EmployeeAdvanceOverPayment) + frappe.throw( + _("Row {0}# Paid Amount cannot be greater than requested advance amount"), + EmployeeAdvanceOverPayment, + ) if flt(return_amount) > self.paid_amount - self.claimed_amount: frappe.throw(_("Return amount cannot be greater unclaimed amount")) @@ -86,11 +90,12 @@ class EmployeeAdvance(Document): self.db_set("paid_amount", paid_amount) self.db_set("return_amount", return_amount) self.set_status() - frappe.db.set_value("Employee Advance", self.name , "status", self.status) - + frappe.db.set_value("Employee Advance", self.name, "status", self.status) def update_claimed_amount(self): - claimed_amount = frappe.db.sql(""" + claimed_amount = ( + frappe.db.sql( + """ SELECT sum(ifnull(allocated_amount, 0)) FROM `tabExpense Claim Advance` eca, `tabExpense Claim` ec WHERE @@ -99,65 +104,83 @@ class EmployeeAdvance(Document): AND ec.name = eca.parent AND ec.docstatus=1 AND eca.allocated_amount > 0 - """, self.name)[0][0] or 0 + """, + self.name, + )[0][0] + or 0 + ) frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount)) self.reload() self.set_status() frappe.db.set_value("Employee Advance", self.name, "status", self.status) + @frappe.whitelist() def get_pending_amount(employee, posting_date): - employee_due_amount = frappe.get_all("Employee Advance", \ - filters = {"employee":employee, "docstatus":1, "posting_date":("<=", posting_date)}, \ - fields = ["advance_amount", "paid_amount"]) + employee_due_amount = frappe.get_all( + "Employee Advance", + filters={"employee": employee, "docstatus": 1, "posting_date": ("<=", posting_date)}, + fields=["advance_amount", "paid_amount"], + ) return sum([(emp.advance_amount - emp.paid_amount) for emp in employee_due_amount]) + @frappe.whitelist() def make_bank_entry(dt, dn): doc = frappe.get_doc(dt, dn) - payment_account = get_default_bank_cash_account(doc.company, account_type="Cash", - mode_of_payment=doc.mode_of_payment) + payment_account = get_default_bank_cash_account( + doc.company, account_type="Cash", mode_of_payment=doc.mode_of_payment + ) if not payment_account: frappe.throw(_("Please set a Default Cash Account in Company defaults")) - advance_account_currency = frappe.db.get_value('Account', doc.advance_account, 'account_currency') + advance_account_currency = frappe.db.get_value("Account", doc.advance_account, "account_currency") - advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate(advance_account_currency,doc ) + advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate( + advance_account_currency, doc + ) paying_amount, paying_exchange_rate = get_paying_amount_paying_exchange_rate(payment_account, doc) je = frappe.new_doc("Journal Entry") je.posting_date = nowdate() - je.voucher_type = 'Bank Entry' + je.voucher_type = "Bank Entry" je.company = doc.company - je.remark = 'Payment against Employee Advance: ' + dn + '\n' + doc.purpose + je.remark = "Payment against Employee Advance: " + dn + "\n" + doc.purpose je.multi_currency = 1 if advance_account_currency != payment_account.account_currency else 0 - je.append("accounts", { - "account": doc.advance_account, - "account_currency": advance_account_currency, - "exchange_rate": flt(advance_exchange_rate), - "debit_in_account_currency": flt(advance_amount), - "reference_type": "Employee Advance", - "reference_name": doc.name, - "party_type": "Employee", - "cost_center": erpnext.get_default_cost_center(doc.company), - "party": doc.employee, - "is_advance": "Yes" - }) + je.append( + "accounts", + { + "account": doc.advance_account, + "account_currency": advance_account_currency, + "exchange_rate": flt(advance_exchange_rate), + "debit_in_account_currency": flt(advance_amount), + "reference_type": "Employee Advance", + "reference_name": doc.name, + "party_type": "Employee", + "cost_center": erpnext.get_default_cost_center(doc.company), + "party": doc.employee, + "is_advance": "Yes", + }, + ) - je.append("accounts", { - "account": payment_account.account, - "cost_center": erpnext.get_default_cost_center(doc.company), - "credit_in_account_currency": flt(paying_amount), - "account_currency": payment_account.account_currency, - "account_type": payment_account.account_type, - "exchange_rate": flt(paying_exchange_rate) - }) + je.append( + "accounts", + { + "account": payment_account.account, + "cost_center": erpnext.get_default_cost_center(doc.company), + "credit_in_account_currency": flt(paying_amount), + "account_currency": payment_account.account_currency, + "account_type": payment_account.account_type, + "exchange_rate": flt(paying_exchange_rate), + }, + ) return je.as_dict() + def get_advance_amount_advance_exchange_rate(advance_account_currency, doc): if advance_account_currency != doc.currency: advance_amount = flt(doc.advance_amount) * flt(doc.exchange_rate) @@ -168,6 +191,7 @@ def get_advance_amount_advance_exchange_rate(advance_account_currency, doc): return advance_amount, advance_exchange_rate + def get_paying_amount_paying_exchange_rate(payment_account, doc): if payment_account.account_currency != doc.currency: paying_amount = flt(doc.advance_amount) * flt(doc.exchange_rate) @@ -178,6 +202,7 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc): return paying_amount, paying_exchange_rate + @frappe.whitelist() def create_return_through_additional_salary(doc): import json @@ -185,7 +210,7 @@ def create_return_through_additional_salary(doc): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = doc.employee additional_salary.currency = doc.currency additional_salary.amount = doc.paid_amount - doc.claimed_amount @@ -195,54 +220,79 @@ def create_return_through_additional_salary(doc): return additional_salary + @frappe.whitelist() -def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, currency, exchange_rate, mode_of_payment=None): - bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) +def make_return_entry( + employee, + company, + employee_advance_name, + return_amount, + advance_account, + currency, + exchange_rate, + mode_of_payment=None, +): + bank_cash_account = get_default_bank_cash_account( + company, account_type="Cash", mode_of_payment=mode_of_payment + ) if not bank_cash_account: frappe.throw(_("Please set a Default Cash Account in Company defaults")) - advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') + advance_account_currency = frappe.db.get_value("Account", advance_account, "account_currency") - je = frappe.new_doc('Journal Entry') + je = frappe.new_doc("Journal Entry") je.posting_date = nowdate() je.voucher_type = get_voucher_type(mode_of_payment) je.company = company - je.remark = 'Return against Employee Advance: ' + employee_advance_name + je.remark = "Return against Employee Advance: " + employee_advance_name je.multi_currency = 1 if advance_account_currency != bank_cash_account.account_currency else 0 - advance_account_amount = flt(return_amount) if advance_account_currency==currency \ + advance_account_amount = ( + flt(return_amount) + if advance_account_currency == currency else flt(return_amount) * flt(exchange_rate) + ) - je.append('accounts', { - 'account': advance_account, - 'credit_in_account_currency': advance_account_amount, - 'account_currency': advance_account_currency, - 'exchange_rate': flt(exchange_rate) if advance_account_currency == currency else 1, - 'reference_type': 'Employee Advance', - 'reference_name': employee_advance_name, - 'party_type': 'Employee', - 'party': employee, - 'is_advance': 'Yes' - }) + je.append( + "accounts", + { + "account": advance_account, + "credit_in_account_currency": advance_account_amount, + "account_currency": advance_account_currency, + "exchange_rate": flt(exchange_rate) if advance_account_currency == currency else 1, + "reference_type": "Employee Advance", + "reference_name": employee_advance_name, + "party_type": "Employee", + "party": employee, + "is_advance": "Yes", + }, + ) - bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \ + bank_amount = ( + flt(return_amount) + if bank_cash_account.account_currency == currency else flt(return_amount) * flt(exchange_rate) + ) - je.append("accounts", { - "account": bank_cash_account.account, - "debit_in_account_currency": bank_amount, - "account_currency": bank_cash_account.account_currency, - "account_type": bank_cash_account.account_type, - "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1 - }) + je.append( + "accounts", + { + "account": bank_cash_account.account, + "debit_in_account_currency": bank_amount, + "account_currency": bank_cash_account.account_currency, + "account_type": bank_cash_account.account_type, + "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1, + }, + ) return je.as_dict() + def get_voucher_type(mode_of_payment=None): voucher_type = "Cash Entry" if mode_of_payment: - mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type') + mode_of_payment_type = frappe.get_cached_value("Mode of Payment", mode_of_payment, "type") if mode_of_payment_type == "Bank": voucher_type = "Bank Entry" diff --git a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py index 089bd2c1b5c..73fac5131ea 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py @@ -1,18 +1,9 @@ - - def get_data(): return { - 'fieldname': 'employee_advance', - 'non_standard_fieldnames': { - 'Payment Entry': 'reference_name', - 'Journal Entry': 'reference_name' + "fieldname": "employee_advance", + "non_standard_fieldnames": { + "Payment Entry": "reference_name", + "Journal Entry": "reference_name", }, - 'transactions': [ - { - 'items': ['Expense Claim'] - }, - { - 'items': ['Payment Entry', 'Journal Entry'] - } - ] + "transactions": [{"items": ["Expense Claim"]}, {"items": ["Payment Entry", "Journal Entry"]}], } diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 5f2e720eb46..9b006ffcffe 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import nowdate +from frappe.utils import flt, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee @@ -13,6 +13,7 @@ from erpnext.hr.doctype.employee_advance.employee_advance import ( create_return_through_additional_salary, make_bank_entry, ) +from erpnext.hr.doctype.expense_claim.expense_claim import get_advances from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure @@ -58,7 +59,9 @@ class TestEmployeeAdvance(unittest.TestCase): args = {"type": "Deduction"} create_salary_component("Advance Salary - Deduction", **args) - make_salary_structure("Test Additional Salary for Advance Return", "Monthly", employee=employee_name) + make_salary_structure( + "Test Additional Salary for Advance Return", "Monthly", employee=employee_name + ) # additional salary for 700 first advance.reload() @@ -100,10 +103,11 @@ def make_payment_entry(advance): return journal_entry + def make_employee_advance(employee_name, args=None): doc = frappe.new_doc("Employee Advance") doc.employee = employee_name - doc.company = "_Test company" + doc.company = "_Test company" doc.purpose = "For site visit" doc.currency = erpnext.get_company_currency("_Test company") doc.exchange_rate = 1 @@ -118,3 +122,27 @@ def make_employee_advance(employee_name, args=None): doc.submit() return doc + + +def get_advances_for_claim(claim, advance_name, amount=None): + advances = get_advances(claim.employee, advance_name) + + for entry in advances: + if amount: + allocated_amount = amount + else: + allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount) + + claim.append( + "advances", + { + "employee_advance": entry.name, + "posting_date": entry.posting_date, + "advance_account": entry.advance_account, + "advance_paid": entry.paid_amount, + "unclaimed_amount": allocated_amount, + "allocated_amount": allocated_amount, + }, + ) + + return claim diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py index af2ca50b78a..43665cc8b22 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py +++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py @@ -14,32 +14,31 @@ class EmployeeAttendanceTool(Document): @frappe.whitelist() -def get_employees(date, department = None, branch = None, company = None): +def get_employees(date, department=None, branch=None, company=None): attendance_not_marked = [] attendance_marked = [] filters = {"status": "Active", "date_of_joining": ["<=", date]} - for field, value in {'department': department, - 'branch': branch, 'company': company}.items(): + for field, value in {"department": department, "branch": branch, "company": company}.items(): if value: filters[field] = value - employee_list = frappe.get_list("Employee", fields=["employee", "employee_name"], filters=filters, order_by="employee_name") + employee_list = frappe.get_list( + "Employee", fields=["employee", "employee_name"], filters=filters, order_by="employee_name" + ) marked_employee = {} - for emp in frappe.get_list("Attendance", fields=["employee", "status"], - filters={"attendance_date": date}): - marked_employee[emp['employee']] = emp['status'] + for emp in frappe.get_list( + "Attendance", fields=["employee", "status"], filters={"attendance_date": date} + ): + marked_employee[emp["employee"]] = emp["status"] for employee in employee_list: - employee['status'] = marked_employee.get(employee['employee']) - if employee['employee'] not in marked_employee: + employee["status"] = marked_employee.get(employee["employee"]) + if employee["employee"] not in marked_employee: attendance_not_marked.append(employee) else: attendance_marked.append(employee) - return { - "marked": attendance_marked, - "unmarked": attendance_not_marked - } + return {"marked": attendance_marked, "unmarked": attendance_not_marked} @frappe.whitelist() @@ -53,16 +52,18 @@ def mark_employee_attendance(employee_list, status, date, leave_type=None, compa else: leave_type = None - company = frappe.db.get_value("Employee", employee['employee'], "Company", cache=True) + company = frappe.db.get_value("Employee", employee["employee"], "Company", cache=True) - attendance=frappe.get_doc(dict( - doctype='Attendance', - employee=employee.get('employee'), - employee_name=employee.get('employee_name'), - attendance_date=getdate(date), - status=status, - leave_type=leave_type, - company=company - )) + attendance = frappe.get_doc( + dict( + doctype="Attendance", + employee=employee.get("employee"), + employee_name=employee.get("employee_name"), + attendance_date=getdate(date), + status=status, + leave_type=leave_type, + company=company, + ) + ) attendance.insert() - attendance.submit() \ No newline at end of file + attendance.submit() diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index c1d4ac7fded..f3cd864c908 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, get_datetime +from frappe.utils import cint, get_datetime, get_link_to_form from erpnext.hr.doctype.shift_assignment.shift_assignment import ( get_actual_start_end_datetime_of_shift, @@ -20,20 +20,31 @@ class EmployeeCheckin(Document): self.fetch_shift() def validate_duplicate_log(self): - doc = frappe.db.exists('Employee Checkin', { - 'employee': self.employee, - 'time': self.time, - 'name': ['!=', self.name]}) + doc = frappe.db.exists( + "Employee Checkin", {"employee": self.employee, "time": self.time, "name": ["!=", self.name]} + ) if doc: - doc_link = frappe.get_desk_link('Employee Checkin', doc) - frappe.throw(_('This employee already has a log with the same timestamp.{0}') - .format("
" + doc_link)) + doc_link = frappe.get_desk_link("Employee Checkin", doc) + frappe.throw( + _("This employee already has a log with the same timestamp.{0}").format("
" + doc_link) + ) def fetch_shift(self): - shift_actual_timings = get_actual_start_end_datetime_of_shift(self.employee, get_datetime(self.time), True) + shift_actual_timings = get_actual_start_end_datetime_of_shift( + self.employee, get_datetime(self.time), True + ) if shift_actual_timings[0] and shift_actual_timings[1]: - if shift_actual_timings[2].shift_type.determine_check_in_and_check_out == 'Strictly based on Log Type in Employee Checkin' and not self.log_type and not self.skip_auto_attendance: - frappe.throw(_('Log Type is required for check-ins falling in the shift: {0}.').format(shift_actual_timings[2].shift_type.name)) + if ( + shift_actual_timings[2].shift_type.determine_check_in_and_check_out + == "Strictly based on Log Type in Employee Checkin" + and not self.log_type + and not self.skip_auto_attendance + ): + frappe.throw( + _("Log Type is required for check-ins falling in the shift: {0}.").format( + shift_actual_timings[2].shift_type.name + ) + ) if not self.attendance: self.shift = shift_actual_timings[2].shift_type.name self.shift_actual_start = shift_actual_timings[0] @@ -43,8 +54,16 @@ class EmployeeCheckin(Document): else: self.shift = None + @frappe.whitelist() -def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'): +def add_log_based_on_employee_field( + employee_field_value, + timestamp, + device_id=None, + log_type=None, + skip_auto_attendance=0, + employee_fieldname="attendance_device_id", +): """Finds the relevant Employee using the employee field value and creates a Employee Checkin. :param employee_field_value: The value to look for in employee field. @@ -58,11 +77,20 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N if not employee_field_value or not timestamp: frappe.throw(_("'employee_field_value' and 'timestamp' are required.")) - employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value}, ["name", "employee_name", employee_fieldname], as_dict=True) + employee = frappe.db.get_values( + "Employee", + {employee_fieldname: employee_field_value}, + ["name", "employee_name", employee_fieldname], + as_dict=True, + ) if employee: employee = employee[0] else: - frappe.throw(_("No Employee found for the given employee field value. '{}': {}").format(employee_fieldname,employee_field_value)) + frappe.throw( + _("No Employee found for the given employee field value. '{}': {}").format( + employee_fieldname, employee_field_value + ) + ) doc = frappe.new_doc("Employee Checkin") doc.employee = employee.name @@ -70,13 +98,24 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N doc.time = timestamp doc.device_id = device_id doc.log_type = log_type - if cint(skip_auto_attendance) == 1: doc.skip_auto_attendance = '1' + if cint(skip_auto_attendance) == 1: + doc.skip_auto_attendance = "1" doc.insert() return doc -def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, in_time=None, out_time=None, shift=None): +def mark_attendance_and_link_log( + logs, + attendance_status, + attendance_date, + working_hours=None, + late_entry=False, + early_exit=False, + in_time=None, + out_time=None, + shift=None, +): """Creates an attendance and links the attendance to the Employee Checkin. Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown. @@ -87,40 +126,54 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki """ log_names = [x.name for x in logs] employee = logs[0].employee - if attendance_status == 'Skip': - frappe.db.sql("""update `tabEmployee Checkin` - set skip_auto_attendance = %s - where name in %s""", ('1', log_names)) + if attendance_status == "Skip": + skip_attendance_in_checkins(log_names) return None - elif attendance_status in ('Present', 'Absent', 'Half Day'): - employee_doc = frappe.get_doc('Employee', employee) - if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): + + elif attendance_status in ("Present", "Absent", "Half Day"): + employee_doc = frappe.get_doc("Employee", employee) + duplicate = frappe.db.exists( + "Attendance", + {"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")}, + ) + + if not duplicate: doc_dict = { - 'doctype': 'Attendance', - 'employee': employee, - 'attendance_date': attendance_date, - 'status': attendance_status, - 'working_hours': working_hours, - 'company': employee_doc.company, - 'shift': shift, - 'late_entry': late_entry, - 'early_exit': early_exit, - 'in_time': in_time, - 'out_time': out_time + "doctype": "Attendance", + "employee": employee, + "attendance_date": attendance_date, + "status": attendance_status, + "working_hours": working_hours, + "company": employee_doc.company, + "shift": shift, + "late_entry": late_entry, + "early_exit": early_exit, + "in_time": in_time, + "out_time": out_time, } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() - frappe.db.sql("""update `tabEmployee Checkin` + + if attendance_status == "Absent": + attendance.add_comment( + text=_("Employee was marked Absent for not meeting the working hours threshold.") + ) + + frappe.db.sql( + """update `tabEmployee Checkin` set attendance = %s - where name in %s""", (attendance.name, log_names)) + where name in %s""", + (attendance.name, log_names), + ) return attendance else: - frappe.db.sql("""update `tabEmployee Checkin` - set skip_auto_attendance = %s - where name in %s""", ('1', log_names)) + skip_attendance_in_checkins(log_names) + if duplicate: + add_comment_in_checkins(log_names, duplicate) + return None else: - frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status)) + frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status)) def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): @@ -133,29 +186,35 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): """ total_hours = 0 in_time = out_time = None - if check_in_out_type == 'Alternating entries as IN and OUT during the same shift': + if check_in_out_type == "Alternating entries as IN and OUT during the same shift": in_time = logs[0].time if len(logs) >= 2: out_time = logs[-1].time - if working_hours_calc_type == 'First Check-in and Last Check-out': + if working_hours_calc_type == "First Check-in and Last Check-out": # assumption in this case: First log always taken as IN, Last log always taken as OUT total_hours = time_diff_in_hours(in_time, logs[-1].time) - elif working_hours_calc_type == 'Every Valid Check-in and Check-out': + elif working_hours_calc_type == "Every Valid Check-in and Check-out": logs = logs[:] while len(logs) >= 2: total_hours += time_diff_in_hours(logs[0].time, logs[1].time) del logs[:2] - elif check_in_out_type == 'Strictly based on Log Type in Employee Checkin': - if working_hours_calc_type == 'First Check-in and Last Check-out': - first_in_log_index = find_index_in_dict(logs, 'log_type', 'IN') - first_in_log = logs[first_in_log_index] if first_in_log_index or first_in_log_index == 0 else None - last_out_log_index = find_index_in_dict(reversed(logs), 'log_type', 'OUT') - last_out_log = logs[len(logs)-1-last_out_log_index] if last_out_log_index or last_out_log_index == 0 else None + elif check_in_out_type == "Strictly based on Log Type in Employee Checkin": + if working_hours_calc_type == "First Check-in and Last Check-out": + first_in_log_index = find_index_in_dict(logs, "log_type", "IN") + first_in_log = ( + logs[first_in_log_index] if first_in_log_index or first_in_log_index == 0 else None + ) + last_out_log_index = find_index_in_dict(reversed(logs), "log_type", "OUT") + last_out_log = ( + logs[len(logs) - 1 - last_out_log_index] + if last_out_log_index or last_out_log_index == 0 + else None + ) if first_in_log and last_out_log: in_time, out_time = first_in_log.time, last_out_log.time total_hours = time_diff_in_hours(in_time, out_time) - elif working_hours_calc_type == 'Every Valid Check-in and Check-out': + elif working_hours_calc_type == "Every Valid Check-in and Check-out": in_log = out_log = None for log in logs: if in_log and out_log: @@ -165,16 +224,44 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): total_hours += time_diff_in_hours(in_log.time, out_log.time) in_log = out_log = None if not in_log: - in_log = log if log.log_type == 'IN' else None + in_log = log if log.log_type == "IN" else None elif not out_log: - out_log = log if log.log_type == 'OUT' else None + out_log = log if log.log_type == "OUT" else None if in_log and out_log: out_time = out_log.time total_hours += time_diff_in_hours(in_log.time, out_log.time) return total_hours, in_time, out_time + def time_diff_in_hours(start, end): - return round((end-start).total_seconds() / 3600, 1) + return round((end - start).total_seconds() / 3600, 1) + def find_index_in_dict(dict_list, key, value): return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None) + + +def add_comment_in_checkins(log_names, duplicate): + text = _("Auto Attendance skipped due to duplicate attendance record: {}").format( + get_link_to_form("Attendance", duplicate) + ) + + for name in log_names: + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Comment", + "reference_doctype": "Employee Checkin", + "reference_name": name, + "content": text, + } + ).insert(ignore_permissions=True) + + +def skip_attendance_in_checkins(log_names): + EmployeeCheckin = frappe.qb.DocType("Employee Checkin") + ( + frappe.qb.update(EmployeeCheckin) + .set("skip_auto_attendance", 1) + .where(EmployeeCheckin.name.isin(log_names)) + ).run() diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py index 254bf9e2569..97f76b03502 100644 --- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py @@ -19,85 +19,108 @@ class TestEmployeeCheckin(unittest.TestCase): def test_add_log_based_on_employee_field(self): employee = make_employee("test_add_log_based_on_employee_field@example.com") employee = frappe.get_doc("Employee", employee) - employee.attendance_device_id = '3344' + employee.attendance_device_id = "3344" employee.save() time_now = now_datetime().__str__()[:-7] - employee_checkin = add_log_based_on_employee_field('3344', time_now, 'mumbai_first_floor', 'IN') + employee_checkin = add_log_based_on_employee_field("3344", time_now, "mumbai_first_floor", "IN") self.assertEqual(employee_checkin.employee, employee.name) self.assertEqual(employee_checkin.time, time_now) - self.assertEqual(employee_checkin.device_id, 'mumbai_first_floor') - self.assertEqual(employee_checkin.log_type, 'IN') + self.assertEqual(employee_checkin.device_id, "mumbai_first_floor") + self.assertEqual(employee_checkin.log_type, "IN") def test_mark_attendance_and_link_log(self): employee = make_employee("test_mark_attendance_and_link_log@example.com") logs = make_n_checkins(employee, 3) - mark_attendance_and_link_log(logs, 'Skip', nowdate()) + mark_attendance_and_link_log(logs, "Skip", nowdate()) log_names = [log.name for log in logs] - logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'skip_auto_attendance':1}) + logs_count = frappe.db.count( + "Employee Checkin", {"name": ["in", log_names], "skip_auto_attendance": 1} + ) self.assertEqual(logs_count, 3) logs = make_n_checkins(employee, 4, 2) now_date = nowdate() - frappe.db.delete('Attendance', {'employee':employee}) - attendance = mark_attendance_and_link_log(logs, 'Present', now_date, 8.2) + frappe.db.delete("Attendance", {"employee": employee}) + attendance = mark_attendance_and_link_log(logs, "Present", now_date, 8.2) log_names = [log.name for log in logs] - logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'attendance':attendance.name}) + logs_count = frappe.db.count( + "Employee Checkin", {"name": ["in", log_names], "attendance": attendance.name} + ) self.assertEqual(logs_count, 4) - attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_hours':8.2, - 'employee':employee, 'attendance_date':now_date}) + attendance_count = frappe.db.count( + "Attendance", + {"status": "Present", "working_hours": 8.2, "employee": employee, "attendance_date": now_date}, + ) self.assertEqual(attendance_count, 1) def test_calculate_working_hours(self): - check_in_out_type = ['Alternating entries as IN and OUT during the same shift', - 'Strictly based on Log Type in Employee Checkin'] - working_hours_calc_type = ['First Check-in and Last Check-out', - 'Every Valid Check-in and Check-out'] + check_in_out_type = [ + "Alternating entries as IN and OUT during the same shift", + "Strictly based on Log Type in Employee Checkin", + ] + working_hours_calc_type = [ + "First Check-in and Last Check-out", + "Every Valid Check-in and Check-out", + ] logs_type_1 = [ - {'time':now_datetime()-timedelta(minutes=390)}, - {'time':now_datetime()-timedelta(minutes=300)}, - {'time':now_datetime()-timedelta(minutes=270)}, - {'time':now_datetime()-timedelta(minutes=90)}, - {'time':now_datetime()-timedelta(minutes=0)} - ] + {"time": now_datetime() - timedelta(minutes=390)}, + {"time": now_datetime() - timedelta(minutes=300)}, + {"time": now_datetime() - timedelta(minutes=270)}, + {"time": now_datetime() - timedelta(minutes=90)}, + {"time": now_datetime() - timedelta(minutes=0)}, + ] logs_type_2 = [ - {'time':now_datetime()-timedelta(minutes=390),'log_type':'OUT'}, - {'time':now_datetime()-timedelta(minutes=360),'log_type':'IN'}, - {'time':now_datetime()-timedelta(minutes=300),'log_type':'OUT'}, - {'time':now_datetime()-timedelta(minutes=290),'log_type':'IN'}, - {'time':now_datetime()-timedelta(minutes=260),'log_type':'OUT'}, - {'time':now_datetime()-timedelta(minutes=240),'log_type':'IN'}, - {'time':now_datetime()-timedelta(minutes=150),'log_type':'IN'}, - {'time':now_datetime()-timedelta(minutes=60),'log_type':'OUT'} - ] + {"time": now_datetime() - timedelta(minutes=390), "log_type": "OUT"}, + {"time": now_datetime() - timedelta(minutes=360), "log_type": "IN"}, + {"time": now_datetime() - timedelta(minutes=300), "log_type": "OUT"}, + {"time": now_datetime() - timedelta(minutes=290), "log_type": "IN"}, + {"time": now_datetime() - timedelta(minutes=260), "log_type": "OUT"}, + {"time": now_datetime() - timedelta(minutes=240), "log_type": "IN"}, + {"time": now_datetime() - timedelta(minutes=150), "log_type": "IN"}, + {"time": now_datetime() - timedelta(minutes=60), "log_type": "OUT"}, + ] logs_type_1 = [frappe._dict(x) for x in logs_type_1] logs_type_2 = [frappe._dict(x) for x in logs_type_2] - working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0]) + working_hours = calculate_working_hours( + logs_type_1, check_in_out_type[0], working_hours_calc_type[0] + ) self.assertEqual(working_hours, (6.5, logs_type_1[0].time, logs_type_1[-1].time)) - working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1]) + working_hours = calculate_working_hours( + logs_type_1, check_in_out_type[0], working_hours_calc_type[1] + ) self.assertEqual(working_hours, (4.5, logs_type_1[0].time, logs_type_1[-1].time)) - working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0]) + working_hours = calculate_working_hours( + logs_type_2, check_in_out_type[1], working_hours_calc_type[0] + ) self.assertEqual(working_hours, (5, logs_type_2[1].time, logs_type_2[-1].time)) - working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1]) + working_hours = calculate_working_hours( + logs_type_2, check_in_out_type[1], working_hours_calc_type[1] + ) self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time)) + def make_n_checkins(employee, n, hours_to_reverse=1): - logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))] - for i in range(n-1): - logs.append(make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n-i))) + logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n + 1))] + for i in range(n - 1): + logs.append( + make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n - i)) + ) return logs def make_checkin(employee, time=now_datetime()): - log = frappe.get_doc({ - "doctype": "Employee Checkin", - "employee" : employee, - "time" : time, - "device_id" : "device1", - "log_type" : "IN" - }).insert() + log = frappe.get_doc( + { + "doctype": "Employee Checkin", + "employee": employee, + "time": time, + "device_id": "device1", + "log_type": "IN", + } + ).insert() return log diff --git a/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py b/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py index 1dd6ad3b15a..efc68ce87a3 100644 --- a/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py +++ b/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py @@ -1,13 +1,9 @@ - - def get_data(): return { - 'transactions': [ + "transactions": [ { - 'items': ['Employee', 'Leave Period'], + "items": ["Employee", "Leave Period"], }, - { - 'items': ['Employee Onboarding Template', 'Employee Separation Template'] - } + {"items": ["Employee Onboarding Template", "Employee Separation Template"]}, ] } diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py index fd9a33b3771..45de79f4f5e 100644 --- a/erpnext/hr/doctype/employee_grievance/employee_grievance.py +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py @@ -9,7 +9,8 @@ from frappe.model.document import Document class EmployeeGrievance(Document): def on_submit(self): if self.status not in ["Invalid", "Resolved"]: - frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format( - bold("Invalid"), - bold("Resolved")) + frappe.throw( + _("Only Employee Grievance with status {0} or {1} can be submitted").format( + bold("Invalid"), bold("Resolved") + ) ) diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py index e2d0002aa62..910d8828603 100644 --- a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py +++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py @@ -13,6 +13,7 @@ class TestEmployeeGrievance(unittest.TestCase): def test_create_employee_grievance(self): create_employee_grievance() + def create_employee_grievance(): grievance_type = create_grievance_type() emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company") @@ -27,10 +28,10 @@ def create_employee_grievance(): grievance.grievance_against = emp_2 grievance.description = "test descrip" - #set cause + # set cause grievance.cause_of_grievance = "test cause" - #resolution details + # resolution details grievance.resolution_date = today() grievance.resolution_detail = "test resolution detail" grievance.resolved_by = "test_emp_grievance_@example.com" diff --git a/erpnext/hr/doctype/employee_group/test_employee_group.py b/erpnext/hr/doctype/employee_group/test_employee_group.py index a87f4007bd8..3922f54f331 100644 --- a/erpnext/hr/doctype/employee_group/test_employee_group.py +++ b/erpnext/hr/doctype/employee_group/test_employee_group.py @@ -11,17 +11,16 @@ from erpnext.hr.doctype.employee.test_employee import make_employee class TestEmployeeGroup(unittest.TestCase): pass + def make_employee_group(): employee = make_employee("testemployee@example.com") - employee_group = frappe.get_doc({ - "doctype": "Employee Group", - "employee_group_name": "_Test Employee Group", - "employee_list": [ - { - "employee": employee - } - ] - }) + employee_group = frappe.get_doc( + { + "doctype": "Employee Group", + "employee_group_name": "_Test Employee Group", + "employee_list": [{"employee": employee}], + } + ) employee_group_exist = frappe.db.exists("Employee Group", "_Test Employee Group") if not employee_group_exist: employee_group.insert() @@ -29,6 +28,7 @@ def make_employee_group(): else: return employee_group_exist + def get_employee_group(): employee_group = frappe.db.exists("Employee Group", "_Test Employee Group") return employee_group diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py index 01a9fe29a98..14b75bef569 100644 --- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py @@ -9,7 +9,9 @@ from frappe.model.mapper import get_mapped_doc from erpnext.hr.utils import EmployeeBoardingController -class IncompleteTaskError(frappe.ValidationError): pass +class IncompleteTaskError(frappe.ValidationError): + pass + class EmployeeOnboarding(EmployeeBoardingController): def validate(self): @@ -17,9 +19,13 @@ class EmployeeOnboarding(EmployeeBoardingController): self.validate_duplicate_employee_onboarding() def validate_duplicate_employee_onboarding(self): - emp_onboarding = frappe.db.exists("Employee Onboarding",{"job_applicant": self.job_applicant}) + emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant}) if emp_onboarding and emp_onboarding != self.name: - frappe.throw(_("Employee Onboarding: {0} is already for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant))) + frappe.throw( + _("Employee Onboarding: {0} is already for Job Applicant: {1}").format( + frappe.bold(emp_onboarding), frappe.bold(self.job_applicant) + ) + ) def validate_employee_creation(self): if self.docstatus != 1: @@ -31,7 +37,9 @@ class EmployeeOnboarding(EmployeeBoardingController): else: task_status = frappe.db.get_value("Task", activity.task, "status") if task_status not in ["Completed", "Cancelled"]: - frappe.throw(_("All the mandatory Task for employee creation hasn't been done yet."), IncompleteTaskError) + frappe.throw( + _("All the mandatory Task for employee creation hasn't been done yet."), IncompleteTaskError + ) def on_submit(self): super(EmployeeOnboarding, self).on_submit() @@ -42,19 +50,29 @@ class EmployeeOnboarding(EmployeeBoardingController): def on_cancel(self): super(EmployeeOnboarding, self).on_cancel() + @frappe.whitelist() def make_employee(source_name, target_doc=None): doc = frappe.get_doc("Employee Onboarding", source_name) doc.validate_employee_creation() + def set_missing_values(source, target): target.personal_email = frappe.db.get_value("Job Applicant", source.job_applicant, "email_id") target.status = "Active" - doc = get_mapped_doc("Employee Onboarding", source_name, { + + doc = get_mapped_doc( + "Employee Onboarding", + source_name, + { "Employee Onboarding": { "doctype": "Employee", "field_map": { "first_name": "employee_name", "employee_grade": "grade", - }} - }, target_doc, set_missing_values) + }, + } + }, + target_doc, + set_missing_values, + ) return doc diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index daa068e6e03..21d00ce2de7 100644 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -15,8 +15,8 @@ from erpnext.hr.doctype.job_offer.test_job_offer import create_job_offer class TestEmployeeOnboarding(unittest.TestCase): def test_employee_onboarding_incomplete_task(self): - if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}): - frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'}) + if frappe.db.exists("Employee Onboarding", {"employee_name": "Test Researcher"}): + frappe.delete_doc("Employee Onboarding", {"employee_name": "Test Researcher"}) frappe.db.sql("delete from `tabEmployee Onboarding`") project = "Employee Onboarding : test@researcher.com" frappe.db.sql("delete from tabProject where name=%s", project) @@ -26,35 +26,31 @@ class TestEmployeeOnboarding(unittest.TestCase): job_offer = create_job_offer(job_applicant=applicant.name) job_offer.submit() - onboarding = frappe.new_doc('Employee Onboarding') + onboarding = frappe.new_doc("Employee Onboarding") onboarding.job_applicant = applicant.name onboarding.job_offer = job_offer.name - onboarding.company = '_Test Company' - onboarding.designation = 'Researcher' - onboarding.append('activities', { - 'activity_name': 'Assign ID Card', - 'role': 'HR User', - 'required_for_employee_creation': 1 - }) - onboarding.append('activities', { - 'activity_name': 'Assign a laptop', - 'role': 'HR User' - }) - onboarding.status = 'Pending' + onboarding.company = "_Test Company" + onboarding.designation = "Researcher" + onboarding.append( + "activities", + {"activity_name": "Assign ID Card", "role": "HR User", "required_for_employee_creation": 1}, + ) + onboarding.append("activities", {"activity_name": "Assign a laptop", "role": "HR User"}) + onboarding.status = "Pending" onboarding.insert() onboarding.submit() project_name = frappe.db.get_value("Project", onboarding.project, "project_name") - self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com') + self.assertEqual(project_name, "Employee Onboarding : test@researcher.com") # don't allow making employee if onboarding is not complete self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) # complete the task - project = frappe.get_doc('Project', onboarding.project) - for task in frappe.get_all('Task', dict(project=project.name)): - task = frappe.get_doc('Task', task.name) - task.status = 'Completed' + project = frappe.get_doc("Project", onboarding.project) + for task in frappe.get_all("Task", dict(project=project.name)): + task = frappe.get_doc("Task", task.name) + task.status = "Completed" task.save() # make employee @@ -62,23 +58,25 @@ class TestEmployeeOnboarding(unittest.TestCase): employee = make_employee(onboarding.name) employee.first_name = employee.employee_name employee.date_of_joining = nowdate() - employee.date_of_birth = '1990-05-08' - employee.gender = 'Female' + employee.date_of_birth = "1990-05-08" + employee.gender = "Female" employee.insert() - self.assertEqual(employee.employee_name, 'Test Researcher') + self.assertEqual(employee.employee_name, "Test Researcher") + def get_job_applicant(): - if frappe.db.exists('Job Applicant', 'test@researcher.com'): - return frappe.get_doc('Job Applicant', 'test@researcher.com') - applicant = frappe.new_doc('Job Applicant') - applicant.applicant_name = 'Test Researcher' - applicant.email_id = 'test@researcher.com' - applicant.designation = 'Researcher' - applicant.status = 'Open' - applicant.cover_letter = 'I am a great Researcher.' + if frappe.db.exists("Job Applicant", "test@researcher.com"): + return frappe.get_doc("Job Applicant", "test@researcher.com") + applicant = frappe.new_doc("Job Applicant") + applicant.applicant_name = "Test Researcher" + applicant.email_id = "test@researcher.com" + applicant.designation = "Researcher" + applicant.status = "Open" + applicant.cover_letter = "I am a great Researcher." applicant.insert() return applicant + def _set_up(): for doctype in ["Employee Onboarding"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) diff --git a/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py b/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py index 48f2c1d2709..93237ee3797 100644 --- a/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py +++ b/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'employee_onboarding_template', - 'transactions': [ - { - 'items': ['Employee Onboarding'] - }, - ], - } + return { + "fieldname": "employee_onboarding_template", + "transactions": [ + {"items": ["Employee Onboarding"]}, + ], + } diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py index cf6156e3264..d77c1dddfd0 100644 --- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py @@ -16,12 +16,16 @@ class EmployeePromotion(Document): def before_submit(self): if getdate(self.promotion_date) > getdate(): - frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date"), - frappe.DocstatusTransitionError) + frappe.throw( + _("Employee Promotion cannot be submitted before Promotion Date"), + frappe.DocstatusTransitionError, + ) def on_submit(self): employee = frappe.get_doc("Employee", self.employee) - employee = update_employee_work_history(employee, self.promotion_details, date=self.promotion_date) + employee = update_employee_work_history( + employee, self.promotion_details, date=self.promotion_date + ) employee.save() def on_cancel(self): diff --git a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py index fc9d195a3f3..06825ece910 100644 --- a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py @@ -15,18 +15,20 @@ class TestEmployeePromotion(unittest.TestCase): frappe.db.sql("""delete from `tabEmployee Promotion`""") def test_submit_before_promotion_date(self): - promotion_obj = frappe.get_doc({ - "doctype": "Employee Promotion", - "employee": self.employee, - "promotion_details" :[ - { - "property": "Designation", - "current": "Software Developer", - "new": "Project Manager", - "fieldname": "designation" - } - ] - }) + promotion_obj = frappe.get_doc( + { + "doctype": "Employee Promotion", + "employee": self.employee, + "promotion_details": [ + { + "property": "Designation", + "current": "Software Developer", + "new": "Project Manager", + "fieldname": "designation", + } + ], + } + ) promotion_obj.promotion_date = add_days(getdate(), 1) promotion_obj.save() self.assertRaises(frappe.DocstatusTransitionError, promotion_obj.submit) diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py index 4e1780b9978..e349c674d8b 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral.py +++ b/erpnext/hr/doctype/employee_referral/employee_referral.py @@ -30,7 +30,7 @@ class EmployeeReferral(Document): @frappe.whitelist() def create_job_applicant(source_name, target_doc=None): emp_ref = frappe.get_doc("Employee Referral", source_name) - #just for Api call if some set status apart from default Status + # just for Api call if some set status apart from default Status status = emp_ref.status if emp_ref.status in ["Pending", "In process"]: status = "Open" @@ -47,9 +47,13 @@ def create_job_applicant(source_name, target_doc=None): job_applicant.resume_link = emp_ref.resume_link job_applicant.save() - frappe.msgprint(_("Job Applicant {0} created successfully.").format( - get_link_to_form("Job Applicant", job_applicant.name)), - title=_("Success"), indicator="green") + frappe.msgprint( + _("Job Applicant {0} created successfully.").format( + get_link_to_form("Job Applicant", job_applicant.name) + ), + title=_("Success"), + indicator="green", + ) emp_ref.db_set("status", "In Process") diff --git a/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py b/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py index 1733ac9726e..4d683fbfcf3 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py +++ b/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py @@ -1,15 +1,8 @@ - - def get_data(): return { - 'fieldname': 'employee_referral', - 'non_standard_fieldnames': { - 'Additional Salary': 'ref_docname' - }, - 'transactions': [ - { - 'items': ['Job Applicant', 'Additional Salary'] - }, - - ] + "fieldname": "employee_referral", + "non_standard_fieldnames": {"Additional Salary": "ref_docname"}, + "transactions": [ + {"items": ["Job Applicant", "Additional Salary"]}, + ], } diff --git a/erpnext/hr/doctype/employee_referral/test_employee_referral.py b/erpnext/hr/doctype/employee_referral/test_employee_referral.py index 529e3551454..475a935e864 100644 --- a/erpnext/hr/doctype/employee_referral/test_employee_referral.py +++ b/erpnext/hr/doctype/employee_referral/test_employee_referral.py @@ -15,7 +15,6 @@ from erpnext.hr.doctype.employee_referral.employee_referral import ( class TestEmployeeReferral(unittest.TestCase): - def setUp(self): frappe.db.sql("DELETE FROM `tabJob Applicant`") frappe.db.sql("DELETE FROM `tabEmployee Referral`") @@ -23,13 +22,12 @@ class TestEmployeeReferral(unittest.TestCase): def test_workflow_and_status_sync(self): emp_ref = create_employee_referral() - #Check Initial status + # Check Initial status self.assertTrue(emp_ref.status, "Pending") job_applicant = create_job_applicant(emp_ref.name) - - #Check status sync + # Check status sync emp_ref.reload() self.assertTrue(emp_ref.status, "In Process") @@ -47,7 +45,6 @@ class TestEmployeeReferral(unittest.TestCase): emp_ref.reload() self.assertTrue(emp_ref.status, "Accepted") - # Check for Referral reference in additional salary add_sal = create_additional_salary(emp_ref) diff --git a/erpnext/hr/doctype/employee_separation/test_employee_separation.py b/erpnext/hr/doctype/employee_separation/test_employee_separation.py index 0007b9e1f38..5ba57bad899 100644 --- a/erpnext/hr/doctype/employee_separation/test_employee_separation.py +++ b/erpnext/hr/doctype/employee_separation/test_employee_separation.py @@ -7,17 +7,15 @@ import frappe test_dependencies = ["Employee Onboarding"] + class TestEmployeeSeparation(unittest.TestCase): def test_employee_separation(self): employee = frappe.db.get_value("Employee", {"status": "Active"}) - separation = frappe.new_doc('Employee Separation') + separation = frappe.new_doc("Employee Separation") separation.employee = employee - separation.company = '_Test Company' - separation.append('activities', { - 'activity_name': 'Deactivate Employee', - 'role': 'HR User' - }) - separation.boarding_status = 'Pending' + separation.company = "_Test Company" + separation.append("activities", {"activity_name": "Deactivate Employee", "role": "HR User"}) + separation.boarding_status = "Pending" separation.insert() separation.submit() self.assertEqual(separation.docstatus, 1) diff --git a/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py b/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py index f165d0a0eb4..3ffd8dd6e2c 100644 --- a/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py +++ b/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'employee_separation_template', - 'transactions': [ - { - 'items': ['Employee Separation'] - }, - ], - } + return { + "fieldname": "employee_separation_template", + "transactions": [ + {"items": ["Employee Separation"]}, + ], + } diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py index f927d413ae3..6dbefe59da5 100644 --- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py @@ -13,8 +13,10 @@ from erpnext.hr.utils import update_employee_work_history class EmployeeTransfer(Document): def before_submit(self): if getdate(self.transfer_date) > getdate(): - frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), - frappe.DocstatusTransitionError) + frappe.throw( + _("Employee Transfer cannot be submitted before Transfer Date"), + frappe.DocstatusTransitionError, + ) def on_submit(self): employee = frappe.get_doc("Employee", self.employee) @@ -22,22 +24,26 @@ class EmployeeTransfer(Document): new_employee = frappe.copy_doc(employee) new_employee.name = None new_employee.employee_number = None - new_employee = update_employee_work_history(new_employee, self.transfer_details, date=self.transfer_date) + new_employee = update_employee_work_history( + new_employee, self.transfer_details, date=self.transfer_date + ) if self.new_company and self.company != self.new_company: new_employee.internal_work_history = [] new_employee.date_of_joining = self.transfer_date new_employee.company = self.new_company - #move user_id to new employee before insert + # move user_id to new employee before insert if employee.user_id and not self.validate_user_in_details(): new_employee.user_id = employee.user_id employee.db_set("user_id", "") new_employee.insert() self.db_set("new_employee_id", new_employee.name) - #relieve the old employee + # relieve the old employee employee.db_set("relieving_date", self.transfer_date) employee.db_set("status", "Left") else: - employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date) + employee = update_employee_work_history( + employee, self.transfer_details, date=self.transfer_date + ) if self.new_company and self.company != self.new_company: employee.company = self.new_company employee.date_of_joining = self.transfer_date @@ -47,14 +53,18 @@ class EmployeeTransfer(Document): employee = frappe.get_doc("Employee", self.employee) if self.create_new_employee_id: if self.new_employee_id: - frappe.throw(_("Please delete the Employee {0} to cancel this document").format( - "{0}".format(self.new_employee_id) - )) - #mark the employee as active + frappe.throw( + _("Please delete the Employee {0} to cancel this document").format( + "{0}".format(self.new_employee_id) + ) + ) + # mark the employee as active employee.status = "Active" - employee.relieving_date = '' + employee.relieving_date = "" else: - employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date, cancel=True) + employee = update_employee_work_history( + employee, self.transfer_details, date=self.transfer_date, cancel=True + ) if self.new_company != self.company: employee.company = self.company employee.save() diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py index 64eee402fec..37a190a1627 100644 --- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py @@ -19,18 +19,20 @@ class TestEmployeeTransfer(unittest.TestCase): def test_submit_before_transfer_date(self): make_employee("employee2@transfers.com") - transfer_obj = frappe.get_doc({ - "doctype": "Employee Transfer", - "employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"), - "transfer_details" :[ - { - "property": "Designation", - "current": "Software Developer", - "new": "Project Manager", - "fieldname": "designation" - } - ] - }) + transfer_obj = frappe.get_doc( + { + "doctype": "Employee Transfer", + "employee": frappe.get_value("Employee", {"user_id": "employee2@transfers.com"}, "name"), + "transfer_details": [ + { + "property": "Designation", + "current": "Software Developer", + "new": "Project Manager", + "fieldname": "designation", + } + ], + } + ) transfer_obj.transfer_date = add_days(getdate(), 1) transfer_obj.save() self.assertRaises(frappe.DocstatusTransitionError, transfer_obj.submit) @@ -42,32 +44,35 @@ class TestEmployeeTransfer(unittest.TestCase): def test_new_employee_creation(self): make_employee("employee3@transfers.com") - transfer = frappe.get_doc({ - "doctype": "Employee Transfer", - "employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"), - "create_new_employee_id": 1, - "transfer_date": getdate(), - "transfer_details" :[ - { - "property": "Designation", - "current": "Software Developer", - "new": "Project Manager", - "fieldname": "designation" - } - ] - }).insert() + transfer = frappe.get_doc( + { + "doctype": "Employee Transfer", + "employee": frappe.get_value("Employee", {"user_id": "employee3@transfers.com"}, "name"), + "create_new_employee_id": 1, + "transfer_date": getdate(), + "transfer_details": [ + { + "property": "Designation", + "current": "Software Developer", + "new": "Project Manager", + "fieldname": "designation", + } + ], + } + ).insert() transfer.submit() self.assertTrue(transfer.new_employee_id) self.assertEqual(frappe.get_value("Employee", transfer.new_employee_id, "status"), "Active") self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left") def test_employee_history(self): - employee = make_employee("employee4@transfers.com", + employee = make_employee( + "employee4@transfers.com", company="Test Company", date_of_birth=getdate("30-09-1980"), date_of_joining=getdate("01-10-2021"), department="Accounts - TC", - designation="Accountant" + designation="Accountant", ) transfer = create_employee_transfer(employee) @@ -94,36 +99,40 @@ class TestEmployeeTransfer(unittest.TestCase): def create_company(): if not frappe.db.exists("Company", "Test Company"): - frappe.get_doc({ - "doctype": "Company", - "company_name": "Test Company", - "default_currency": "INR", - "country": "India" - }).insert() + frappe.get_doc( + { + "doctype": "Company", + "company_name": "Test Company", + "default_currency": "INR", + "country": "India", + } + ).insert() def create_employee_transfer(employee): - doc = frappe.get_doc({ - "doctype": "Employee Transfer", - "employee": employee, - "transfer_date": getdate(), - "transfer_details": [ - { - "property": "Designation", - "current": "Accountant", - "new": "Manager", - "fieldname": "designation" - }, - { - "property": "Department", - "current": "Accounts - TC", - "new": "Management - TC", - "fieldname": "department" - } - ] - }) + doc = frappe.get_doc( + { + "doctype": "Employee Transfer", + "employee": employee, + "transfer_date": getdate(), + "transfer_details": [ + { + "property": "Designation", + "current": "Accountant", + "new": "Manager", + "fieldname": "designation", + }, + { + "property": "Department", + "current": "Accounts - TC", + "new": "Management - TC", + "fieldname": "department", + }, + ], + } + ) doc.save() doc.submit() - return doc \ No newline at end of file + return doc diff --git a/erpnext/hr/doctype/employment_type/test_employment_type.py b/erpnext/hr/doctype/employment_type/test_employment_type.py index c43f9636c70..fdf6965e48a 100644 --- a/erpnext/hr/doctype/employment_type/test_employment_type.py +++ b/erpnext/hr/doctype/employment_type/test_employment_type.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Employment Type') +test_records = frappe.get_test_records("Employment Type") diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 7e3898b7d51..311a1eb81c8 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -13,20 +13,26 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee -class InvalidExpenseApproverError(frappe.ValidationError): pass -class ExpenseApproverIdentityError(frappe.ValidationError): pass +class InvalidExpenseApproverError(frappe.ValidationError): + pass + + +class ExpenseApproverIdentityError(frappe.ValidationError): + pass + class ExpenseClaim(AccountsController): def onload(self): - self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value('Accounts Settings', - 'make_payment_via_journal_entry') + self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value( + "Accounts Settings", "make_payment_via_journal_entry" + ) def validate(self): validate_active_employee(self.employee) - self.validate_advances() + set_employee_name(self) self.validate_sanctioned_amount() self.calculate_total_amount() - set_employee_name(self) + self.validate_advances() self.set_expense_account(validate=True) self.set_payable_account() self.set_cost_center() @@ -36,21 +42,35 @@ class ExpenseClaim(AccountsController): self.project = frappe.db.get_value("Task", self.task, "project") def set_status(self, update=False): - status = { - "0": "Draft", - "1": "Submitted", - "2": "Cancelled" - }[cstr(self.docstatus or 0)] + status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[cstr(self.docstatus or 0)] - paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) precision = self.precision("grand_total") - if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 - and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved': + + if ( + # set as paid + self.is_paid + or ( + flt(self.total_sanctioned_amount > 0) + and ( + # grand total is reimbursed + ( + self.docstatus == 1 + and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision) + ) + # grand total (to be paid) is 0 since linked advances already cover the claimed amount + or (flt(self.grand_total, precision) == 0) + ) + ) + ) and self.approval_status == "Approved": status = "Paid" - elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': + elif ( + flt(self.total_sanctioned_amount) > 0 + and self.docstatus == 1 + and self.approval_status == "Approved" + ): status = "Unpaid" - elif self.docstatus == 1 and self.approval_status == 'Rejected': - status = 'Rejected' + elif self.docstatus == 1 and self.approval_status == "Rejected": + status = "Rejected" if update: self.db_set("status", status) @@ -62,14 +82,16 @@ class ExpenseClaim(AccountsController): def set_payable_account(self): if not self.payable_account and not self.is_paid: - self.payable_account = frappe.get_cached_value('Company', self.company, 'default_expense_claim_payable_account') + self.payable_account = frappe.get_cached_value( + "Company", self.company, "default_expense_claim_payable_account" + ) def set_cost_center(self): if not self.cost_center: - self.cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') + self.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") def on_submit(self): - if self.approval_status=="Draft": + if self.approval_status == "Draft": frappe.throw(_("""Approval Status must be 'Approved' or 'Rejected'""")) self.update_task_and_project() @@ -83,7 +105,7 @@ class ExpenseClaim(AccountsController): def on_cancel(self): self.update_task_and_project() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") if self.payable_account: self.make_gl_entries(cancel=True) @@ -114,43 +136,51 @@ class ExpenseClaim(AccountsController): # payable entry if self.grand_total: gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "credit": self.grand_total, - "credit_in_account_currency": self.grand_total, - "against": ",".join([d.default_account for d in self.expenses]), - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.payable_account, + "credit": self.grand_total, + "credit_in_account_currency": self.grand_total, + "against": ",".join([d.default_account for d in self.expenses]), + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": self.doctype, + "against_voucher": self.name, + "cost_center": self.cost_center, + }, + item=self, + ) ) # expense entries for data in self.expenses: gl_entry.append( - self.get_gl_dict({ - "account": data.default_account, - "debit": data.sanctioned_amount, - "debit_in_account_currency": data.sanctioned_amount, - "against": self.employee, - "cost_center": data.cost_center or self.cost_center - }, item=data) + self.get_gl_dict( + { + "account": data.default_account, + "debit": data.sanctioned_amount, + "debit_in_account_currency": data.sanctioned_amount, + "against": self.employee, + "cost_center": data.cost_center or self.cost_center, + }, + item=data, + ) ) for data in self.advances: gl_entry.append( - self.get_gl_dict({ - "account": data.advance_account, - "credit": data.allocated_amount, - "credit_in_account_currency": data.allocated_amount, - "against": ",".join([d.default_account for d in self.expenses]), - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": "Employee Advance", - "against_voucher": data.employee_advance - }) + self.get_gl_dict( + { + "account": data.advance_account, + "credit": data.allocated_amount, + "credit_in_account_currency": data.allocated_amount, + "against": ",".join([d.default_account for d in self.expenses]), + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": "Employee Advance", + "against_voucher": data.employee_advance, + } + ) ) self.add_tax_gl_entries(gl_entry) @@ -159,25 +189,31 @@ class ExpenseClaim(AccountsController): # payment entry payment_account = get_bank_cash_account(self.mode_of_payment, self.company).get("account") gl_entry.append( - self.get_gl_dict({ - "account": payment_account, - "credit": self.grand_total, - "credit_in_account_currency": self.grand_total, - "against": self.employee - }, item=self) + self.get_gl_dict( + { + "account": payment_account, + "credit": self.grand_total, + "credit_in_account_currency": self.grand_total, + "against": self.employee, + }, + item=self, + ) ) gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "party_type": "Employee", - "party": self.employee, - "against": payment_account, - "debit": self.grand_total, - "debit_in_account_currency": self.grand_total, - "against_voucher": self.name, - "against_voucher_type": self.doctype, - }, item=self) + self.get_gl_dict( + { + "account": self.payable_account, + "party_type": "Employee", + "party": self.employee, + "against": payment_account, + "debit": self.grand_total, + "debit_in_account_currency": self.grand_total, + "against_voucher": self.name, + "against_voucher_type": self.doctype, + }, + item=self, + ) ) return gl_entry @@ -186,22 +222,28 @@ class ExpenseClaim(AccountsController): # tax table gl entries for tax in self.get("taxes"): gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "debit": tax.tax_amount, - "debit_in_account_currency": tax.tax_amount, - "against": self.employee, - "cost_center": self.cost_center, - "against_voucher_type": self.doctype, - "against_voucher": self.name - }, item=tax) + self.get_gl_dict( + { + "account": tax.account_head, + "debit": tax.tax_amount, + "debit_in_account_currency": tax.tax_amount, + "against": self.employee, + "cost_center": self.cost_center, + "against_voucher_type": self.doctype, + "against_voucher": self.name, + }, + item=tax, + ) ) def validate_account_details(self): for data in self.expenses: if not data.cost_center: - frappe.throw(_("Row {0}: {1} is required in the expenses table to book an expense claim.") - .format(data.idx, frappe.bold("Cost Center"))) + frappe.throw( + _("Row {0}: {1} is required in the expenses table to book an expense claim.").format( + data.idx, frappe.bold("Cost Center") + ) + ) if self.is_paid: if not self.mode_of_payment: @@ -210,8 +252,8 @@ class ExpenseClaim(AccountsController): def calculate_total_amount(self): self.total_claimed_amount = 0 self.total_sanctioned_amount = 0 - for d in self.get('expenses'): - if self.approval_status == 'Rejected': + for d in self.get("expenses"): + if self.approval_status == "Rejected": d.sanctioned_amount = 0.0 self.total_claimed_amount += flt(d.amount) @@ -222,12 +264,16 @@ class ExpenseClaim(AccountsController): self.total_taxes_and_charges = 0 for tax in self.taxes: if tax.rate: - tax.tax_amount = flt(self.total_sanctioned_amount) * flt(tax.rate/100) + tax.tax_amount = flt(self.total_sanctioned_amount) * flt(tax.rate / 100) tax.total = flt(tax.tax_amount) + flt(self.total_sanctioned_amount) self.total_taxes_and_charges += flt(tax.tax_amount) - self.grand_total = flt(self.total_sanctioned_amount) + flt(self.total_taxes_and_charges) - flt(self.total_advance_amount) + self.grand_total = ( + flt(self.total_sanctioned_amount) + + flt(self.total_taxes_and_charges) + - flt(self.total_advance_amount) + ) def update_task(self): task = frappe.get_doc("Task", self.task) @@ -237,16 +283,23 @@ class ExpenseClaim(AccountsController): def validate_advances(self): self.total_advance_amount = 0 for d in self.get("advances"): - ref_doc = frappe.db.get_value("Employee Advance", d.employee_advance, - ["posting_date", "paid_amount", "claimed_amount", "advance_account"], as_dict=1) + ref_doc = frappe.db.get_value( + "Employee Advance", + d.employee_advance, + ["posting_date", "paid_amount", "claimed_amount", "advance_account"], + as_dict=1, + ) d.posting_date = ref_doc.posting_date d.advance_account = ref_doc.advance_account d.advance_paid = ref_doc.paid_amount d.unclaimed_amount = flt(ref_doc.paid_amount) - flt(ref_doc.claimed_amount) if d.allocated_amount and flt(d.allocated_amount) > flt(d.unclaimed_amount): - frappe.throw(_("Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2}") - .format(d.idx, d.allocated_amount, d.unclaimed_amount)) + frappe.throw( + _("Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2}").format( + d.idx, d.allocated_amount, d.unclaimed_amount + ) + ) self.total_advance_amount += flt(d.allocated_amount) @@ -255,27 +308,36 @@ class ExpenseClaim(AccountsController): if flt(self.total_advance_amount, precision) > flt(self.total_claimed_amount, precision): frappe.throw(_("Total advance amount cannot be greater than total claimed amount")) - if self.total_sanctioned_amount \ - and flt(self.total_advance_amount, precision) > flt(self.total_sanctioned_amount, precision): + if self.total_sanctioned_amount and flt(self.total_advance_amount, precision) > flt( + self.total_sanctioned_amount, precision + ): frappe.throw(_("Total advance amount cannot be greater than total sanctioned amount")) def validate_sanctioned_amount(self): - for d in self.get('expenses'): + for d in self.get("expenses"): if flt(d.sanctioned_amount) > flt(d.amount): - frappe.throw(_("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx)) + frappe.throw( + _("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx) + ) def set_expense_account(self, validate=False): for expense in self.expenses: if not expense.default_account or not validate: - expense.default_account = get_expense_claim_account(expense.expense_type, self.company)["account"] + expense.default_account = get_expense_claim_account(expense.expense_type, self.company)[ + "account" + ] + def update_reimbursed_amount(doc, amount): doc.total_amount_reimbursed += amount - frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", doc.total_amount_reimbursed) + frappe.db.set_value( + "Expense Claim", doc.name, "total_amount_reimbursed", doc.total_amount_reimbursed + ) doc.set_status() - frappe.db.set_value("Expense Claim", doc.name , "status", doc.status) + frappe.db.set_value("Expense Claim", doc.name, "status", doc.status) + @frappe.whitelist() def make_bank_entry(dt, dn): @@ -286,96 +348,115 @@ def make_bank_entry(dt, dn): if not default_bank_cash_account: default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash") - payable_amount = flt(expense_claim.total_sanctioned_amount) \ - - flt(expense_claim.total_amount_reimbursed) - flt(expense_claim.total_advance_amount) + payable_amount = ( + flt(expense_claim.total_sanctioned_amount) + - flt(expense_claim.total_amount_reimbursed) + - flt(expense_claim.total_advance_amount) + ) je = frappe.new_doc("Journal Entry") - je.voucher_type = 'Bank Entry' + je.voucher_type = "Bank Entry" je.company = expense_claim.company - je.remark = 'Payment against Expense Claim: ' + dn + je.remark = "Payment against Expense Claim: " + dn - je.append("accounts", { - "account": expense_claim.payable_account, - "debit_in_account_currency": payable_amount, - "reference_type": "Expense Claim", - "party_type": "Employee", - "party": expense_claim.employee, - "cost_center": erpnext.get_default_cost_center(expense_claim.company), - "reference_name": expense_claim.name - }) + je.append( + "accounts", + { + "account": expense_claim.payable_account, + "debit_in_account_currency": payable_amount, + "reference_type": "Expense Claim", + "party_type": "Employee", + "party": expense_claim.employee, + "cost_center": erpnext.get_default_cost_center(expense_claim.company), + "reference_name": expense_claim.name, + }, + ) - je.append("accounts", { - "account": default_bank_cash_account.account, - "credit_in_account_currency": payable_amount, - "reference_type": "Expense Claim", - "reference_name": expense_claim.name, - "balance": default_bank_cash_account.balance, - "account_currency": default_bank_cash_account.account_currency, - "cost_center": erpnext.get_default_cost_center(expense_claim.company), - "account_type": default_bank_cash_account.account_type - }) + je.append( + "accounts", + { + "account": default_bank_cash_account.account, + "credit_in_account_currency": payable_amount, + "reference_type": "Expense Claim", + "reference_name": expense_claim.name, + "balance": default_bank_cash_account.balance, + "account_currency": default_bank_cash_account.account_currency, + "cost_center": erpnext.get_default_cost_center(expense_claim.company), + "account_type": default_bank_cash_account.account_type, + }, + ) return je.as_dict() + @frappe.whitelist() def get_expense_claim_account_and_cost_center(expense_claim_type, company): data = get_expense_claim_account(expense_claim_type, company) cost_center = erpnext.get_default_cost_center(company) - return { - "account": data.get("account"), - "cost_center": cost_center - } + return {"account": data.get("account"), "cost_center": cost_center} + @frappe.whitelist() def get_expense_claim_account(expense_claim_type, company): - account = frappe.db.get_value("Expense Claim Account", - {"parent": expense_claim_type, "company": company}, "default_account") + account = frappe.db.get_value( + "Expense Claim Account", {"parent": expense_claim_type, "company": company}, "default_account" + ) if not account: - frappe.throw(_("Set the default account for the {0} {1}") - .format(frappe.bold("Expense Claim Type"), get_link_to_form("Expense Claim Type", expense_claim_type))) + frappe.throw( + _("Set the default account for the {0} {1}").format( + frappe.bold("Expense Claim Type"), get_link_to_form("Expense Claim Type", expense_claim_type) + ) + ) + + return {"account": account} - return { - "account": account - } @frappe.whitelist() def get_advances(employee, advance_id=None): if not advance_id: - condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee)) + condition = "docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount".format( + frappe.db.escape(employee) + ) else: - condition = 'name={0}'.format(frappe.db.escape(advance_id)) + condition = "name={0}".format(frappe.db.escape(advance_id)) - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, posting_date, paid_amount, claimed_amount, advance_account from `tabEmployee Advance` where {0} - """.format(condition), as_dict=1) + """.format( + condition + ), + as_dict=1, + ) @frappe.whitelist() def get_expense_claim( - employee_name, company, employee_advance_name, posting_date, paid_amount, claimed_amount): - default_payable_account = frappe.get_cached_value('Company', company, "default_payable_account") - default_cost_center = frappe.get_cached_value('Company', company, 'cost_center') + employee_name, company, employee_advance_name, posting_date, paid_amount, claimed_amount +): + default_payable_account = frappe.get_cached_value("Company", company, "default_payable_account") + default_cost_center = frappe.get_cached_value("Company", company, "cost_center") - expense_claim = frappe.new_doc('Expense Claim') + expense_claim = frappe.new_doc("Expense Claim") expense_claim.company = company expense_claim.employee = employee_name expense_claim.payable_account = default_payable_account expense_claim.cost_center = default_cost_center expense_claim.is_paid = 1 if flt(paid_amount) else 0 expense_claim.append( - 'advances', + "advances", { - 'employee_advance': employee_advance_name, - 'posting_date': posting_date, - 'advance_paid': flt(paid_amount), - 'unclaimed_amount': flt(paid_amount) - flt(claimed_amount), - 'allocated_amount': flt(paid_amount) - flt(claimed_amount) - } + "employee_advance": employee_advance_name, + "posting_date": posting_date, + "advance_paid": flt(paid_amount), + "unclaimed_amount": flt(paid_amount) - flt(claimed_amount), + "allocated_amount": flt(paid_amount) - flt(claimed_amount), + }, ) return expense_claim diff --git a/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py b/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py index 44052cc8e6b..8b1acc619ee 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'reference_name', - 'internal_links': { - 'Employee Advance': ['advances', 'employee_advance'] - }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Journal Entry'] - }, - { - 'label': _('Reference'), - 'items': ['Employee Advance'] - }, - ] + "fieldname": "reference_name", + "internal_links": {"Employee Advance": ["advances", "employee_advance"]}, + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}, + {"label": _("Reference"), "items": ["Employee Advance"]}, + ], } diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 46958b1ec4c..9b3d53a2105 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -10,8 +10,8 @@ from erpnext.accounts.doctype.account.test_account import create_account from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry -test_dependencies = ['Employee'] -company_name = '_Test Company 3' +test_dependencies = ["Employee"] +company_name = "_Test Company 3" class TestExpenseClaim(unittest.TestCase): @@ -23,28 +23,26 @@ class TestExpenseClaim(unittest.TestCase): frappe.db.sql("""delete from `tabProject`""") frappe.db.sql("update `tabExpense Claim` set project = '', task = ''") - project = frappe.get_doc({ - "project_name": "_Test Project 1", - "doctype": "Project" - }) + project = frappe.get_doc({"project_name": "_Test Project 1", "doctype": "Project"}) project.save() - task = frappe.get_doc(dict( - doctype = 'Task', - subject = '_Test Project Task 1', - status = 'Open', - project = project.name - )).insert() + task = frappe.get_doc( + dict(doctype="Task", subject="_Test Project Task 1", status="Open", project=project.name) + ).insert() task_name = task.name payable_account = get_payable_account(company_name) - make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name) + make_expense_claim( + payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name + ) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200) - expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name) + expense_claim2 = make_expense_claim( + payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name + ) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700) self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700) @@ -56,7 +54,9 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_status(self): payable_account = get_payable_account(company_name) - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3") + expense_claim = make_expense_claim( + payable_account, 300, 200, company_name, "Travel Expenses - _TC3" + ) je_dict = make_bank_entry("Expense Claim", expense_claim.name) je = frappe.get_doc(je_dict) @@ -72,24 +72,110 @@ class TestExpenseClaim(unittest.TestCase): expense_claim = frappe.get_doc("Expense Claim", expense_claim.name) self.assertEqual(expense_claim.status, "Unpaid") + # expense claim without any sanctioned amount should not have status as Paid + claim = make_expense_claim(payable_account, 1000, 0, "_Test Company", "Travel Expenses - _TC") + self.assertEqual(claim.total_sanctioned_amount, 0) + self.assertEqual(claim.status, "Submitted") + + # no gl entries created + gl_entry = frappe.get_all( + "GL Entry", {"voucher_type": "Expense Claim", "voucher_no": claim.name} + ) + self.assertEqual(len(gl_entry), 0) + + def test_expense_claim_against_fully_paid_advances(self): + from erpnext.hr.doctype.employee_advance.test_employee_advance import ( + get_advances_for_claim, + make_employee_advance, + make_payment_entry, + ) + + frappe.db.delete("Employee Advance") + + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim( + payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + # claim for already paid out advances + claim = get_advances_for_claim(claim, advance.name) + claim.save() + claim.submit() + + self.assertEqual(claim.grand_total, 0) + self.assertEqual(claim.status, "Paid") + + def test_expense_claim_partially_paid_via_advance(self): + from erpnext.hr.doctype.employee_advance.test_employee_advance import ( + get_advances_for_claim, + make_employee_advance, + ) + from erpnext.hr.doctype.employee_advance.test_employee_advance import ( + make_payment_entry as make_advance_payment, + ) + + frappe.db.delete("Employee Advance") + + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim( + payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) + + # link advance for partial amount + advance = make_employee_advance(claim.employee, {"advance_amount": 500}) + pe = make_advance_payment(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name) + claim.save() + claim.submit() + + self.assertEqual(claim.grand_total, 500) + self.assertEqual(claim.status, "Unpaid") + + # reimburse remaning amount + make_payment_entry(claim, payable_account, 500) + claim.reload() + + self.assertEqual(claim.total_amount_reimbursed, 500) + self.assertEqual(claim.status, "Paid") + def test_expense_claim_gl_entry(self): payable_account = get_payable_account(company_name) taxes = generate_taxes() - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", - do_not_submit=True, taxes=taxes) + expense_claim = make_expense_claim( + payable_account, + 300, + 200, + company_name, + "Travel Expenses - _TC3", + do_not_submit=True, + taxes=taxes, + ) expense_claim.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Expense Claim' and voucher_no=%s - order by account asc""", expense_claim.name, as_dict=1) + order by account asc""", + expense_claim.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - ['Output Tax CGST - _TC3',18.0, 0.0], - [payable_account, 0.0, 218.0], - ["Travel Expenses - _TC3", 200.0, 0.0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + ["Output Tax CGST - _TC3", 18.0, 0.0], + [payable_account, 0.0, 218.0], + ["Travel Expenses - _TC3", 200.0, 0.0], + ] + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) @@ -98,20 +184,30 @@ class TestExpenseClaim(unittest.TestCase): def test_rejected_expense_claim(self): payable_account = get_payable_account(company_name) - expense_claim = frappe.get_doc({ - "doctype": "Expense Claim", - "employee": "_T-Employee-00001", - "payable_account": payable_account, - "approval_status": "Rejected", - "expenses": - [{"expense_type": "Travel", "default_account": "Travel Expenses - _TC3", "amount": 300, "sanctioned_amount": 200}] - }) + expense_claim = frappe.get_doc( + { + "doctype": "Expense Claim", + "employee": "_T-Employee-00001", + "payable_account": payable_account, + "approval_status": "Rejected", + "expenses": [ + { + "expense_type": "Travel", + "default_account": "Travel Expenses - _TC3", + "amount": 300, + "sanctioned_amount": 200, + } + ], + } + ) expense_claim.submit() - self.assertEqual(expense_claim.status, 'Rejected') + self.assertEqual(expense_claim.status, "Rejected") self.assertEqual(expense_claim.total_sanctioned_amount, 0.0) - gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name}) + gl_entry = frappe.get_all( + "GL Entry", {"voucher_type": "Expense Claim", "voucher_no": expense_claim.name} + ) self.assertEqual(len(gl_entry), 0) def test_expense_approver_perms(self): @@ -120,7 +216,9 @@ class TestExpenseClaim(unittest.TestCase): # check doc shared payable_account = get_payable_account("_Test Company") - expense_claim = make_expense_claim(payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + expense_claim = make_expense_claim( + payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) expense_claim.expense_approver = user expense_claim.save() self.assertTrue(expense_claim.name in frappe.share.get_shared("Expense Claim", user)) @@ -144,51 +242,76 @@ class TestExpenseClaim(unittest.TestCase): def test_multiple_payment_entries_against_expense(self): # Creating expense claim payable_account = get_payable_account("_Test Company") - expense_claim = make_expense_claim(payable_account, 5500, 5500, "_Test Company", "Travel Expenses - _TC") + expense_claim = make_expense_claim( + payable_account, 5500, 5500, "_Test Company", "Travel Expenses - _TC" + ) expense_claim.save() expense_claim.submit() # Payment entry 1: paying 500 - make_payment_entry(expense_claim, payable_account,500) - outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + make_payment_entry(expense_claim, payable_account, 500) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts( + expense_claim + ) self.assertEqual(outstanding_amount, 5000) self.assertEqual(total_amount_reimbursed, 500) # Payment entry 1: paying 2000 - make_payment_entry(expense_claim, payable_account,2000) - outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + make_payment_entry(expense_claim, payable_account, 2000) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts( + expense_claim + ) self.assertEqual(outstanding_amount, 3000) self.assertEqual(total_amount_reimbursed, 2500) # Payment entry 1: paying 3000 - make_payment_entry(expense_claim, payable_account,3000) - outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + make_payment_entry(expense_claim, payable_account, 3000) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts( + expense_claim + ) self.assertEqual(outstanding_amount, 0) self.assertEqual(total_amount_reimbursed, 5500) def get_payable_account(company): - return frappe.get_cached_value('Company', company, 'default_payable_account') + return frappe.get_cached_value("Company", company, "default_payable_account") + def generate_taxes(): - parent_account = frappe.db.get_value('Account', - {'company': company_name, 'is_group':1, 'account_type': 'Tax'}, - 'name') - account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account) - return {'taxes':[{ - "account_head": account, - "rate": 9, - "description": "CGST", - "tax_amount": 10, - "total": 210 - }]} + parent_account = frappe.db.get_value( + "Account", {"company": company_name, "is_group": 1, "account_type": "Tax"}, "name" + ) + account = create_account( + company=company_name, + account_name="Output Tax CGST", + account_type="Tax", + parent_account=parent_account, + ) + return { + "taxes": [ + {"account_head": account, "rate": 9, "description": "CGST", "tax_amount": 10, "total": 210} + ] + } -def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None): + +def make_expense_claim( + payable_account, + amount, + sanctioned_amount, + company, + account, + project=None, + task_name=None, + do_not_submit=False, + taxes=None, +): employee = frappe.db.get_value("Employee", {"status": "Active"}) if not employee: employee = make_employee("test_employee@expense_claim.com", company=company) - currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) + currency, cost_center = frappe.db.get_value( + "Company", company, ["default_currency", "cost_center"] + ) expense_claim = { "doctype": "Expense Claim", "employee": employee, @@ -196,14 +319,16 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco "approval_status": "Approved", "company": company, "currency": currency, - "expenses": [{ - "expense_type": "Travel", - "default_account": account, - "currency": currency, - "amount": amount, - "sanctioned_amount": sanctioned_amount, - "cost_center": cost_center - }] + "expenses": [ + { + "expense_type": "Travel", + "default_account": account, + "currency": currency, + "amount": amount, + "sanctioned_amount": sanctioned_amount, + "cost_center": cost_center, + } + ], } if taxes: expense_claim.update(taxes) @@ -220,17 +345,24 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco expense_claim.submit() return expense_claim -def get_outstanding_and_total_reimbursed_amounts(expense_claim): - outstanding_amount = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_sanctioned_amount")) - \ - flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) - total_amount_reimbursed = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) - return outstanding_amount,total_amount_reimbursed +def get_outstanding_and_total_reimbursed_amounts(expense_claim): + outstanding_amount = flt( + frappe.db.get_value("Expense Claim", expense_claim.name, "total_sanctioned_amount") + ) - flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) + total_amount_reimbursed = flt( + frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed") + ) + + return outstanding_amount, total_amount_reimbursed + def make_payment_entry(expense_claim, payable_account, amt): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - pe = get_payment_entry("Expense Claim", expense_claim.name, bank_account="_Test Bank USD - _TC", bank_amount=amt) + pe = get_payment_entry( + "Expense Claim", expense_claim.name, bank_account="_Test Bank USD - _TC", bank_amount=amt + ) pe.reference_no = "1" pe.reference_date = nowdate() pe.source_exchange_rate = 1 @@ -238,4 +370,3 @@ def make_payment_entry(expense_claim, payable_account, amt): pe.references[0].allocated_amount = amt pe.insert() pe.submit() - diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py index 570b2c115fa..6d29f7d8e71 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py @@ -18,12 +18,13 @@ class ExpenseClaimType(Document): for entry in self.accounts: accounts_list.append(entry.company) - if len(accounts_list)!= len(set(accounts_list)): + if len(accounts_list) != len(set(accounts_list)): frappe.throw(_("Same Company is entered more than once")) def validate_accounts(self): for entry in self.accounts: """Error when Company of Ledger account doesn't match with Company Selected""" if frappe.db.get_value("Account", entry.default_account, "company") != entry.company: - frappe.throw(_("Account {0} does not match with Company {1}" - ).format(entry.default_account, entry.company)) + frappe.throw( + _("Account {0} does not match with Company {1}").format(entry.default_account, entry.company) + ) diff --git a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py b/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py index a2403b6eb8f..62348e28255 100644 --- a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py +++ b/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Expense Claim Type') + class TestExpenseClaimType(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index 35775ab816c..ea32ba744d8 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -10,7 +10,9 @@ from frappe.model.document import Document from frappe.utils import cint, formatdate, getdate, today -class OverlapError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + class HolidayList(Document): def validate(self): @@ -21,9 +23,14 @@ class HolidayList(Document): def get_weekly_off_dates(self): self.validate_values() date_list = self.get_weekly_off_date_list(self.from_date, self.to_date) - last_idx = max([cint(d.idx) for d in self.get("holidays")] or [0,]) + last_idx = max( + [cint(d.idx) for d in self.get("holidays")] + or [ + 0, + ] + ) for i, d in enumerate(date_list): - ch = self.append('holidays', {}) + ch = self.append("holidays", {}) ch.description = self.weekly_off ch.holiday_date = d ch.weekly_off = 1 @@ -33,14 +40,17 @@ class HolidayList(Document): if not self.weekly_off: throw(_("Please select weekly off day")) - def validate_days(self): if getdate(self.from_date) > getdate(self.to_date): throw(_("To Date cannot be before From Date")) for day in self.get("holidays"): if not (getdate(self.from_date) <= getdate(day.holiday_date) <= getdate(self.to_date)): - frappe.throw(_("The holiday on {0} is not between From Date and To Date").format(formatdate(day.holiday_date))) + frappe.throw( + _("The holiday on {0} is not between From Date and To Date").format( + formatdate(day.holiday_date) + ) + ) def get_weekly_off_date_list(self, start_date, end_date): start_date, end_date = getdate(start_date), getdate(end_date) @@ -66,7 +76,8 @@ class HolidayList(Document): @frappe.whitelist() def clear_table(self): - self.set('holidays', []) + self.set("holidays", []) + @frappe.whitelist() def get_events(start, end, filters=None): @@ -82,23 +93,28 @@ def get_events(start, end, filters=None): filters = [] if start: - filters.append(['Holiday', 'holiday_date', '>', getdate(start)]) + filters.append(["Holiday", "holiday_date", ">", getdate(start)]) if end: - filters.append(['Holiday', 'holiday_date', '<', getdate(end)]) + filters.append(["Holiday", "holiday_date", "<", getdate(end)]) - return frappe.get_list('Holiday List', - fields=['name', '`tabHoliday`.holiday_date', '`tabHoliday`.description', '`tabHoliday List`.color'], - filters = filters, - update={"allDay": 1}) + return frappe.get_list( + "Holiday List", + fields=[ + "name", + "`tabHoliday`.holiday_date", + "`tabHoliday`.description", + "`tabHoliday List`.color", + ], + filters=filters, + update={"allDay": 1}, + ) def is_holiday(holiday_list, date=None): - """Returns true if the given date is a holiday in the given holiday list - """ + """Returns true if the given date is a holiday in the given holiday list""" if date is None: date = today() if holiday_list: - return bool(frappe.get_all('Holiday List', - dict(name=holiday_list, holiday_date=date))) + return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date))) else: return False diff --git a/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py b/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py index e074e266b87..0cbf09461b5 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py @@ -1,21 +1,15 @@ - - def get_data(): return { - 'fieldname': 'holiday_list', - 'non_standard_fieldnames': { - 'Company': 'default_holiday_list', - 'Leave Period': 'optional_holiday_list' + "fieldname": "holiday_list", + "non_standard_fieldnames": { + "Company": "default_holiday_list", + "Leave Period": "optional_holiday_list", }, - 'transactions': [ + "transactions": [ { - 'items': ['Company', 'Employee', 'Workstation'], + "items": ["Company", "Employee", "Workstation"], }, - { - 'items': ['Leave Period', 'Shift Type'] - }, - { - 'items': ['Service Level', 'Service Level Agreement'] - } - ] + {"items": ["Leave Period", "Shift Type"]}, + {"items": ["Service Level", "Service Level Agreement"]}, + ], } diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py index c9239edb720..d32cfe82650 100644 --- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import unittest +from contextlib import contextmanager from datetime import timedelta import frappe @@ -11,22 +12,50 @@ from frappe.utils import getdate class TestHolidayList(unittest.TestCase): def test_holiday_list(self): today_date = getdate() - test_holiday_dates = [today_date-timedelta(days=5), today_date-timedelta(days=4)] - holiday_list = make_holiday_list("test_holiday_list", + test_holiday_dates = [today_date - timedelta(days=5), today_date - timedelta(days=4)] + holiday_list = make_holiday_list( + "test_holiday_list", holiday_dates=[ - {'holiday_date': test_holiday_dates[0], 'description': 'test holiday'}, - {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'} - ]) - fetched_holiday_list = frappe.get_value('Holiday List', holiday_list.name) + {"holiday_date": test_holiday_dates[0], "description": "test holiday"}, + {"holiday_date": test_holiday_dates[1], "description": "test holiday2"}, + ], + ) + fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name) self.assertEqual(holiday_list.name, fetched_holiday_list) -def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getdate(), holiday_dates=None): + +def make_holiday_list( + name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None +): frappe.delete_doc_if_exists("Holiday List", name, force=1) - doc = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": name, - "from_date" : from_date, - "to_date" : to_date, - "holidays" : holiday_dates - }).insert() + doc = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": name, + "from_date": from_date, + "to_date": to_date, + "holidays": holiday_dates, + } + ).insert() return doc + + +@contextmanager +def set_holiday_list(holiday_list, company_name): + """ + Context manager for setting holiday list in tests + """ + try: + company = frappe.get_doc("Company", company_name) + previous_holiday_list = company.default_holiday_list + + company.default_holiday_list = holiday_list + company.save() + + yield + + finally: + # restore holiday list setup + company = frappe.get_doc("Company", company_name) + company.default_holiday_list = previous_holiday_list + company.save() diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index c295bcbc0d9..72a49e285a0 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -10,6 +10,7 @@ from frappe.utils import format_date # Wether to proceed with frequency change PROCEED_WITH_FREQUENCY_CHANGE = False + class HRSettings(Document): def validate(self): self.set_naming_series() @@ -22,21 +23,24 @@ class HRSettings(Document): def set_naming_series(self): from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Employee", "employee_number", - self.get("emp_created_by")=="Naming Series", hide_name_field=True) + + set_by_naming_series( + "Employee", + "employee_number", + self.get("emp_created_by") == "Naming Series", + hide_name_field=True, + ) def validate_frequency_change(self): weekly_job, monthly_job = None, None try: weekly_job = frappe.get_doc( - 'Scheduled Job Type', - 'employee_reminders.send_reminders_in_advance_weekly' + "Scheduled Job Type", "employee_reminders.send_reminders_in_advance_weekly" ) monthly_job = frappe.get_doc( - 'Scheduled Job Type', - 'employee_reminders.send_reminders_in_advance_monthly' + "Scheduled Job Type", "employee_reminders.send_reminders_in_advance_monthly" ) except frappe.DoesNotExistError: return @@ -62,17 +66,20 @@ class HRSettings(Document): from_date = frappe.bold(format_date(from_date)) to_date = frappe.bold(format_date(to_date)) frappe.msgprint( - msg=frappe._('Employees will miss holiday reminders from {} until {}.
Do you want to proceed with this change?').format(from_date, to_date), - title='Confirm change in Frequency', + msg=frappe._( + "Employees will miss holiday reminders from {} until {}.
Do you want to proceed with this change?" + ).format(from_date, to_date), + title="Confirm change in Frequency", primary_action={ - 'label': frappe._('Yes, Proceed'), - 'client_action': 'erpnext.proceed_save_with_reminders_frequency_change' + "label": frappe._("Yes, Proceed"), + "client_action": "erpnext.proceed_save_with_reminders_frequency_change", }, - raise_exception=frappe.ValidationError + raise_exception=frappe.ValidationError, ) + @frappe.whitelist() def set_proceed_with_frequency_change(): - '''Enables proceed with frequency change''' + """Enables proceed with frequency change""" global PROCEED_WITH_FREQUENCY_CHANGE PROCEED_WITH_FREQUENCY_CHANGE = True diff --git a/erpnext/hr/doctype/interest/test_interest.py b/erpnext/hr/doctype/interest/test_interest.py index d4ecd9b841e..eacb57f7587 100644 --- a/erpnext/hr/doctype/interest/test_interest.py +++ b/erpnext/hr/doctype/interest/test_interest.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Interest') + class TestInterest(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py index f5312476a28..8f0ff021335 100644 --- a/erpnext/hr/doctype/interview/interview.py +++ b/erpnext/hr/doctype/interview/interview.py @@ -13,6 +13,7 @@ from frappe.utils import cstr, get_datetime, get_link_to_form class DuplicateInterviewRoundError(frappe.ValidationError): pass + class Interview(Document): def validate(self): self.validate_duplicate_interview() @@ -20,37 +21,47 @@ class Interview(Document): self.validate_overlap() def on_submit(self): - if self.status not in ['Cleared', 'Rejected']: - frappe.throw(_('Only Interviews with Cleared or Rejected status can be submitted.'), title=_('Not Allowed')) + if self.status not in ["Cleared", "Rejected"]: + frappe.throw( + _("Only Interviews with Cleared or Rejected status can be submitted."), title=_("Not Allowed") + ) def validate_duplicate_interview(self): - duplicate_interview = frappe.db.exists('Interview', { - 'job_applicant': self.job_applicant, - 'interview_round': self.interview_round, - 'docstatus': 1 - } + duplicate_interview = frappe.db.exists( + "Interview", + {"job_applicant": self.job_applicant, "interview_round": self.interview_round, "docstatus": 1}, ) if duplicate_interview: - frappe.throw(_('Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}').format( - frappe.bold(get_link_to_form('Interview', duplicate_interview)), - frappe.bold(self.job_applicant) - )) + frappe.throw( + _( + "Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}" + ).format( + frappe.bold(get_link_to_form("Interview", duplicate_interview)), + frappe.bold(self.job_applicant), + ) + ) def validate_designation(self): - applicant_designation = frappe.db.get_value('Job Applicant', self.job_applicant, 'designation') - if self.designation : + applicant_designation = frappe.db.get_value("Job Applicant", self.job_applicant, "designation") + if self.designation: if self.designation != applicant_designation: - frappe.throw(_('Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}').format( - self.interview_round, frappe.bold(self.designation), applicant_designation), - exc=DuplicateInterviewRoundError) + frappe.throw( + _( + "Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}" + ).format( + self.interview_round, frappe.bold(self.designation), applicant_designation + ), + exc=DuplicateInterviewRoundError, + ) else: self.designation = applicant_designation def validate_overlap(self): - interviewers = [entry.interviewer for entry in self.interview_details] or [''] + interviewers = [entry.interviewer for entry in self.interview_details] or [""] - overlaps = frappe.db.sql(""" + overlaps = frappe.db.sql( + """ SELECT interview.name FROM `tabInterview` as interview INNER JOIN `tabInterview Detail` as detail @@ -60,13 +71,25 @@ class Interview(Document): ((from_time < %s and to_time > %s) or (from_time > %s and to_time < %s) or (from_time = %s)) - """, (self.scheduled_on, self.name, self.job_applicant, interviewers, - self.from_time, self.to_time, self.from_time, self.to_time, self.from_time)) + """, + ( + self.scheduled_on, + self.name, + self.job_applicant, + interviewers, + self.from_time, + self.to_time, + self.from_time, + self.to_time, + self.from_time, + ), + ) if overlaps: - overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0])) - frappe.throw(overlapping_details, title=_('Overlap')) - + overlapping_details = _("Interview overlaps with {0}").format( + get_link_to_form("Interview", overlaps[0][0]) + ) + frappe.throw(overlapping_details, title=_("Overlap")) @frappe.whitelist() def reschedule_interview(self, scheduled_on, from_time, to_time): @@ -74,116 +97,135 @@ class Interview(Document): from_time = self.from_time to_time = self.to_time - self.db_set({ - 'scheduled_on': scheduled_on, - 'from_time': from_time, - 'to_time': to_time - }) + self.db_set({"scheduled_on": scheduled_on, "from_time": from_time, "to_time": to_time}) self.notify_update() recipients = get_recipients(self.name) try: frappe.sendmail( - recipients= recipients, - subject=_('Interview: {0} Rescheduled').format(self.name), - message=_('Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}').format( - original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time), + recipients=recipients, + subject=_("Interview: {0} Rescheduled").format(self.name), + message=_("Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}").format( + original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time + ), reference_doctype=self.doctype, - reference_name=self.name + reference_name=self.name, ) except Exception: - frappe.msgprint(_('Failed to send the Interview Reschedule notification. Please configure your email account.')) + frappe.msgprint( + _("Failed to send the Interview Reschedule notification. Please configure your email account.") + ) - frappe.msgprint(_('Interview Rescheduled successfully'), indicator='green') + frappe.msgprint(_("Interview Rescheduled successfully"), indicator="green") def get_recipients(name, for_feedback=0): - interview = frappe.get_doc('Interview', name) + interview = frappe.get_doc("Interview", name) if for_feedback: recipients = [d.interviewer for d in interview.interview_details if not d.interview_feedback] else: recipients = [d.interviewer for d in interview.interview_details] - recipients.append(frappe.db.get_value('Job Applicant', interview.job_applicant, 'email_id')) + recipients.append(frappe.db.get_value("Job Applicant", interview.job_applicant, "email_id")) return recipients @frappe.whitelist() def get_interviewers(interview_round): - return frappe.get_all('Interviewer', filters={'parent': interview_round}, fields=['user as interviewer']) + return frappe.get_all( + "Interviewer", filters={"parent": interview_round}, fields=["user as interviewer"] + ) def send_interview_reminder(): - reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings', - ['send_interview_reminder', 'interview_reminder_template'], as_dict=True) + reminder_settings = frappe.db.get_value( + "HR Settings", + "HR Settings", + ["send_interview_reminder", "interview_reminder_template"], + as_dict=True, + ) if not reminder_settings.send_interview_reminder: return - remind_before = cstr(frappe.db.get_single_value('HR Settings', 'remind_before')) or '01:00:00' - remind_before = datetime.datetime.strptime(remind_before, '%H:%M:%S') + remind_before = cstr(frappe.db.get_single_value("HR Settings", "remind_before")) or "01:00:00" + remind_before = datetime.datetime.strptime(remind_before, "%H:%M:%S") reminder_date_time = datetime.datetime.now() + datetime.timedelta( - hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second) + hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second + ) - interviews = frappe.get_all('Interview', filters={ - 'scheduled_on': ['between', (datetime.datetime.now(), reminder_date_time)], - 'status': 'Pending', - 'reminded': 0, - 'docstatus': ['!=', 2] - }) + interviews = frappe.get_all( + "Interview", + filters={ + "scheduled_on": ["between", (datetime.datetime.now(), reminder_date_time)], + "status": "Pending", + "reminded": 0, + "docstatus": ["!=", 2], + }, + ) - interview_template = frappe.get_doc('Email Template', reminder_settings.interview_reminder_template) + interview_template = frappe.get_doc( + "Email Template", reminder_settings.interview_reminder_template + ) for d in interviews: - doc = frappe.get_doc('Interview', d.name) + doc = frappe.get_doc("Interview", d.name) context = doc.as_dict() message = frappe.render_template(interview_template.response, context) recipients = get_recipients(doc.name) frappe.sendmail( - recipients= recipients, + recipients=recipients, subject=interview_template.subject, message=message, reference_doctype=doc.doctype, - reference_name=doc.name + reference_name=doc.name, ) - doc.db_set('reminded', 1) + doc.db_set("reminded", 1) def send_daily_feedback_reminder(): - reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings', - ['send_interview_feedback_reminder', 'feedback_reminder_notification_template'], as_dict=True) + reminder_settings = frappe.db.get_value( + "HR Settings", + "HR Settings", + ["send_interview_feedback_reminder", "feedback_reminder_notification_template"], + as_dict=True, + ) if not reminder_settings.send_interview_feedback_reminder: return - interview_feedback_template = frappe.get_doc('Email Template', reminder_settings.feedback_reminder_notification_template) - interviews = frappe.get_all('Interview', filters={'status': ['in', ['Under Review', 'Pending']], 'docstatus': ['!=', 2]}) + interview_feedback_template = frappe.get_doc( + "Email Template", reminder_settings.feedback_reminder_notification_template + ) + interviews = frappe.get_all( + "Interview", filters={"status": ["in", ["Under Review", "Pending"]], "docstatus": ["!=", 2]} + ) for entry in interviews: recipients = get_recipients(entry.name, for_feedback=1) - doc = frappe.get_doc('Interview', entry.name) + doc = frappe.get_doc("Interview", entry.name) context = doc.as_dict() message = frappe.render_template(interview_feedback_template.response, context) if len(recipients): frappe.sendmail( - recipients= recipients, + recipients=recipients, subject=interview_feedback_template.subject, message=message, - reference_doctype='Interview', - reference_name=entry.name + reference_doctype="Interview", + reference_name=entry.name, ) @frappe.whitelist() def get_expected_skill_set(interview_round): - return frappe.get_all('Expected Skill Set', filters ={'parent': interview_round}, fields=['skill']) + return frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields=["skill"]) @frappe.whitelist() @@ -196,16 +238,16 @@ def create_interview_feedback(data, interview_name, interviewer, job_applicant): data = frappe._dict(json.loads(data)) if frappe.session.user != interviewer: - frappe.throw(_('Only Interviewer Are allowed to submit Interview Feedback')) + frappe.throw(_("Only Interviewer Are allowed to submit Interview Feedback")) - interview_feedback = frappe.new_doc('Interview Feedback') + interview_feedback = frappe.new_doc("Interview Feedback") interview_feedback.interview = interview_name interview_feedback.interviewer = interviewer interview_feedback.job_applicant = job_applicant for d in data.skill_set: d = frappe._dict(d) - interview_feedback.append('skill_assessment', {'skill': d.skill, 'rating': d.rating}) + interview_feedback.append("skill_assessment", {"skill": d.skill, "rating": d.rating}) interview_feedback.feedback = data.feedback interview_feedback.result = data.result @@ -213,24 +255,33 @@ def create_interview_feedback(data, interview_name, interviewer, job_applicant): interview_feedback.save() interview_feedback.submit() - frappe.msgprint(_('Interview Feedback {0} submitted successfully').format( - get_link_to_form('Interview Feedback', interview_feedback.name))) + frappe.msgprint( + _("Interview Feedback {0} submitted successfully").format( + get_link_to_form("Interview Feedback", interview_feedback.name) + ) + ) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_interviewer_list(doctype, txt, searchfield, start, page_len, filters): filters = [ - ['Has Role', 'parent', 'like', '%{}%'.format(txt)], - ['Has Role', 'role', '=', 'interviewer'], - ['Has Role', 'parenttype', '=', 'User'] + ["Has Role", "parent", "like", "%{}%".format(txt)], + ["Has Role", "role", "=", "interviewer"], + ["Has Role", "parenttype", "=", "User"], ] if filters and isinstance(filters, list): filters.extend(filters) - return frappe.get_all('Has Role', limit_start=start, limit_page_length=page_len, - filters=filters, fields = ['parent'], as_list=1) + return frappe.get_all( + "Has Role", + limit_start=start, + limit_page_length=page_len, + filters=filters, + fields=["parent"], + as_list=1, + ) @frappe.whitelist() @@ -249,12 +300,13 @@ def get_events(start, end, filters=None): "Pending": "#fff4f0", "Under Review": "#d3e8fc", "Cleared": "#eaf5ed", - "Rejected": "#fce7e7" + "Rejected": "#fce7e7", } - conditions = get_event_conditions('Interview', filters) + conditions = get_event_conditions("Interview", filters) - interviews = frappe.db.sql(""" + interviews = frappe.db.sql( + """ SELECT DISTINCT `tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round, `tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time, @@ -265,10 +317,13 @@ def get_events(start, end, filters=None): (`tabInterview`.scheduled_on between %(start)s and %(end)s) and docstatus != 2 {conditions} - """.format(conditions=conditions), { - "start": start, - "end": end - }, as_dict=True, update={"allDay": 0}) + """.format( + conditions=conditions + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) for d in interviews: subject_data = [] @@ -279,13 +334,13 @@ def get_events(start, end, filters=None): color = event_color.get(d.status) interview_data = { - 'from': get_datetime('%s %s' % (d.scheduled_on, d.from_time or '00:00:00')), - 'to': get_datetime('%s %s' % (d.scheduled_on, d.to_time or '00:00:00')), - 'name': d.name, - 'subject': '\n'.join(subject_data), - 'color': color if color else "#89bcde" + "from": get_datetime("%s %s" % (d.scheduled_on, d.from_time or "00:00:00")), + "to": get_datetime("%s %s" % (d.scheduled_on, d.to_time or "00:00:00")), + "name": d.name, + "subject": "\n".join(subject_data), + "color": color if color else "#89bcde", } events.append(interview_data) - return events \ No newline at end of file + return events diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py index 1a2257a6d90..840a5ad919d 100644 --- a/erpnext/hr/doctype/interview/test_interview.py +++ b/erpnext/hr/doctype/interview/test_interview.py @@ -18,23 +18,30 @@ from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_appli class TestInterview(unittest.TestCase): def test_validations_for_designation(self): job_applicant = create_job_applicant() - interview = create_interview_and_dependencies(job_applicant.name, designation='_Test_Sales_manager', save=0) + interview = create_interview_and_dependencies( + job_applicant.name, designation="_Test_Sales_manager", save=0 + ) self.assertRaises(DuplicateInterviewRoundError, interview.save) def test_notification_on_rescheduling(self): job_applicant = create_job_applicant() - interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -4)) + interview = create_interview_and_dependencies( + job_applicant.name, scheduled_on=add_days(getdate(), -4) + ) previous_scheduled_date = interview.scheduled_on frappe.db.sql("DELETE FROM `tabEmail Queue`") - interview.reschedule_interview(add_days(getdate(previous_scheduled_date), 2), - from_time=nowtime(), to_time=nowtime()) + interview.reschedule_interview( + add_days(getdate(previous_scheduled_date), 2), from_time=nowtime(), to_time=nowtime() + ) interview.reload() self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2)) - notification = frappe.get_all("Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")}) + notification = frappe.get_all( + "Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")} + ) self.assertIsNotNone(notification) def test_notification_for_scheduling(self): @@ -74,16 +81,17 @@ class TestInterview(unittest.TestCase): frappe.db.rollback() -def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1): +def create_interview_and_dependencies( + job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1 +): if designation: - designation=create_designation(designation_name = "_Test_Sales_manager").name + designation = create_designation(designation_name="_Test_Sales_manager").name interviewer_1 = create_user("test_interviewer1@example.com", "Interviewer") interviewer_2 = create_user("test_interviewer2@example.com", "Interviewer") interview_round = create_interview_round( - "Technical Round", ["Python", "JS"], - designation=designation, save=True + "Technical Round", ["Python", "JS"], designation=designation, save=True ) interview = frappe.new_doc("Interview") @@ -101,6 +109,7 @@ def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_tim return interview + def create_interview_round(name, skill_set, interviewers=[], designation=None, save=True): create_skill_set(skill_set) interview_round = frappe.new_doc("Interview Round") @@ -114,15 +123,14 @@ def create_interview_round(name, skill_set, interviewers=[], designation=None, s interview_round.append("expected_skill_set", {"skill": skill}) for interviewer in interviewers: - interview_round.append("interviewer", { - "user": interviewer - }) + interview_round.append("interviewer", {"user": interviewer}) if save: interview_round.save() return interview_round + def create_skill_set(skill_set): for skill in skill_set: if not frappe.db.exists("Skill", skill): @@ -130,6 +138,7 @@ def create_skill_set(skill_set): doc.skill_name = skill doc.save() + def create_interview_type(name="test_interview_type"): if frappe.db.exists("Interview Type", name): return frappe.get_doc("Interview Type", name).name @@ -141,32 +150,41 @@ def create_interview_type(name="test_interview_type"): return doc.name + def setup_reminder_settings(): - if not frappe.db.exists('Email Template', _('Interview Reminder')): - base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') - response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html')) + if not frappe.db.exists("Email Template", _("Interview Reminder")): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file( + os.path.join(base_path, "interview/interview_reminder_notification_template.html") + ) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _('Interview Reminder'), - 'response': response, - 'subject': _('Interview Reminder'), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Interview Reminder"), + "response": response, + "subject": _("Interview Reminder"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')): - base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') - response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html')) + if not frappe.db.exists("Email Template", _("Interview Feedback Reminder")): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file( + os.path.join(base_path, "interview/interview_feedback_reminder_template.html") + ) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _('Interview Feedback Reminder'), - 'response': response, - 'subject': _('Interview Feedback Reminder'), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Interview Feedback Reminder"), + "response": response, + "subject": _("Interview Feedback Reminder"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - hr_settings = frappe.get_doc('HR Settings') - hr_settings.interview_reminder_template = _('Interview Reminder') - hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder') + hr_settings = frappe.get_doc("HR Settings") + hr_settings.interview_reminder_template = _("Interview Reminder") + hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") hr_settings.save() diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py index d046458f196..b8f8aee524d 100644 --- a/erpnext/hr/doctype/interview_feedback/interview_feedback.py +++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py @@ -24,28 +24,36 @@ class InterviewFeedback(Document): def validate_interviewer(self): applicable_interviewers = get_applicable_interviewers(self.interview) if self.interviewer not in applicable_interviewers: - frappe.throw(_('{0} is not allowed to submit Interview Feedback for the Interview: {1}').format( - frappe.bold(self.interviewer), frappe.bold(self.interview))) + frappe.throw( + _("{0} is not allowed to submit Interview Feedback for the Interview: {1}").format( + frappe.bold(self.interviewer), frappe.bold(self.interview) + ) + ) def validate_interview_date(self): - scheduled_date = frappe.db.get_value('Interview', self.interview, 'scheduled_on') + scheduled_date = frappe.db.get_value("Interview", self.interview, "scheduled_on") if getdate() < getdate(scheduled_date) and self.docstatus == 1: - frappe.throw(_('{0} submission before {1} is not allowed').format( - frappe.bold('Interview Feedback'), - frappe.bold('Interview Scheduled Date') - )) + frappe.throw( + _("{0} submission before {1} is not allowed").format( + frappe.bold("Interview Feedback"), frappe.bold("Interview Scheduled Date") + ) + ) def validate_duplicate(self): - duplicate_feedback = frappe.db.exists('Interview Feedback', { - 'interviewer': self.interviewer, - 'interview': self.interview, - 'docstatus': 1 - }) + duplicate_feedback = frappe.db.exists( + "Interview Feedback", + {"interviewer": self.interviewer, "interview": self.interview, "docstatus": 1}, + ) if duplicate_feedback: - frappe.throw(_('Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue.').format( - self.interview, get_link_to_form('Interview Feedback', duplicate_feedback))) + frappe.throw( + _( + "Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue." + ).format( + self.interview, get_link_to_form("Interview Feedback", duplicate_feedback) + ) + ) def calculate_average_rating(self): total_rating = 0 @@ -53,10 +61,12 @@ class InterviewFeedback(Document): if d.rating: total_rating += d.rating - self.average_rating = flt(total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0) + self.average_rating = flt( + total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0 + ) def update_interview_details(self): - doc = frappe.get_doc('Interview', self.interview) + doc = frappe.get_doc("Interview", self.interview) total_rating = 0 if self.docstatus == 2: @@ -75,12 +85,14 @@ class InterviewFeedback(Document): if entry.average_rating: total_rating += entry.average_rating - doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0) + doc.average_rating = flt( + total_rating / len(doc.interview_details) if len(doc.interview_details) else 0 + ) doc.save() doc.notify_update() @frappe.whitelist() def get_applicable_interviewers(interview): - data = frappe.get_all('Interview Detail', filters={'parent': interview}, fields=['interviewer']) + data = frappe.get_all("Interview Detail", filters={"parent": interview}, fields=["interviewer"]) return [d.interviewer for d in data] diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py index 4185f2827a5..0c408b4d35d 100644 --- a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py +++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py @@ -17,14 +17,16 @@ class TestInterviewFeedback(unittest.TestCase): def test_validation_for_skill_set(self): frappe.set_user("Administrator") job_applicant = create_job_applicant() - interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1)) + interview = create_interview_and_dependencies( + job_applicant.name, scheduled_on=add_days(getdate(), -1) + ) skill_ratings = get_skills_rating(interview.interview_round) interviewer = interview.interview_details[0].interviewer - create_skill_set(['Leadership']) + create_skill_set(["Leadership"]) interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings) - interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4}) + interview_feedback.append("skill_assessment", {"skill": "Leadership", "rating": 4}) frappe.set_user(interviewer) self.assertRaises(frappe.ValidationError, interview_feedback.save) @@ -33,7 +35,9 @@ class TestInterviewFeedback(unittest.TestCase): def test_average_ratings_on_feedback_submission_and_cancellation(self): job_applicant = create_job_applicant() - interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1)) + interview = create_interview_and_dependencies( + job_applicant.name, scheduled_on=add_days(getdate(), -1) + ) skill_ratings = get_skills_rating(interview.interview_round) # For First Interviewer Feedback @@ -48,20 +52,26 @@ class TestInterviewFeedback(unittest.TestCase): if d.rating: total_rating += d.rating - avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0) + avg_rating = flt( + total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0 + ) self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating) - avg_on_interview_detail = frappe.db.get_value('Interview Detail', { - 'parent': feedback_1.interview, - 'interviewer': feedback_1.interviewer, - 'interview_feedback': feedback_1.name - }, 'average_rating') + avg_on_interview_detail = frappe.db.get_value( + "Interview Detail", + { + "parent": feedback_1.interview, + "interviewer": feedback_1.interviewer, + "interview_feedback": feedback_1.name, + }, + "average_rating", + ) # 1. average should be reflected in Interview Detail. self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating)) - '''For Second Interviewer Feedback''' + """For Second Interviewer Feedback""" interviewer = interview.interview_details[1].interviewer frappe.set_user(interviewer) @@ -95,7 +105,9 @@ def create_interview_feedback(interview, interviewer, skills_ratings): def get_skills_rating(interview_round): import random - skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"]) + skills = frappe.get_all( + "Expected Skill Set", filters={"parent": interview_round}, fields=["skill"] + ) for d in skills: d["rating"] = random.randint(1, 5) return skills diff --git a/erpnext/hr/doctype/interview_round/interview_round.py b/erpnext/hr/doctype/interview_round/interview_round.py index 0f442c320ad..83dbf0ea98e 100644 --- a/erpnext/hr/doctype/interview_round/interview_round.py +++ b/erpnext/hr/doctype/interview_round/interview_round.py @@ -11,6 +11,7 @@ from frappe.model.document import Document class InterviewRound(Document): pass + @frappe.whitelist() def create_interview(doc): if isinstance(doc, str): @@ -24,10 +25,5 @@ def create_interview(doc): if doc.interviewers: interview.interview_details = [] for data in doc.interviewers: - interview.append("interview_details", { - "interviewer": data.user - }) + interview.append("interview_details", {"interviewer": data.user}) return interview - - - diff --git a/erpnext/hr/doctype/interview_round/test_interview_round.py b/erpnext/hr/doctype/interview_round/test_interview_round.py index dcec9419c07..95681653743 100644 --- a/erpnext/hr/doctype/interview_round/test_interview_round.py +++ b/erpnext/hr/doctype/interview_round/test_interview_round.py @@ -8,4 +8,3 @@ import unittest class TestInterviewRound(unittest.TestCase): pass - diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py index 54ccfca38f7..14ebca432e5 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant.py @@ -13,7 +13,9 @@ from frappe.utils import validate_email_address from erpnext.hr.doctype.interview.interview import get_interviewers -class DuplicationError(frappe.ValidationError): pass +class DuplicationError(frappe.ValidationError): + pass + class JobApplicant(Document): def onload(self): @@ -36,8 +38,8 @@ class JobApplicant(Document): self.set_status_for_employee_referral() if not self.applicant_name and self.email_id: - guess = self.email_id.split('@')[0] - self.applicant_name = ' '.join([p.capitalize() for p in guess.split('.')]) + guess = self.email_id.split("@")[0] + self.applicant_name = " ".join([p.capitalize() for p in guess.split(".")]) def set_status_for_employee_referral(self): emp_ref = frappe.get_doc("Employee Referral", self.employee_referral) @@ -46,6 +48,7 @@ class JobApplicant(Document): elif self.status in ["Accepted", "Rejected"]: emp_ref.db_set("status", self.status) + @frappe.whitelist() def create_interview(doc, interview_round): import json @@ -59,7 +62,11 @@ def create_interview(doc, interview_round): round_designation = frappe.db.get_value("Interview Round", interview_round, "designation") if round_designation and doc.designation and round_designation != doc.designation: - frappe.throw(_("Interview Round {0} is only applicable for the Designation {1}").format(interview_round, round_designation)) + frappe.throw( + _("Interview Round {0} is only applicable for the Designation {1}").format( + interview_round, round_designation + ) + ) interview = frappe.new_doc("Interview") interview.interview_round = interview_round @@ -70,16 +77,16 @@ def create_interview(doc, interview_round): interviewer_detail = get_interviewers(interview_round) for d in interviewer_detail: - interview.append("interview_details", { - "interviewer": d.interviewer - }) + interview.append("interview_details", {"interviewer": d.interviewer}) return interview + @frappe.whitelist() def get_interview_details(job_applicant): - interview_details = frappe.db.get_all("Interview", - filters={"job_applicant":job_applicant, "docstatus": ["!=", 2]}, - fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"] + interview_details = frappe.db.get_all( + "Interview", + filters={"job_applicant": job_applicant, "docstatus": ["!=", 2]}, + fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"], ) interview_detail_map = {} diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py index 9406fc54855..14b944ac614 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py @@ -1,17 +1,9 @@ - - def get_data(): return { - 'fieldname': 'job_applicant', - 'transactions': [ - { - 'items': ['Employee', 'Employee Onboarding'] - }, - { - 'items': ['Job Offer', 'Appointment Letter'] - }, - { - 'items': ['Interview'] - } + "fieldname": "job_applicant", + "transactions": [ + {"items": ["Employee", "Employee Onboarding"]}, + {"items": ["Job Offer", "Appointment Letter"]}, + {"items": ["Interview"]}, ], } diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py index bf1622028d8..99d11619781 100644 --- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py @@ -10,21 +10,25 @@ from erpnext.hr.doctype.designation.test_designation import create_designation class TestJobApplicant(unittest.TestCase): def test_job_applicant_naming(self): - applicant = frappe.get_doc({ - "doctype": "Job Applicant", - "status": "Open", - "applicant_name": "_Test Applicant", - "email_id": "job_applicant_naming@example.com" - }).insert() - self.assertEqual(applicant.name, 'job_applicant_naming@example.com') + applicant = frappe.get_doc( + { + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com", + } + ).insert() + self.assertEqual(applicant.name, "job_applicant_naming@example.com") - applicant = frappe.get_doc({ - "doctype": "Job Applicant", - "status": "Open", - "applicant_name": "_Test Applicant", - "email_id": "job_applicant_naming@example.com" - }).insert() - self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1') + applicant = frappe.get_doc( + { + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com", + } + ).insert() + self.assertEqual(applicant.name, "job_applicant_naming@example.com-1") def tearDown(self): frappe.db.rollback() @@ -41,11 +45,13 @@ def create_job_applicant(**args): if frappe.db.exists("Job Applicant", filters): return frappe.get_doc("Job Applicant", filters) - job_applicant = frappe.get_doc({ - "doctype": "Job Applicant", - "status": args.status or "Open", - "designation": create_designation().name - }) + job_applicant = frappe.get_doc( + { + "doctype": "Job Applicant", + "status": args.status or "Open", + "designation": create_designation().name, + } + ) job_applicant.update(filters) job_applicant.save() diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index 39f471929b4..dc76a2d0038 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -17,9 +17,15 @@ class JobOffer(Document): def validate(self): self.validate_vacancies() - job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant, "docstatus": ["!=", 2]}) + job_offer = frappe.db.exists( + "Job Offer", {"job_applicant": self.job_applicant, "docstatus": ["!=", 2]} + ) if job_offer and job_offer != self.name: - frappe.throw(_("Job Offer: {0} is already for Job Applicant: {1}").format(frappe.bold(job_offer), frappe.bold(self.job_applicant))) + frappe.throw( + _("Job Offer: {0} is already for Job Applicant: {1}").format( + frappe.bold(job_offer), frappe.bold(self.job_applicant) + ) + ) def validate_vacancies(self): staffing_plan = get_staffing_plan_detail(self.designation, self.company, self.offer_date) @@ -27,7 +33,7 @@ class JobOffer(Document): if staffing_plan and check_vacancies: job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) if not staffing_plan.get("vacancies") or cint(staffing_plan.vacancies) - len(job_offers) <= 0: - error_variable = 'for ' + frappe.bold(self.designation) + error_variable = "for " + frappe.bold(self.designation) if staffing_plan.get("parent"): error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) @@ -37,20 +43,27 @@ class JobOffer(Document): update_job_applicant(self.status, self.job_applicant) def get_job_offer(self, from_date, to_date): - ''' Returns job offer created during a time period ''' - return frappe.get_all("Job Offer", filters={ - "offer_date": ['between', (from_date, to_date)], + """Returns job offer created during a time period""" + return frappe.get_all( + "Job Offer", + filters={ + "offer_date": ["between", (from_date, to_date)], "designation": self.designation, "company": self.company, - "docstatus": 1 - }, fields=['name']) + "docstatus": 1, + }, + fields=["name"], + ) + def update_job_applicant(status, job_applicant): if status in ("Accepted", "Rejected"): frappe.set_value("Job Applicant", job_applicant, "status", status) + def get_staffing_plan_detail(designation, company, offer_date): - detail = frappe.db.sql(""" + detail = frappe.db.sql( + """ SELECT DISTINCT spd.parent, sp.from_date as from_date, sp.to_date as to_date, @@ -64,20 +77,33 @@ def get_staffing_plan_detail(designation, company, offer_date): AND sp.company=%s AND spd.parent = sp.name AND %s between sp.from_date and sp.to_date - """, (designation, company, offer_date), as_dict=1) + """, + (designation, company, offer_date), + as_dict=1, + ) return frappe._dict(detail[0]) if (detail and detail[0].parent) else None + @frappe.whitelist() def make_employee(source_name, target_doc=None): def set_missing_values(source, target): - target.personal_email, target.first_name = frappe.db.get_value("Job Applicant", \ - source.job_applicant, ["email_id", "applicant_name"]) - doc = get_mapped_doc("Job Offer", source_name, { + target.personal_email, target.first_name = frappe.db.get_value( + "Job Applicant", source.job_applicant, ["email_id", "applicant_name"] + ) + + doc = get_mapped_doc( + "Job Offer", + source_name, + { "Job Offer": { "doctype": "Employee", "field_map": { "applicant_name": "employee_name", - }} - }, target_doc, set_missing_values) + }, + } + }, + target_doc, + set_missing_values, + ) return doc diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py index d94e03ca63f..7d8ef115d16 100644 --- a/erpnext/hr/doctype/job_offer/test_job_offer.py +++ b/erpnext/hr/doctype/job_offer/test_job_offer.py @@ -12,17 +12,19 @@ from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company # test_records = frappe.get_test_records('Job Offer') + class TestJobOffer(unittest.TestCase): def test_job_offer_creation_against_vacancies(self): frappe.db.set_value("HR Settings", None, "check_vacancies", 1) job_applicant = create_job_applicant(email_id="test_job_offer@example.com") job_offer = create_job_offer(job_applicant=job_applicant.name, designation="UX Designer") - create_staffing_plan(name='Test No Vacancies', staffing_details=[{ - "designation": "UX Designer", - "vacancies": 0, - "estimated_cost_per_position": 5000 - }]) + create_staffing_plan( + name="Test No Vacancies", + staffing_details=[ + {"designation": "UX Designer", "vacancies": 0, "estimated_cost_per_position": 5000} + ], + ) self.assertRaises(frappe.ValidationError, job_offer.submit) # test creation of job offer when vacancies are not present @@ -49,6 +51,7 @@ class TestJobOffer(unittest.TestCase): def tearDown(self): frappe.db.sql("DELETE FROM `tabJob Offer` WHERE 1") + def create_job_offer(**args): args = frappe._dict(args) if not args.job_applicant: @@ -57,32 +60,34 @@ def create_job_offer(**args): if not frappe.db.exists("Designation", args.designation): designation = create_designation(designation_name=args.designation) - job_offer = frappe.get_doc({ - "doctype": "Job Offer", - "job_applicant": args.job_applicant or job_applicant.name, - "offer_date": args.offer_date or nowdate(), - "designation": args.designation or "Researcher", - "status": args.status or "Accepted" - }) + job_offer = frappe.get_doc( + { + "doctype": "Job Offer", + "job_applicant": args.job_applicant or job_applicant.name, + "offer_date": args.offer_date or nowdate(), + "designation": args.designation or "Researcher", + "status": args.status or "Accepted", + } + ) return job_offer + def create_staffing_plan(**args): args = frappe._dict(args) make_company() frappe.db.set_value("Company", "_Test Company", "is_group", 1) if frappe.db.exists("Staffing Plan", args.name or "Test"): return - staffing_plan = frappe.get_doc({ - "doctype": "Staffing Plan", - "name": args.name or "Test", - "from_date": args.from_date or nowdate(), - "to_date": args.to_date or add_days(nowdate(), 10), - "staffing_details": args.staffing_details or [{ - "designation": "Researcher", - "vacancies": 1, - "estimated_cost_per_position": 50000 - }] - }) + staffing_plan = frappe.get_doc( + { + "doctype": "Staffing Plan", + "name": args.name or "Test", + "from_date": args.from_date or nowdate(), + "to_date": args.to_date or add_days(nowdate(), 10), + "staffing_details": args.staffing_details + or [{"designation": "Researcher", "vacancies": 1, "estimated_cost_per_position": 50000}], + } + ) staffing_plan.insert() staffing_plan.submit() return staffing_plan diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index d53daf17d87..c71407d71d4 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -16,67 +16,78 @@ from erpnext.hr.doctype.staffing_plan.staffing_plan import ( class JobOpening(WebsiteGenerator): website = frappe._dict( - template = "templates/generators/job_opening.html", - condition_field = "publish", - page_title_field = "job_title", + template="templates/generators/job_opening.html", + condition_field="publish", + page_title_field="job_title", ) def validate(self): if not self.route: - self.route = frappe.scrub(self.job_title).replace('_', '-') + self.route = frappe.scrub(self.job_title).replace("_", "-") self.validate_current_vacancies() def validate_current_vacancies(self): if not self.staffing_plan: - staffing_plan = get_active_staffing_plan_details(self.company, - self.designation) + staffing_plan = get_active_staffing_plan_details(self.company, self.designation) if staffing_plan: self.staffing_plan = staffing_plan[0].name self.planned_vacancies = staffing_plan[0].vacancies elif not self.planned_vacancies: - planned_vacancies = frappe.db.sql(""" + planned_vacancies = frappe.db.sql( + """ select vacancies from `tabStaffing Plan Detail` - where parent=%s and designation=%s""", (self.staffing_plan, self.designation)) + where parent=%s and designation=%s""", + (self.staffing_plan, self.designation), + ) self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None if self.staffing_plan and self.planned_vacancies: staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company") - lft, rgt = frappe.get_cached_value('Company', staffing_plan_company, ["lft", "rgt"]) + lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"]) designation_counts = get_designation_counts(self.designation, self.company) - current_count = designation_counts['employee_count'] + designation_counts['job_openings'] + current_count = designation_counts["employee_count"] + designation_counts["job_openings"] if self.planned_vacancies <= current_count: - frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format( - self.designation, self.staffing_plan)) + frappe.throw( + _( + "Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}" + ).format(self.designation, self.staffing_plan) + ) def get_context(self, context): - context.parents = [{'route': 'jobs', 'title': _('All Jobs') }] + context.parents = [{"route": "jobs", "title": _("All Jobs")}] + def get_list_context(context): context.title = _("Jobs") - context.introduction = _('Current Job Openings') + context.introduction = _("Current Job Openings") context.get_list = get_job_openings -def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None): - fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range', - 'lower_range', 'upper_range', 'currency', 'job_application_route'] + +def get_job_openings( + doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None +): + fields = [ + "name", + "status", + "job_title", + "description", + "publish_salary_range", + "lower_range", + "upper_range", + "currency", + "job_application_route", + ] filters = filters or {} - filters.update({ - 'status': 'Open' - }) + filters.update({"status": "Open"}) if txt: - filters.update({ - 'job_title': ['like', '%{0}%'.format(txt)], - 'description': ['like', '%{0}%'.format(txt)] - }) + filters.update( + {"job_title": ["like", "%{0}%".format(txt)], "description": ["like", "%{0}%".format(txt)]} + ) - return frappe.get_all(doctype, - filters, - fields, - start=limit_start, - page_length=limit_page_length, - order_by=order_by + return frappe.get_all( + doctype, filters, fields, start=limit_start, page_length=limit_page_length, order_by=order_by ) diff --git a/erpnext/hr/doctype/job_opening/job_opening_dashboard.py b/erpnext/hr/doctype/job_opening/job_opening_dashboard.py index 817969004f9..a30932870d0 100644 --- a/erpnext/hr/doctype/job_opening/job_opening_dashboard.py +++ b/erpnext/hr/doctype/job_opening/job_opening_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): - return { - 'fieldname': 'job_title', - 'transactions': [ - { - 'items': ['Job Applicant'] - } - ], - } + return { + "fieldname": "job_title", + "transactions": [{"items": ["Job Applicant"]}], + } diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.py b/erpnext/hr/doctype/job_opening/test_job_opening.py index a1c3a1d49e3..a72a6eb3384 100644 --- a/erpnext/hr/doctype/job_opening/test_job_opening.py +++ b/erpnext/hr/doctype/job_opening/test_job_opening.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Job Opening') + class TestJobOpening(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js index 9742387c16a..aef44122513 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js @@ -34,6 +34,15 @@ frappe.ui.form.on("Leave Allocation", { }); } } + + // make new leaves allocated field read only if allocation is created via leave policy assignment + // and leave type is earned leave, since these leaves would be allocated via the scheduler + if (frm.doc.leave_policy_assignment) { + frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => { + if (r && cint(r.is_earned_leave)) + frm.set_df_property("new_leaves_allocated", "read_only", 1); + }); + } }, expire_allocation: function(frm) { diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 52ee463db02..4a9b54034af 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -237,7 +237,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-10-01 15:28:26.335104", + "modified": "2022-04-07 09:50:33.145825", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -278,5 +278,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "timeline_field": "employee" + "timeline_field": "employee", + "title_field": "employee_name", + "track_changes": 1 } diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 232118fd67c..27479a5e81f 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -15,36 +15,65 @@ from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import ( from erpnext.hr.utils import get_leave_period, set_employee_name -class OverlapError(frappe.ValidationError): pass -class BackDatedAllocationError(frappe.ValidationError): pass -class OverAllocationError(frappe.ValidationError): pass -class LessAllocationError(frappe.ValidationError): pass -class ValueMultiplierError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + + +class BackDatedAllocationError(frappe.ValidationError): + pass + + +class OverAllocationError(frappe.ValidationError): + pass + + +class LessAllocationError(frappe.ValidationError): + pass + + +class ValueMultiplierError(frappe.ValidationError): + pass + class LeaveAllocation(Document): def validate(self): self.validate_period() self.validate_allocation_overlap() - self.validate_back_dated_allocation() - self.set_total_leaves_allocated() - self.validate_total_leaves_allocated() self.validate_lwp() set_employee_name(self) + self.set_total_leaves_allocated() + self.validate_leave_days_and_dates() + + def validate_leave_days_and_dates(self): + # all validations that should run on save as well as on update after submit + self.validate_back_dated_allocation() + self.validate_total_leaves_allocated() self.validate_leave_allocation_days() def validate_leave_allocation_days(self): company = frappe.db.get_value("Employee", self.employee, "company") leave_period = get_leave_period(self.from_date, self.to_date, company) - max_leaves_allowed = flt(frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")) + max_leaves_allowed = flt( + frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") + ) if max_leaves_allowed > 0: leave_allocated = 0 if leave_period: - leave_allocated = get_leave_allocation_for_period(self.employee, self.leave_type, - leave_period[0].from_date, leave_period[0].to_date) + leave_allocated = get_leave_allocation_for_period( + self.employee, + self.leave_type, + leave_period[0].from_date, + leave_period[0].to_date, + exclude_allocation=self.name, + ) leave_allocated += flt(self.new_leaves_allocated) if leave_allocated > max_leaves_allowed: - frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period") - .format(self.leave_type, self.employee)) + frappe.throw( + _( + "Total allocated leaves are more than maximum allocation allowed for {0} leave type for employee {1} in the period" + ).format(self.leave_type, self.employee), + OverAllocationError, + ) def on_submit(self): self.create_leave_ledger_entry() @@ -64,25 +93,34 @@ class LeaveAllocation(Document): def on_update_after_submit(self): if self.has_value_changed("new_leaves_allocated"): self.validate_against_leave_applications() + + # recalculate total leaves allocated + self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated) + # run required validations again since total leaves are being updated + self.validate_leave_days_and_dates() + leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count() args = { "leaves": leaves_to_be_added, "from_date": self.from_date, "to_date": self.to_date, - "is_carry_forward": 0 + "is_carry_forward": 0, } create_leave_ledger_entry(self, args, True) + self.db_update() def get_existing_leave_count(self): - ledger_entries = frappe.get_all("Leave Ledger Entry", - filters={ - "transaction_type": "Leave Allocation", - "transaction_name": self.name, - "employee": self.employee, - "company": self.company, - "leave_type": self.leave_type - }, - pluck="leaves") + ledger_entries = frappe.get_all( + "Leave Ledger Entry", + filters={ + "transaction_type": "Leave Allocation", + "transaction_name": self.name, + "employee": self.employee, + "company": self.company, + "leave_type": self.leave_type, + }, + pluck="leaves", + ) total_existing_leaves = 0 for entry in ledger_entries: total_existing_leaves += entry @@ -90,21 +128,33 @@ class LeaveAllocation(Document): return total_existing_leaves def validate_against_leave_applications(self): - leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, - self.from_date, self.to_date) + leaves_taken = get_approved_leaves_for_period( + self.employee, self.leave_type, self.from_date, self.to_date + ) if flt(leaves_taken) > flt(self.total_leaves_allocated): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): - frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) + frappe.msgprint( + _( + "Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period" + ).format(self.total_leaves_allocated, leaves_taken) + ) else: - frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError) + frappe.throw( + _( + "Total allocated leaves {0} cannot be less than already approved leaves {1} for the period" + ).format(self.total_leaves_allocated, leaves_taken), + LessAllocationError, + ) def update_leave_policy_assignments_when_no_allocations_left(self): - allocations = frappe.db.get_list("Leave Allocation", filters = { - "docstatus": 1, - "leave_policy_assignment": self.leave_policy_assignment - }) + allocations = frappe.db.get_list( + "Leave Allocation", + filters={"docstatus": 1, "leave_policy_assignment": self.leave_policy_assignment}, + ) if len(allocations) == 0: - frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0) + frappe.db.set_value( + "Leave Policy Assignment", self.leave_policy_assignment, "leaves_allocated", 0 + ) def validate_period(self): if date_diff(self.to_date, self.from_date) <= 0: @@ -112,10 +162,13 @@ class LeaveAllocation(Document): def validate_lwp(self): if frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"): - frappe.throw(_("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type)) + frappe.throw( + _("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type) + ) def validate_allocation_overlap(self): - leave_allocation = frappe.db.sql(""" + leave_allocation = frappe.db.sql( + """ SELECT name FROM `tabLeave Allocation` @@ -123,29 +176,44 @@ class LeaveAllocation(Document): employee=%s AND leave_type=%s AND name <> %s AND docstatus=1 AND to_date >= %s AND from_date <= %s""", - (self.employee, self.leave_type, self.name, self.from_date, self.to_date)) + (self.employee, self.leave_type, self.name, self.from_date, self.to_date), + ) if leave_allocation: - frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}") - .format(self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date))) + frappe.msgprint( + _("{0} already allocated for Employee {1} for period {2} to {3}").format( + self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date) + ) + ) - frappe.throw(_('Reference') + ': {0}' - .format(leave_allocation[0][0]), OverlapError) + frappe.throw( + _("Reference") + + ': {0}'.format(leave_allocation[0][0]), + OverlapError, + ) def validate_back_dated_allocation(self): - future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` + future_allocation = frappe.db.sql( + """select name, from_date from `tabLeave Allocation` where employee=%s and leave_type=%s and docstatus=1 and from_date > %s - and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1) + and carry_forward=1""", + (self.employee, self.leave_type, self.to_date), + as_dict=1, + ) if future_allocation: - frappe.throw(_("Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") - .format(formatdate(future_allocation[0].from_date), future_allocation[0].name), - BackDatedAllocationError) + frappe.throw( + _( + "Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}" + ).format(formatdate(future_allocation[0].from_date), future_allocation[0].name), + BackDatedAllocationError, + ) @frappe.whitelist() def set_total_leaves_allocated(self): - self.unused_leaves = get_carry_forwarded_leaves(self.employee, - self.leave_type, self.from_date, self.carry_forward) + self.unused_leaves = get_carry_forwarded_leaves( + self.employee, self.leave_type, self.from_date, self.carry_forward + ) self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated) @@ -154,11 +222,14 @@ class LeaveAllocation(Document): if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation() - if not self.total_leaves_allocated \ - and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") \ - and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory"): - frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}") - .format(self.leave_type)) + if ( + not self.total_leaves_allocated + and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") + and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory") + ): + frappe.throw( + _("Total leaves allocated is mandatory for Leave Type {0}").format(self.leave_type) + ) def limit_carry_forward_based_on_max_allowed_leaves(self): max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") @@ -167,13 +238,17 @@ class LeaveAllocation(Document): self.unused_leaves = max_leaves_allowed - flt(self.new_leaves_allocated) def set_carry_forwarded_leaves_in_previous_allocation(self, on_cancel=False): - ''' Set carry forwarded leaves in previous allocation ''' + """Set carry forwarded leaves in previous allocation""" previous_allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee) if on_cancel: self.unused_leaves = 0.0 if previous_allocation: - frappe.db.set_value("Leave Allocation", previous_allocation.name, - 'carry_forwarded_leaves_count', self.unused_leaves) + frappe.db.set_value( + "Leave Allocation", + previous_allocation.name, + "carry_forwarded_leaves_count", + self.unused_leaves, + ) def validate_total_leaves_allocated(self): # Adding a day to include To Date in the difference @@ -183,13 +258,15 @@ class LeaveAllocation(Document): def create_leave_ledger_entry(self, submit=True): if self.unused_leaves: - expiry_days = frappe.db.get_value("Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days") + expiry_days = frappe.db.get_value( + "Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days" + ) end_date = add_days(self.from_date, expiry_days - 1) if expiry_days else self.to_date args = dict( leaves=self.unused_leaves, from_date=self.from_date, - to_date= min(getdate(end_date), getdate(self.to_date)), - is_carry_forward=1 + to_date=min(getdate(end_date), getdate(self.to_date)), + is_carry_forward=1, ) create_leave_ledger_entry(self, args, submit) @@ -197,74 +274,85 @@ class LeaveAllocation(Document): leaves=self.new_leaves_allocated, from_date=self.from_date, to_date=self.to_date, - is_carry_forward=0 + is_carry_forward=0, ) create_leave_ledger_entry(self, args, submit) + def get_previous_allocation(from_date, leave_type, employee): - ''' Returns document properties of previous allocation ''' - return frappe.db.get_value("Leave Allocation", + """Returns document properties of previous allocation""" + return frappe.db.get_value( + "Leave Allocation", filters={ - 'to_date': ("<", from_date), - 'leave_type': leave_type, - 'employee': employee, - 'docstatus': 1 + "to_date": ("<", from_date), + "leave_type": leave_type, + "employee": employee, + "docstatus": 1, }, - order_by='to_date DESC', - fieldname=['name', 'from_date', 'to_date', 'employee', 'leave_type'], as_dict=1) + order_by="to_date DESC", + fieldname=["name", "from_date", "to_date", "employee", "leave_type"], + as_dict=1, + ) -def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): - leave_allocated = 0 - leave_allocations = frappe.db.sql(""" - select employee, leave_type, from_date, to_date, total_leaves_allocated - from `tabLeave Allocation` - where employee=%(employee)s and leave_type=%(leave_type)s - and docstatus=1 - and (from_date between %(from_date)s and %(to_date)s - or to_date between %(from_date)s and %(to_date)s - or (from_date < %(from_date)s and to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) - if leave_allocations: - for leave_alloc in leave_allocations: - leave_allocated += leave_alloc.total_leaves_allocated +def get_leave_allocation_for_period( + employee, leave_type, from_date, to_date, exclude_allocation=None +): + from frappe.query_builder.functions import Sum + + Allocation = frappe.qb.DocType("Leave Allocation") + return ( + frappe.qb.from_(Allocation) + .select(Sum(Allocation.total_leaves_allocated).as_("total_allocated_leaves")) + .where( + (Allocation.employee == employee) + & (Allocation.leave_type == leave_type) + & (Allocation.docstatus == 1) + & (Allocation.name != exclude_allocation) + & ( + (Allocation.from_date.between(from_date, to_date)) + | (Allocation.to_date.between(from_date, to_date)) + | ((Allocation.from_date < from_date) & (Allocation.to_date > to_date)) + ) + ) + ).run()[0][0] or 0.0 - return leave_allocated @frappe.whitelist() def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None): - ''' Returns carry forwarded leaves for the given employee ''' + """Returns carry forwarded leaves for the given employee""" unused_leaves = 0.0 previous_allocation = get_previous_allocation(date, leave_type, employee) if carry_forward and previous_allocation: validate_carry_forward(leave_type) - unused_leaves = get_unused_leaves(employee, leave_type, - previous_allocation.from_date, previous_allocation.to_date) + unused_leaves = get_unused_leaves( + employee, leave_type, previous_allocation.from_date, previous_allocation.to_date + ) if unused_leaves: - max_carry_forwarded_leaves = frappe.db.get_value("Leave Type", - leave_type, "maximum_carry_forwarded_leaves") + max_carry_forwarded_leaves = frappe.db.get_value( + "Leave Type", leave_type, "maximum_carry_forwarded_leaves" + ) if max_carry_forwarded_leaves and unused_leaves > flt(max_carry_forwarded_leaves): unused_leaves = flt(max_carry_forwarded_leaves) return unused_leaves + def get_unused_leaves(employee, leave_type, from_date, to_date): - ''' Returns unused leaves between the given period while skipping leave allocation expiry ''' - leaves = frappe.get_all("Leave Ledger Entry", filters={ - 'employee': employee, - 'leave_type': leave_type, - 'from_date': ('>=', from_date), - 'to_date': ('<=', to_date) - }, or_filters={ - 'is_expired': 0, - 'is_carry_forward': 1 - }, fields=['sum(leaves) as leaves']) - return flt(leaves[0]['leaves']) + """Returns unused leaves between the given period while skipping leave allocation expiry""" + leaves = frappe.get_all( + "Leave Ledger Entry", + filters={ + "employee": employee, + "leave_type": leave_type, + "from_date": (">=", from_date), + "to_date": ("<=", to_date), + }, + or_filters={"is_expired": 0, "is_carry_forward": 1}, + fields=["sum(leaves) as leaves"], + ) + return flt(leaves[0]["leaves"]) + def validate_carry_forward(leave_type): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py index 08861b8bce3..96e81db617c 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py @@ -1,19 +1,6 @@ - - def get_data(): - return { - 'fieldname': 'leave_allocation', - 'transactions': [ - { - 'items': ['Compensatory Leave Request'] - }, - { - 'items': ['Leave Encashment'] - } - ], - 'reports': [ - { - 'items': ['Employee Leave Balance'] - } - ] - } + return { + "fieldname": "leave_allocation", + "transactions": [{"items": ["Compensatory Leave Request"]}, {"items": ["Leave Encashment"]}], + "reports": [{"items": ["Employee Leave Balance"]}], + } diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 1310ca65ecf..dde52d7ad8e 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -1,25 +1,26 @@ - import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, add_months, getdate, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.leave_allocation.leave_allocation import ( + BackDatedAllocationError, + OverAllocationError, +) from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type -class TestLeaveAllocation(unittest.TestCase): - @classmethod - def setUpClass(cls): - frappe.db.sql("delete from `tabLeave Period`") +class TestLeaveAllocation(FrappeTestCase): + def setUp(self): + frappe.db.delete("Leave Period") + frappe.db.delete("Leave Allocation") - emp_id = make_employee("test_emp_leave_allocation@salary.com") - cls.employee = frappe.get_doc("Employee", emp_id) - - def tearDown(self): - frappe.db.rollback() + emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company") + self.employee = frappe.get_doc("Employee", emp_id) def test_overlapping_allocation(self): leaves = [ @@ -32,7 +33,7 @@ class TestLeaveAllocation(unittest.TestCase): "from_date": getdate("2015-10-01"), "to_date": getdate("2015-10-31"), "new_leaves_allocated": 5, - "docstatus": 1 + "docstatus": 1, }, { "doctype": "Leave Allocation", @@ -42,42 +43,174 @@ class TestLeaveAllocation(unittest.TestCase): "leave_type": "_Test Leave Type", "from_date": getdate("2015-09-01"), "to_date": getdate("2015-11-30"), - "new_leaves_allocated": 5 - } + "new_leaves_allocated": 5, + }, ] frappe.get_doc(leaves[0]).save() self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save) def test_invalid_period(self): - doc = frappe.get_doc({ - "doctype": "Leave Allocation", - "__islocal": 1, - "employee": self.employee.name, - "employee_name": self.employee.employee_name, - "leave_type": "_Test Leave Type", - "from_date": getdate("2015-09-30"), - "to_date": getdate("2015-09-1"), - "new_leaves_allocated": 5 - }) + doc = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, + "leave_type": "_Test Leave Type", + "from_date": getdate("2015-09-30"), + "to_date": getdate("2015-09-1"), + "new_leaves_allocated": 5, + } + ) # invalid period self.assertRaises(frappe.ValidationError, doc.save) - def test_allocated_leave_days_over_period(self): - doc = frappe.get_doc({ - "doctype": "Leave Allocation", - "__islocal": 1, - "employee": self.employee.name, - "employee_name": self.employee.employee_name, - "leave_type": "_Test Leave Type", - "from_date": getdate("2015-09-1"), - "to_date": getdate("2015-09-30"), - "new_leaves_allocated": 35 - }) + def test_validation_for_over_allocation(self): + doc = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, + "leave_type": "_Test Leave Type", + "from_date": getdate("2015-09-1"), + "to_date": getdate("2015-09-30"), + "new_leaves_allocated": 35, + } + ) # allocated leave more than period - self.assertRaises(frappe.ValidationError, doc.save) + self.assertRaises(OverAllocationError, doc.save) + + def test_validation_for_over_allocation_post_submission(self): + allocation = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, + "leave_type": "_Test Leave Type", + "from_date": getdate("2015-09-1"), + "to_date": getdate("2015-09-30"), + "new_leaves_allocated": 15, + } + ).submit() + allocation.reload() + # allocated leaves more than period after submission + allocation.new_leaves_allocated = 35 + self.assertRaises(OverAllocationError, allocation.save) + + def test_validation_for_over_allocation_based_on_leave_setup(self): + frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period") + leave_period = frappe.get_doc( + dict( + name="Test Allocation Period", + doctype="Leave Period", + from_date=add_months(nowdate(), -6), + to_date=add_months(nowdate(), 6), + company="_Test Company", + is_active=1, + ) + ).insert() + + leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1) + leave_type.max_leaves_allowed = 25 + leave_type.save() + + # 15 leaves allocated in this period + allocation = create_leave_allocation( + leave_type=leave_type.name, + employee=self.employee.name, + employee_name=self.employee.employee_name, + from_date=leave_period.from_date, + to_date=nowdate(), + ) + allocation.submit() + + # trying to allocate additional 15 leaves + allocation = create_leave_allocation( + leave_type=leave_type.name, + employee=self.employee.name, + employee_name=self.employee.employee_name, + from_date=add_days(nowdate(), 1), + to_date=leave_period.to_date, + ) + self.assertRaises(OverAllocationError, allocation.save) + + def test_validation_for_over_allocation_based_on_leave_setup_post_submission(self): + frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period") + leave_period = frappe.get_doc( + dict( + name="Test Allocation Period", + doctype="Leave Period", + from_date=add_months(nowdate(), -6), + to_date=add_months(nowdate(), 6), + company="_Test Company", + is_active=1, + ) + ).insert() + + leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1) + leave_type.max_leaves_allowed = 30 + leave_type.save() + + # 15 leaves allocated + allocation = create_leave_allocation( + leave_type=leave_type.name, + employee=self.employee.name, + employee_name=self.employee.employee_name, + from_date=leave_period.from_date, + to_date=nowdate(), + ) + allocation.submit() + allocation.reload() + + # allocate additional 15 leaves + allocation = create_leave_allocation( + leave_type=leave_type.name, + employee=self.employee.name, + employee_name=self.employee.employee_name, + from_date=add_days(nowdate(), 1), + to_date=leave_period.to_date, + ) + allocation.submit() + allocation.reload() + + # trying to allocate 25 leaves in 2nd alloc within leave period + # total leaves = 40 which is more than `max_leaves_allowed` setting i.e. 30 + allocation.new_leaves_allocated = 25 + self.assertRaises(OverAllocationError, allocation.save) + + def test_validate_back_dated_allocation_update(self): + leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1) + leave_type.save() + + # initial leave allocation = 15 + leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, + leave_type="_Test_CF_leave", + from_date=add_months(nowdate(), -12), + to_date=add_months(nowdate(), -1), + carry_forward=0, + ) + leave_allocation.submit() + + # new_leaves = 15, carry_forwarded = 10 + leave_allocation_1 = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, + leave_type="_Test_CF_leave", + carry_forward=1, + ) + leave_allocation_1.submit() + + # try updating initial leave allocation + leave_allocation.reload() + leave_allocation.new_leaves_allocated = 20 + self.assertRaises(BackDatedAllocationError, leave_allocation.save) def test_carry_forward_calculation(self): leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1) @@ -92,7 +225,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_type="_Test_CF_leave", from_date=add_months(nowdate(), -12), to_date=add_months(nowdate(), -1), - carry_forward=0) + carry_forward=0, + ) leave_allocation.submit() # carry forwarded leaves considering maximum_carry_forwarded_leaves @@ -101,10 +235,13 @@ class TestLeaveAllocation(unittest.TestCase): employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="_Test_CF_leave", - carry_forward=1) + carry_forward=1, + ) leave_allocation_1.submit() + leave_allocation_1.reload() self.assertEqual(leave_allocation_1.unused_leaves, 10) + self.assertEqual(leave_allocation_1.total_leaves_allocated, 25) leave_allocation_1.cancel() @@ -115,7 +252,8 @@ class TestLeaveAllocation(unittest.TestCase): employee_name=self.employee.employee_name, leave_type="_Test_CF_leave", carry_forward=1, - new_leaves_allocated=25) + new_leaves_allocated=25, + ) leave_allocation_2.submit() self.assertEqual(leave_allocation_2.unused_leaves, 5) @@ -124,7 +262,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) + expire_carry_forwarded_leaves_after_days=90, + ) leave_type.save() # initial leave allocation @@ -134,7 +273,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_type="_Test_CF_leave_expiry", from_date=add_months(nowdate(), -24), to_date=add_months(nowdate(), -12), - carry_forward=0) + carry_forward=0, + ) leave_allocation.submit() leave_allocation = create_leave_allocation( @@ -143,7 +283,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_type="_Test_CF_leave_expiry", from_date=add_days(nowdate(), -90), to_date=add_days(nowdate(), 100), - carry_forward=1) + carry_forward=1, + ) leave_allocation.submit() # expires all the carry forwarded leaves after 90 days @@ -156,19 +297,21 @@ class TestLeaveAllocation(unittest.TestCase): leave_type="_Test_CF_leave_expiry", carry_forward=1, from_date=add_months(nowdate(), 6), - to_date=add_months(nowdate(), 12)) + to_date=add_months(nowdate(), 12), + ) leave_allocation_1.submit() self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated) def test_creation_of_leave_ledger_entry_on_submit(self): leave_allocation = create_leave_allocation( - employee=self.employee.name, - employee_name=self.employee.employee_name + employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name)) + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_allocation.name) + ) self.assertEqual(len(leave_ledger_entry), 1) self.assertEqual(leave_ledger_entry[0].employee, leave_allocation.employee) @@ -177,54 +320,63 @@ class TestLeaveAllocation(unittest.TestCase): # check if leave ledger entry is deleted on cancellation leave_allocation.cancel() - self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) + self.assertFalse( + frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_allocation.name}) + ) def test_leave_addition_after_submit(self): leave_allocation = create_leave_allocation( - employee=self.employee.name, - employee_name=self.employee.employee_name + employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() + leave_allocation.reload() self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 40 leave_allocation.submit() + leave_allocation.reload() self.assertTrue(leave_allocation.total_leaves_allocated, 40) def test_leave_subtraction_after_submit(self): leave_allocation = create_leave_allocation( - employee=self.employee.name, - employee_name=self.employee.employee_name + employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() + leave_allocation.reload() self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 10 leave_allocation.submit() + leave_allocation.reload() self.assertTrue(leave_allocation.total_leaves_allocated, 10) def test_validation_against_leave_application_after_submit(self): from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list make_holiday_list() - frappe.db.set_value("Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value( + "Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List" + ) leave_allocation = create_leave_allocation( - employee=self.employee.name, - employee_name=self.employee.employee_name + employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) - leave_application = frappe.get_doc({ - "doctype": 'Leave Application', - "employee": self.employee.name, - "leave_type": "_Test Leave Type", - "from_date": add_months(nowdate(), 2), - "to_date": add_months(add_days(nowdate(), 10), 2), - "company": self.employee.company, - "docstatus": 1, - "status": "Approved", - "leave_approver": 'test@example.com' - }) + leave_application = frappe.get_doc( + { + "doctype": "Leave Application", + "employee": self.employee.name, + "leave_type": "_Test Leave Type", + "from_date": add_months(nowdate(), 2), + "to_date": add_months(add_days(nowdate(), 10), 2), + "company": self.employee.company, + "docstatus": 1, + "status": "Approved", + "leave_approver": "test@example.com", + } + ) leave_application.submit() leave_application.reload() @@ -233,22 +385,26 @@ class TestLeaveAllocation(unittest.TestCase): leave_allocation.total_leaves_allocated = leave_application.total_leave_days - 1 self.assertRaises(frappe.ValidationError, leave_allocation.submit) + def create_leave_allocation(**args): args = frappe._dict(args) emp_id = make_employee("test_emp_leave_allocation@salary.com") employee = frappe.get_doc("Employee", emp_id) - return frappe.get_doc({ - "doctype": "Leave Allocation", - "__islocal": 1, - "employee": args.employee or employee.name, - "employee_name": args.employee_name or employee.employee_name, - "leave_type": args.leave_type or "_Test Leave Type", - "from_date": args.from_date or nowdate(), - "new_leaves_allocated": args.new_leaves_allocated or 15, - "carry_forward": args.carry_forward or 0, - "to_date": args.to_date or add_months(nowdate(), 12) - }) + return frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": args.employee or employee.name, + "employee_name": args.employee_name or employee.employee_name, + "leave_type": args.leave_type or "_Test Leave Type", + "from_date": args.from_date or nowdate(), + "new_leaves_allocated": args.new_leaves_allocated or 15, + "carry_forward": args.carry_forward or 0, + "to_date": args.to_date or add_months(nowdate(), 12), + } + ) + test_dependencies = ["Employee", "Leave Type"] diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 9e8cb5516f3..85997a4087f 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -52,7 +52,7 @@ frappe.ui.form.on("Leave Application", { make_dashboard: function(frm) { var leave_details; let lwps; - if (frm.doc.employee) { + if (frm.doc.employee && frm.doc.from_date) { frappe.call({ method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details", async: false, @@ -146,6 +146,7 @@ frappe.ui.form.on("Leave Application", { }, to_date: function(frm) { + frm.trigger("make_dashboard"); frm.trigger("half_day_datepicker"); frm.trigger("calculate_total_days"); }, diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 70250f5bcf8..e6fc2e6fc06 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -1,9 +1,11 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from typing import Dict, Optional, Tuple import frappe from frappe import _ +from frappe.query_builder.functions import Max, Min, Sum from frappe.utils import ( add_days, cint, @@ -30,10 +32,29 @@ from erpnext.hr.utils import ( ) -class LeaveDayBlockedError(frappe.ValidationError): pass -class OverlapError(frappe.ValidationError): pass -class AttendanceAlreadyMarkedError(frappe.ValidationError): pass -class NotAnOptionalHoliday(frappe.ValidationError): pass +class LeaveDayBlockedError(frappe.ValidationError): + pass + + +class OverlapError(frappe.ValidationError): + pass + + +class AttendanceAlreadyMarkedError(frappe.ValidationError): + pass + + +class NotAnOptionalHoliday(frappe.ValidationError): + pass + + +class InsufficientLeaveBalanceError(frappe.ValidationError): + pass + + +class LeaveAcrossAllocationsError(frappe.ValidationError): + pass + from frappe.model.document import Document @@ -54,7 +75,7 @@ class LeaveApplication(Document): self.validate_salary_processed_days() self.validate_attendance() self.set_half_day_date() - if frappe.db.get_value("Leave Type", self.leave_type, 'is_optional_leave'): + if frappe.db.get_value("Leave Type", self.leave_type, "is_optional_leave"): self.validate_optional_leave() self.validate_applicable_after() @@ -68,7 +89,9 @@ class LeaveApplication(Document): def on_submit(self): if self.status == "Open": - frappe.throw(_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")) + frappe.throw( + _("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted") + ) self.validate_back_dated_application() self.update_attendance() @@ -95,7 +118,9 @@ class LeaveApplication(Document): leave_type = frappe.get_doc("Leave Type", self.leave_type) if leave_type.applicable_after > 0: date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") - leave_days = get_approved_leaves_for_period(self.employee, False, date_of_joining, self.from_date) + leave_days = get_approved_leaves_for_period( + self.employee, False, date_of_joining, self.from_date + ) number_of_days = date_diff(getdate(self.from_date), date_of_joining) if number_of_days >= 0: holidays = 0 @@ -103,29 +128,48 @@ class LeaveApplication(Document): holidays = get_holidays(self.employee, date_of_joining, self.from_date) number_of_days = number_of_days - leave_days - holidays if number_of_days < leave_type.applicable_after: - frappe.throw(_("{0} applicable after {1} working days").format(self.leave_type, leave_type.applicable_after)) + frappe.throw( + _("{0} applicable after {1} working days").format( + self.leave_type, leave_type.applicable_after + ) + ) def validate_dates(self): if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"): 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") + allowed_role = frappe.db.get_single_value( + "HR Settings", "role_allowed_to_create_backdated_leave_application" + ) user = frappe.get_doc("User", frappe.session.user) user_roles = [d.role for d in user.roles] if not allowed_role: - frappe.throw(_("Backdated Leave Application is restricted. Please set the {} in {}").format( - frappe.bold("Role Allowed to Create Backdated Leave Application"), get_link_to_form("HR Settings", "HR Settings"))) + frappe.throw( + _("Backdated Leave Application is restricted. Please set the {} in {}").format( + frappe.bold("Role Allowed to Create Backdated Leave Application"), + get_link_to_form("HR Settings", "HR Settings"), + ) + ) - if (allowed_role and allowed_role not in user_roles): - frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role)) + if allowed_role and allowed_role not in user_roles: + frappe.throw( + _("Only users with the {0} role can create backdated leave applications").format( + allowed_role + ) + ) if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)): frappe.throw(_("To date cannot be before from date")) - if self.half_day and self.half_day_date \ - and (getdate(self.half_day_date) < getdate(self.from_date) - or getdate(self.half_day_date) > getdate(self.to_date)): + if ( + self.half_day + and self.half_day_date + and ( + getdate(self.half_day_date) < getdate(self.from_date) + or getdate(self.half_day_date) > getdate(self.to_date) + ) + ): - frappe.throw(_("Half Day Date should be between From Date and To Date")) + frappe.throw(_("Half Day Date should be between From Date and To Date")) if not is_lwp(self.leave_type): self.validate_dates_across_allocation() @@ -134,30 +178,55 @@ class LeaveApplication(Document): def validate_dates_across_allocation(self): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): return - def _get_leave_allocation_record(date): - allocation = frappe.db.sql("""select name from `tabLeave Allocation` - where employee=%s and leave_type=%s and docstatus=1 - and %s between from_date and to_date""", (self.employee, self.leave_type, date)) - return allocation and allocation[0][0] + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + + if not (alloc_on_from_date or alloc_on_to_date): + frappe.throw(_("Application period cannot be outside leave allocation period")) + elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): + frappe.throw( + _("Application period cannot be across two allocation records"), + exc=LeaveAcrossAllocationsError, + ) + + def get_allocation_based_on_application_dates(self) -> Tuple[Dict, Dict]: + """Returns allocation name, from and to dates for application dates""" + + def _get_leave_allocation_record(date): + LeaveAllocation = frappe.qb.DocType("Leave Allocation") + allocation = ( + frappe.qb.from_(LeaveAllocation) + .select(LeaveAllocation.name, LeaveAllocation.from_date, LeaveAllocation.to_date) + .where( + (LeaveAllocation.employee == self.employee) + & (LeaveAllocation.leave_type == self.leave_type) + & (LeaveAllocation.docstatus == 1) + & ((date >= LeaveAllocation.from_date) & (date <= LeaveAllocation.to_date)) + ) + ).run(as_dict=True) + + return allocation and allocation[0] allocation_based_on_from_date = _get_leave_allocation_record(self.from_date) allocation_based_on_to_date = _get_leave_allocation_record(self.to_date) - if not (allocation_based_on_from_date or allocation_based_on_to_date): - frappe.throw(_("Application period cannot be outside leave allocation period")) - - elif allocation_based_on_from_date != allocation_based_on_to_date: - frappe.throw(_("Application period cannot be across two allocation records")) + return allocation_based_on_from_date, allocation_based_on_to_date def validate_back_dated_application(self): - future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` + future_allocation = frappe.db.sql( + """select name, from_date from `tabLeave Allocation` where employee=%s and leave_type=%s and docstatus=1 and from_date > %s - and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1) + and carry_forward=1""", + (self.employee, self.leave_type, self.to_date), + as_dict=1, + ) if future_allocation: - frappe.throw(_("Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") - .format(formatdate(future_allocation[0].from_date), future_allocation[0].name)) + frappe.throw( + _( + "Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}" + ).format(formatdate(future_allocation[0].from_date), future_allocation[0].name) + ) def update_attendance(self): if self.status != "Approved": @@ -169,8 +238,9 @@ class LeaveApplication(Document): for dt in daterange(getdate(self.from_date), getdate(self.to_date)): date = dt.strftime("%Y-%m-%d") - attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee, - attendance_date = date, docstatus = ('!=', 2))) + attendance_name = frappe.db.exists( + "Attendance", dict(employee=self.employee, attendance_date=date, docstatus=("!=", 2)) + ) # don't mark attendance for holidays # if leave type does not include holidays within leaves as leaves @@ -187,17 +257,17 @@ class LeaveApplication(Document): self.create_or_update_attendance(attendance_name, date) def create_or_update_attendance(self, attendance_name, date): - status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave" + status = ( + "Half Day" + if self.half_day_date and getdate(date) == getdate(self.half_day_date) + else "On Leave" + ) if attendance_name: # update existing attendance, change absent to on leave - doc = frappe.get_doc('Attendance', attendance_name) + doc = frappe.get_doc("Attendance", attendance_name) if doc.status != status: - doc.db_set({ - 'status': status, - 'leave_type': self.leave_type, - 'leave_application': self.name - }) + doc.db_set({"status": status, "leave_type": self.leave_type, "leave_application": self.name}) else: # make new attendance and submit it doc = frappe.new_doc("Attendance") @@ -214,8 +284,12 @@ class LeaveApplication(Document): def cancel_attendance(self): if self.docstatus == 2: - attendance = frappe.db.sql("""select name from `tabAttendance` where employee = %s\ - and (attendance_date between %s and %s) and docstatus < 2 and status in ('On Leave', 'Half Day')""",(self.employee, self.from_date, self.to_date), as_dict=1) + attendance = frappe.db.sql( + """select name from `tabAttendance` where employee = %s\ + and (attendance_date between %s and %s) and docstatus < 2 and status in ('On Leave', 'Half Day')""", + (self.employee, self.from_date, self.to_date), + as_dict=1, + ) for name in attendance: frappe.db.set_value("Attendance", name, "docstatus", 2) @@ -223,21 +297,29 @@ class LeaveApplication(Document): if not frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"): return - last_processed_pay_slip = frappe.db.sql(""" + last_processed_pay_slip = frappe.db.sql( + """ select start_date, end_date from `tabSalary Slip` where docstatus = 1 and employee = %s and ((%s between start_date and end_date) or (%s between start_date and end_date)) order by modified desc limit 1 - """,(self.employee, self.to_date, self.from_date)) + """, + (self.employee, self.to_date, self.from_date), + ) if last_processed_pay_slip: - frappe.throw(_("Salary already processed for period between {0} and {1}, Leave application period cannot be between this date range.").format(formatdate(last_processed_pay_slip[0][0]), - formatdate(last_processed_pay_slip[0][1]))) - + frappe.throw( + _( + "Salary already processed for period between {0} and {1}, Leave application period cannot be between this date range." + ).format( + formatdate(last_processed_pay_slip[0][0]), formatdate(last_processed_pay_slip[0][1]) + ) + ) def show_block_day_warning(self): - block_dates = get_applicable_block_dates(self.from_date, self.to_date, - self.employee, self.company, all_lists=True) + block_dates = get_applicable_block_dates( + self.from_date, self.to_date, self.employee, self.company, all_lists=True + ) if block_dates: frappe.msgprint(_("Warning: Leave application contains following block dates") + ":") @@ -245,53 +327,99 @@ class LeaveApplication(Document): frappe.msgprint(formatdate(d.block_date) + ": " + d.reason) def validate_block_days(self): - block_dates = get_applicable_block_dates(self.from_date, self.to_date, - self.employee, self.company) + block_dates = get_applicable_block_dates( + self.from_date, self.to_date, self.employee, self.company + ) if block_dates and self.status == "Approved": frappe.throw(_("You are not authorized to approve leaves on Block Dates"), LeaveDayBlockedError) def validate_balance_leaves(self): if self.from_date and self.to_date: - self.total_leave_days = get_number_of_leave_days(self.employee, self.leave_type, - self.from_date, self.to_date, self.half_day, self.half_day_date) + self.total_leave_days = get_number_of_leave_days( + self.employee, self.leave_type, self.from_date, self.to_date, self.half_day, self.half_day_date + ) if self.total_leave_days <= 0: - frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave.")) + frappe.throw( + _( + "The day(s) on which you are applying for leave are holidays. You need not apply for leave." + ) + ) if not is_lwp(self.leave_type): - self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date, - consider_all_leaves_in_the_allocation_period=True) - if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance): - if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): - frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}") - .format(self.leave_type)) - else: - frappe.throw(_("There is not enough leave balance for Leave Type {0}") - .format(self.leave_type)) + leave_balance = get_leave_balance_on( + self.employee, + self.leave_type, + self.from_date, + self.to_date, + consider_all_leaves_in_the_allocation_period=True, + for_consumption=True, + ) + self.leave_balance = leave_balance.get("leave_balance") + leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption") + + if self.status != "Rejected" and ( + leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption + ): + self.show_insufficient_balance_message(leave_balance_for_consumption) + + def show_insufficient_balance_message(self, leave_balance_for_consumption: float) -> None: + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + + if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): + if leave_balance_for_consumption != self.leave_balance: + msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format( + frappe.bold(self.leave_type) + ) + msg += "

" + msg += _( + "Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation." + ) + else: + msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format( + frappe.bold(self.leave_type) + ) + + frappe.msgprint(msg, title=_("Warning"), indicator="orange") + else: + frappe.throw( + _("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)), + exc=InsufficientLeaveBalanceError, + title=_("Insufficient Balance"), + ) def validate_leave_overlap(self): if not self.name: # hack! if name is null, it could cause problems with != self.name = "New Leave Application" - for d in frappe.db.sql(""" + for d in frappe.db.sql( + """ select name, leave_type, posting_date, from_date, to_date, total_leave_days, half_day_date from `tabLeave Application` where employee = %(employee)s and docstatus < 2 and status in ("Open", "Approved") and to_date >= %(from_date)s and from_date <= %(to_date)s - and name != %(name)s""", { + and name != %(name)s""", + { "employee": self.employee, "from_date": self.from_date, "to_date": self.to_date, - "name": self.name - }, as_dict = 1): + "name": self.name, + }, + as_dict=1, + ): - if cint(self.half_day)==1 and getdate(self.half_day_date) == getdate(d.half_day_date) and ( - flt(self.total_leave_days)==0.5 - or getdate(self.from_date) == getdate(d.to_date) - or getdate(self.to_date) == getdate(d.from_date)): + if ( + cint(self.half_day) == 1 + and getdate(self.half_day_date) == getdate(d.half_day_date) + and ( + flt(self.total_leave_days) == 0.5 + or getdate(self.from_date) == getdate(d.to_date) + or getdate(self.to_date) == getdate(d.from_date) + ) + ): total_leaves_on_half_day = self.get_total_leaves_on_half_day() if total_leaves_on_half_day >= 1: @@ -301,22 +429,22 @@ class LeaveApplication(Document): def throw_overlap_error(self, d): 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) + 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): - leave_count_on_half_day_date = frappe.db.sql("""select count(name) from `tabLeave Application` + leave_count_on_half_day_date = frappe.db.sql( + """select count(name) from `tabLeave Application` where employee = %(employee)s and docstatus < 2 and status in ("Open", "Approved") and half_day = 1 and half_day_date = %(half_day_date)s - and name != %(name)s""", { - "employee": self.employee, - "half_day_date": self.half_day_date, - "name": self.name - })[0][0] + and name != %(name)s""", + {"employee": self.employee, "half_day_date": self.half_day_date, "name": self.name}, + )[0][0] return leave_count_on_half_day_date * 0.5 @@ -326,24 +454,36 @@ class LeaveApplication(Document): frappe.throw(_("Leave of type {0} cannot be longer than {1}").format(self.leave_type, max_days)) def validate_attendance(self): - attendance = frappe.db.sql("""select name from `tabAttendance` where employee = %s and (attendance_date between %s and %s) + attendance = frappe.db.sql( + """select name from `tabAttendance` where employee = %s and (attendance_date between %s and %s) and status = "Present" and docstatus = 1""", - (self.employee, self.from_date, self.to_date)) + (self.employee, self.from_date, self.to_date), + ) if attendance: - frappe.throw(_("Attendance for employee {0} is already marked for this day").format(self.employee), - AttendanceAlreadyMarkedError) + frappe.throw( + _("Attendance for employee {0} is already marked for this day").format(self.employee), + AttendanceAlreadyMarkedError, + ) def validate_optional_leave(self): leave_period = get_leave_period(self.from_date, self.to_date, self.company) if not leave_period: frappe.throw(_("Cannot find active Leave Period")) - optional_holiday_list = frappe.db.get_value("Leave Period", leave_period[0]["name"], "optional_holiday_list") + optional_holiday_list = frappe.db.get_value( + "Leave Period", leave_period[0]["name"], "optional_holiday_list" + ) if not optional_holiday_list: - frappe.throw(_("Optional Holiday List not set for leave period {0}").format(leave_period[0]["name"])) + frappe.throw( + _("Optional Holiday List not set for leave period {0}").format(leave_period[0]["name"]) + ) day = getdate(self.from_date) while day <= getdate(self.to_date): - if not frappe.db.exists({"doctype": "Holiday", "parent": optional_holiday_list, "holiday_date": day}): - frappe.throw(_("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday) + if not frappe.db.exists( + {"doctype": "Holiday", "parent": optional_holiday_list, "holiday_date": day} + ): + frappe.throw( + _("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday + ) day = add_days(day, 1) def set_half_day_date(self): @@ -358,44 +498,50 @@ class LeaveApplication(Document): if not employee.user_id: return - parent_doc = frappe.get_doc('Leave Application', self.name) + parent_doc = frappe.get_doc("Leave Application", self.name) args = parent_doc.as_dict() - template = frappe.db.get_single_value('HR Settings', 'leave_status_notification_template') + template = frappe.db.get_single_value("HR Settings", "leave_status_notification_template") if not template: frappe.msgprint(_("Please set default template for Leave Status Notification in HR Settings.")) return email_template = frappe.get_doc("Email Template", template) message = frappe.render_template(email_template.response, args) - self.notify({ - # for post in messages - "message": message, - "message_to": employee.user_id, - # for email - "subject": email_template.subject, - "notify": "employee" - }) + self.notify( + { + # for post in messages + "message": message, + "message_to": employee.user_id, + # for email + "subject": email_template.subject, + "notify": "employee", + } + ) def notify_leave_approver(self): if self.leave_approver: - parent_doc = frappe.get_doc('Leave Application', self.name) + parent_doc = frappe.get_doc("Leave Application", self.name) args = parent_doc.as_dict() - template = frappe.db.get_single_value('HR Settings', 'leave_approval_notification_template') + template = frappe.db.get_single_value("HR Settings", "leave_approval_notification_template") if not template: - frappe.msgprint(_("Please set default template for Leave Approval Notification in HR Settings.")) + frappe.msgprint( + _("Please set default template for Leave Approval Notification in HR Settings.") + ) return email_template = frappe.get_doc("Email Template", template) message = frappe.render_template(email_template.response, args) - self.notify({ - # for post in messages - "message": message, - "message_to": self.leave_approver, - # for email - "subject": email_template.subject - }) + self.notify( + { + # for post in messages + "message": message, + "message_to": self.leave_approver, + # for email + "subject": email_template.subject, + } + ) def notify(self, args): args = frappe._dict(args) @@ -404,94 +550,195 @@ class LeaveApplication(Document): contact = args.message_to if not isinstance(contact, list): if not args.notify == "employee": - contact = frappe.get_doc('User', contact).email or contact + contact = frappe.get_doc("User", contact).email or contact - sender = dict() - sender['email'] = frappe.get_doc('User', frappe.session.user).email - sender['full_name'] = get_fullname(sender['email']) + sender = dict() + sender["email"] = frappe.get_doc("User", frappe.session.user).email + sender["full_name"] = get_fullname(sender["email"]) try: frappe.sendmail( - recipients = contact, - sender = sender['email'], - subject = args.subject, - message = args.message, + recipients=contact, + sender=sender["email"], + subject=args.subject, + message=args.message, ) frappe.msgprint(_("Email sent to {0}").format(contact)) except frappe.OutgoingEmailError: pass def create_leave_ledger_entry(self, submit=True): - if self.status != 'Approved' and submit: + if self.status != "Approved" and submit: return - expiry_date = get_allocation_expiry(self.employee, self.leave_type, - self.to_date, self.from_date) - + expiry_date = get_allocation_expiry_for_cf_leaves( + self.employee, self.leave_type, self.to_date, self.from_date + ) lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp") if expiry_date: self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp) else: - raise_exception = True - if frappe.flags.in_patch: - raise_exception=False + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): + # required only if negative balance is allowed for leave type + # else will be stopped in validation itself + self.create_separate_ledger_entries(alloc_on_from_date, alloc_on_to_date, submit, lwp) + else: + raise_exception = False if frappe.flags.in_patch else True + args = dict( + leaves=self.total_leave_days * -1, + from_date=self.from_date, + to_date=self.to_date, + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) + or "", + ) + create_leave_ledger_entry(self, args, submit) - args = dict( - leaves=self.total_leave_days * -1, - from_date=self.from_date, - to_date=self.to_date, - is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + def is_separate_ledger_entry_required( + self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None + ) -> bool: + """Checks if application dates fall in separate allocations""" + if ( + (alloc_on_from_date and not alloc_on_to_date) + or (not alloc_on_from_date and alloc_on_to_date) + or ( + alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name + ) + ): + return True + return False + + def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp): + """Creates separate ledger entries for application period falling into separate allocations""" + # for creating separate ledger entries existing allocation periods should be consecutive + if ( + submit + and alloc_on_from_date + and alloc_on_to_date + and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date + ): + frappe.throw( + _( + "Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}." + ).format( + get_link_to_form("Leave Allocation", alloc_on_from_date.name), + get_link_to_form("Leave Allocation", alloc_on_to_date), + ) + ) + + raise_exception = False if frappe.flags.in_patch else True + + if alloc_on_from_date: + first_alloc_end = alloc_on_from_date.to_date + second_alloc_start = add_days(alloc_on_from_date.to_date, 1) + else: + first_alloc_end = add_days(alloc_on_to_date.from_date, -1) + second_alloc_start = alloc_on_to_date.from_date + + leaves_in_first_alloc = get_number_of_leave_days( + self.employee, + self.leave_type, + self.from_date, + first_alloc_end, + self.half_day, + self.half_day_date, + ) + leaves_in_second_alloc = get_number_of_leave_days( + self.employee, + self.leave_type, + second_alloc_start, + self.to_date, + self.half_day, + self.half_day_date, + ) + + args = dict( + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) + or "", + ) + + if leaves_in_first_alloc: + args.update( + dict(from_date=self.from_date, to_date=first_alloc_end, leaves=leaves_in_first_alloc * -1) + ) + create_leave_ledger_entry(self, args, submit) + + if leaves_in_second_alloc: + args.update( + dict(from_date=second_alloc_start, to_date=self.to_date, leaves=leaves_in_second_alloc * -1) ) create_leave_ledger_entry(self, args, submit) def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp): - ''' splits leave application into two ledger entries to consider expiry of allocation ''' + """Splits leave application into two ledger entries to consider expiry of allocation""" + raise_exception = False if frappe.flags.in_patch else True - raise_exception = True - if frappe.flags.in_patch: - raise_exception=False - - args = dict( - from_date=self.from_date, - to_date=expiry_date, - leaves=(date_diff(expiry_date, self.from_date) + 1) * -1, - is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + leaves = get_number_of_leave_days( + self.employee, self.leave_type, self.from_date, expiry_date, self.half_day, self.half_day_date ) - create_leave_ledger_entry(self, args, submit) + + if leaves: + args = dict( + from_date=self.from_date, + to_date=expiry_date, + leaves=leaves * -1, + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) + or "", + ) + create_leave_ledger_entry(self, args, submit) if getdate(expiry_date) != getdate(self.to_date): start_date = add_days(expiry_date, 1) - args.update(dict( - from_date=start_date, - to_date=self.to_date, - leaves=date_diff(self.to_date, expiry_date) * -1 - )) - create_leave_ledger_entry(self, args, submit) + leaves = get_number_of_leave_days( + self.employee, self.leave_type, start_date, self.to_date, self.half_day, self.half_day_date + ) + + if leaves: + args.update(dict(from_date=start_date, to_date=self.to_date, leaves=leaves * -1)) + create_leave_ledger_entry(self, args, submit) -def get_allocation_expiry(employee, leave_type, to_date, from_date): - ''' Returns expiry of carry forward allocation in leave ledger entry ''' - expiry = frappe.get_all("Leave Ledger Entry", +def get_allocation_expiry_for_cf_leaves( + employee: str, leave_type: str, to_date: str, from_date: str +) -> str: + """Returns expiry of carry forward allocation in leave ledger entry""" + expiry = frappe.get_all( + "Leave Ledger Entry", filters={ - 'employee': employee, - 'leave_type': leave_type, - 'is_carry_forward': 1, - 'transaction_type': 'Leave Allocation', - 'to_date': ['between', (from_date, to_date)] - },fields=['to_date']) - return expiry[0]['to_date'] if expiry else None + "employee": employee, + "leave_type": leave_type, + "is_carry_forward": 1, + "transaction_type": "Leave Allocation", + "to_date": ["between", (from_date, to_date)], + "docstatus": 1, + }, + fields=["to_date"], + ) + return expiry[0]["to_date"] if expiry else "" + @frappe.whitelist() -def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None): +def get_number_of_leave_days( + employee: str, + leave_type: str, + from_date: str, + to_date: str, + half_day: Optional[int] = None, + half_day_date: Optional[str] = None, + holiday_list: Optional[str] = None, +) -> float: + """Returns number of leave days between 2 dates after considering half day and holidays + (Based on the include_holiday setting in Leave Type)""" number_of_days = 0 if cint(half_day) == 1: - if from_date == to_date: + if getdate(from_date) == getdate(to_date): number_of_days = 0.5 - elif half_day_date and half_day_date <= to_date: - number_of_days = date_diff(to_date, from_date) + .5 + elif half_day_date and getdate(from_date) <= getdate(half_day_date) <= getdate(to_date): + number_of_days = date_diff(to_date, from_date) + 0.5 else: number_of_days = date_diff(to_date, from_date) + 1 @@ -499,9 +746,12 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day number_of_days = date_diff(to_date, from_date) + 1 if not frappe.db.get_value("Leave Type", leave_type, "include_holiday"): - number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list)) + number_of_days = flt(number_of_days) - flt( + get_holidays(employee, from_date, to_date, holiday_list=holiday_list) + ) return number_of_days + @frappe.whitelist() def get_leave_details(employee, date): allocation_records = get_leave_allocation_records(employee, date) @@ -509,49 +759,71 @@ def get_leave_details(employee, date): for d in allocation_records: allocation = allocation_records.get(d, frappe._dict()) - total_allocated_leaves = frappe.db.get_value('Leave Allocation', { - 'from_date': ('<=', date), - 'to_date': ('>=', date), - 'employee': employee, - 'leave_type': allocation.leave_type, - }, 'SUM(total_leaves_allocated)') or 0 + total_allocated_leaves = ( + frappe.db.get_value( + "Leave Allocation", + { + "from_date": ("<=", date), + "to_date": (">=", date), + "employee": employee, + "leave_type": allocation.leave_type, + "docstatus": 1, + }, + "SUM(total_leaves_allocated)", + ) + or 0 + ) - remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, - consider_all_leaves_in_the_allocation_period=True) + remaining_leaves = get_leave_balance_on( + employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True + ) end_date = allocation.to_date leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1 - leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date) + leaves_pending = get_leaves_pending_approval_for_period( + employee, d, allocation.from_date, end_date + ) leave_allocation[d] = { "total_leaves": total_allocated_leaves, "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken), "leaves_taken": leaves_taken, - "pending_leaves": leaves_pending, - "remaining_leaves": remaining_leaves} + "leaves_pending_approval": leaves_pending, + "remaining_leaves": remaining_leaves, + } - #is used in set query - lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1}) - lwps = [lwp.name for lwp in lwps] + # is used in set query + lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name") - ret = { - 'leave_allocation': leave_allocation, - 'leave_approver': get_leave_approver(employee), - 'lwps': lwps + return { + "leave_allocation": leave_allocation, + "leave_approver": get_leave_approver(employee), + "lwps": lwp, } - return ret @frappe.whitelist() -def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False): - ''' - Returns leave balance till date - :param employee: employee name - :param leave_type: leave type - :param date: date to check balance on - :param to_date: future date to check for allocation expiry - :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date - ''' +def get_leave_balance_on( + employee: str, + leave_type: str, + date: str, + to_date: str = None, + consider_all_leaves_in_the_allocation_period: bool = False, + for_consumption: bool = False, +): + """ + Returns leave balance till date + :param employee: employee name + :param leave_type: leave type + :param date: date to check balance on + :param to_date: future date to check for allocation expiry + :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date + :param for_consumption: flag to check if leave balance is required for consumption or display + eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave + in this case leave_balance = 10 but leave_balance_for_consumption = 1 + if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1} + else, returns leave_balance (in this case 10) + """ if not to_date: to_date = nowdate() @@ -560,97 +832,147 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_ allocation = allocation_records.get(leave_type, frappe._dict()) end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date - expiry = get_allocation_expiry(employee, leave_type, to_date, date) + cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date) leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date) - return get_remaining_leaves(allocation, leaves_taken, date, expiry) + remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) + + if for_consumption: + return remaining_leaves + else: + return remaining_leaves.get("leave_balance") + def get_leave_allocation_records(employee, date, leave_type=None): - ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries ''' + """Returns the total allocated leaves and carry forwarded leaves based on ledger entries""" + Ledger = frappe.qb.DocType("Leave Ledger Entry") - conditions = ("and leave_type='%s'" % leave_type) if leave_type else "" - allocation_details = frappe.db.sql(""" - SELECT - SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves, - SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves, - MIN(from_date) as from_date, - MAX(to_date) as to_date, - leave_type - FROM `tabLeave Ledger Entry` - WHERE - from_date <= %(date)s - AND to_date >= %(date)s - AND docstatus=1 - AND transaction_type="Leave Allocation" - AND employee=%(employee)s - AND is_expired=0 - AND is_lwp=0 - {0} - GROUP BY employee, leave_type - """.format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec + cf_leave_case = ( + frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0) + ) + sum_cf_leaves = Sum(cf_leave_case).as_("cf_leaves") + + new_leaves_case = ( + frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0) + ) + sum_new_leaves = Sum(new_leaves_case).as_("new_leaves") + + query = ( + frappe.qb.from_(Ledger) + .select( + sum_cf_leaves, + sum_new_leaves, + Min(Ledger.from_date).as_("from_date"), + Max(Ledger.to_date).as_("to_date"), + Ledger.leave_type, + ) + .where( + (Ledger.from_date <= date) + & (Ledger.to_date >= date) + & (Ledger.docstatus == 1) + & (Ledger.transaction_type == "Leave Allocation") + & (Ledger.employee == employee) + & (Ledger.is_expired == 0) + & (Ledger.is_lwp == 0) + ) + ) + + if leave_type: + query = query.where((Ledger.leave_type == leave_type)) + query = query.groupby(Ledger.employee, Ledger.leave_type) + + allocation_details = query.run(as_dict=True) allocated_leaves = frappe._dict() for d in allocation_details: - allocated_leaves.setdefault(d.leave_type, frappe._dict({ - "from_date": d.from_date, - "to_date": d.to_date, - "total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves), - "unused_leaves": d.cf_leaves, - "new_leaves_allocated": d.new_leaves, - "leave_type": d.leave_type - })) + allocated_leaves.setdefault( + d.leave_type, + frappe._dict( + { + "from_date": d.from_date, + "to_date": d.to_date, + "total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves), + "unused_leaves": d.cf_leaves, + "new_leaves_allocated": d.new_leaves, + "leave_type": d.leave_type, + } + ), + ) return allocated_leaves -def get_pending_leaves_for_period(employee, leave_type, from_date, to_date): - ''' Returns leaves that are pending approval ''' - leaves = frappe.get_all("Leave Application", - filters={ - "employee": employee, - "leave_type": leave_type, - "status": "Open" - }, + +def get_leaves_pending_approval_for_period( + employee: str, leave_type: str, from_date: str, to_date: str +) -> float: + """Returns leaves that are pending for approval""" + leaves = frappe.get_all( + "Leave Application", + filters={"employee": employee, "leave_type": leave_type, "status": "Open"}, or_filters={ "from_date": ["between", (from_date, to_date)], - "to_date": ["between", (from_date, to_date)] - }, fields=['SUM(total_leave_days) as leaves'])[0] - return leaves['leaves'] if leaves['leaves'] else 0.0 + "to_date": ["between", (from_date, to_date)], + }, + fields=["SUM(total_leave_days) as leaves"], + )[0] + return leaves["leaves"] if leaves["leaves"] else 0.0 + + +def get_remaining_leaves( + allocation: Dict, leaves_taken: float, date: str, cf_expiry: str +) -> Dict[str, float]: + """Returns a dict of leave_balance and leave_balance_for_consumption + leave_balance returns the available leave balance + leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry + """ -def get_remaining_leaves(allocation, leaves_taken, date, expiry): - ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' def _get_remaining_leaves(remaining_leaves, end_date): - + """Returns minimum leaves remaining after comparing with remaining days for allocation expiry""" if remaining_leaves > 0: remaining_days = date_diff(end_date, date) + 1 remaining_leaves = min(remaining_days, remaining_leaves) return remaining_leaves - total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken) + leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt( + leaves_taken + ) - if expiry and allocation.unused_leaves: - remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) - remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry) + # balance for carry forwarded leaves + if cf_expiry and allocation.unused_leaves: + cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) + remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry) - total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves) + leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves) + leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves) - return _get_remaining_leaves(total_leaves, allocation.to_date) + remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date) + return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves) -def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False): + +def get_leaves_for_period( + employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True +) -> float: leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) leave_days = 0 for leave_entry in leave_entries: - inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date) + inclusive_period = leave_entry.from_date >= getdate( + from_date + ) and leave_entry.to_date <= getdate(to_date) - if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': + if inclusive_period and leave_entry.transaction_type == "Leave Encashment": leave_days += leave_entry.leaves - elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \ - and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)): + elif ( + inclusive_period + and leave_entry.transaction_type == "Leave Allocation" + and leave_entry.is_expired + and not skip_expired_leaves + ): leave_days += leave_entry.leaves - elif leave_entry.transaction_type == 'Leave Application': + elif leave_entry.transaction_type == "Leave Application": if leave_entry.from_date < getdate(from_date): leave_entry.from_date = from_date if leave_entry.to_date > getdate(to_date): @@ -661,23 +983,30 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_ # fetch half day date for leaves with half days if leave_entry.leaves % 1: half_day = 1 - half_day_date = frappe.db.get_value('Leave Application', - {'name': leave_entry.transaction_name}, ['half_day_date']) + half_day_date = frappe.db.get_value( + "Leave Application", {"name": leave_entry.transaction_name}, ["half_day_date"] + ) - leave_days += get_number_of_leave_days(employee, leave_type, - leave_entry.from_date, leave_entry.to_date, half_day, half_day_date, holiday_list=leave_entry.holiday_list) * -1 + leave_days += ( + get_number_of_leave_days( + employee, + leave_type, + leave_entry.from_date, + leave_entry.to_date, + half_day, + half_day_date, + holiday_list=leave_entry.holiday_list, + ) + * -1 + ) return leave_days -def skip_expiry_leaves(leave_entry, date): - ''' Checks whether the expired leaves coincide with the to_date of leave balance check. - This allows backdated leave entry creation for non carry forwarded allocation ''' - end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date']) - return True if end_date == date and not leave_entry.is_carry_forward else False def get_leave_entries(employee, leave_type, from_date, to_date): - ''' Returns leave entries between from_date and to_date. ''' - return frappe.db.sql(""" + """Returns leave entries between from_date and to_date.""" + return frappe.db.sql( + """ SELECT employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, holiday_list, is_carry_forward, is_expired @@ -689,44 +1018,47 @@ def get_leave_entries(employee, leave_type, from_date, to_date): AND (from_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s OR (from_date < %(from_date)s AND to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + """, + {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, + as_dict=1, + ) + @frappe.whitelist() -def get_holidays(employee, from_date, to_date, holiday_list = None): - '''get holidays between two dates for the given employee''' +def get_holidays(employee, from_date, to_date, holiday_list=None): + """get holidays between two dates for the given employee""" if not holiday_list: holiday_list = get_holiday_list_for_employee(employee) - holidays = frappe.db.sql("""select count(distinct holiday_date) from `tabHoliday` h1, `tabHoliday List` h2 + holidays = frappe.db.sql( + """select count(distinct holiday_date) from `tabHoliday` h1, `tabHoliday List` h2 where h1.parent = h2.name and h1.holiday_date between %s and %s - and h2.name = %s""", (from_date, to_date, holiday_list))[0][0] + and h2.name = %s""", + (from_date, to_date, holiday_list), + )[0][0] return holidays + def is_lwp(leave_type): lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", leave_type) return lwp and cint(lwp[0][0]) or 0 + @frappe.whitelist() def get_events(start, end, filters=None): from frappe.desk.reportview import get_filters_cond + events = [] - employee = frappe.db.get_value("Employee", - filters={"user_id": frappe.session.user}, - fieldname=["name", "company"], - as_dict=True + employee = frappe.db.get_value( + "Employee", filters={"user_id": frappe.session.user}, fieldname=["name", "company"], as_dict=True ) if employee: employee, company = employee.name, employee.company else: - employee = '' + employee = "" company = frappe.db.get_value("Global Defaults", None, "default_company") conditions = get_filters_cond("Leave Application", filters, []) @@ -740,6 +1072,7 @@ def get_events(start, end, filters=None): return events + def add_department_leaves(events, start, end, employee, company): department = frappe.db.get_value("Employee", employee, "department") @@ -747,18 +1080,24 @@ def add_department_leaves(events, start, end, employee, company): return # department leaves - department_employees = frappe.db.sql_list("""select name from tabEmployee where department=%s - and company=%s""", (department, company)) + department_employees = frappe.db.sql_list( + """select name from tabEmployee where department=%s + and company=%s""", + (department, company), + ) - filter_conditions = " and employee in (\"%s\")" % '", "'.join(department_employees) + filter_conditions = ' and employee in ("%s")' % '", "'.join(department_employees) add_leaves(events, start, end, filter_conditions=filter_conditions) def add_leaves(events, start, end, filter_conditions=None): from frappe.desk.reportview import build_match_conditions + conditions = [] - if not cint(frappe.db.get_value("HR Settings", None, "show_leaves_of_all_department_members_in_calendar")): + if not cint( + frappe.db.get_value("HR Settings", None, "show_leaves_of_all_department_members_in_calendar") + ): match_conditions = build_match_conditions("Leave Application") if match_conditions: @@ -783,12 +1122,12 @@ def add_leaves(events, start, end, filter_conditions=None): """ if conditions: - query += ' AND ' + ' AND '.join(conditions) + query += " AND " + " AND ".join(conditions) if filter_conditions: query += filter_conditions - for d in frappe.db.sql(query, {"start":start, "end": end}, as_dict=True): + for d in frappe.db.sql(query, {"start": start, "end": end}, as_dict=True): e = { "name": d.name, "doctype": "Leave Application", @@ -797,7 +1136,9 @@ def add_leaves(events, start, end, filter_conditions=None): "docstatus": d.docstatus, "color": d.color, "all_day": int(not d.half_day), - "title": cstr(d.employee_name) + f' ({cstr(d.leave_type)})' + (' ' + _('(Half Day)') if d.half_day else ''), + "title": cstr(d.employee_name) + + f" ({cstr(d.leave_type)})" + + (" " + _("(Half Day)") if d.half_day else ""), } if e not in events: events.append(e) @@ -811,43 +1152,55 @@ def add_block_dates(events, start, end, employee, company): block_dates = get_applicable_block_dates(start, end, employee, company, all_lists=True) for block_date in block_dates: - events.append({ - "doctype": "Leave Block List Date", - "from_date": block_date.block_date, - "to_date": block_date.block_date, - "title": _("Leave Blocked") + ": " + block_date.reason, - "name": "_" + str(cnt), - }) - cnt+=1 + events.append( + { + "doctype": "Leave Block List Date", + "from_date": block_date.block_date, + "to_date": block_date.block_date, + "title": _("Leave Blocked") + ": " + block_date.reason, + "name": "_" + str(cnt), + } + ) + cnt += 1 + def add_holidays(events, start, end, employee, company): applicable_holiday_list = get_holiday_list_for_employee(employee, company) if not applicable_holiday_list: return - for holiday in frappe.db.sql("""select name, holiday_date, description + for holiday in frappe.db.sql( + """select name, holiday_date, description from `tabHoliday` where parent=%s and holiday_date between %s and %s""", - (applicable_holiday_list, start, end), as_dict=True): - events.append({ + (applicable_holiday_list, start, end), + as_dict=True, + ): + events.append( + { "doctype": "Holiday", "from_date": holiday.holiday_date, - "to_date": holiday.holiday_date, + "to_date": holiday.holiday_date, "title": _("Holiday") + ": " + cstr(holiday.description), - "name": holiday.name - }) + "name": holiday.name, + } + ) + @frappe.whitelist() def get_mandatory_approval(doctype): mandatory = "" if doctype == "Leave Application": - mandatory = frappe.db.get_single_value('HR Settings', - 'leave_approver_mandatory_in_leave_application') + mandatory = frappe.db.get_single_value( + "HR Settings", "leave_approver_mandatory_in_leave_application" + ) else: - mandatory = frappe.db.get_single_value('HR Settings', - 'expense_approver_mandatory_in_expense_claim') + mandatory = frappe.db.get_single_value( + "HR Settings", "expense_approver_mandatory_in_expense_claim" + ) return mandatory + def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): query = """ select employee, leave_type, from_date, to_date, total_leave_days @@ -861,12 +1214,11 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): if leave_type: query += "and leave_type=%(leave_type)s" - leave_applications = frappe.db.sql(query,{ - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + leave_applications = frappe.db.sql( + query, + {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, + as_dict=1, + ) leave_days = 0 for leave_app in leave_applications: @@ -878,18 +1230,24 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): if leave_app.to_date > getdate(to_date): leave_app.to_date = to_date - leave_days += get_number_of_leave_days(employee, leave_type, - leave_app.from_date, leave_app.to_date) + leave_days += get_number_of_leave_days( + employee, leave_type, leave_app.from_date, leave_app.to_date + ) return leave_days + @frappe.whitelist() def get_leave_approver(employee): - leave_approver, department = frappe.db.get_value("Employee", - employee, ["leave_approver", "department"]) + leave_approver, department = frappe.db.get_value( + "Employee", employee, ["leave_approver", "department"] + ) if not leave_approver and department: - leave_approver = frappe.db.get_value('Department Approver', {'parent': department, - 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') + leave_approver = frappe.db.get_value( + "Department Approver", + {"parent": department, "parentfield": "leave_approvers", "idx": 1}, + "approver", + ) return leave_approver diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html index 9f667a68356..e755322efda 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html @@ -4,11 +4,11 @@ {{ __("Leave Type") }} - {{ __("Total Allocated Leave") }} - {{ __("Expired Leave") }} - {{ __("Used Leave") }} - {{ __("Pending Leave") }} - {{ __("Available Leave") }} + {{ __("Total Allocated Leave(s)") }} + {{ __("Expired Leave(s)") }} + {{ __("Used Leave(s)") }} + {{ __("Leave(s) Pending Approval") }} + {{ __("Available Leave(s)") }} @@ -18,7 +18,7 @@ {%= value["total_leaves"] %} {%= value["expired_leaves"] %} {%= value["leaves_taken"] %} - {%= value["pending_leaves"] %} + {%= value["leaves_pending_approval"] %} {%= value["remaining_leaves"] %} {% } %} diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py index d56133b5660..ee5cbe99f31 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py @@ -1,19 +1,9 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'leave_application', - 'transactions': [ - { - 'items': ['Attendance'] - } - ], - 'reports': [ - { - 'label': _('Reports'), - 'items': ['Employee Leave Balance'] - } - ] - } + "fieldname": "leave_application", + "transactions": [{"items": ["Attendance"]}], + "reports": [{"label": _("Reports"), "items": ["Employee Leave Balance"]}], + } diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 39356bdcf18..8924a57708e 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -17,12 +17,17 @@ from frappe.utils import ( ) from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_application.leave_application import ( + InsufficientLeaveBalanceError, + LeaveAcrossAllocationsError, LeaveDayBlockedError, NotAnOptionalHoliday, OverlapError, + get_leave_allocation_records, get_leave_balance_on, + get_leave_details, ) from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( create_assignment_for_multiple_employees, @@ -33,7 +38,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_leave_application, ) -test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"] +test_dependencies = ["Leave Type", "Leave Allocation", "Leave Block List", "Employee"] _test_records = [ { @@ -44,7 +49,7 @@ _test_records = [ "description": "_Test Reason", "leave_type": "_Test Leave Type", "posting_date": "2013-01-02", - "to_date": "2013-05-05" + "to_date": "2013-05-05", }, { "company": "_Test Company", @@ -54,7 +59,7 @@ _test_records = [ "description": "_Test Reason", "leave_type": "_Test Leave Type", "posting_date": "2013-01-02", - "to_date": "2013-05-05" + "to_date": "2013-05-05", }, { "company": "_Test Company", @@ -64,27 +69,40 @@ _test_records = [ "description": "_Test Reason", "leave_type": "_Test Leave Type LWP", "posting_date": "2013-01-02", - "to_date": "2013-01-15" - } + "to_date": "2013-01-15", + }, ] class TestLeaveApplication(unittest.TestCase): def setUp(self): for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]: - frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec + frappe.db.delete(dt) frappe.set_user("Administrator") set_leave_approver() - frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'") + frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"}) + frappe.db.set_value("Employee", "_T-Employee-00001", "holiday_list", "") + + from_date = get_year_start(getdate()) + to_date = get_year_ending(getdate()) + self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date) + + if not frappe.db.exists("Leave Type", "_Test Leave Type"): + frappe.get_doc( + dict(leave_type_name="_Test Leave Type", doctype="Leave Type", include_holiday=True) + ).insert() def tearDown(self): frappe.db.rollback() + frappe.set_user("Administrator") def _clear_roles(self): - frappe.db.sql("""delete from `tabHas Role` where parent in - ("test@example.com", "test1@example.com", "test2@example.com")""") + frappe.db.sql( + """delete from `tabHas Role` where parent in + ("test@example.com", "test1@example.com", "test2@example.com")""" + ) def _clear_applications(self): frappe.db.sql("""delete from `tabLeave Application`""") @@ -95,77 +113,232 @@ class TestLeaveApplication(unittest.TestCase): application.to_date = "2013-01-05" return application + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_validate_application_across_allocations(self): + # Test validation for application dates when negative balance is disabled + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=False) + ).insert() + + employee = get_employee() + date = getdate() + first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date)) + + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, 1), + to_date=add_days(first_sunday, 4), + company="_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ) + # Application period cannot be outside leave allocation period + self.assertRaises(frappe.ValidationError, leave_application.insert) + + make_allocation_record( + leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date) + ) + + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, -10), + to_date=add_days(first_sunday, 1), + company="_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ) + + # Application period cannot be across two allocation records + self.assertRaises(LeaveAcrossAllocationsError, leave_application.insert) + + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_insufficient_leave_balance_validation(self): + # CASE 1: Validation when allow negative is disabled + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=False) + ).insert() + + employee = get_employee() + date = getdate() + first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date)) + + # allocate 2 leaves, apply for more + make_allocation_record( + leave_type=leave_type.name, + from_date=get_year_start(date), + to_date=get_year_ending(date), + leaves=2, + ) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, 1), + to_date=add_days(first_sunday, 3), + company="_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ) + self.assertRaises(InsufficientLeaveBalanceError, leave_application.insert) + + # CASE 2: Allows creating application with a warning message when allow negative is enabled + frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True) + make_leave_application( + employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name + ) + + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_separate_leave_ledger_entry_for_boundary_applications(self): + # When application falls in 2 different allocations and Allow Negative is enabled + # creates separate leave ledger entries + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc( + dict( + leave_type_name="Test Leave Validation", + doctype="Leave Type", + allow_negative=True, + include_holiday=True, + ) + ).insert() + + employee = get_employee() + date = getdate() + year_start = getdate(get_year_start(date)) + year_end = getdate(get_year_ending(date)) + + make_allocation_record(leave_type=leave_type.name, from_date=year_start, to_date=year_end) + # application across allocations + + # CASE 1: from date has no allocation, to date has an allocation / both dates have allocation + start_date = add_days(year_start, -10) + application = make_leave_application( + employee.name, + start_date, + add_days(year_start, 3), + leave_type.name, + half_day=1, + half_day_date=start_date, + ) + + # 2 separate leave ledger entries + ledgers = frappe.db.get_all( + "Leave Ledger Entry", + {"transaction_type": "Leave Application", "transaction_name": application.name}, + ["leaves", "from_date", "to_date"], + order_by="from_date", + ) + self.assertEqual(len(ledgers), 2) + + self.assertEqual(ledgers[0].from_date, application.from_date) + self.assertEqual(ledgers[0].to_date, add_days(year_start, -1)) + + self.assertEqual(ledgers[1].from_date, year_start) + self.assertEqual(ledgers[1].to_date, application.to_date) + + # CASE 2: from date has an allocation, to date has no allocation + application = make_leave_application( + employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name + ) + + # 2 separate leave ledger entries + ledgers = frappe.db.get_all( + "Leave Ledger Entry", + {"transaction_type": "Leave Application", "transaction_name": application.name}, + ["leaves", "from_date", "to_date"], + order_by="from_date", + ) + self.assertEqual(len(ledgers), 2) + + self.assertEqual(ledgers[0].from_date, application.from_date) + self.assertEqual(ledgers[0].to_date, year_end) + + self.assertEqual(ledgers[1].from_date, add_days(year_end, 1)) + self.assertEqual(ledgers[1].to_date, application.to_date) + def test_overwrite_attendance(self): - '''check attendance is automatically created on leave approval''' + """check attendance is automatically created on leave approval""" make_allocation_record() application = self.get_application(_test_records[0]) - application.status = 'Approved' - application.from_date = '2018-01-01' - application.to_date = '2018-01-03' + application.status = "Approved" + application.from_date = "2018-01-01" + application.to_date = "2018-01-03" application.insert() application.submit() - attendance = frappe.get_all('Attendance', ['name', 'status', 'attendance_date'], - dict(attendance_date=('between', ['2018-01-01', '2018-01-03']), docstatus=("!=", 2))) + attendance = frappe.get_all( + "Attendance", + ["name", "status", "attendance_date"], + dict(attendance_date=("between", ["2018-01-01", "2018-01-03"]), docstatus=("!=", 2)), + ) # attendance created for all 3 days self.assertEqual(len(attendance), 3) # all on leave - self.assertTrue(all([d.status == 'On Leave' for d in attendance])) + self.assertTrue(all([d.status == "On Leave" for d in attendance])) # dates dates = [d.attendance_date for d in attendance] - for d in ('2018-01-01', '2018-01-02', '2018-01-03'): + for d in ("2018-01-01", "2018-01-02", "2018-01-03"): self.assertTrue(getdate(d) in dates) + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_attendance_for_include_holidays(self): # Case 1: leave type with 'Include holidays within leaves as leaves' enabled frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name="Test Include Holidays", - doctype="Leave Type", - include_holiday=True - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Include Holidays", doctype="Leave Type", include_holiday=True) + ).insert() date = getdate() - make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + make_allocation_record( + leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date) + ) - holiday_list = make_holiday_list() employee = get_employee() - frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) - first_sunday = get_first_sunday(holiday_list) + first_sunday = get_first_sunday(self.holiday_list) - leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application( + employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name + ) leave_application.reload() self.assertEqual(leave_application.total_leave_days, 4) - self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) + self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 4) leave_application.cancel() + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_attendance_update_for_exclude_holidays(self): # Case 2: leave type with 'Include holidays within leaves as leaves' disabled frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name="Test Do Not Include Holidays", - doctype="Leave Type", - include_holiday=False - )).insert() + leave_type = frappe.get_doc( + dict( + leave_type_name="Test Do Not Include Holidays", doctype="Leave Type", include_holiday=False + ) + ).insert() date = getdate() - make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + make_allocation_record( + leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date) + ) - holiday_list = make_holiday_list() employee = get_employee() - frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) - first_sunday = get_first_sunday(holiday_list) + first_sunday = get_first_sunday(self.holiday_list) # already marked attendance on a holiday should be deleted in this case - config = { - "doctype": "Attendance", - "employee": employee.name, - "status": "Present" - } + config = {"doctype": "Attendance", "employee": employee.name, "status": "Present"} attendance_on_holiday = frappe.get_doc(config) attendance_on_holiday.attendance_date = first_sunday attendance_on_holiday.flags.ignore_validate = True @@ -177,8 +350,11 @@ class TestLeaveApplication(unittest.TestCase): attendance.flags.ignore_validate = True attendance.save() - leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application( + employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company + ) leave_application.reload() + # holiday should be excluded while marking attendance self.assertEqual(leave_application.total_leave_days, 3) self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3) @@ -193,11 +369,13 @@ class TestLeaveApplication(unittest.TestCase): self._clear_roles() from frappe.utils.user import add_role + add_role("test@example.com", "HR User") clear_user_permissions_for_doctype("Employee") - frappe.db.set_value("Department", "_Test Department - _TC", - "leave_block_list", "_Test Leave Block List") + frappe.db.set_value( + "Department", "_Test Department - _TC", "leave_block_list", "_Test Leave Block List" + ) make_allocation_record() @@ -220,6 +398,7 @@ class TestLeaveApplication(unittest.TestCase): self._clear_applications() from frappe.utils.user import add_role + add_role("test@example.com", "Employee") frappe.set_user("test@example.com") @@ -236,6 +415,7 @@ class TestLeaveApplication(unittest.TestCase): self._clear_applications() from frappe.utils.user import add_role + add_role("test@example.com", "Employee") frappe.set_user("test@example.com") @@ -269,6 +449,7 @@ class TestLeaveApplication(unittest.TestCase): self._clear_applications() from frappe.utils.user import add_role + add_role("test@example.com", "Employee") frappe.set_user("test@example.com") @@ -291,6 +472,7 @@ class TestLeaveApplication(unittest.TestCase): self._clear_applications() from frappe.utils.user import add_role + add_role("test@example.com", "Employee") frappe.set_user("test@example.com") @@ -320,51 +502,49 @@ class TestLeaveApplication(unittest.TestCase): application.half_day_date = "2013-01-05" application.insert() + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_optional_leave(self): leave_period = get_leave_period() today = nowdate() - holiday_list = 'Test Holiday List for Optional Holiday' + holiday_list = "Test Holiday List for Optional Holiday" employee = get_employee() - default_holiday_list = make_holiday_list() - frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list) - first_sunday = get_first_sunday(default_holiday_list) - + first_sunday = get_first_sunday(self.holiday_list) optional_leave_date = add_days(first_sunday, 1) - if not frappe.db.exists('Holiday List', holiday_list): - frappe.get_doc(dict( - doctype = 'Holiday List', - holiday_list_name = holiday_list, - from_date = add_months(today, -6), - to_date = add_months(today, 6), - holidays = [ - dict(holiday_date = optional_leave_date, description = 'Test') - ] - )).insert() + if not frappe.db.exists("Holiday List", holiday_list): + frappe.get_doc( + dict( + doctype="Holiday List", + holiday_list_name=holiday_list, + from_date=add_months(today, -6), + to_date=add_months(today, 6), + holidays=[dict(holiday_date=optional_leave_date, description="Test")], + ) + ).insert() - frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list) - leave_type = 'Test Optional Type' - if not frappe.db.exists('Leave Type', leave_type): - frappe.get_doc(dict( - leave_type_name = leave_type, - doctype = 'Leave Type', - is_optional_leave = 1 - )).insert() + frappe.db.set_value("Leave Period", leave_period.name, "optional_holiday_list", holiday_list) + leave_type = "Test Optional Type" + if not frappe.db.exists("Leave Type", leave_type): + frappe.get_doc( + dict(leave_type_name=leave_type, doctype="Leave Type", is_optional_leave=1) + ).insert() allocate_leaves(employee, leave_period, leave_type, 10) date = add_days(first_sunday, 2) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - company = '_Test Company', - description = "_Test Reason", - leave_type = leave_type, - from_date = date, - to_date = date, - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + company="_Test Company", + description="_Test Reason", + leave_type=leave_type, + from_date=date, + to_date=date, + ) + ) # can only apply on optional holidays self.assertRaises(NotAnOptionalHoliday, leave_application.insert) @@ -382,118 +562,125 @@ class TestLeaveApplication(unittest.TestCase): employee = get_employee() leave_period = get_leave_period() frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name = 'Test Leave Type', - doctype = 'Leave Type', - max_leaves_allowed = 5 - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Type", doctype="Leave Type", max_leaves_allowed=5) + ).insert() date = add_days(nowdate(), -7) allocate_leaves(employee, leave_period, leave_type.name, 5) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - description = "_Test Reason", - from_date = date, - to_date = add_days(date, 2), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + description="_Test Reason", + from_date=date, + to_date=add_days(date, 2), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) leave_application.submit() - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - description = "_Test Reason", - from_date = add_days(date, 4), - to_date = add_days(date, 8), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + description="_Test Reason", + from_date=add_days(date, 4), + to_date=add_days(date, 8), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) self.assertRaises(frappe.ValidationError, leave_application.insert) def test_applicable_after(self): employee = get_employee() leave_period = get_leave_period() frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name = 'Test Leave Type', - doctype = 'Leave Type', - applicable_after = 15 - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Type", doctype="Leave Type", applicable_after=15) + ).insert() date = add_days(nowdate(), -7) - frappe.db.set_value('Employee', employee.name, "date_of_joining", date) + frappe.db.set_value("Employee", employee.name, "date_of_joining", date) allocate_leaves(employee, leave_period, leave_type.name, 10) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - description = "_Test Reason", - from_date = date, - to_date = add_days(date, 4), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + description="_Test Reason", + from_date=date, + to_date=add_days(date, 4), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) self.assertRaises(frappe.ValidationError, leave_application.insert) frappe.delete_doc_if_exists("Leave Type", "Test Leave Type 1", force=1) - leave_type_1 = frappe.get_doc(dict( - leave_type_name = 'Test Leave Type 1', - doctype = 'Leave Type' - )).insert() + leave_type_1 = frappe.get_doc( + dict(leave_type_name="Test Leave Type 1", doctype="Leave Type") + ).insert() allocate_leaves(employee, leave_period, leave_type_1.name, 10) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type_1.name, - description = "_Test Reason", - from_date = date, - to_date = add_days(date, 4), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type_1.name, + description="_Test Reason", + from_date=date, + to_date=add_days(date, 4), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) self.assertTrue(leave_application.insert()) - frappe.db.set_value('Employee', employee.name, "date_of_joining", "2010-01-01") + frappe.db.set_value("Employee", employee.name, "date_of_joining", "2010-01-01") def test_max_continuous_leaves(self): employee = get_employee() leave_period = get_leave_period() frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name = 'Test Leave Type', - doctype = 'Leave Type', - max_leaves_allowed = 15, - max_continuous_days_allowed = 3 - )).insert() + leave_type = frappe.get_doc( + dict( + leave_type_name="Test Leave Type", + doctype="Leave Type", + max_leaves_allowed=15, + max_continuous_days_allowed=3, + ) + ).insert() date = add_days(nowdate(), -7) allocate_leaves(employee, leave_period, leave_type.name, 10) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - description = "_Test Reason", - from_date = date, - to_date = add_days(date, 4), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + description="_Test Reason", + from_date=date, + to_date=add_days(date, 4), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) self.assertRaises(frappe.ValidationError, leave_application.insert) @@ -502,57 +689,69 @@ class TestLeaveApplication(unittest.TestCase): leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) - leave_type.submit() + expire_carry_forwarded_leaves_after_days=90, + ) + leave_type.insert() create_carry_forwarded_allocation(employee, leave_type) + details = get_leave_balance_on( + employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True + ) - self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21) + self.assertEqual(details.leave_balance_for_consumption, 21) + self.assertEqual(details.leave_balance, 30) def test_earned_leaves_creation(self): - frappe.db.sql('''delete from `tabLeave Period`''') - frappe.db.sql('''delete from `tabLeave Policy Assignment`''') - frappe.db.sql('''delete from `tabLeave Allocation`''') - frappe.db.sql('''delete from `tabLeave Ledger Entry`''') + frappe.db.sql("""delete from `tabLeave Period`""") + frappe.db.sql("""delete from `tabLeave Policy Assignment`""") + frappe.db.sql("""delete from `tabLeave Allocation`""") + frappe.db.sql("""delete from `tabLeave Ledger Entry`""") leave_period = get_leave_period() employee = get_employee() - leave_type = 'Test Earned Leave Type' - frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1) - frappe.get_doc(dict( - leave_type_name = leave_type, - doctype = 'Leave Type', - is_earned_leave = 1, - earned_leave_frequency = 'Monthly', - rounding = 0.5, - max_leaves_allowed = 6 - )).insert() + leave_type = "Test Earned Leave Type" + frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1) + frappe.get_doc( + dict( + leave_type_name=leave_type, + doctype="Leave Type", + is_earned_leave=1, + earned_leave_frequency="Monthly", + rounding=0.5, + max_leaves_allowed=6, + ) + ).insert() - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}] - }).insert() + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}], + } + ).insert() data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [employee.name], frappe._dict(data) + ) from erpnext.hr.utils import allocate_earned_leaves + i = 0 - while(i<14): + while i < 14: allocate_earned_leaves(ignore_duplicates=True) i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) # validate earned leaves creation without maximum leaves - frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0) + frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0) i = 0 - while(i<6): + while i < 6: allocate_earned_leaves(ignore_duplicates=True) i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) @@ -560,28 +759,36 @@ class TestLeaveApplication(unittest.TestCase): # test to not consider current leave in leave balance while submitting def test_current_leave_on_submit(self): employee = get_employee() - leave_type = 'Sick leave' - allocation = frappe.get_doc(dict( - doctype = 'Leave Allocation', - employee = employee.name, - leave_type = leave_type, - from_date = '2018-10-01', - to_date = '2018-10-10', - new_leaves_allocated = 1 - )) + + leave_type = "Sick Leave" + if not frappe.db.exists("Leave Type", leave_type): + frappe.get_doc(dict(leave_type_name=leave_type, doctype="Leave Type")).insert() + + allocation = frappe.get_doc( + dict( + doctype="Leave Allocation", + employee=employee.name, + leave_type=leave_type, + from_date="2018-10-01", + to_date="2018-10-10", + new_leaves_allocated=1, + ) + ) allocation.insert(ignore_permissions=True) allocation.submit() - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type, - description = "_Test Reason", - from_date = '2018-10-02', - to_date = '2018-10-02', - company = '_Test Company', - status = 'Approved', - leave_approver = 'test@example.com' - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type, + description="_Test Reason", + from_date="2018-10-02", + to_date="2018-10-02", + company="_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ) self.assertTrue(leave_application.insert()) leave_application.submit() self.assertEqual(leave_application.docstatus, 1) @@ -589,26 +796,31 @@ class TestLeaveApplication(unittest.TestCase): def test_creation_of_leave_ledger_entry_on_submit(self): employee = get_employee() - leave_type = create_leave_type(leave_type_name = 'Test Leave Type 1') + leave_type = create_leave_type(leave_type_name="Test Leave Type 1") leave_type.save() - leave_allocation = create_leave_allocation(employee=employee.name, employee_name=employee.employee_name, - leave_type=leave_type.name) + leave_allocation = create_leave_allocation( + employee=employee.name, employee_name=employee.employee_name, leave_type=leave_type.name + ) leave_allocation.submit() - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - from_date = add_days(nowdate(), 1), - to_date = add_days(nowdate(), 4), - description = "_Test Reason", - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(nowdate(), 1), + to_date=add_days(nowdate(), 4), + description="_Test Reason", + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) leave_application.submit() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name)) + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_application.name) + ) self.assertEqual(leave_ledger_entry[0].employee, leave_application.employee) self.assertEqual(leave_ledger_entry[0].leave_type, leave_application.leave_type) @@ -616,37 +828,47 @@ class TestLeaveApplication(unittest.TestCase): # check if leave ledger entry is deleted on cancellation leave_application.cancel() - self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_application.name})) + self.assertFalse( + frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_application.name}) + ) def test_ledger_entry_creation_on_intermediate_allocation_expiry(self): employee = get_employee() leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) + expire_carry_forwarded_leaves_after_days=90, + include_holiday=True, + ) leave_type.submit() create_carry_forwarded_allocation(employee, leave_type) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - from_date = add_days(nowdate(), -3), - to_date = add_days(nowdate(), 7), - description = "_Test Reason", - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(nowdate(), -3), + to_date=add_days(nowdate(), 7), + half_day=1, + half_day_date=add_days(nowdate(), -3), + description="_Test Reason", + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) leave_application.submit() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', '*', filters=dict(transaction_name=leave_application.name)) + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", "*", filters=dict(transaction_name=leave_application.name) + ) self.assertEqual(len(leave_ledger_entry), 2) self.assertEqual(leave_ledger_entry[0].employee, leave_application.employee) self.assertEqual(leave_ledger_entry[0].leave_type, leave_application.leave_type) - self.assertEqual(leave_ledger_entry[0].leaves, -9) + self.assertEqual(leave_ledger_entry[0].leaves, -8.5) self.assertEqual(leave_ledger_entry[1].leaves, -2) def test_leave_application_creation_after_expiry(self): @@ -655,12 +877,18 @@ class TestLeaveApplication(unittest.TestCase): leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) + expire_carry_forwarded_leaves_after_days=90, + ) leave_type.submit() create_carry_forwarded_allocation(employee, leave_type) - self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0) + self.assertEqual( + get_leave_balance_on( + employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84) + ), + 0, + ) def test_leave_approver_perms(self): employee = get_employee() @@ -676,8 +904,8 @@ class TestLeaveApplication(unittest.TestCase): make_allocation_record(employee.name) application = self.get_application(_test_records[0]) - application.from_date = '2018-01-01' - application.to_date = '2018-01-03' + application.from_date = "2018-01-01" + application.to_date = "2018-01-03" application.leave_approver = user application.insert() self.assertTrue(application.name in frappe.share.get_shared("Leave Application", user)) @@ -703,92 +931,171 @@ class TestLeaveApplication(unittest.TestCase): employee.leave_approver = "" employee.save() + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_get_leave_details_for_dashboard(self): + employee = get_employee() + date = getdate() + year_start = getdate(get_year_start(date)) + year_end = getdate(get_year_ending(date)) + + # ALLOCATION = 30 + allocation = make_allocation_record( + employee=employee.name, from_date=year_start, to_date=year_end + ) + + # USED LEAVES = 4 + first_sunday = get_first_sunday(self.holiday_list) + leave_application = make_leave_application( + employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) + leave_application.reload() + + # LEAVES PENDING APPROVAL = 1 + leave_application = make_leave_application( + employee.name, + add_days(first_sunday, 5), + add_days(first_sunday, 5), + "_Test Leave Type", + submit=False, + ) + leave_application.status = "Open" + leave_application.save() + + details = get_leave_details(employee.name, allocation.from_date) + leave_allocation = details["leave_allocation"]["_Test Leave Type"] + self.assertEqual(leave_allocation["total_leaves"], 30) + self.assertEqual(leave_allocation["leaves_taken"], 4) + self.assertEqual(leave_allocation["expired_leaves"], 0) + self.assertEqual(leave_allocation["leaves_pending_approval"], 1) + self.assertEqual(leave_allocation["remaining_leaves"], 26) + + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_get_leave_allocation_records(self): + employee = get_employee() + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave_expiry", + is_carry_forward=1, + expire_carry_forwarded_leaves_after_days=90, + ) + leave_type.insert() + + leave_alloc = create_carry_forwarded_allocation(employee, leave_type) + details = get_leave_allocation_records(employee.name, getdate(), leave_type.name) + expected_data = { + "from_date": getdate(leave_alloc.from_date), + "to_date": getdate(leave_alloc.to_date), + "total_leaves_allocated": 30.0, + "unused_leaves": 15.0, + "new_leaves_allocated": 15.0, + "leave_type": leave_type.name, + } + self.assertEqual(details.get(leave_type.name), expected_data) + def create_carry_forwarded_allocation(employee, leave_type): - # initial leave allocation - leave_allocation = create_leave_allocation( - leave_type="_Test_CF_leave_expiry", - employee=employee.name, - employee_name=employee.employee_name, - from_date=add_months(nowdate(), -24), - to_date=add_months(nowdate(), -12), - carry_forward=0) - leave_allocation.submit() + # initial leave allocation + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + employee=employee.name, + employee_name=employee.employee_name, + from_date=add_months(nowdate(), -24), + to_date=add_months(nowdate(), -12), + carry_forward=0, + ) + leave_allocation.submit() - leave_allocation = create_leave_allocation( - leave_type="_Test_CF_leave_expiry", - employee=employee.name, - employee_name=employee.employee_name, - from_date=add_days(nowdate(), -84), - to_date=add_days(nowdate(), 100), - carry_forward=1) - leave_allocation.submit() + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + employee=employee.name, + employee_name=employee.employee_name, + from_date=add_days(nowdate(), -84), + to_date=add_days(nowdate(), 100), + carry_forward=1, + ) + leave_allocation.submit() -def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None): - allocation = frappe.get_doc({ - "doctype": "Leave Allocation", - "employee": employee or "_T-Employee-00001", - "leave_type": leave_type or "_Test Leave Type", - "from_date": from_date or "2013-01-01", - "to_date": to_date or "2019-12-31", - "new_leaves_allocated": 30 - }) + return leave_allocation + + +def make_allocation_record( + employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None +): + allocation = frappe.get_doc( + { + "doctype": "Leave Allocation", + "employee": employee or "_T-Employee-00001", + "leave_type": leave_type or "_Test Leave Type", + "from_date": from_date or "2013-01-01", + "to_date": to_date or "2019-12-31", + "new_leaves_allocated": leaves or 30, + "carry_forward": carry_forward, + } + ) allocation.insert(ignore_permissions=True) allocation.submit() + return allocation + + def get_employee(): return frappe.get_doc("Employee", "_T-Employee-00001") + def set_leave_approver(): employee = get_employee() dept_doc = frappe.get_doc("Department", employee.department) - dept_doc.append('leave_approvers', { - 'approver': 'test@example.com' - }) + dept_doc.append("leave_approvers", {"approver": "test@example.com"}) dept_doc.save(ignore_permissions=True) + def get_leave_period(): - leave_period_name = frappe.db.exists({ - "doctype": "Leave Period", - "company": "_Test Company" - }) + leave_period_name = frappe.db.exists({"doctype": "Leave Period", "company": "_Test Company"}) if leave_period_name: return frappe.get_doc("Leave Period", leave_period_name[0][0]) else: - return frappe.get_doc(dict( - name = 'Test Leave Period', - doctype = 'Leave Period', - from_date = add_months(nowdate(), -6), - to_date = add_months(nowdate(), 6), - company = "_Test Company", - is_active = 1 - )).insert() + return frappe.get_doc( + dict( + name="Test Leave Period", + doctype="Leave Period", + from_date=add_months(nowdate(), -6), + to_date=add_months(nowdate(), 6), + company="_Test Company", + is_active=1, + ) + ).insert() + def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, eligible_leaves=0): - allocate_leave = frappe.get_doc({ - "doctype": "Leave Allocation", - "__islocal": 1, - "employee": employee.name, - "employee_name": employee.employee_name, - "leave_type": leave_type, - "from_date": leave_period.from_date, - "to_date": leave_period.to_date, - "new_leaves_allocated": new_leaves_allocated, - "docstatus": 1 - }).insert() + allocate_leave = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": employee.name, + "employee_name": employee.employee_name, + "leave_type": leave_type, + "from_date": leave_period.from_date, + "to_date": leave_period.to_date, + "new_leaves_allocated": new_leaves_allocated, + "docstatus": 1, + } + ).insert() allocate_leave.submit() -def get_first_sunday(holiday_list): - month_start_date = get_first_day(nowdate()) - month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" +def get_first_sunday(holiday_list, for_date=None): + date = for_date or getdate() + month_start_date = get_first_day(date) + month_end_date = get_last_day(date) + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = %s and holiday_date between %s and %s order by holiday_date - """, (holiday_list, month_start_date, month_end_date))[0][0] + """, + (holiday_list, month_start_date, month_end_date), + )[0][0] - return first_sunday \ No newline at end of file + return first_sunday diff --git a/erpnext/hr/doctype/leave_block_list/leave_block_list.py b/erpnext/hr/doctype/leave_block_list/leave_block_list.py index d6b77f984cf..a57ba84e38d 100644 --- a/erpnext/hr/doctype/leave_block_list/leave_block_list.py +++ b/erpnext/hr/doctype/leave_block_list/leave_block_list.py @@ -10,7 +10,6 @@ from frappe.model.document import Document class LeaveBlockList(Document): - def validate(self): dates = [] for d in self.get("leave_block_list_dates"): @@ -20,23 +19,29 @@ class LeaveBlockList(Document): frappe.msgprint(_("Date is repeated") + ":" + d.block_date, raise_exception=1) dates.append(d.block_date) + @frappe.whitelist() -def get_applicable_block_dates(from_date, to_date, employee=None, - company=None, all_lists=False): +def get_applicable_block_dates(from_date, to_date, employee=None, company=None, all_lists=False): block_dates = [] for block_list in get_applicable_block_lists(employee, company, all_lists): - block_dates.extend(frappe.db.sql("""select block_date, reason + block_dates.extend( + frappe.db.sql( + """select block_date, reason from `tabLeave Block List Date` where parent=%s - and block_date between %s and %s""", (block_list, from_date, to_date), - as_dict=1)) + and block_date between %s and %s""", + (block_list, from_date, to_date), + as_dict=1, + ) + ) return block_dates + def get_applicable_block_lists(employee=None, company=None, all_lists=False): block_lists = [] if not employee: - employee = frappe.db.get_value("Employee", {"user_id":frappe.session.user}) + employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}) if not employee: return [] @@ -49,18 +54,25 @@ def get_applicable_block_lists(employee=None, company=None, all_lists=False): block_lists.append(block_list) # per department - department = frappe.db.get_value("Employee",employee, "department") + department = frappe.db.get_value("Employee", employee, "department") if department: block_list = frappe.db.get_value("Department", department, "leave_block_list") add_block_list(block_list) # global - for block_list in frappe.db.sql_list("""select name from `tabLeave Block List` - where applies_to_all_departments=1 and company=%s""", company): + for block_list in frappe.db.sql_list( + """select name from `tabLeave Block List` + where applies_to_all_departments=1 and company=%s""", + company, + ): add_block_list(block_list) return list(set(block_lists)) + def is_user_in_allow_list(block_list): - return frappe.session.user in frappe.db.sql_list("""select allow_user - from `tabLeave Block List Allow` where parent=%s""", block_list) + return frappe.session.user in frappe.db.sql_list( + """select allow_user + from `tabLeave Block List Allow` where parent=%s""", + block_list, + ) diff --git a/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py b/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py index f91a8fe5201..afeb5ded39e 100644 --- a/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py +++ b/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'leave_block_list', - 'transactions': [ - { - 'items': ['Department'] - } - ] - } + return {"fieldname": "leave_block_list", "transactions": [{"items": ["Department"]}]} diff --git a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py index afbabb66a4a..be85a354149 100644 --- a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py +++ b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py @@ -15,24 +15,36 @@ class TestLeaveBlockList(unittest.TestCase): def test_get_applicable_block_dates(self): frappe.set_user("test@example.com") - frappe.db.set_value("Department", "_Test Department - _TC", "leave_block_list", - "_Test Leave Block List") - self.assertTrue(getdate("2013-01-02") in - [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")]) + frappe.db.set_value( + "Department", "_Test Department - _TC", "leave_block_list", "_Test Leave Block List" + ) + self.assertTrue( + getdate("2013-01-02") + in [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")] + ) def test_get_applicable_block_dates_for_allowed_user(self): frappe.set_user("test1@example.com") - frappe.db.set_value("Department", "_Test Department 1 - _TC", "leave_block_list", - "_Test Leave Block List") - self.assertEqual([], [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")]) + frappe.db.set_value( + "Department", "_Test Department 1 - _TC", "leave_block_list", "_Test Leave Block List" + ) + self.assertEqual( + [], [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")] + ) def test_get_applicable_block_dates_all_lists(self): frappe.set_user("test1@example.com") - frappe.db.set_value("Department", "_Test Department 1 - _TC", "leave_block_list", - "_Test Leave Block List") - self.assertTrue(getdate("2013-01-02") in - [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03", all_lists=True)]) + frappe.db.set_value( + "Department", "_Test Department 1 - _TC", "leave_block_list", "_Test Leave Block List" + ) + self.assertTrue( + getdate("2013-01-02") + in [ + d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03", all_lists=True) + ] + ) + test_dependencies = ["Employee"] -test_records = frappe.get_test_records('Leave Block List') +test_records = frappe.get_test_records("Leave Block List") diff --git a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py index 19f97b83d47..c57f8ae72bf 100644 --- a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py +++ b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py @@ -18,8 +18,12 @@ class LeaveControlPanel(Document): condition_str = " and " + " and ".join(conditions) if len(conditions) else "" - e = frappe.db.sql("select name from tabEmployee where status='Active' {condition}" - .format(condition=condition_str), tuple(values)) + e = frappe.db.sql( + "select name from tabEmployee where status='Active' {condition}".format( + condition=condition_str + ), + tuple(values), + ) return e @@ -27,7 +31,7 @@ class LeaveControlPanel(Document): for f in ["from_date", "to_date", "leave_type", "no_of_days"]: if not self.get(f): frappe.throw(_("{0} is required").format(self.meta.get_label(f))) - self.validate_from_to_dates('from_date', 'to_date') + self.validate_from_to_dates("from_date", "to_date") @frappe.whitelist() def allocate_leave(self): @@ -39,10 +43,10 @@ class LeaveControlPanel(Document): for d in self.get_employees(): try: - la = frappe.new_doc('Leave Allocation') + la = frappe.new_doc("Leave Allocation") la.set("__islocal", 1) la.employee = cstr(d[0]) - la.employee_name = frappe.db.get_value('Employee',cstr(d[0]),'employee_name') + la.employee_name = frappe.db.get_value("Employee", cstr(d[0]), "employee_name") la.leave_type = self.leave_type la.from_date = self.from_date la.to_date = self.to_date diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 8ef0e36fb8d..0f655e3e0fc 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -26,9 +26,12 @@ class LeaveEncashment(Document): self.encashment_date = getdate(nowdate()) def validate_salary_structure(self): - if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) - + if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}): + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + self.employee + ) + ) def before_submit(self): if self.encashment_amount <= 0: @@ -36,7 +39,7 @@ class LeaveEncashment(Document): def on_submit(self): if not self.leave_allocation: - self.leave_allocation = self.get_leave_allocation().get('name') + self.leave_allocation = self.get_leave_allocation().get("name") additional_salary = frappe.new_doc("Additional Salary") additional_salary.company = frappe.get_value("Employee", self.employee, "company") additional_salary.employee = self.employee @@ -52,8 +55,13 @@ class LeaveEncashment(Document): additional_salary.submit() # Set encashed leaves in Allocation - frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", - frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days) + frappe.db.set_value( + "Leave Allocation", + self.leave_allocation, + "total_leaves_encashed", + frappe.db.get_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed") + + self.encashable_days, + ) self.create_leave_ledger_entry() @@ -63,40 +71,69 @@ class LeaveEncashment(Document): self.db_set("additional_salary", "") if self.leave_allocation: - frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", - frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days) + frappe.db.set_value( + "Leave Allocation", + self.leave_allocation, + "total_leaves_encashed", + frappe.db.get_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed") + - self.encashable_days, + ) self.create_leave_ledger_entry(submit=False) @frappe.whitelist() def get_leave_details_for_encashment(self): - salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate())) + salary_structure = get_assigned_salary_structure( + self.employee, self.encashment_date or getdate(nowdate()) + ) if not salary_structure: - frappe.throw(_("No Salary Structure assigned for Employee {0} on given date {1}").format(self.employee, self.encashment_date)) + frappe.throw( + _("No Salary Structure assigned for Employee {0} on given date {1}").format( + self.employee, self.encashment_date + ) + ) - if not frappe.db.get_value("Leave Type", self.leave_type, 'allow_encashment'): + if not frappe.db.get_value("Leave Type", self.leave_type, "allow_encashment"): frappe.throw(_("Leave Type {0} is not encashable").format(self.leave_type)) allocation = self.get_leave_allocation() if not allocation: - frappe.throw(_("No Leaves Allocated to Employee: {0} for Leave Type: {1}").format(self.employee, self.leave_type)) + frappe.throw( + _("No Leaves Allocated to Employee: {0} for Leave Type: {1}").format( + self.employee, self.leave_type + ) + ) - self.leave_balance = allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count\ + self.leave_balance = ( + allocation.total_leaves_allocated + - allocation.carry_forwarded_leaves_count - get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date) + ) - encashable_days = self.leave_balance - frappe.db.get_value('Leave Type', self.leave_type, 'encashment_threshold_days') + encashable_days = self.leave_balance - frappe.db.get_value( + "Leave Type", self.leave_type, "encashment_threshold_days" + ) self.encashable_days = encashable_days if encashable_days > 0 else 0 - per_day_encashment = frappe.db.get_value('Salary Structure', salary_structure , 'leave_encashment_amount_per_day') - self.encashment_amount = self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0 + per_day_encashment = frappe.db.get_value( + "Salary Structure", salary_structure, "leave_encashment_amount_per_day" + ) + self.encashment_amount = ( + self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0 + ) self.leave_allocation = allocation.name return True def get_leave_allocation(self): - leave_allocation = frappe.db.sql("""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}' + leave_allocation = frappe.db.sql( + """select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}' between from_date and to_date and docstatus=1 and leave_type='{1}' - and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee), as_dict=1) #nosec + and employee= '{2}'""".format( + self.encashment_date or getdate(nowdate()), self.leave_type, self.employee + ), + as_dict=1, + ) # nosec return leave_allocation[0] if leave_allocation else None @@ -105,7 +142,7 @@ class LeaveEncashment(Document): leaves=self.encashable_days * -1, from_date=self.encashment_date, to_date=self.encashment_date, - is_carry_forward=0 + is_carry_forward=0, ) create_leave_ledger_entry(self, args, submit) @@ -114,27 +151,26 @@ class LeaveEncashment(Document): if not leave_allocation: return - to_date = leave_allocation.get('to_date') + to_date = leave_allocation.get("to_date") if to_date < getdate(nowdate()): args = frappe._dict( - leaves=self.encashable_days, - from_date=to_date, - to_date=to_date, - is_carry_forward=0 + leaves=self.encashable_days, from_date=to_date, to_date=to_date, is_carry_forward=0 ) create_leave_ledger_entry(self, args, submit) def create_leave_encashment(leave_allocation): - ''' Creates leave encashment for the given allocations ''' + """Creates leave encashment for the given allocations""" for allocation in leave_allocation: if not get_assigned_salary_structure(allocation.employee, allocation.to_date): continue - leave_encashment = frappe.get_doc(dict( - doctype="Leave Encashment", - leave_period=allocation.leave_period, - employee=allocation.employee, - leave_type=allocation.leave_type, - encashment_date=allocation.to_date - )) + leave_encashment = frappe.get_doc( + dict( + doctype="Leave Encashment", + leave_period=allocation.leave_period, + employee=allocation.employee, + leave_type=allocation.leave_type, + encashment_date=allocation.to_date, + ) + ) leave_encashment.insert(ignore_permissions=True) diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 99a479d3e5c..83eb969feb0 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -16,18 +16,19 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_ test_dependencies = ["Leave Type"] + class TestLeaveEncashment(unittest.TestCase): def setUp(self): - frappe.db.sql('''delete from `tabLeave Period`''') - frappe.db.sql('''delete from `tabLeave Policy Assignment`''') - frappe.db.sql('''delete from `tabLeave Allocation`''') - frappe.db.sql('''delete from `tabLeave Ledger Entry`''') - frappe.db.sql('''delete from `tabAdditional Salary`''') + frappe.db.sql("""delete from `tabLeave Period`""") + frappe.db.sql("""delete from `tabLeave Policy Assignment`""") + frappe.db.sql("""delete from `tabLeave Allocation`""") + frappe.db.sql("""delete from `tabLeave Ledger Entry`""") + frappe.db.sql("""delete from `tabAdditional Salary`""") # create the leave policy leave_policy = create_leave_policy( - leave_type="_Test Leave Type Encashment", - annual_allocation=10) + leave_type="_Test Leave Type Encashment", annual_allocation=10 + ) leave_policy.submit() # create employee, salary structure and assignment @@ -38,28 +39,44 @@ class TestLeaveEncashment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": self.leave_period.name + "leave_period": self.leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee], frappe._dict(data) + ) - salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee, - other_details={"leave_encashment_amount_per_day": 50}) + salary_structure = make_salary_structure( + "Salary Structure for Encashment", + "Monthly", + self.employee, + other_details={"leave_encashment_amount_per_day": 50}, + ) def tearDown(self): - for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]: + for dt in [ + "Leave Period", + "Leave Allocation", + "Leave Ledger Entry", + "Additional Salary", + "Leave Encashment", + "Salary Structure", + "Leave Policy", + ]: frappe.db.sql("delete from `tab%s`" % dt) def test_leave_balance_value_and_amount(self): - frappe.db.sql('''delete from `tabLeave Encashment`''') - leave_encashment = frappe.get_doc(dict( - doctype='Leave Encashment', - employee=self.employee, - leave_type="_Test Leave Type Encashment", - leave_period=self.leave_period.name, - payroll_date=today(), - currency="INR" - )).insert() + frappe.db.sql("""delete from `tabLeave Encashment`""") + leave_encashment = frappe.get_doc( + dict( + doctype="Leave Encashment", + employee=self.employee, + leave_type="_Test Leave Type Encashment", + leave_period=self.leave_period.name, + payroll_date=today(), + currency="INR", + ) + ).insert() self.assertEqual(leave_encashment.leave_balance, 10) self.assertEqual(leave_encashment.encashable_days, 5) @@ -68,23 +85,27 @@ class TestLeaveEncashment(unittest.TestCase): leave_encashment.submit() # assert links - add_sal = frappe.get_all("Additional Salary", filters = {"ref_docname": leave_encashment.name})[0] + add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0] self.assertTrue(add_sal) def test_creation_of_leave_ledger_entry_on_submit(self): - frappe.db.sql('''delete from `tabLeave Encashment`''') - leave_encashment = frappe.get_doc(dict( - doctype='Leave Encashment', - employee=self.employee, - leave_type="_Test Leave Type Encashment", - leave_period=self.leave_period.name, - payroll_date=today(), - currency="INR" - )).insert() + frappe.db.sql("""delete from `tabLeave Encashment`""") + leave_encashment = frappe.get_doc( + dict( + doctype="Leave Encashment", + employee=self.employee, + leave_type="_Test Leave Type Encashment", + leave_period=self.leave_period.name, + payroll_date=today(), + currency="INR", + ) + ).insert() leave_encashment.submit() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_encashment.name)) + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_encashment.name) + ) self.assertEqual(len(leave_ledger_entry), 1) self.assertEqual(leave_ledger_entry[0].employee, leave_encashment.employee) @@ -93,7 +114,11 @@ class TestLeaveEncashment(unittest.TestCase): # check if leave ledger entry is deleted on cancellation - frappe.db.sql("Delete from `tabAdditional Salary` WHERE ref_docname = %s", (leave_encashment.name) ) + frappe.db.sql( + "Delete from `tabAdditional Salary` WHERE ref_docname = %s", (leave_encashment.name) + ) leave_encashment.cancel() - self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_encashment.name})) + self.assertFalse( + frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_encashment.name}) + ) diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index 5c5299ea7eb..fed9f770dfc 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -20,9 +20,11 @@ class LeaveLedgerEntry(Document): else: frappe.throw(_("Only expired allocation can be cancelled")) + def validate_leave_allocation_against_leave_application(ledger): - ''' Checks that leave allocation has no leave application against it ''' - leave_application_records = frappe.db.sql_list(""" + """Checks that leave allocation has no leave application against it""" + leave_application_records = frappe.db.sql_list( + """ SELECT transaction_name FROM `tabLeave Ledger Entry` WHERE @@ -31,15 +33,21 @@ def validate_leave_allocation_against_leave_application(ledger): AND transaction_type='Leave Application' AND from_date>=%s AND to_date<=%s - """, (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date)) + """, + (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date), + ) if leave_application_records: - frappe.throw(_("Leave allocation {0} is linked with the Leave Application {1}").format( - ledger.transaction_name, ', '.join(leave_application_records))) + frappe.throw( + _("Leave allocation {0} is linked with the Leave Application {1}").format( + ledger.transaction_name, ", ".join(leave_application_records) + ) + ) + def create_leave_ledger_entry(ref_doc, args, submit=True): ledger = frappe._dict( - doctype='Leave Ledger Entry', + doctype="Leave Ledger Entry", employee=ref_doc.employee, employee_name=ref_doc.employee_name, leave_type=ref_doc.leave_type, @@ -47,7 +55,7 @@ def create_leave_ledger_entry(ref_doc, args, submit=True): transaction_name=ref_doc.name, is_carry_forward=0, is_expired=0, - is_lwp=0 + is_lwp=0, ) ledger.update(args) @@ -58,54 +66,69 @@ def create_leave_ledger_entry(ref_doc, args, submit=True): else: delete_ledger_entry(ledger) + def delete_ledger_entry(ledger): - ''' Delete ledger entry on cancel of leave application/allocation/encashment ''' + """Delete ledger entry on cancel of leave application/allocation/encashment""" if ledger.transaction_type == "Leave Allocation": validate_leave_allocation_against_leave_application(ledger) expired_entry = get_previous_expiry_ledger_entry(ledger) - frappe.db.sql("""DELETE + frappe.db.sql( + """DELETE FROM `tabLeave Ledger Entry` WHERE `transaction_name`=%s - OR `name`=%s""", (ledger.transaction_name, expired_entry)) + OR `name`=%s""", + (ledger.transaction_name, expired_entry), + ) + def get_previous_expiry_ledger_entry(ledger): - ''' Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled ''' - creation_date = frappe.db.get_value("Leave Ledger Entry", filters={ - 'transaction_name': ledger.transaction_name, - 'is_expired': 0, - 'transaction_type': 'Leave Allocation' - }, fieldname=['creation']) + """Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled""" + creation_date = frappe.db.get_value( + "Leave Ledger Entry", + filters={ + "transaction_name": ledger.transaction_name, + "is_expired": 0, + "transaction_type": "Leave Allocation", + }, + fieldname=["creation"], + ) - creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else '' + creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else "" + + return frappe.db.get_value( + "Leave Ledger Entry", + filters={ + "creation": ("like", creation_date + "%"), + "employee": ledger.employee, + "leave_type": ledger.leave_type, + "is_expired": 1, + "docstatus": 1, + "is_carry_forward": 0, + }, + fieldname=["name"], + ) - return frappe.db.get_value("Leave Ledger Entry", filters={ - 'creation': ('like', creation_date+"%"), - 'employee': ledger.employee, - 'leave_type': ledger.leave_type, - 'is_expired': 1, - 'docstatus': 1, - 'is_carry_forward': 0 - }, fieldname=['name']) def process_expired_allocation(): - ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry - Case 1: carry forwarded expiry period is set for the leave type, - create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves - Case 2: leave type has no specific expiry period for carry forwarded leaves - and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves. - ''' + """Check if a carry forwarded allocation has expired and create a expiry ledger entry + Case 1: carry forwarded expiry period is set for the leave type, + create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves + Case 2: leave type has no specific expiry period for carry forwarded leaves + and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves. + """ # fetch leave type records that has carry forwarded leaves expiry - leave_type_records = frappe.db.get_values("Leave Type", filters={ - 'expire_carry_forwarded_leaves_after_days': (">", 0) - }, fieldname=['name']) + leave_type_records = frappe.db.get_values( + "Leave Type", filters={"expire_carry_forwarded_leaves_after_days": (">", 0)}, fieldname=["name"] + ) - leave_type = [record[0] for record in leave_type_records] or [''] + leave_type = [record[0] for record in leave_type_records] or [""] # fetch non expired leave ledger entry of transaction_type allocation - expire_allocation = frappe.db.sql(""" + expire_allocation = frappe.db.sql( + """ SELECT leaves, to_date, employee, leave_type, is_carry_forward, transaction_name as name, transaction_type @@ -123,32 +146,41 @@ def process_expired_allocation(): OR (is_carry_forward = 0 AND leave_type not in %s) ))) AND transaction_type = 'Leave Allocation' - AND to_date < %s""", (leave_type, today()), as_dict=1) + AND to_date < %s""", + (leave_type, today()), + as_dict=1, + ) if expire_allocation: create_expiry_ledger_entry(expire_allocation) + def create_expiry_ledger_entry(allocations): - ''' Create ledger entry for expired allocation ''' + """Create ledger entry for expired allocation""" for allocation in allocations: if allocation.is_carry_forward: expire_carried_forward_allocation(allocation) else: expire_allocation(allocation) + def get_remaining_leaves(allocation): - ''' Returns remaining leaves from the given allocation ''' - return frappe.db.get_value("Leave Ledger Entry", + """Returns remaining leaves from the given allocation""" + return frappe.db.get_value( + "Leave Ledger Entry", filters={ - 'employee': allocation.employee, - 'leave_type': allocation.leave_type, - 'to_date': ('<=', allocation.to_date), - 'docstatus': 1 - }, fieldname=['SUM(leaves)']) + "employee": allocation.employee, + "leave_type": allocation.leave_type, + "to_date": ("<=", allocation.to_date), + "docstatus": 1, + }, + fieldname=["SUM(leaves)"], + ) + @frappe.whitelist() def expire_allocation(allocation, expiry_date=None): - ''' expires non-carry forwarded allocation ''' + """expires non-carry forwarded allocation""" leaves = get_remaining_leaves(allocation) expiry_date = expiry_date if expiry_date else allocation.to_date @@ -157,21 +189,28 @@ def expire_allocation(allocation, expiry_date=None): args = dict( leaves=flt(leaves) * -1, transaction_name=allocation.name, - transaction_type='Leave Allocation', + transaction_type="Leave Allocation", from_date=expiry_date, to_date=expiry_date, is_carry_forward=0, - is_expired=1 + is_expired=1, ) create_leave_ledger_entry(allocation, args) frappe.db.set_value("Leave Allocation", allocation.name, "expired", 1) + def expire_carried_forward_allocation(allocation): - ''' Expires remaining leaves in the on carried forward allocation ''' + """Expires remaining leaves in the on carried forward allocation""" from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period - leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, - allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True) + + leaves_taken = get_leaves_for_period( + allocation.employee, + allocation.leave_type, + allocation.from_date, + allocation.to_date, + skip_expired_leaves=False, + ) leaves = flt(allocation.leaves) + flt(leaves_taken) # allow expired leaves entry to be created @@ -183,6 +222,6 @@ def expire_carried_forward_allocation(allocation): is_carry_forward=allocation.is_carry_forward, is_expired=1, from_date=allocation.to_date, - to_date=allocation.to_date + to_date=allocation.to_date, ) create_leave_ledger_entry(allocation, args) diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py index b1cb6887d99..6e62bb58765 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.py +++ b/erpnext/hr/doctype/leave_period/leave_period.py @@ -11,7 +11,6 @@ from erpnext.hr.utils import validate_overlap class LeavePeriod(Document): - def validate(self): self.validate_dates() validate_overlap(self, self.from_date, self.to_date, self.company) diff --git a/erpnext/hr/doctype/leave_period/leave_period_dashboard.py b/erpnext/hr/doctype/leave_period/leave_period_dashboard.py index fbe56e2b700..854f988f35b 100644 --- a/erpnext/hr/doctype/leave_period/leave_period_dashboard.py +++ b/erpnext/hr/doctype/leave_period/leave_period_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'leave_period', - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Leave Allocation'] - } - ] + "fieldname": "leave_period", + "transactions": [{"label": _("Transactions"), "items": ["Leave Allocation"]}], } diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 10936dddc98..09235741b6f 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -9,23 +9,32 @@ import erpnext test_dependencies = ["Employee", "Leave Type", "Leave Policy"] + class TestLeavePeriod(unittest.TestCase): pass + def create_leave_period(from_date, to_date, company=None): - leave_period = frappe.db.get_value('Leave Period', - dict(company=company or erpnext.get_default_company(), + leave_period = frappe.db.get_value( + "Leave Period", + dict( + company=company or erpnext.get_default_company(), from_date=from_date, to_date=to_date, - is_active=1), 'name') + is_active=1, + ), + "name", + ) if leave_period: return frappe.get_doc("Leave Period", leave_period) - leave_period = frappe.get_doc({ - "doctype": "Leave Period", - "company": company or erpnext.get_default_company(), - "from_date": from_date, - "to_date": to_date, - "is_active": 1 - }).insert() + leave_period = frappe.get_doc( + { + "doctype": "Leave Period", + "company": company or erpnext.get_default_company(), + "from_date": from_date, + "to_date": to_date, + "is_active": 1, + } + ).insert() return leave_period diff --git a/erpnext/hr/doctype/leave_policy/leave_policy.py b/erpnext/hr/doctype/leave_policy/leave_policy.py index 80450d5d6e0..33c949354cc 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy.py @@ -11,6 +11,12 @@ class LeavePolicy(Document): def validate(self): if self.leave_policy_details: for lp_detail in self.leave_policy_details: - max_leaves_allowed = frappe.db.get_value("Leave Type", lp_detail.leave_type, "max_leaves_allowed") + max_leaves_allowed = frappe.db.get_value( + "Leave Type", lp_detail.leave_type, "max_leaves_allowed" + ) if max_leaves_allowed > 0 and lp_detail.annual_allocation > max_leaves_allowed: - frappe.throw(_("Maximum leave allowed in the leave type {0} is {1}").format(lp_detail.leave_type, max_leaves_allowed)) + frappe.throw( + _("Maximum leave allowed in the leave type {0} is {1}").format( + lp_detail.leave_type, max_leaves_allowed + ) + ) diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py index 8311fd2f93e..57ea93ee466 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py @@ -1,14 +1,10 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'leave_policy', - 'transactions': [ - { - 'label': _('Leaves'), - 'items': ['Leave Policy Assignment', 'Leave Allocation'] - }, - ] + "fieldname": "leave_policy", + "transactions": [ + {"label": _("Leaves"), "items": ["Leave Policy Assignment", "Leave Allocation"]}, + ], } diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.py b/erpnext/hr/doctype/leave_policy/test_leave_policy.py index 3dbbef857ec..0e1ccad6019 100644 --- a/erpnext/hr/doctype/leave_policy/test_leave_policy.py +++ b/erpnext/hr/doctype/leave_policy/test_leave_policy.py @@ -15,17 +15,24 @@ class TestLeavePolicy(unittest.TestCase): leave_type.max_leaves_allowed = 2 leave_type.save() - leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1) + leave_policy = create_leave_policy( + leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1 + ) self.assertRaises(frappe.ValidationError, leave_policy.insert) + def create_leave_policy(**args): - ''' Returns an object of leave policy ''' + """Returns an object of leave policy""" args = frappe._dict(args) - return frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{ - "leave_type": args.leave_type or "_Test Leave Type", - "annual_allocation": args.annual_allocation or 10 - }] - }) + return frappe.get_doc( + { + "doctype": "Leave Policy", + "leave_policy_details": [ + { + "leave_type": args.leave_type or "_Test Leave Type", + "annual_allocation": args.annual_allocation or 10, + } + ], + } + ) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 6e6943f71aa..bb19ffa9d1e 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,36 +8,63 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document -from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate +from frappe.utils import date_diff, flt, formatdate, get_last_day, get_link_to_form, getdate from six import string_types class LeavePolicyAssignment(Document): def validate(self): - self.validate_policy_assignment_overlap() self.set_dates() + self.validate_policy_assignment_overlap() + self.warn_about_carry_forwarding() def on_submit(self): self.grant_leave_alloc_for_employee() def set_dates(self): if self.assignment_based_on == "Leave Period": - self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"]) + self.effective_from, self.effective_to = frappe.db.get_value( + "Leave Period", self.leave_period, ["from_date", "to_date"] + ) elif self.assignment_based_on == "Joining Date": self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining") def validate_policy_assignment_overlap(self): - leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = { - "employee": self.employee, - "name": ("!=", self.name), - "docstatus": 1, - "effective_to": (">=", self.effective_from), - "effective_from": ("<=", self.effective_to) - }) + leave_policy_assignments = frappe.get_all( + "Leave Policy Assignment", + filters={ + "employee": self.employee, + "name": ("!=", self.name), + "docstatus": 1, + "effective_to": (">=", self.effective_from), + "effective_from": ("<=", self.effective_to), + }, + ) if len(leave_policy_assignments): - frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") - .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) + frappe.throw( + _("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}").format( + bold(self.leave_policy), + bold(self.employee), + bold(formatdate(self.effective_from)), + bold(formatdate(self.effective_to)), + ) + ) + + def warn_about_carry_forwarding(self): + if not self.carry_forward: + return + + leave_types = get_leave_type_details() + leave_policy = frappe.get_doc("Leave Policy", self.leave_policy) + + for policy in leave_policy.leave_policy_details: + leave_type = leave_types.get(policy.leave_type) + if not leave_type.is_carry_forward: + msg = _( + "Leaves for the Leave Type {0} won't be carry-forwarded since carry-forwarding is disabled." + ).format(frappe.bold(get_link_to_form("Leave Type", leave_type.name))) + frappe.msgprint(msg, indicator="orange", alert=True) @frappe.whitelist() def grant_leave_alloc_for_employee(self): @@ -53,41 +80,54 @@ class LeavePolicyAssignment(Document): for leave_policy_detail in leave_policy.leave_policy_details: if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: leave_allocation, new_leaves_allocated = self.create_leave_allocation( - leave_policy_detail.leave_type, leave_policy_detail.annual_allocation, - leave_type_details, date_of_joining + leave_policy_detail.leave_type, + leave_policy_detail.annual_allocation, + leave_type_details, + date_of_joining, ) - leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated} + leave_allocations[leave_policy_detail.leave_type] = { + "name": leave_allocation, + "leaves": new_leaves_allocated, + } self.db_set("leaves_allocated", 1) return leave_allocations - def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + def create_leave_allocation( + self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining + ): # Creates leave allocation for the given employee in the provided leave period carry_forward = self.carry_forward if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward: carry_forward = 0 - new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated, - leave_type_details, date_of_joining) + new_leaves_allocated = self.get_new_leaves( + leave_type, new_leaves_allocated, leave_type_details, date_of_joining + ) - allocation = frappe.get_doc(dict( - doctype="Leave Allocation", - employee=self.employee, - leave_type=leave_type, - from_date=self.effective_from, - to_date=self.effective_to, - new_leaves_allocated=new_leaves_allocated, - leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else '', - leave_policy_assignment = self.name, - leave_policy = self.leave_policy, - carry_forward=carry_forward - )) - allocation.save(ignore_permissions = True) + allocation = frappe.get_doc( + dict( + doctype="Leave Allocation", + employee=self.employee, + leave_type=leave_type, + from_date=self.effective_from, + to_date=self.effective_to, + new_leaves_allocated=new_leaves_allocated, + leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else "", + leave_policy_assignment=self.name, + leave_policy=self.leave_policy, + carry_forward=carry_forward, + ) + ) + allocation.save(ignore_permissions=True) allocation.submit() return allocation.name, new_leaves_allocated def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): from frappe.model.meta import get_field_precision - precision = get_field_precision(frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated")) + + precision = get_field_precision( + frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated") + ) # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 if leave_type_details.get(leave_type).is_compensatory == 1: @@ -98,16 +138,22 @@ class LeavePolicyAssignment(Document): new_leaves_allocated = 0 else: # get leaves for past months if assignment is based on Leave Period / Joining Date - new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining) + new_leaves_allocated = self.get_leaves_for_passed_months( + leave_type, new_leaves_allocated, leave_type_details, date_of_joining + ) # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period elif getdate(date_of_joining) > getdate(self.effective_from): - remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1)) + remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / ( + date_diff(self.effective_to, self.effective_from) + 1 + ) new_leaves_allocated = ceil(new_leaves_allocated * remaining_period) return flt(new_leaves_allocated, precision) - def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + def get_leaves_for_passed_months( + self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining + ): from erpnext.hr.utils import get_monthly_earned_leave current_date = frappe.flags.current_date or getdate() @@ -130,8 +176,11 @@ class LeavePolicyAssignment(Document): months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) if months_passed > 0: - monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, - leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding) + monthly_earned_leave = get_monthly_earned_leave( + new_leaves_allocated, + leave_type_details.get(leave_type).earned_leave_frequency, + leave_type_details.get(leave_type).rounding, + ) new_leaves_allocated = monthly_earned_leave * months_passed else: new_leaves_allocated = 0 @@ -160,7 +209,7 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj def create_assignment_for_multiple_employees(employees, data): if isinstance(employees, string_types): - employees= json.loads(employees) + employees = json.loads(employees) if isinstance(data, string_types): data = frappe._dict(json.loads(data)) @@ -187,11 +236,23 @@ def create_assignment_for_multiple_employees(employees, data): return docs_name + def get_leave_type_details(): leave_type_details = frappe._dict() - leave_types = frappe.get_all("Leave Type", - fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "based_on_date_of_joining", - "is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"]) + leave_types = frappe.get_all( + "Leave Type", + fields=[ + "name", + "is_lwp", + "is_earned_leave", + "is_compensatory", + "based_on_date_of_joining", + "is_carry_forward", + "expire_carry_forwarded_leaves_after_days", + "earned_leave_frequency", + "rounding", + ], + ) for d in leave_types: leave_type_details.setdefault(d.name, d) return leave_type_details diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py index ec6592cb72a..13b39c7ee6e 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py @@ -1,14 +1,10 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'leave_policy_assignment', - 'transactions': [ - { - 'label': _('Leaves'), - 'items': ['Leave Allocation'] - }, - ] + "fieldname": "leave_policy_assignment", + "transactions": [ + {"label": _("Leaves"), "items": ["Leave Allocation"]}, + ], } diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 8d7b27ee5af..20249b38ef7 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import add_months, get_first_day, get_last_day, getdate +from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate from erpnext.hr.doctype.leave_application.test_leave_application import ( get_employee, @@ -17,9 +17,16 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( test_dependencies = ["Employee"] + class TestLeavePolicyAssignment(unittest.TestCase): def setUp(self): - for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + for doctype in [ + "Leave Period", + "Leave Application", + "Leave Allocation", + "Leave Policy Assignment", + "Leave Ledger Entry", + ]: frappe.db.delete(doctype) employee = get_employee() @@ -35,16 +42,25 @@ class TestLeavePolicyAssignment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) - self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) + self.assertEqual( + frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), + 1, + ) - leave_allocation = frappe.get_list("Leave Allocation", filters={ - "employee": self.employee.name, - "leave_policy":leave_policy.name, - "leave_policy_assignment": leave_policy_assignments[0], - "docstatus": 1})[0] + leave_allocation = frappe.get_list( + "Leave Allocation", + filters={ + "employee": self.employee.name, + "leave_policy": leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1, + }, + )[0] leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) @@ -63,64 +79,93 @@ class TestLeavePolicyAssignment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) # every leave is allocated no more leave can be granted now - self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1) - leave_allocation = frappe.get_list("Leave Allocation", filters={ - "employee": self.employee.name, - "leave_policy":leave_policy.name, - "leave_policy_assignment": leave_policy_assignments[0], - "docstatus": 1})[0] + self.assertEqual( + frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), + 1, + ) + leave_allocation = frappe.get_list( + "Leave Allocation", + filters={ + "employee": self.employee.name, + "leave_policy": leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1, + }, + )[0] leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) leave_alloc_doc.cancel() leave_alloc_doc.delete() - self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 0) + self.assertEqual( + frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), + 0, + ) def test_earned_leave_allocation(self): leave_period = create_leave_period("Test Earned Leave Period") leave_type = create_earned_leave_type("Test Earned Leave") - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}] - }).submit() + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}], + } + ).submit() data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + # second last day of the month # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + frappe.flags.current_date = add_days(get_last_day(getdate()), -1) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) + + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 0) def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): - leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1))) + leave_period, leave_policy = setup_leave_period_and_policy( + get_first_day(add_months(getdate(), -1)) + ) # Case 1: assignment created one month after the leave period, should allocate 1 leave frappe.flags.current_date = get_first_day(getdate()) data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 1) def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self): - leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) + leave_period, leave_policy = setup_leave_period_and_policy( + get_first_day(add_months(getdate(), -2)) + ) # Case 2: assignment created on the last day of the leave period's latter month # should allocate 1 leave for current month even though the month has not ended # since the daily job might have already executed @@ -129,32 +174,48 @@ class TestLeavePolicyAssignment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) # if the daily job is not completed yet, there is another check present # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import allocate_earned_leaves + allocate_earned_leaves() - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self): from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation - leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) + leave_period, leave_policy = setup_leave_period_and_policy( + get_first_day(add_months(getdate(), -2)) + ) # initial leave allocation = 5 - leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave", - from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0) + leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, + leave_type="Test Earned Leave", + from_date=add_months(getdate(), -12), + to_date=add_months(getdate(), -3), + new_leaves_allocated=5, + carry_forward=0, + ) leave_allocation.submit() # Case 3: assignment created on the last day of the leave period's latter month with carry forwarding @@ -163,14 +224,19 @@ class TestLeavePolicyAssignment(unittest.TestCase): "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, "leave_period": leave_period.name, - "carry_forward": 1 + "carry_forward": 1, } # carry forwarded leaves = 5, 3 leaves allocated for passed months - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - details = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True) + details = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], + as_dict=True, + ) self.assertEqual(details.new_leaves_allocated, 2) self.assertEqual(details.unused_leaves, 5) self.assertEqual(details.total_leaves_allocated, 7) @@ -178,20 +244,27 @@ class TestLeavePolicyAssignment(unittest.TestCase): # if the daily job is not completed yet, there is another check present # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import is_earned_leave_already_allocated + frappe.flags.current_date = get_last_day(getdate()) allocation = frappe.get_doc("Leave Allocation", details.name) # 1 leave is still pending to be allocated, irrespective of carry forwarded leaves - self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation)) + self.assertFalse( + is_earned_leave_already_allocated( + allocation, leave_policy.leave_policy_details[0].annual_allocation + ) + ) def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self): # tests leave alloc for earned leaves for assignment based on joining date in policy assignment leave_type = create_earned_leave_type("Test Earned Leave") - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).submit() + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}], + } + ).submit() # joining date set to 2 months back self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) @@ -199,29 +272,39 @@ class TestLeavePolicyAssignment(unittest.TestCase): # assignment created on the last day of the current month frappe.flags.current_date = get_last_day(getdate()) - data = { - "assignment_based_on": "Joining Date", - "leave_policy": leave_policy.name - } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) - leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated") - effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + data = {"assignment_based_on": "Joining Date", "leave_policy": leave_policy.name} + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) + effective_from = frappe.db.get_value( + "Leave Policy Assignment", leave_policy_assignments[0], "effective_from" + ) self.assertEqual(effective_from, self.employee.date_of_joining) self.assertEqual(leaves_allocated, 3) # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_last_day(getdate()) allocate_earned_leaves() - leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): # tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type - leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True) + leave_period, leave_policy = setup_leave_period_and_policy( + get_first_day(add_months(getdate(), -2)), based_on_doj=True + ) # joining date set to 2 months back self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) @@ -233,34 +316,43 @@ class TestLeavePolicyAssignment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) # if the daily job is not completed yet, there is another check present # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) allocate_earned_leaves() - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): # tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).submit() + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}], + } + ).submit() # joining date set to 2 months back # leave should be allocated for current month too since this day is same as the joining day @@ -269,24 +361,32 @@ class TestLeavePolicyAssignment(unittest.TestCase): # assignment created on the first day of the current month frappe.flags.current_date = get_first_day(getdate()) - data = { - "assignment_based_on": "Joining Date", - "leave_policy": leave_policy.name - } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) - leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated") - effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + data = {"assignment_based_on": "Joining Date", "leave_policy": leave_policy.name} + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) + effective_from = frappe.db.get_value( + "Leave Policy Assignment", leave_policy_assignments[0], "effective_from" + ) self.assertEqual(effective_from, self.employee.date_of_joining) self.assertEqual(leaves_allocated, 3) # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) allocate_earned_leaves() - leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) def tearDown(self): @@ -298,15 +398,17 @@ class TestLeavePolicyAssignment(unittest.TestCase): def create_earned_leave_type(leave_type, based_on_doj=False): frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) - return frappe.get_doc(dict( - leave_type_name=leave_type, - doctype="Leave Type", - is_earned_leave=1, - earned_leave_frequency="Monthly", - rounding=0.5, - is_carry_forward=1, - based_on_date_of_joining=based_on_doj - )).insert() + return frappe.get_doc( + dict( + leave_type_name=leave_type, + doctype="Leave Type", + is_earned_leave=1, + earned_leave_frequency="Monthly", + rounding=0.5, + is_carry_forward=1, + based_on_date_of_joining=based_on_doj, + ) + ).insert() def create_leave_period(name, start_date=None): @@ -314,24 +416,27 @@ def create_leave_period(name, start_date=None): if not start_date: start_date = get_first_day(getdate()) - return frappe.get_doc(dict( - name=name, - doctype="Leave Period", - from_date=start_date, - to_date=add_months(start_date, 12), - company="_Test Company", - is_active=1 - )).insert() + return frappe.get_doc( + dict( + name=name, + doctype="Leave Period", + from_date=start_date, + to_date=add_months(start_date, 12), + company="_Test Company", + is_active=1, + ) + ).insert() def setup_leave_period_and_policy(start_date, based_on_doj=False): leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj) - leave_period = create_leave_period("Test Earned Leave Period", - start_date=start_date) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).insert() + leave_period = create_leave_period("Test Earned Leave Period", start_date=start_date) + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}], + } + ).insert() - return leave_period, leave_policy \ No newline at end of file + return leave_period, leave_policy diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py index 4b59c2c09b4..82b9bd65753 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.py +++ b/erpnext/hr/doctype/leave_type/leave_type.py @@ -11,17 +11,23 @@ from frappe.utils import today class LeaveType(Document): def validate(self): if self.is_lwp: - leave_allocation = frappe.get_all("Leave Allocation", filters={ - 'leave_type': self.name, - 'from_date': ("<=", today()), - 'to_date': (">=", today()) - }, fields=['name']) - leave_allocation = [l['name'] for l in leave_allocation] + leave_allocation = frappe.get_all( + "Leave Allocation", + filters={"leave_type": self.name, "from_date": ("<=", today()), "to_date": (">=", today())}, + fields=["name"], + ) + leave_allocation = [l["name"] for l in leave_allocation] if leave_allocation: - frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec + frappe.throw( + _( + "Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay" + ).format(", ".join(leave_allocation)) + ) # nosec if self.is_lwp and self.is_ppl: frappe.throw(_("Leave Type can be either without pay or partial pay")) - if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1): + if self.is_ppl and ( + self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1 + ): frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1")) diff --git a/erpnext/hr/doctype/leave_type/leave_type_dashboard.py b/erpnext/hr/doctype/leave_type/leave_type_dashboard.py index 8dc9402d1eb..269a1ecc696 100644 --- a/erpnext/hr/doctype/leave_type/leave_type_dashboard.py +++ b/erpnext/hr/doctype/leave_type/leave_type_dashboard.py @@ -1,14 +1,10 @@ - - def get_data(): return { - 'fieldname': 'leave_type', - 'transactions': [ + "fieldname": "leave_type", + "transactions": [ { - 'items': ['Leave Allocation', 'Leave Application'], + "items": ["Leave Allocation", "Leave Application"], }, - { - 'items': ['Attendance', 'Leave Encashment'] - } - ] + {"items": ["Attendance", "Leave Encashment"]}, + ], } diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py index c1b64e99eff..69f9e125203 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.py +++ b/erpnext/hr/doctype/leave_type/test_leave_type.py @@ -3,27 +3,30 @@ import frappe -test_records = frappe.get_test_records('Leave Type') +test_records = frappe.get_test_records("Leave Type") + def create_leave_type(**args): - args = frappe._dict(args) - if frappe.db.exists("Leave Type", args.leave_type_name): - return frappe.get_doc("Leave Type", args.leave_type_name) - leave_type = frappe.get_doc({ - "doctype": "Leave Type", - "leave_type_name": args.leave_type_name or "_Test Leave Type", - "include_holiday": args.include_holidays or 1, - "allow_encashment": args.allow_encashment or 0, - "is_earned_leave": args.is_earned_leave or 0, - "is_lwp": args.is_lwp or 0, - "is_ppl":args.is_ppl or 0, - "is_carry_forward": args.is_carry_forward or 0, - "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0, - "encashment_threshold_days": args.encashment_threshold_days or 5, - "earning_component": "Leave Encashment" - }) + args = frappe._dict(args) + if frappe.db.exists("Leave Type", args.leave_type_name): + return frappe.get_doc("Leave Type", args.leave_type_name) + leave_type = frappe.get_doc( + { + "doctype": "Leave Type", + "leave_type_name": args.leave_type_name or "_Test Leave Type", + "include_holiday": args.include_holidays or 1, + "allow_encashment": args.allow_encashment or 0, + "is_earned_leave": args.is_earned_leave or 0, + "is_lwp": args.is_lwp or 0, + "is_ppl": args.is_ppl or 0, + "is_carry_forward": args.is_carry_forward or 0, + "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0, + "encashment_threshold_days": args.encashment_threshold_days or 5, + "earning_component": "Leave Encashment", + } + ) - if leave_type.is_ppl: - leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5 + if leave_type.is_ppl: + leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5 - return leave_type + return leave_type diff --git a/erpnext/hr/doctype/offer_term/test_offer_term.py b/erpnext/hr/doctype/offer_term/test_offer_term.py index 2e5ed75438b..2bea7b2597f 100644 --- a/erpnext/hr/doctype/offer_term/test_offer_term.py +++ b/erpnext/hr/doctype/offer_term/test_offer_term.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Offer Term') + class TestOfferTerm(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 517730281fc..868be6ef719 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -20,7 +20,7 @@ class ShiftAssignment(Document): self.validate_overlapping_dates() if self.end_date: - self.validate_from_to_dates('start_date', 'end_date') + self.validate_from_to_dates("start_date", "end_date") def validate_overlapping_dates(self): if not self.name: @@ -33,7 +33,7 @@ class ShiftAssignment(Document): """ if self.end_date: - condition += """ or + condition += """ or %(end_date)s between start_date and end_date or start_date between %(start_date)s and %(end_date)s @@ -41,7 +41,8 @@ class ShiftAssignment(Document): else: condition += """ ) """ - assigned_shifts = frappe.db.sql(""" + assigned_shifts = frappe.db.sql( + """ select name, shift_type, start_date ,end_date, docstatus, status from `tabShift Assignment` where @@ -49,13 +50,18 @@ class ShiftAssignment(Document): and name != %(name)s and status = "Active" {0} - """.format(condition), { - "employee": self.employee, - "shift_type": self.shift_type, - "start_date": self.start_date, - "end_date": self.end_date, - "name": self.name - }, as_dict = 1) + """.format( + condition + ), + { + "employee": self.employee, + "shift_type": self.shift_type, + "start_date": self.start_date, + "end_date": self.end_date, + "name": self.name, + }, + as_dict=1, + ) if len(assigned_shifts): self.throw_overlap_error(assigned_shifts[0]) @@ -63,7 +69,9 @@ class ShiftAssignment(Document): def throw_overlap_error(self, shift_details): shift_details = frappe._dict(shift_details) if shift_details.docstatus == 1 and shift_details.status == "Active": - msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name)) + msg = _("Employee {0} already has Active Shift {1}: {2}").format( + frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name) + ) if shift_details.start_date: msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y")) title = "Ongoing Shift" @@ -73,35 +81,41 @@ class ShiftAssignment(Document): if msg: frappe.throw(msg, title=title) + @frappe.whitelist() def get_events(start, end, filters=None): - events = [] + from frappe.desk.calendar import get_event_conditions - employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}, ["name", "company"], - as_dict=True) + employee = frappe.db.get_value( + "Employee", {"user_id": frappe.session.user}, ["name", "company"], as_dict=True + ) if employee: employee, company = employee.name, employee.company else: - employee='' - company=frappe.db.get_value("Global Defaults", None, "default_company") + employee = "" + company = frappe.db.get_value("Global Defaults", None, "default_company") - from frappe.desk.reportview import get_filters_cond - conditions = get_filters_cond("Shift Assignment", filters, []) - add_assignments(events, start, end, conditions=conditions) + conditions = get_event_conditions("Shift Assignment", filters) + events = add_assignments(start, end, conditions=conditions) return events -def add_assignments(events, start, end, conditions=None): + +def add_assignments(start, end, conditions=None): + events = [] + query = """select name, start_date, end_date, employee_name, employee, docstatus, shift_type from `tabShift Assignment` where - start_date >= %(start_date)s - or end_date <= %(end_date)s - or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date) + ( + start_date >= %(start_date)s + or end_date <= %(end_date)s + or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date) + ) and docstatus = 1""" if conditions: query += conditions - records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True) + records = frappe.db.sql(query, {"start_date": start, "end_date": end}, as_dict=True) shift_timing_map = get_shift_type_timing([d.shift_type for d in records]) for d in records: @@ -109,27 +123,33 @@ def add_assignments(events, start, end, conditions=None): daily_event_end = d.end_date if d.end_date else getdate() delta = timedelta(days=1) while daily_event_start <= daily_event_end: - start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time'] - end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time'] + start_timing = ( + frappe.utils.get_datetime(daily_event_start) + shift_timing_map[d.shift_type]["start_time"] + ) + end_timing = ( + frappe.utils.get_datetime(daily_event_start) + shift_timing_map[d.shift_type]["end_time"] + ) daily_event_start += delta e = { "name": d.name, "doctype": "Shift Assignment", "start_date": start_timing, "end_date": end_timing, - "title": cstr(d.employee_name) + ": "+ \ - cstr(d.shift_type), + "title": cstr(d.employee_name) + ": " + cstr(d.shift_type), "docstatus": d.docstatus, - "allDay": 0 + "allDay": 0, } if e not in events: events.append(e) return events + def get_shift_type_timing(shift_types): shift_timing_map = {} - data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time']) + data = frappe.get_all( + "Shift Type", filters={"name": ("IN", shift_types)}, fields=["name", "start_time", "end_time"] + ) for d in data: shift_timing_map[d.name] = d @@ -137,7 +157,9 @@ def get_shift_type_timing(shift_types): return shift_timing_map -def get_employee_shift(employee, for_date=None, consider_default_shift=False, next_shift_direction=None): +def get_employee_shift( + employee, for_date=None, consider_default_shift=False, next_shift_direction=None +): """Returns a Shift Type for the given employee on the given date. (excluding the holidays) :param employee: Employee for which shift is required. @@ -147,21 +169,25 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne """ if for_date is None: for_date = nowdate() - default_shift = frappe.db.get_value('Employee', employee, 'default_shift') + default_shift = frappe.db.get_value("Employee", employee, "default_shift") shift_type_name = None - shift_assignment_details = frappe.db.get_value('Shift Assignment', {'employee':employee, 'start_date':('<=', for_date), 'docstatus': '1', 'status': "Active"}, ['shift_type', 'end_date']) + shift_assignment_details = frappe.db.get_value( + "Shift Assignment", + {"employee": employee, "start_date": ("<=", for_date), "docstatus": "1", "status": "Active"}, + ["shift_type", "end_date"], + ) if shift_assignment_details: shift_type_name = shift_assignment_details[0] # if end_date present means that shift is over after end_date else it is a ongoing shift. - if shift_assignment_details[1] and for_date >= shift_assignment_details[1] : + if shift_assignment_details[1] and for_date >= shift_assignment_details[1]: shift_type_name = None if not shift_type_name and consider_default_shift: shift_type_name = default_shift if shift_type_name: - holiday_list_name = frappe.db.get_value('Shift Type', shift_type_name, 'holiday_list') + holiday_list_name = frappe.db.get_value("Shift Type", shift_type_name, "holiday_list") if not holiday_list_name: holiday_list_name = get_holiday_list_for_employee(employee, False) if holiday_list_name and is_holiday(holiday_list_name, for_date): @@ -170,22 +196,30 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne if not shift_type_name and next_shift_direction: MAX_DAYS = 366 if consider_default_shift and default_shift: - direction = -1 if next_shift_direction == 'reverse' else +1 + direction = -1 if next_shift_direction == "reverse" else +1 for i in range(MAX_DAYS): - date = for_date+timedelta(days=direction*(i+1)) + date = for_date + timedelta(days=direction * (i + 1)) shift_details = get_employee_shift(employee, date, consider_default_shift, None) if shift_details: shift_type_name = shift_details.shift_type.name for_date = date break else: - direction = '<' if next_shift_direction == 'reverse' else '>' - sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc' - dates = frappe.db.get_all('Shift Assignment', - ['start_date', 'end_date'], - {'employee':employee, 'start_date':(direction, for_date), 'docstatus': '1', "status": "Active"}, + direction = "<" if next_shift_direction == "reverse" else ">" + sort_order = "desc" if next_shift_direction == "reverse" else "asc" + dates = frappe.db.get_all( + "Shift Assignment", + ["start_date", "end_date"], + { + "employee": employee, + "start_date": (direction, for_date), + "docstatus": "1", + "status": "Active", + }, as_list=True, - limit=MAX_DAYS, order_by="start_date "+sort_order) + limit=MAX_DAYS, + order_by="start_date " + sort_order, + ) if dates: for date in dates: @@ -201,35 +235,57 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne def get_employee_shift_timings(employee, for_timestamp=None, consider_default_shift=False): - """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee - """ + """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee""" if for_timestamp is None: for_timestamp = now_datetime() # write and verify a test case for midnight shift. prev_shift = curr_shift = next_shift = None - curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward') + curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, "forward") if curr_shift: - next_shift = get_employee_shift(employee, curr_shift.start_datetime.date()+timedelta(days=1), consider_default_shift, 'forward') - prev_shift = get_employee_shift(employee, for_timestamp.date()+timedelta(days=-1), consider_default_shift, 'reverse') + next_shift = get_employee_shift( + employee, + curr_shift.start_datetime.date() + timedelta(days=1), + consider_default_shift, + "forward", + ) + prev_shift = get_employee_shift( + employee, for_timestamp.date() + timedelta(days=-1), consider_default_shift, "reverse" + ) if curr_shift: if prev_shift: - curr_shift.actual_start = prev_shift.end_datetime if curr_shift.actual_start < prev_shift.end_datetime else curr_shift.actual_start - prev_shift.actual_end = curr_shift.actual_start if prev_shift.actual_end > curr_shift.actual_start else prev_shift.actual_end + curr_shift.actual_start = ( + prev_shift.end_datetime + if curr_shift.actual_start < prev_shift.end_datetime + else curr_shift.actual_start + ) + prev_shift.actual_end = ( + curr_shift.actual_start + if prev_shift.actual_end > curr_shift.actual_start + else prev_shift.actual_end + ) if next_shift: - next_shift.actual_start = curr_shift.end_datetime if next_shift.actual_start < curr_shift.end_datetime else next_shift.actual_start - curr_shift.actual_end = next_shift.actual_start if curr_shift.actual_end > next_shift.actual_start else curr_shift.actual_end + next_shift.actual_start = ( + curr_shift.end_datetime + if next_shift.actual_start < curr_shift.end_datetime + else next_shift.actual_start + ) + curr_shift.actual_end = ( + next_shift.actual_start + if curr_shift.actual_end > next_shift.actual_start + else curr_shift.actual_end + ) return prev_shift, curr_shift, next_shift def get_shift_details(shift_type_name, for_date=None): """Returns Shift Details which contain some additional information as described below. 'shift_details' contains the following keys: - 'shift_type' - Object of DocType Shift Type, - 'start_datetime' - Date and Time of shift start on given date, - 'end_datetime' - Date and Time of shift end on given date, - 'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time', - 'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time'(None is returned if this is zero) + 'shift_type' - Object of DocType Shift Type, + 'start_datetime' - Date and Time of shift start on given date, + 'end_datetime' - Date and Time of shift end on given date, + 'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time', + 'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time'(None is returned if this is zero) :param shift_type_name: shift type name for which shift_details is required. :param for_date: Date on which shift_details are required @@ -238,30 +294,38 @@ def get_shift_details(shift_type_name, for_date=None): return None if not for_date: for_date = nowdate() - shift_type = frappe.get_doc('Shift Type', shift_type_name) + shift_type = frappe.get_doc("Shift Type", shift_type_name) start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time - for_date = for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date + for_date = ( + for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date + ) end_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.end_time - actual_start = start_datetime - timedelta(minutes=shift_type.begin_check_in_before_shift_start_time) + actual_start = start_datetime - timedelta( + minutes=shift_type.begin_check_in_before_shift_start_time + ) actual_end = end_datetime + timedelta(minutes=shift_type.allow_check_out_after_shift_end_time) - return frappe._dict({ - 'shift_type': shift_type, - 'start_datetime': start_datetime, - 'end_datetime': end_datetime, - 'actual_start': actual_start, - 'actual_end': actual_end - }) + return frappe._dict( + { + "shift_type": shift_type, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "actual_start": actual_start, + "actual_end": actual_end, + } + ) def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_default_shift=False): """Takes a datetime and returns the 'actual' start datetime and end datetime of the shift in which the timestamp belongs. - Here 'actual' means - taking in to account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time". - None is returned if the timestamp is outside any actual shift timings. - Shift Details is also returned(current/upcoming i.e. if timestamp not in any actual shift then details of next shift returned) + Here 'actual' means - taking in to account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time". + None is returned if the timestamp is outside any actual shift timings. + Shift Details is also returned(current/upcoming i.e. if timestamp not in any actual shift then details of next shift returned) """ actual_shift_start = actual_shift_end = shift_details = None - shift_timings_as_per_timestamp = get_employee_shift_timings(employee, for_datetime, consider_default_shift) + shift_timings_as_per_timestamp = get_employee_shift_timings( + employee, for_datetime, consider_default_shift + ) timestamp_list = [] for shift in shift_timings_as_per_timestamp: if shift: @@ -273,11 +337,11 @@ def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_defa if timestamp and for_datetime <= timestamp: timestamp_index = index break - if timestamp_index and timestamp_index%2 == 1: - shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)] + if timestamp_index and timestamp_index % 2 == 1: + shift_details = shift_timings_as_per_timestamp[int((timestamp_index - 1) / 2)] actual_shift_start = shift_details.actual_start actual_shift_end = shift_details.actual_end elif timestamp_index: - shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)] + shift_details = shift_timings_as_per_timestamp[int(timestamp_index / 2)] return actual_shift_start, actual_shift_end, shift_details diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py index d4900814ffe..ea37424762b 100644 --- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py @@ -4,23 +4,33 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, nowdate +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.shift_assignment.shift_assignment import get_events + test_dependencies = ["Shift Type"] -class TestShiftAssignment(unittest.TestCase): +class TestShiftAssignment(FrappeTestCase): def setUp(self): - frappe.db.sql("delete from `tabShift Assignment`") + frappe.db.delete("Shift Assignment") + if not frappe.db.exists("Shift Type", "Day Shift"): + frappe.get_doc( + {"doctype": "Shift Type", "name": "Day Shift", "start_time": "9:00:00", "end_time": "18:00:00"} + ).insert() def test_make_shift_assignment(self): - shift_assignment = frappe.get_doc({ - "doctype": "Shift Assignment", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "start_date": nowdate() - }).insert() + shift_assignment = frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": nowdate(), + } + ).insert() shift_assignment.submit() self.assertEqual(shift_assignment.docstatus, 1) @@ -28,52 +38,92 @@ class TestShiftAssignment(unittest.TestCase): def test_overlapping_for_ongoing_shift(self): # shift should be Ongoing if Only start_date is present and status = Active - shift_assignment_1 = frappe.get_doc({ - "doctype": "Shift Assignment", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "start_date": nowdate(), - "status": 'Active' - }).insert() + shift_assignment_1 = frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": nowdate(), + "status": "Active", + } + ).insert() shift_assignment_1.submit() self.assertEqual(shift_assignment_1.docstatus, 1) - shift_assignment = frappe.get_doc({ - "doctype": "Shift Assignment", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "start_date": add_days(nowdate(), 2) - }) + shift_assignment = frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": add_days(nowdate(), 2), + } + ) self.assertRaises(frappe.ValidationError, shift_assignment.save) def test_overlapping_for_fixed_period_shift(self): # shift should is for Fixed period if Only start_date and end_date both are present and status = Active - shift_assignment_1 = frappe.get_doc({ + shift_assignment_1 = frappe.get_doc( + { "doctype": "Shift Assignment", "shift_type": "Day Shift", "company": "_Test Company", "employee": "_T-Employee-00001", "start_date": nowdate(), "end_date": add_days(nowdate(), 30), - "status": 'Active' - }).insert() - shift_assignment_1.submit() + "status": "Active", + } + ).insert() + shift_assignment_1.submit() - - # it should not allowed within period of any shift. - shift_assignment_3 = frappe.get_doc({ + # it should not allowed within period of any shift. + shift_assignment_3 = frappe.get_doc( + { "doctype": "Shift Assignment", "shift_type": "Day Shift", "company": "_Test Company", "employee": "_T-Employee-00001", - "start_date":add_days(nowdate(), 10), + "start_date": add_days(nowdate(), 10), "end_date": add_days(nowdate(), 35), - "status": 'Active' - }) + "status": "Active", + } + ) - self.assertRaises(frappe.ValidationError, shift_assignment_3.save) + self.assertRaises(frappe.ValidationError, shift_assignment_3.save) + + def test_shift_assignment_calendar(self): + employee1 = make_employee("test_shift_assignment1@example.com", company="_Test Company") + employee2 = make_employee("test_shift_assignment2@example.com", company="_Test Company") + date = nowdate() + + shift_1 = frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": employee1, + "start_date": date, + "status": "Active", + } + ).submit() + + frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": employee2, + "start_date": date, + "status": "Active", + } + ).submit() + + events = get_events( + start=date, end=date, filters=[["Shift Assignment", "employee", "=", employee1, False]] + ) + self.assertEqual(len(events), 1) + self.assertEqual(events[0]["name"], shift_1.name) diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py index d4fcf99d7d8..1e3e8ff6464 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.py +++ b/erpnext/hr/doctype/shift_request/shift_request.py @@ -10,7 +10,9 @@ from frappe.utils import formatdate, getdate from erpnext.hr.utils import share_doc_with_approver, validate_active_employee -class OverlapError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + class ShiftRequest(Document): def validate(self): @@ -39,24 +41,35 @@ class ShiftRequest(Document): assignment_doc.insert() assignment_doc.submit() - frappe.msgprint(_("Shift Assignment: {0} created for Employee: {1}").format(frappe.bold(assignment_doc.name), frappe.bold(self.employee))) + frappe.msgprint( + _("Shift Assignment: {0} created for Employee: {1}").format( + frappe.bold(assignment_doc.name), frappe.bold(self.employee) + ) + ) def on_cancel(self): - shift_assignment_list = frappe.get_list("Shift Assignment", {'employee': self.employee, 'shift_request': self.name}) + shift_assignment_list = frappe.get_list( + "Shift Assignment", {"employee": self.employee, "shift_request": self.name} + ) if shift_assignment_list: for shift in shift_assignment_list: - shift_assignment_doc = frappe.get_doc("Shift Assignment", shift['name']) + shift_assignment_doc = frappe.get_doc("Shift Assignment", shift["name"]) shift_assignment_doc.cancel() def validate_default_shift(self): default_shift = frappe.get_value("Employee", self.employee, "default_shift") if self.shift_type == default_shift: - frappe.throw(_("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type))) + frappe.throw( + _("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type)) + ) def validate_approver(self): department = frappe.get_value("Employee", self.employee, "department") shift_approver = frappe.get_value("Employee", self.employee, "shift_request_approver") - approvers = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department)) + approvers = frappe.db.sql( + """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", + (department), + ) approvers = [approver[0] for approver in approvers] approvers.append(shift_approver) if self.approver not in approvers: @@ -67,10 +80,11 @@ class ShiftRequest(Document): frappe.throw(_("To date cannot be before from date")) def validate_shift_request_overlap_dates(self): - if not self.name: - self.name = "New Shift Request" + if not self.name: + self.name = "New Shift Request" - d = frappe.db.sql(""" + d = frappe.db.sql( + """ select name, shift_type, from_date, to_date from `tabShift Request` @@ -79,20 +93,23 @@ class ShiftRequest(Document): and %(from_date)s <= to_date) or ( %(to_date)s >= from_date and %(to_date)s <= to_date )) - and name != %(name)s""", { - "employee": self.employee, - "shift_type": self.shift_type, - "from_date": self.from_date, - "to_date": self.to_date, - "name": self.name - }, as_dict=1) + and name != %(name)s""", + { + "employee": self.employee, + "shift_type": self.shift_type, + "from_date": self.from_date, + "to_date": self.to_date, + "name": self.name, + }, + as_dict=1, + ) - for date_overlap in d: - if date_overlap ['name']: - self.throw_overlap_error(date_overlap) + for date_overlap in d: + if date_overlap["name"]: + self.throw_overlap_error(date_overlap) def throw_overlap_error(self, d): - msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee, - d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \ - + """ {0}""".format(d["name"]) + msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format( + self.employee, d["shift_type"], formatdate(d["from_date"]), formatdate(d["to_date"]) + ) + """ {0}""".format(d["name"]) frappe.throw(msg, OverlapError) diff --git a/erpnext/hr/doctype/shift_request/shift_request_dashboard.py b/erpnext/hr/doctype/shift_request/shift_request_dashboard.py index cd4519e8797..2859b8f7717 100644 --- a/erpnext/hr/doctype/shift_request/shift_request_dashboard.py +++ b/erpnext/hr/doctype/shift_request/shift_request_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'shift_request', - 'transactions': [ - { - 'items': ['Shift Assignment'] - }, - ], - } + return { + "fieldname": "shift_request", + "transactions": [ + {"items": ["Shift Assignment"]}, + ], + } diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py index 3633c9b3003..b4f51772159 100644 --- a/erpnext/hr/doctype/shift_request/test_shift_request.py +++ b/erpnext/hr/doctype/shift_request/test_shift_request.py @@ -10,6 +10,7 @@ from erpnext.hr.doctype.employee.test_employee import make_employee test_dependencies = ["Shift Type"] + class TestShiftRequest(unittest.TestCase): def setUp(self): for doctype in ["Shift Request", "Shift Assignment"]: @@ -20,9 +21,12 @@ class TestShiftRequest(unittest.TestCase): def test_make_shift_request(self): "Test creation/updation of Shift Assignment from Shift Request." - department = frappe.get_value("Employee", "_T-Employee-00001", 'department') + department = frappe.get_value("Employee", "_T-Employee-00001", "department") set_shift_approver(department) - approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] + approver = frappe.db.sql( + """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", + (department), + )[0][0] shift_request = make_shift_request(approver) @@ -31,7 +35,7 @@ class TestShiftRequest(unittest.TestCase): "Shift Assignment", filters={"shift_request": shift_request.name}, fieldname=["employee", "docstatus"], - as_dict=True + as_dict=True, ) self.assertEqual(shift_request.employee, shift_assignment.employee) self.assertEqual(shift_assignment.docstatus, 1) @@ -39,9 +43,7 @@ class TestShiftRequest(unittest.TestCase): shift_request.cancel() shift_assignment_docstatus = frappe.db.get_value( - "Shift Assignment", - filters={"shift_request": shift_request.name}, - fieldname="docstatus" + "Shift Assignment", filters={"shift_request": shift_request.name}, fieldname="docstatus" ) self.assertEqual(shift_assignment_docstatus, 2) @@ -62,7 +64,10 @@ class TestShiftRequest(unittest.TestCase): shift_request.reload() department = frappe.get_value("Employee", "_T-Employee-00001", "department") set_shift_approver(department) - department_approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] + department_approver = frappe.db.sql( + """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", + (department), + )[0][0] shift_request.approver = department_approver shift_request.save() self.assertTrue(shift_request.name not in frappe.share.get_shared("Shift Request", user)) @@ -85,22 +90,25 @@ class TestShiftRequest(unittest.TestCase): def set_shift_approver(department): department_doc = frappe.get_doc("Department", department) - department_doc.append('shift_request_approver',{'approver': "test1@example.com"}) + department_doc.append("shift_request_approver", {"approver": "test1@example.com"}) department_doc.save() department_doc.reload() + def make_shift_request(approver, do_not_submit=0): - shift_request = frappe.get_doc({ - "doctype": "Shift Request", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "employee_name": "_Test Employee", - "from_date": nowdate(), - "to_date": add_days(nowdate(), 10), - "approver": approver, - "status": "Approved" - }).insert() + shift_request = frappe.get_doc( + { + "doctype": "Shift Request", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "employee_name": "_Test Employee", + "from_date": nowdate(), + "to_date": add_days(nowdate(), 10), + "approver": approver, + "status": "Approved", + } + ).insert() if do_not_submit: return shift_request diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index 562a5739d67..2000eeb5443 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -24,20 +24,45 @@ from erpnext.hr.doctype.shift_assignment.shift_assignment import ( class ShiftType(Document): @frappe.whitelist() def process_auto_attendance(self): - if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin: + if ( + not cint(self.enable_auto_attendance) + or not self.process_attendance_after + or not self.last_sync_of_checkin + ): return filters = { - 'skip_auto_attendance':'0', - 'attendance':('is', 'not set'), - 'time':('>=', self.process_attendance_after), - 'shift_actual_end': ('<', self.last_sync_of_checkin), - 'shift': self.name + "skip_auto_attendance": "0", + "attendance": ("is", "not set"), + "time": (">=", self.process_attendance_after), + "shift_actual_end": ("<", self.last_sync_of_checkin), + "shift": self.name, } - logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time") - for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])): + logs = frappe.db.get_list( + "Employee Checkin", fields="*", filters=filters, order_by="employee,time" + ) + for key, group in itertools.groupby( + logs, key=lambda x: (x["employee"], x["shift_actual_start"]) + ): single_shift_logs = list(group) - attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs) - mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, in_time, out_time, self.name) + ( + attendance_status, + working_hours, + late_entry, + early_exit, + in_time, + out_time, + ) = self.get_attendance(single_shift_logs) + mark_attendance_and_link_log( + single_shift_logs, + attendance_status, + key[1].date(), + working_hours, + late_entry, + early_exit, + in_time, + out_time, + self.name, + ) for employee in self.get_assigned_employee(self.process_attendance_after, True): self.mark_absent_for_dates_with_no_attendance(employee) @@ -45,36 +70,66 @@ class ShiftType(Document): """Return attendance_status, working_hours, late_entry, early_exit, in_time, out_time for a set of logs belonging to a single shift. Assumtion: - 1. These logs belongs to an single shift, single employee and is not in a holiday date. - 2. Logs are in chronological order + 1. These logs belongs to an single shift, single employee and is not in a holiday date. + 2. Logs are in chronological order """ late_entry = early_exit = False - total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on) - if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)): + total_working_hours, in_time, out_time = calculate_working_hours( + logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on + ) + if ( + cint(self.enable_entry_grace_period) + and in_time + and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)) + ): late_entry = True - if cint(self.enable_exit_grace_period) and out_time and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)): + if ( + cint(self.enable_exit_grace_period) + and out_time + and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)) + ): early_exit = True - if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: - return 'Absent', total_working_hours, late_entry, early_exit, in_time, out_time - if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day: - return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time - return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time + if ( + self.working_hours_threshold_for_absent + and total_working_hours < self.working_hours_threshold_for_absent + ): + return "Absent", total_working_hours, late_entry, early_exit, in_time, out_time + if ( + self.working_hours_threshold_for_half_day + and total_working_hours < self.working_hours_threshold_for_half_day + ): + return "Half Day", total_working_hours, late_entry, early_exit, in_time, out_time + return "Present", total_working_hours, late_entry, early_exit, in_time, out_time def mark_absent_for_dates_with_no_attendance(self, employee): """Marks Absents for the given employee on working days in this shift which have no attendance marked. The Absent is marked starting from 'process_attendance_after' or employee creation date. """ - date_of_joining, relieving_date, employee_creation = frappe.db.get_value("Employee", employee, ["date_of_joining", "relieving_date", "creation"]) + date_of_joining, relieving_date, employee_creation = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date", "creation"] + ) if not date_of_joining: date_of_joining = employee_creation.date() start_date = max(getdate(self.process_attendance_after), date_of_joining) - actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, get_datetime(self.last_sync_of_checkin), True) - last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else get_datetime(self.last_sync_of_checkin) - prev_shift = get_employee_shift(employee, last_shift_time.date()-timedelta(days=1), True, 'reverse') + actual_shift_datetime = get_actual_start_end_datetime_of_shift( + employee, get_datetime(self.last_sync_of_checkin), True + ) + last_shift_time = ( + actual_shift_datetime[0] + if actual_shift_datetime[0] + else get_datetime(self.last_sync_of_checkin) + ) + prev_shift = get_employee_shift( + employee, last_shift_time.date() - timedelta(days=1), True, "reverse" + ) if prev_shift: - end_date = min(prev_shift.start_datetime.date(), relieving_date) if relieving_date else prev_shift.start_datetime.date() + end_date = ( + min(prev_shift.start_datetime.date(), relieving_date) + if relieving_date + else prev_shift.start_datetime.date() + ) else: return holiday_list_name = self.holiday_list @@ -84,37 +139,50 @@ class ShiftType(Document): for date in dates: shift_details = get_employee_shift(employee, date, True) if shift_details and shift_details.shift_type.name == self.name: - mark_attendance(employee, date, 'Absent', self.name) + attendance = mark_attendance(employee, date, "Absent", self.name) + if attendance: + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Comment", + "reference_doctype": "Attendance", + "reference_name": attendance, + "content": frappe._("Employee was marked Absent due to missing Employee Checkins."), + } + ).insert(ignore_permissions=True) def get_assigned_employee(self, from_date=None, consider_default_shift=False): - filters = {'start_date':('>', from_date), 'shift_type': self.name, 'docstatus': '1'} + filters = {"start_date": (">", from_date), "shift_type": self.name, "docstatus": "1"} if not from_date: del filters["start_date"] - assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True) + assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True) assigned_employees = [x[0] for x in assigned_employees] if consider_default_shift: - filters = {'default_shift': self.name, 'status': ['!=', 'Inactive']} - default_shift_employees = frappe.get_all('Employee', 'name', filters, as_list=True) + filters = {"default_shift": self.name, "status": ["!=", "Inactive"]} + default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True) default_shift_employees = [x[0] for x in default_shift_employees] - return list(set(assigned_employees+default_shift_employees)) + return list(set(assigned_employees + default_shift_employees)) return assigned_employees + def process_auto_attendance_for_all_shifts(): - shift_list = frappe.get_all('Shift Type', 'name', {'enable_auto_attendance':'1'}, as_list=True) + shift_list = frappe.get_all("Shift Type", "name", {"enable_auto_attendance": "1"}, as_list=True) for shift in shift_list: - doc = frappe.get_doc('Shift Type', shift[0]) + doc = frappe.get_doc("Shift Type", shift[0]) doc.process_auto_attendance() -def get_filtered_date_list(employee, start_date, end_date, filter_attendance=True, holiday_list=None): - """Returns a list of dates after removing the dates with attendance and holidays - """ + +def get_filtered_date_list( + employee, start_date, end_date, filter_attendance=True, holiday_list=None +): + """Returns a list of dates after removing the dates with attendance and holidays""" base_dates_query = """select adddate(%(start_date)s, t2.i*100 + t1.i*10 + t0.i) selected_date from (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0, (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1, (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2""" - condition_query = '' + condition_query = "" if filter_attendance: condition_query += """ and a.selected_date not in ( select attendance_date from `tabAttendance` @@ -126,10 +194,20 @@ def get_filtered_date_list(employee, start_date, end_date, filter_attendance=Tru parentfield = 'holidays' and parent = %(holiday_list)s and holiday_date between %(start_date)s and %(end_date)s)""" - dates = frappe.db.sql("""select * from + dates = frappe.db.sql( + """select * from ({base_dates_query}) as a where a.selected_date <= %(end_date)s {condition_query} - """.format(base_dates_query=base_dates_query, condition_query=condition_query), - {"employee":employee, "start_date":start_date, "end_date":end_date, "holiday_list":holiday_list}, as_list=True) + """.format( + base_dates_query=base_dates_query, condition_query=condition_query + ), + { + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "holiday_list": holiday_list, + }, + as_list=True, + ) return [getdate(date[0]) for date in dates] diff --git a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py index b523f0e01ce..920d8fd5475 100644 --- a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py +++ b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py @@ -1,15 +1,8 @@ - - def get_data(): return { - 'fieldname': 'shift', - 'non_standard_fieldnames': { - 'Shift Request': 'shift_type', - 'Shift Assignment': 'shift_type' - }, - 'transactions': [ - { - 'items': ['Attendance', 'Employee Checkin', 'Shift Request', 'Shift Assignment'] - } - ] + "fieldname": "shift", + "non_standard_fieldnames": {"Shift Request": "shift_type", "Shift Assignment": "shift_type"}, + "transactions": [ + {"items": ["Attendance", "Employee Checkin", "Shift Request", "Shift Assignment"]} + ], } diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 7b2ea215ad8..93a493c9d25 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -9,8 +9,13 @@ from frappe.utils import cint, flt, getdate, nowdate from frappe.utils.nestedset import get_descendants_of -class SubsidiaryCompanyError(frappe.ValidationError): pass -class ParentCompanyError(frappe.ValidationError): pass +class SubsidiaryCompanyError(frappe.ValidationError): + pass + + +class ParentCompanyError(frappe.ValidationError): + pass + class StaffingPlan(Document): def validate(self): @@ -33,11 +38,11 @@ class StaffingPlan(Document): self.total_estimated_budget = 0 for detail in self.get("staffing_details"): - #Set readonly fields + # Set readonly fields self.set_number_of_positions(detail) designation_counts = get_designation_counts(detail.designation, self.company) - detail.current_count = designation_counts['employee_count'] - detail.current_openings = designation_counts['job_openings'] + detail.current_count = designation_counts["employee_count"] + detail.current_openings = designation_counts["job_openings"] detail.total_estimated_cost = 0 if detail.number_of_positions > 0: @@ -52,80 +57,122 @@ class StaffingPlan(Document): def validate_overlap(self, staffing_plan_detail): # Validate if any submitted Staffing Plan exist for any Designations in this plan # and spd.vacancies>0 ? - overlap = frappe.db.sql("""select spd.parent + overlap = frappe.db.sql( + """select spd.parent from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name where spd.designation=%s and sp.docstatus=1 and sp.to_date >= %s and sp.from_date <= %s and sp.company = %s - """, (staffing_plan_detail.designation, self.from_date, self.to_date, self.company)) - if overlap and overlap [0][0]: - frappe.throw(_("Staffing Plan {0} already exist for designation {1}") - .format(overlap[0][0], staffing_plan_detail.designation)) + """, + (staffing_plan_detail.designation, self.from_date, self.to_date, self.company), + ) + if overlap and overlap[0][0]: + frappe.throw( + _("Staffing Plan {0} already exist for designation {1}").format( + overlap[0][0], staffing_plan_detail.designation + ) + ) def validate_with_parent_plan(self, staffing_plan_detail): - if not frappe.get_cached_value('Company', self.company, "parent_company"): - return # No parent, nothing to validate + if not frappe.get_cached_value("Company", self.company, "parent_company"): + return # No parent, nothing to validate # Get staffing plan applicable for the company (Parent Company) - parent_plan_details = get_active_staffing_plan_details(self.company, staffing_plan_detail.designation, self.from_date, self.to_date) + parent_plan_details = get_active_staffing_plan_details( + self.company, staffing_plan_detail.designation, self.from_date, self.to_date + ) if not parent_plan_details: - return #no staffing plan for any parent Company in hierarchy + return # no staffing plan for any parent Company in hierarchy # Fetch parent company which owns the staffing plan. NOTE: Parent could be higher up in the hierarchy parent_company = frappe.db.get_value("Staffing Plan", parent_plan_details[0].name, "company") # Parent plan available, validate with parent, siblings as well as children of staffing plan Company - if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ - flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): - frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ - for {2} as per staffing plan {3} for parent company {4}.").format( + if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or flt( + staffing_plan_detail.total_estimated_cost + ) > flt(parent_plan_details[0].total_estimated_cost): + frappe.throw( + _( + "You can only plan for upto {0} vacancies and budget {1} \ + for {2} as per staffing plan {3} for parent company {4}." + ).format( cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_plan_details[0].name, - parent_company), ParentCompanyError) + parent_company, + ), + ParentCompanyError, + ) - #Get vacanices already planned for all companies down the hierarchy of Parent Company - lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) - all_sibling_details = frappe.db.sql("""select sum(spd.vacancies) as vacancies, + # Get vacanices already planned for all companies down the hierarchy of Parent Company + lft, rgt = frappe.get_cached_value("Company", parent_company, ["lft", "rgt"]) + all_sibling_details = frappe.db.sql( + """select sum(spd.vacancies) as vacancies, sum(spd.total_estimated_cost) as total_estimated_cost from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name where spd.designation=%s and sp.docstatus=1 and sp.to_date >= %s and sp.from_date <=%s and sp.company in (select name from tabCompany where lft > %s and rgt < %s) - """, (staffing_plan_detail.designation, self.from_date, self.to_date, lft, rgt), as_dict = 1)[0] + """, + (staffing_plan_detail.designation, self.from_date, self.to_date, lft, rgt), + as_dict=1, + )[0] - if (cint(parent_plan_details[0].vacancies) < \ - (cint(staffing_plan_detail.vacancies) + cint(all_sibling_details.vacancies))) or \ - (flt(parent_plan_details[0].total_estimated_cost) < \ - (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): - frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ - You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format( + if ( + cint(parent_plan_details[0].vacancies) + < (cint(staffing_plan_detail.vacancies) + cint(all_sibling_details.vacancies)) + ) or ( + flt(parent_plan_details[0].total_estimated_cost) + < ( + flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost) + ) + ): + frappe.throw( + _( + "{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ + You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." + ).format( cint(all_sibling_details.vacancies), all_sibling_details.total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_company, cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, - parent_plan_details[0].name)) + parent_plan_details[0].name, + ) + ) def validate_with_subsidiary_plans(self, staffing_plan_detail): - #Valdate this plan with all child company plan - children_details = frappe.db.sql("""select sum(spd.vacancies) as vacancies, + # Valdate this plan with all child company plan + children_details = frappe.db.sql( + """select sum(spd.vacancies) as vacancies, sum(spd.total_estimated_cost) as total_estimated_cost from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name where spd.designation=%s and sp.docstatus=1 and sp.to_date >= %s and sp.from_date <=%s and sp.company in (select name from tabCompany where parent_company = %s) - """, (staffing_plan_detail.designation, self.from_date, self.to_date, self.company), as_dict = 1)[0] + """, + (staffing_plan_detail.designation, self.from_date, self.to_date, self.company), + as_dict=1, + )[0] - if children_details and \ - cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ - flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): - frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ - Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format( + if ( + children_details + and cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) + or flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost) + ): + frappe.throw( + _( + "Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ + Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" + ).format( self.company, cint(children_details.vacancies), children_details.total_estimated_cost, - frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError) + frappe.bold(staffing_plan_detail.designation), + ), + SubsidiaryCompanyError, + ) + @frappe.whitelist() def get_designation_counts(designation, company): @@ -133,25 +180,24 @@ def get_designation_counts(designation, company): return False employee_counts = {} - company_set = get_descendants_of('Company', company) + company_set = get_descendants_of("Company", company) company_set.append(company) - employee_counts["employee_count"] = frappe.db.get_value("Employee", - filters={ - 'designation': designation, - 'status': 'Active', - 'company': ('in', company_set) - }, fieldname=['count(name)']) + employee_counts["employee_count"] = frappe.db.get_value( + "Employee", + filters={"designation": designation, "status": "Active", "company": ("in", company_set)}, + fieldname=["count(name)"], + ) - employee_counts['job_openings'] = frappe.db.get_value("Job Opening", - filters={ - 'designation': designation, - 'status': 'Open', - 'company': ('in', company_set) - }, fieldname=['count(name)']) + employee_counts["job_openings"] = frappe.db.get_value( + "Job Opening", + filters={"designation": designation, "status": "Open", "company": ("in", company_set)}, + fieldname=["count(name)"], + ) return employee_counts + @frappe.whitelist() def get_active_staffing_plan_details(company, designation, from_date=None, to_date=None): if from_date is None: @@ -161,17 +207,22 @@ def get_active_staffing_plan_details(company, designation, from_date=None, to_da if not company or not designation: frappe.throw(_("Please select Company and Designation")) - staffing_plan = frappe.db.sql(""" + staffing_plan = frappe.db.sql( + """ select sp.name, spd.vacancies, spd.total_estimated_cost from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name where company=%s and spd.designation=%s and sp.docstatus=1 - and to_date >= %s and from_date <= %s """, (company, designation, from_date, to_date), as_dict = 1) + and to_date >= %s and from_date <= %s """, + (company, designation, from_date, to_date), + as_dict=1, + ) if not staffing_plan: - parent_company = frappe.get_cached_value('Company', company, "parent_company") + parent_company = frappe.get_cached_value("Company", company, "parent_company") if parent_company: - staffing_plan = get_active_staffing_plan_details(parent_company, - designation, from_date, to_date) + staffing_plan = get_active_staffing_plan_details( + parent_company, designation, from_date, to_date + ) # Only a single staffing plan can be active for a designation on given date return staffing_plan if staffing_plan else None diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py b/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py index c04e5853a52..0f555d9db2d 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): - return { - 'fieldname': 'staffing_plan', - 'transactions': [ - { - 'items': ['Job Opening'] - } - ], - } + return { + "fieldname": "staffing_plan", + "transactions": [{"items": ["Job Opening"]}], + } diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py index 8ff0dbbc28e..a3adbbd56a5 100644 --- a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py @@ -13,6 +13,7 @@ from erpnext.hr.doctype.staffing_plan.staffing_plan import ( test_dependencies = ["Designation"] + class TestStaffingPlan(unittest.TestCase): def test_staffing_plan(self): _set_up() @@ -24,11 +25,10 @@ class TestStaffingPlan(unittest.TestCase): staffing_plan.name = "Test" staffing_plan.from_date = nowdate() staffing_plan.to_date = add_days(nowdate(), 10) - staffing_plan.append("staffing_details", { - "designation": "Designer", - "vacancies": 6, - "estimated_cost_per_position": 50000 - }) + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 6, "estimated_cost_per_position": 50000}, + ) staffing_plan.insert() staffing_plan.submit() self.assertEqual(staffing_plan.total_estimated_budget, 300000.00) @@ -42,11 +42,10 @@ class TestStaffingPlan(unittest.TestCase): staffing_plan.name = "Test 1" staffing_plan.from_date = nowdate() staffing_plan.to_date = add_days(nowdate(), 10) - staffing_plan.append("staffing_details", { - "designation": "Designer", - "vacancies": 3, - "estimated_cost_per_position": 45000 - }) + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 3, "estimated_cost_per_position": 45000}, + ) self.assertRaises(SubsidiaryCompanyError, staffing_plan.insert) def test_staffing_plan_parent_company(self): @@ -58,11 +57,10 @@ class TestStaffingPlan(unittest.TestCase): staffing_plan.name = "Test" staffing_plan.from_date = nowdate() staffing_plan.to_date = add_days(nowdate(), 10) - staffing_plan.append("staffing_details", { - "designation": "Designer", - "vacancies": 7, - "estimated_cost_per_position": 50000 - }) + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 7, "estimated_cost_per_position": 50000}, + ) staffing_plan.insert() staffing_plan.submit() self.assertEqual(staffing_plan.total_estimated_budget, 350000.00) @@ -73,19 +71,20 @@ class TestStaffingPlan(unittest.TestCase): staffing_plan.name = "Test 1" staffing_plan.from_date = nowdate() staffing_plan.to_date = add_days(nowdate(), 10) - staffing_plan.append("staffing_details", { - "designation": "Designer", - "vacancies": 7, - "estimated_cost_per_position": 60000 - }) + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 7, "estimated_cost_per_position": 60000}, + ) staffing_plan.insert() self.assertRaises(ParentCompanyError, staffing_plan.submit) + def _set_up(): for doctype in ["Staffing Plan", "Staffing Plan Detail"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) make_company() + def make_company(): if frappe.db.exists("Company", "_Test Company 10"): return diff --git a/erpnext/hr/doctype/training_event/test_training_event.py b/erpnext/hr/doctype/training_event/test_training_event.py index f4329c9fe70..ec7eb74da9e 100644 --- a/erpnext/hr/doctype/training_event/test_training_event.py +++ b/erpnext/hr/doctype/training_event/test_training_event.py @@ -14,10 +14,7 @@ class TestTrainingEvent(unittest.TestCase): create_training_program("Basic Training") employee = make_employee("robert_loan@trainig.com") employee2 = make_employee("suzie.tan@trainig.com") - self.attendees = [ - {"employee": employee}, - {"employee": employee2} - ] + self.attendees = [{"employee": employee}, {"employee": employee2}] def test_training_event_status_update(self): training_event = create_training_event(self.attendees) @@ -43,20 +40,25 @@ class TestTrainingEvent(unittest.TestCase): def create_training_program(training_program): if not frappe.db.get_value("Training Program", training_program): - frappe.get_doc({ - "doctype": "Training Program", - "training_program": training_program, - "description": training_program - }).insert() + frappe.get_doc( + { + "doctype": "Training Program", + "training_program": training_program, + "description": training_program, + } + ).insert() + def create_training_event(attendees): - return frappe.get_doc({ - "doctype": "Training Event", - "event_name": "Basic Training Event", - "training_program": "Basic Training", - "location": "Union Square", - "start_time": add_days(today(), 5), - "end_time": add_days(today(), 6), - "introduction": "Welcome to the Basic Training Event", - "employees": attendees - }).insert() + return frappe.get_doc( + { + "doctype": "Training Event", + "event_name": "Basic Training Event", + "training_program": "Basic Training", + "location": "Union Square", + "start_time": add_days(today(), 5), + "end_time": add_days(today(), 6), + "introduction": "Welcome to the Basic Training Event", + "employees": attendees, + } + ).insert() diff --git a/erpnext/hr/doctype/training_event/training_event.py b/erpnext/hr/doctype/training_event/training_event.py index c8c8bbe7339..59972bb2f3f 100644 --- a/erpnext/hr/doctype/training_event/training_event.py +++ b/erpnext/hr/doctype/training_event/training_event.py @@ -19,21 +19,20 @@ class TrainingEvent(Document): self.set_status_for_attendees() def set_employee_emails(self): - self.employee_emails = ', '.join(get_employee_emails([d.employee - for d in self.employees])) + self.employee_emails = ", ".join(get_employee_emails([d.employee for d in self.employees])) def validate_period(self): if time_diff_in_seconds(self.end_time, self.start_time) <= 0: - frappe.throw(_('End time cannot be before start time')) + frappe.throw(_("End time cannot be before start time")) def set_status_for_attendees(self): - if self.event_status == 'Completed': + if self.event_status == "Completed": for employee in self.employees: - if employee.attendance == 'Present' and employee.status != 'Feedback Submitted': - employee.status = 'Completed' + if employee.attendance == "Present" and employee.status != "Feedback Submitted": + employee.status = "Completed" - elif self.event_status == 'Scheduled': + elif self.event_status == "Scheduled": for employee in self.employees: - employee.status = 'Open' + employee.status = "Open" self.db_update_all() diff --git a/erpnext/hr/doctype/training_event/training_event_dashboard.py b/erpnext/hr/doctype/training_event/training_event_dashboard.py index 8c4162d5010..ca13938e585 100644 --- a/erpnext/hr/doctype/training_event/training_event_dashboard.py +++ b/erpnext/hr/doctype/training_event/training_event_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'training_event', - 'transactions': [ - { - 'items': ['Training Result', 'Training Feedback'] - }, - ], - } + return { + "fieldname": "training_event", + "transactions": [ + {"items": ["Training Result", "Training Feedback"]}, + ], + } diff --git a/erpnext/hr/doctype/training_feedback/test_training_feedback.py b/erpnext/hr/doctype/training_feedback/test_training_feedback.py index 58ed6231003..c787b7038fe 100644 --- a/erpnext/hr/doctype/training_feedback/test_training_feedback.py +++ b/erpnext/hr/doctype/training_feedback/test_training_feedback.py @@ -32,10 +32,9 @@ class TestTrainingFeedback(unittest.TestCase): self.assertRaises(frappe.ValidationError, feedback.save) # cannot record feedback for absent employee - employee = frappe.db.get_value("Training Event Employee", { - "parent": training_event.name, - "employee": self.employee - }, "name") + employee = frappe.db.get_value( + "Training Event Employee", {"parent": training_event.name, "employee": self.employee}, "name" + ) frappe.db.set_value("Training Event Employee", employee, "attendance", "Absent") feedback = create_training_feedback(training_event.name, self.employee) @@ -52,10 +51,9 @@ class TestTrainingFeedback(unittest.TestCase): feedback = create_training_feedback(training_event.name, self.employee) feedback.submit() - status = frappe.db.get_value("Training Event Employee", { - "parent": training_event.name, - "employee": self.employee - }, "status") + status = frappe.db.get_value( + "Training Event Employee", {"parent": training_event.name, "employee": self.employee}, "status" + ) self.assertEqual(status, "Feedback Submitted") @@ -64,9 +62,11 @@ class TestTrainingFeedback(unittest.TestCase): def create_training_feedback(event, employee): - return frappe.get_doc({ - "doctype": "Training Feedback", - "training_event": event, - "employee": employee, - "feedback": "Test" - }) + return frappe.get_doc( + { + "doctype": "Training Feedback", + "training_event": event, + "employee": employee, + "feedback": "Test", + } + ) diff --git a/erpnext/hr/doctype/training_feedback/training_feedback.py b/erpnext/hr/doctype/training_feedback/training_feedback.py index 1f9ec3b0b8a..d5de28ed2d5 100644 --- a/erpnext/hr/doctype/training_feedback/training_feedback.py +++ b/erpnext/hr/doctype/training_feedback/training_feedback.py @@ -13,32 +13,35 @@ class TrainingFeedback(Document): if training_event.docstatus != 1: frappe.throw(_("{0} must be submitted").format(_("Training Event"))) - emp_event_details = frappe.db.get_value("Training Event Employee", { - "parent": self.training_event, - "employee": self.employee - }, ["name", "attendance"], as_dict=True) + emp_event_details = frappe.db.get_value( + "Training Event Employee", + {"parent": self.training_event, "employee": self.employee}, + ["name", "attendance"], + as_dict=True, + ) if not emp_event_details: - frappe.throw(_("Employee {0} not found in Training Event Participants.").format( - frappe.bold(self.employee_name))) + frappe.throw( + _("Employee {0} not found in Training Event Participants.").format( + frappe.bold(self.employee_name) + ) + ) if emp_event_details.attendance == "Absent": frappe.throw(_("Feedback cannot be recorded for an absent Employee.")) def on_submit(self): - employee = frappe.db.get_value("Training Event Employee", { - "parent": self.training_event, - "employee": self.employee - }) + employee = frappe.db.get_value( + "Training Event Employee", {"parent": self.training_event, "employee": self.employee} + ) if employee: frappe.db.set_value("Training Event Employee", employee, "status", "Feedback Submitted") def on_cancel(self): - employee = frappe.db.get_value("Training Event Employee", { - "parent": self.training_event, - "employee": self.employee - }) + employee = frappe.db.get_value( + "Training Event Employee", {"parent": self.training_event, "employee": self.employee} + ) if employee: frappe.db.set_value("Training Event Employee", employee, "status", "Completed") diff --git a/erpnext/hr/doctype/training_program/training_program_dashboard.py b/erpnext/hr/doctype/training_program/training_program_dashboard.py index 51137d162c7..1735db18e12 100644 --- a/erpnext/hr/doctype/training_program/training_program_dashboard.py +++ b/erpnext/hr/doctype/training_program/training_program_dashboard.py @@ -1,14 +1,10 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'training_program', - 'transactions': [ - { - 'label': _('Training Events'), - 'items': ['Training Event'] - }, - ] + "fieldname": "training_program", + "transactions": [ + {"label": _("Training Events"), "items": ["Training Event"]}, + ], } diff --git a/erpnext/hr/doctype/training_result/test_training_result.py b/erpnext/hr/doctype/training_result/test_training_result.py index 1735ff4e341..136543cbe1a 100644 --- a/erpnext/hr/doctype/training_result/test_training_result.py +++ b/erpnext/hr/doctype/training_result/test_training_result.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Training Result') + class TestTrainingResult(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/training_result/training_result.py b/erpnext/hr/doctype/training_result/training_result.py index bb5c71e7a15..48a5b2c2e9b 100644 --- a/erpnext/hr/doctype/training_result/training_result.py +++ b/erpnext/hr/doctype/training_result/training_result.py @@ -13,22 +13,22 @@ class TrainingResult(Document): def validate(self): training_event = frappe.get_doc("Training Event", self.training_event) if training_event.docstatus != 1: - frappe.throw(_('{0} must be submitted').format(_('Training Event'))) + frappe.throw(_("{0} must be submitted").format(_("Training Event"))) - self.employee_emails = ', '.join(get_employee_emails([d.employee - for d in self.employees])) + self.employee_emails = ", ".join(get_employee_emails([d.employee for d in self.employees])) def on_submit(self): training_event = frappe.get_doc("Training Event", self.training_event) - training_event.status = 'Completed' + training_event.status = "Completed" for e in self.employees: for e1 in training_event.employees: if e1.employee == e.employee: - e1.status = 'Completed' + e1.status = "Completed" break training_event.save() + @frappe.whitelist() def get_employees(training_event): return frappe.get_doc("Training Event", training_event).employees diff --git a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py index 4c7bd805f98..537c20633be 100644 --- a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py @@ -10,12 +10,15 @@ import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.upload_attendance.upload_attendance import get_data -test_dependencies = ['Holiday List'] +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') + 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") @@ -27,14 +30,13 @@ class TestUploadAttendance(unittest.TestCase): employee_doc.date_of_joining = date_of_joining employee_doc.relieving_date = relieving_date employee_doc.save() - args = { - "from_date": from_date, - "to_date": to_date - } + args = {"from_date": from_date, "to_date": to_date} data = get_data(args) filtered_data = [] for row in data: if row[1] == employee: filtered_data.append(row) for row in filtered_data: - self.assertTrue(getdate(row[3]) >= getdate(date_of_joining) and getdate(row[3]) <= getdate(relieving_date)) + self.assertTrue( + getdate(row[3]) >= getdate(date_of_joining) and getdate(row[3]) <= getdate(relieving_date) + ) diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py index 94eb3001009..a66a48124da 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py @@ -17,6 +17,7 @@ from erpnext.hr.utils import get_holiday_dates_for_employee class UploadAttendance(Document): pass + @frappe.whitelist() def get_template(): if not frappe.has_permission("Attendance", "create"): @@ -38,29 +39,37 @@ def get_template(): return # write out response as a type csv - frappe.response['result'] = cstr(w.getvalue()) - frappe.response['type'] = 'csv' - frappe.response['doctype'] = "Attendance" + frappe.response["result"] = cstr(w.getvalue()) + frappe.response["type"] = "csv" + frappe.response["doctype"] = "Attendance" + def add_header(w): - status = ", ".join((frappe.get_meta("Attendance").get_field("status").options or "").strip().split("\n")) + status = ", ".join( + (frappe.get_meta("Attendance").get_field("status").options or "").strip().split("\n") + ) w.writerow(["Notes:"]) w.writerow(["Please do not change the template headings"]) w.writerow(["Status should be one of these values: " + status]) w.writerow(["If you are overwriting existing attendance records, 'ID' column mandatory"]) - w.writerow(["ID", "Employee", "Employee Name", "Date", "Status", "Leave Type", - "Company", "Naming Series"]) + w.writerow( + ["ID", "Employee", "Employee Name", "Date", "Status", "Leave Type", "Company", "Naming Series"] + ) return w + def add_data(w, args): data = get_data(args) writedata(w, data) return w + def get_data(args): dates = get_dates(args) employees = get_active_employees() - holidays = get_holidays_for_employees([employee.name for employee in employees], args["from_date"], args["to_date"]) + holidays = get_holidays_for_employees( + [employee.name for employee in employees], args["from_date"], args["to_date"] + ) existing_attendance_records = get_existing_attendance_records(args) data = [] for date in dates: @@ -71,27 +80,33 @@ def get_data(args): if getdate(date) > getdate(employee.relieving_date): continue existing_attendance = {} - if existing_attendance_records \ - and tuple([getdate(date), employee.name]) in existing_attendance_records \ - and getdate(employee.date_of_joining) <= getdate(date) \ - and getdate(employee.relieving_date) >= getdate(date): - existing_attendance = existing_attendance_records[tuple([getdate(date), employee.name])] + if ( + existing_attendance_records + and tuple([getdate(date), employee.name]) in existing_attendance_records + and getdate(employee.date_of_joining) <= getdate(date) + and getdate(employee.relieving_date) >= getdate(date) + ): + existing_attendance = existing_attendance_records[tuple([getdate(date), employee.name])] employee_holiday_list = get_holiday_list_for_employee(employee.name) row = [ existing_attendance and existing_attendance.name or "", - employee.name, employee.employee_name, date, + employee.name, + employee.employee_name, + date, existing_attendance and existing_attendance.status or "", - existing_attendance and existing_attendance.leave_type or "", employee.company, + existing_attendance and existing_attendance.leave_type or "", + employee.company, existing_attendance and existing_attendance.naming_series or get_naming_series(), ] if date in holidays[employee_holiday_list]: - row[4] = "Holiday" + row[4] = "Holiday" data.append(row) return data + def get_holidays_for_employees(employees, from_date, to_date): holidays = {} for employee in employees: @@ -102,30 +117,35 @@ def get_holidays_for_employees(employees, from_date, to_date): return holidays + def writedata(w, data): for row in data: w.writerow(row) + def get_dates(args): """get list of dates in between from date and to date""" no_of_days = date_diff(add_days(args["to_date"], 1), args["from_date"]) dates = [add_days(args["from_date"], i) for i in range(0, no_of_days)] return dates + def get_active_employees(): - employees = frappe.db.get_all('Employee', - fields=['name', 'employee_name', 'date_of_joining', 'company', 'relieving_date'], - filters={ - 'docstatus': ['<', 2], - 'status': 'Active' - } + employees = frappe.db.get_all( + "Employee", + fields=["name", "employee_name", "date_of_joining", "company", "relieving_date"], + filters={"docstatus": ["<", 2], "status": "Active"}, ) return employees + def get_existing_attendance_records(args): - attendance = frappe.db.sql("""select name, attendance_date, employee, status, leave_type, naming_series + attendance = frappe.db.sql( + """select name, attendance_date, employee, status, leave_type, naming_series from `tabAttendance` where attendance_date between %s and %s and docstatus < 2""", - (args["from_date"], args["to_date"]), as_dict=1) + (args["from_date"], args["to_date"]), + as_dict=1, + ) existing_attendance = {} for att in attendance: @@ -133,6 +153,7 @@ def get_existing_attendance_records(args): return existing_attendance + def get_naming_series(): series = frappe.get_meta("Attendance").get_field("naming_series").options.strip().split("\n") if not series: @@ -146,15 +167,16 @@ def upload(): raise frappe.PermissionError from frappe.utils.csvutils import read_csv_content + rows = read_csv_content(frappe.local.uploaded_file) if not rows: frappe.throw(_("Please select a csv file")) frappe.enqueue(import_attendances, rows=rows, now=True if len(rows) < 200 else False) -def import_attendances(rows): +def import_attendances(rows): def remove_holidays(rows): - rows = [ row for row in rows if row[4] != "Holiday"] + rows = [row for row in rows if row[4] != "Holiday"] return rows from frappe.modules import scrub @@ -172,7 +194,8 @@ def import_attendances(rows): from frappe.utils.csvutils import check_record, import_doc for i, row in enumerate(rows): - if not row: continue + if not row: + continue row_idx = i + 5 d = frappe._dict(zip(columns, row)) @@ -183,16 +206,12 @@ def import_attendances(rows): try: check_record(d) ret.append(import_doc(d, "Attendance", 1, row_idx, submit=True)) - frappe.publish_realtime('import_attendance', dict( - progress=i, - total=len(rows) - )) + frappe.publish_realtime("import_attendance", dict(progress=i, total=len(rows))) except AttributeError: pass except Exception as e: error = True - ret.append('Error for row (#%d) %s : %s' % (row_idx, - len(row)>1 and row[1] or "", cstr(e))) + ret.append("Error for row (#%d) %s : %s" % (row_idx, len(row) > 1 and row[1] or "", cstr(e))) frappe.errprint(frappe.get_traceback()) if error: @@ -200,7 +219,4 @@ def import_attendances(rows): else: frappe.db.commit() - frappe.publish_realtime('import_attendance', dict( - messages=ret, - error=error - )) + frappe.publish_realtime("import_attendance", dict(messages=ret, error=error)) diff --git a/erpnext/hr/doctype/vehicle/test_vehicle.py b/erpnext/hr/doctype/vehicle/test_vehicle.py index c5ea5a38c86..97fe651122c 100644 --- a/erpnext/hr/doctype/vehicle/test_vehicle.py +++ b/erpnext/hr/doctype/vehicle/test_vehicle.py @@ -8,18 +8,21 @@ from frappe.utils import random_string # test_records = frappe.get_test_records('Vehicle') + class TestVehicle(unittest.TestCase): def test_make_vehicle(self): - vehicle = frappe.get_doc({ - "doctype": "Vehicle", - "license_plate": random_string(10).upper(), - "make": "Maruti", - "model": "PCM", - "last_odometer":5000, - "acquisition_date":frappe.utils.nowdate(), - "location": "Mumbai", - "chassis_no": "1234ABCD", - "uom": "Litre", - "vehicle_value":frappe.utils.flt(500000) - }) + vehicle = frappe.get_doc( + { + "doctype": "Vehicle", + "license_plate": random_string(10).upper(), + "make": "Maruti", + "model": "PCM", + "last_odometer": 5000, + "acquisition_date": frappe.utils.nowdate(), + "location": "Mumbai", + "chassis_no": "1234ABCD", + "uom": "Litre", + "vehicle_value": frappe.utils.flt(500000), + } + ) vehicle.insert() diff --git a/erpnext/hr/doctype/vehicle/vehicle.py b/erpnext/hr/doctype/vehicle/vehicle.py index 946233b5481..22c14c37278 100644 --- a/erpnext/hr/doctype/vehicle/vehicle.py +++ b/erpnext/hr/doctype/vehicle/vehicle.py @@ -15,9 +15,15 @@ class Vehicle(Document): if getdate(self.carbon_check_date) > getdate(): frappe.throw(_("Last carbon check date cannot be a future date")) + def get_timeline_data(doctype, name): - '''Return timeline for vehicle log''' - return dict(frappe.db.sql('''select unix_timestamp(date), count(*) + """Return timeline for vehicle log""" + return dict( + frappe.db.sql( + """select unix_timestamp(date), count(*) from `tabVehicle Log` where license_plate=%s and date > date_sub(curdate(), interval 1 year) - group by date''', name)) + group by date""", + name, + ) + ) diff --git a/erpnext/hr/doctype/vehicle/vehicle_dashboard.py b/erpnext/hr/doctype/vehicle/vehicle_dashboard.py index bb38ab9d6b5..758dfbd60a4 100644 --- a/erpnext/hr/doctype/vehicle/vehicle_dashboard.py +++ b/erpnext/hr/doctype/vehicle/vehicle_dashboard.py @@ -1,21 +1,13 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on logs against this Vehicle. See timeline below for details'), - 'fieldname': 'license_plate', - 'non_standard_fieldnames':{ - 'Delivery Trip': 'vehicle' - }, - 'transactions': [ - { - 'items': ['Vehicle Log'] - }, - { - 'items': ['Delivery Trip'] - } - ] + "heatmap": True, + "heatmap_message": _( + "This is based on logs against this Vehicle. See timeline below for details" + ), + "fieldname": "license_plate", + "non_standard_fieldnames": {"Delivery Trip": "vehicle"}, + "transactions": [{"items": ["Vehicle Log"]}, {"items": ["Delivery Trip"]}], } diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py index acd50f278cd..7c6fd8cb212 100644 --- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py @@ -12,7 +12,9 @@ from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim class TestVehicleLog(unittest.TestCase): def setUp(self): - employee_id = frappe.db.sql("""select name from `tabEmployee` where name='testdriver@example.com'""") + 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: @@ -27,11 +29,11 @@ class TestVehicleLog(unittest.TestCase): def test_make_vehicle_log_and_syncing_of_odometer_value(self): vehicle_log = make_vehicle_log(self.license_plate, self.employee_id) - #checking value of vehicle odometer value on submit. + # checking value of vehicle odometer value on submit. vehicle = frappe.get_doc("Vehicle", self.license_plate) self.assertEqual(vehicle.last_odometer, vehicle_log.odometer) - #checking value vehicle odometer on vehicle log cancellation. + # checking value vehicle odometer on vehicle log cancellation. last_odometer = vehicle_log.last_odometer current_odometer = vehicle_log.odometer distance_travelled = current_odometer - last_odometer @@ -48,7 +50,7 @@ class TestVehicleLog(unittest.TestCase): expense_claim = make_expense_claim(vehicle_log.name) fuel_expense = expense_claim.expenses[0].amount - self.assertEqual(fuel_expense, 50*500) + self.assertEqual(fuel_expense, 50 * 500) vehicle_log.cancel() frappe.delete_doc("Expense Claim", expense_claim.name) @@ -67,8 +69,9 @@ class TestVehicleLog(unittest.TestCase): def get_vehicle(employee_id): - license_plate=random_string(10).upper() - vehicle = frappe.get_doc({ + license_plate = random_string(10).upper() + vehicle = frappe.get_doc( + { "doctype": "Vehicle", "license_plate": cstr(license_plate), "make": "Maruti", @@ -79,8 +82,9 @@ def get_vehicle(employee_id): "location": "Mumbai", "chassis_no": "1234ABCD", "uom": "Litre", - "vehicle_value": flt(500000) - }) + "vehicle_value": flt(500000), + } + ) try: vehicle.insert() except frappe.DuplicateEntryError: @@ -89,29 +93,37 @@ def get_vehicle(employee_id): 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) - }) + 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.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() diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.py b/erpnext/hr/doctype/vehicle_log/vehicle_log.py index e414141efb5..2c1d9a4efec 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.py @@ -11,17 +11,24 @@ from frappe.utils import flt class VehicleLog(Document): def validate(self): if flt(self.odometer) < flt(self.last_odometer): - frappe.throw(_("Current Odometer Value should be greater than Last Odometer Value {0}").format(self.last_odometer)) + frappe.throw( + _("Current Odometer Value should be greater than Last Odometer Value {0}").format( + self.last_odometer + ) + ) def on_submit(self): frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", self.odometer) def on_cancel(self): distance_travelled = self.odometer - self.last_odometer - if(distance_travelled > 0): - updated_odometer_value = int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled + if distance_travelled > 0: + updated_odometer_value = ( + int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled + ) frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", updated_odometer_value) + @frappe.whitelist() def make_expense_claim(docname): expense_claim = frappe.db.exists("Expense Claim", {"vehicle_log": docname}) @@ -39,9 +46,8 @@ def make_expense_claim(docname): exp_claim.employee = vehicle_log.employee exp_claim.vehicle_log = vehicle_log.name exp_claim.remark = _("Expense Claim for Vehicle Log {0}").format(vehicle_log.name) - exp_claim.append("expenses", { - "expense_date": vehicle_log.date, - "description": _("Vehicle Expenses"), - "amount": claim_amount - }) + exp_claim.append( + "expenses", + {"expense_date": vehicle_log.date, "description": _("Vehicle Expenses"), "amount": claim_amount}, + ) return exp_claim.as_dict() diff --git a/erpnext/hr/notification/training_feedback/training_feedback.py b/erpnext/hr/notification/training_feedback/training_feedback.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/hr/notification/training_feedback/training_feedback.py +++ b/erpnext/hr/notification/training_feedback/training_feedback.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.py b/erpnext/hr/notification/training_scheduled/training_scheduled.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.py +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index 01d95a7051a..3674912dc06 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -1,29 +1,29 @@ - import frappe @frappe.whitelist() def get_children(parent=None, company=None, exclude_node=None): - filters = [['status', '!=', 'Left']] - if company and company != 'All Companies': - filters.append(['company', '=', company]) + filters = [["status", "!=", "Left"]] + if company and company != "All Companies": + filters.append(["company", "=", company]) if parent and company and parent != company: - filters.append(['reports_to', '=', parent]) + filters.append(["reports_to", "=", parent]) else: - filters.append(['reports_to', '=', '']) + filters.append(["reports_to", "=", ""]) if exclude_node: - filters.append(['name', '!=', exclude_node]) + filters.append(["name", "!=", exclude_node]) - employees = frappe.get_list('Employee', - fields=['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'], + employees = frappe.get_list( + "Employee", + fields=["employee_name as name", "name as id", "reports_to", "image", "designation as title"], filters=filters, - order_by='name' + order_by="name", ) for employee in employees: - is_expandable = frappe.db.count('Employee', filters={'reports_to': employee.get('id')}) + is_expandable = frappe.db.count("Employee", filters={"reports_to": employee.get("id")}) employee.connections = get_connections(employee.id) employee.expandable = 1 if is_expandable else 0 @@ -33,16 +33,12 @@ def get_children(parent=None, company=None, exclude_node=None): def get_connections(employee): num_connections = 0 - nodes_to_expand = frappe.get_list('Employee', filters=[ - ['reports_to', '=', employee] - ]) + nodes_to_expand = frappe.get_list("Employee", filters=[["reports_to", "=", employee]]) num_connections += len(nodes_to_expand) while nodes_to_expand: parent = nodes_to_expand.pop(0) - descendants = frappe.get_list('Employee', filters=[ - ['reports_to', '=', parent.name] - ]) + descendants = frappe.get_list("Employee", filters=[["reports_to", "=", parent.name]]) num_connections += len(descendants) nodes_to_expand.extend(descendants) diff --git a/erpnext/hr/page/team_updates/team_updates.py b/erpnext/hr/page/team_updates/team_updates.py index 126ed898c97..c1fcb735850 100644 --- a/erpnext/hr/page/team_updates/team_updates.py +++ b/erpnext/hr/page/team_updates/team_updates.py @@ -1,19 +1,23 @@ - import frappe from email_reply_parser import EmailReplyParser @frappe.whitelist() def get_data(start=0): - #frappe.only_for('Employee', 'System Manager') - data = frappe.get_all('Communication', - fields=('content', 'text_content', 'sender', 'creation'), - filters=dict(reference_doctype='Daily Work Summary'), - order_by='creation desc', limit=40, start=start) + # frappe.only_for('Employee', 'System Manager') + data = frappe.get_all( + "Communication", + fields=("content", "text_content", "sender", "creation"), + filters=dict(reference_doctype="Daily Work Summary"), + order_by="creation desc", + limit=40, + start=start, + ) for d in data: - d.sender_name = frappe.db.get_value("Employee", {"user_id": d.sender}, - "employee_name") or d.sender + d.sender_name = ( + frappe.db.get_value("Employee", {"user_id": d.sender}, "employee_name") or d.sender + ) if d.text_content: d.content = frappe.utils.md_to_html(EmailReplyParser.parse_reply(d.text_content)) diff --git a/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py b/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py index 63764bb8a9c..d93688a4925 100644 --- a/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py +++ b/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py @@ -9,51 +9,53 @@ from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_user_em def execute(filters=None): - if not filters.group: return [], [] + if not filters.group: + return [], [] columns, data = get_columns(), get_data(filters) return columns, data + def get_columns(filters=None): columns = [ - { - "label": _("User"), - "fieldname": "user", - "fieldtype": "Data", - "width": 300 - }, + {"label": _("User"), "fieldname": "user", "fieldtype": "Data", "width": 300}, { "label": _("Replies"), "fieldname": "count", "fieldtype": "data", "width": 100, - "align": 'right', + "align": "right", }, { "label": _("Total"), "fieldname": "total", "fieldtype": "data", "width": 100, - "align": 'right', - } + "align": "right", + }, ] return columns + def get_data(filters): - daily_summary_emails = frappe.get_all('Daily Work Summary', - fields=["name"], - filters=[["creation","Between", filters.range]]) - daily_summary_emails = [d.get('name') for d in daily_summary_emails] - replies = frappe.get_all('Communication', - fields=['content', 'text_content', 'sender'], - filters=[['reference_doctype','=', 'Daily Work Summary'], - ['reference_name', 'in', daily_summary_emails], - ['communication_type', '=', 'Communication'], - ['sent_or_received', '=', 'Received']], - order_by='creation asc') + daily_summary_emails = frappe.get_all( + "Daily Work Summary", fields=["name"], filters=[["creation", "Between", filters.range]] + ) + daily_summary_emails = [d.get("name") for d in daily_summary_emails] + replies = frappe.get_all( + "Communication", + fields=["content", "text_content", "sender"], + filters=[ + ["reference_doctype", "=", "Daily Work Summary"], + ["reference_name", "in", daily_summary_emails], + ["communication_type", "=", "Communication"], + ["sent_or_received", "=", "Received"], + ], + order_by="creation asc", + ) data = [] total = len(daily_summary_emails) for user in get_user_emails_from_group(filters.group): - user_name = frappe.get_value('User', user, 'full_name') + user_name = frappe.get_value("User", user, "full_name") count = len([d for d in replies if d.sender == user]) data.append([user_name, count, total]) return data diff --git a/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py b/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py index 62b83f26a61..29532f7680a 100644 --- a/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py +++ b/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py @@ -7,7 +7,8 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} advances_list = get_advances(filters) columns = get_columns() @@ -18,8 +19,16 @@ def execute(filters=None): data = [] for advance in advances_list: - row = [advance.name, advance.employee, advance.company, advance.posting_date, - advance.advance_amount, advance.paid_amount, advance.claimed_amount, advance.status] + row = [ + advance.name, + advance.employee, + advance.company, + advance.posting_date, + advance.advance_amount, + advance.paid_amount, + advance.claimed_amount, + advance.status, + ] data.append(row) return columns, data @@ -32,54 +41,40 @@ def get_columns(): "fieldname": "title", "fieldtype": "Link", "options": "Employee Advance", - "width": 120 + "width": 120, }, { "label": _("Employee"), "fieldname": "employee", "fieldtype": "Link", "options": "Employee", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, { "label": _("Advance Amount"), "fieldname": "advance_amount", "fieldtype": "Currency", - "width": 120 - }, - { - "label": _("Paid Amount"), - "fieldname": "paid_amount", - "fieldtype": "Currency", - "width": 120 + "width": 120, }, + {"label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", "width": 120}, { "label": _("Claimed Amount"), "fieldname": "claimed_amount", "fieldtype": "Currency", - "width": 120 + "width": 120, }, - { - "label": _("Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 120 - } + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 120}, ] + def get_conditions(filters): conditions = "" @@ -96,10 +91,15 @@ def get_conditions(filters): return conditions + def get_advances(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select name, employee, paid_amount, status, advance_amount, claimed_amount, company, + return frappe.db.sql( + """select name, employee, paid_amount, status, advance_amount, claimed_amount, company, posting_date, purpose from `tabEmployee Advance` - where docstatus<2 %s order by posting_date, name desc""" % - conditions, filters, as_dict=1) + where docstatus<2 %s order by posting_date, name desc""" + % conditions, + filters, + as_dict=1, + ) diff --git a/erpnext/hr/report/employee_analytics/employee_analytics.py b/erpnext/hr/report/employee_analytics/employee_analytics.py index 3a75276cb07..12be156ab9b 100644 --- a/erpnext/hr/report/employee_analytics/employee_analytics.py +++ b/erpnext/hr/report/employee_analytics/employee_analytics.py @@ -7,10 +7,11 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if not filters["company"]: - frappe.throw(_('{0} is mandatory').format(_('Company'))) + frappe.throw(_("{0} is mandatory").format(_("Company"))) columns = get_columns() employees = get_employees(filters) @@ -20,28 +21,41 @@ def execute(filters=None): for department in parameters_result: parameters.append(department) - chart = get_chart_data(parameters,employees, filters) + chart = get_chart_data(parameters, employees, filters) return columns, employees, None, chart + def get_columns(): return [ - _("Employee") + ":Link/Employee:120", _("Name") + ":Data:200", _("Date of Birth")+ ":Date:100", - _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120", - _("Designation") + ":Link/Designation:120", _("Gender") + "::100", _("Company") + ":Link/Company:120" + _("Employee") + ":Link/Employee:120", + _("Name") + ":Data:200", + _("Date of Birth") + ":Date:100", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Gender") + "::100", + _("Company") + ":Link/Company:120", ] -def get_conditions(filters): - conditions = " and "+filters.get("parameter").lower().replace(" ","_")+" IS NOT NULL " - if filters.get("company"): conditions += " and company = '%s'" % \ - filters["company"].replace("'", "\\'") +def get_conditions(filters): + conditions = " and " + filters.get("parameter").lower().replace(" ", "_") + " IS NOT NULL " + + if filters.get("company"): + conditions += " and company = '%s'" % filters["company"].replace("'", "\\'") return conditions + def get_employees(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select name, employee_name, date_of_birth, + return frappe.db.sql( + """select name, employee_name, date_of_birth, branch, department, designation, - gender, company from `tabEmployee` where status = 'Active' %s""" % conditions, as_list=1) + gender, company from `tabEmployee` where status = 'Active' %s""" + % conditions, + as_list=1, + ) + def get_parameters(filters): if filters.get("parameter") == "Grade": @@ -49,36 +63,37 @@ def get_parameters(filters): else: parameter = filters.get("parameter") - return frappe.db.sql("""select name from `tab"""+ parameter +"""` """, as_list=1) + return frappe.db.sql("""select name from `tab""" + parameter + """` """, as_list=1) -def get_chart_data(parameters,employees, filters): + +def get_chart_data(parameters, employees, filters): if not parameters: parameters = [] datasets = [] - parameter_field_name = filters.get("parameter").lower().replace(" ","_") + parameter_field_name = filters.get("parameter").lower().replace(" ", "_") label = [] for parameter in parameters: if parameter: - total_employee = frappe.db.sql("""select count(*) from - `tabEmployee` where """+ - parameter_field_name + """ = %s and company = %s""" ,( parameter[0], filters.get("company")), as_list=1) + total_employee = frappe.db.sql( + """select count(*) from + `tabEmployee` where """ + + parameter_field_name + + """ = %s and company = %s""", + (parameter[0], filters.get("company")), + as_list=1, + ) if total_employee[0][0]: label.append(parameter) datasets.append(total_employee[0][0]) - values = [ value for value in datasets if value !=0] + values = [value for value in datasets if value != 0] - total_employee = frappe.db.count('Employee', {'status':'Active'}) + total_employee = frappe.db.count("Employee", {"status": "Active"}) others = total_employee - sum(values) label.append(["Not Set"]) values.append(others) - chart = { - "data": { - 'labels': label, - 'datasets': [{'name': 'Employees','values': values}] - } - } + chart = {"data": {"labels": label, "datasets": [{"name": "Employees", "values": values}]}} chart["type"] = "donut" return chart diff --git a/erpnext/hr/report/employee_birthday/employee_birthday.py b/erpnext/hr/report/employee_birthday/employee_birthday.py index cec5a48c199..a6a13d8a4d7 100644 --- a/erpnext/hr/report/employee_birthday/employee_birthday.py +++ b/erpnext/hr/report/employee_birthday/employee_birthday.py @@ -7,34 +7,59 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() data = get_employees(filters) return columns, data + def get_columns(): return [ - _("Employee") + ":Link/Employee:120", _("Name") + ":Data:200", _("Date of Birth")+ ":Date:100", - _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120", - _("Designation") + ":Link/Designation:120", _("Gender") + "::60", _("Company") + ":Link/Company:120" + _("Employee") + ":Link/Employee:120", + _("Name") + ":Data:200", + _("Date of Birth") + ":Date:100", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Gender") + "::60", + _("Company") + ":Link/Company:120", ] + def get_employees(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select name, employee_name, date_of_birth, + return frappe.db.sql( + """select name, employee_name, date_of_birth, branch, department, designation, - gender, company from tabEmployee where status = 'Active' %s""" % conditions, as_list=1) + gender, company from tabEmployee where status = 'Active' %s""" + % conditions, + as_list=1, + ) + def get_conditions(filters): conditions = "" if filters.get("month"): - month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", - "Dec"].index(filters["month"]) + 1 + month = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ].index(filters["month"]) + 1 conditions += " and month(date_of_birth) = '%s'" % month - if filters.get("company"): conditions += " and company = '%s'" % \ - filters["company"].replace("'", "\\'") + if filters.get("company"): + conditions += " and company = '%s'" % filters["company"].replace("'", "\\'") return conditions 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 b375b18b079..ca352f197dc 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -3,18 +3,22 @@ from itertools import groupby +from typing import Dict, List, Optional, Tuple import frappe from frappe import _ -from frappe.utils import add_days +from frappe.utils import add_days, getdate +from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation from erpnext.hr.doctype.leave_application.leave_application import ( get_leave_balance_on, get_leaves_for_period, ) +Filters = frappe._dict -def execute(filters=None): + +def execute(filters: Optional[Filters] = None) -> Tuple: if filters.to_date <= filters.from_date: frappe.throw(_('"From Date" can not be greater than or equal to "To Date"')) @@ -23,146 +27,177 @@ def execute(filters=None): charts = get_chart_data(data) return columns, data, None, charts -def get_columns(): - columns = [{ - 'label': _('Leave Type'), - 'fieldtype': 'Link', - 'fieldname': 'leave_type', - 'width': 200, - 'options': 'Leave Type' - }, { - 'label': _('Employee'), - 'fieldtype': 'Link', - 'fieldname': 'employee', - 'width': 100, - 'options': 'Employee' - }, { - 'label': _('Employee Name'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'employee_name', - 'width': 100, - 'options': 'employee' - }, { - 'label': _('Opening Balance'), - 'fieldtype': 'float', - 'fieldname': 'opening_balance', - 'width': 130, - }, { - 'label': _('Leave Allocated'), - 'fieldtype': 'float', - 'fieldname': 'leaves_allocated', - 'width': 130, - }, { - 'label': _('Leave Taken'), - 'fieldtype': 'float', - 'fieldname': 'leaves_taken', - 'width': 130, - }, { - 'label': _('Leave Expired'), - 'fieldtype': 'float', - 'fieldname': 'leaves_expired', - 'width': 130, - }, { - 'label': _('Closing Balance'), - 'fieldtype': 'float', - 'fieldname': 'closing_balance', - 'width': 130, - }] - return columns +def get_columns() -> List[Dict]: + return [ + { + "label": _("Leave Type"), + "fieldtype": "Link", + "fieldname": "leave_type", + "width": 200, + "options": "Leave Type", + }, + { + "label": _("Employee"), + "fieldtype": "Link", + "fieldname": "employee", + "width": 100, + "options": "Employee", + }, + { + "label": _("Employee Name"), + "fieldtype": "Dynamic Link", + "fieldname": "employee_name", + "width": 100, + "options": "employee", + }, + { + "label": _("Opening Balance"), + "fieldtype": "float", + "fieldname": "opening_balance", + "width": 150, + }, + { + "label": _("New Leave(s) Allocated"), + "fieldtype": "float", + "fieldname": "leaves_allocated", + "width": 200, + }, + { + "label": _("Leave(s) Taken"), + "fieldtype": "float", + "fieldname": "leaves_taken", + "width": 150, + }, + { + "label": _("Leave(s) Expired"), + "fieldtype": "float", + "fieldname": "leaves_expired", + "width": 150, + }, + { + "label": _("Closing Balance"), + "fieldtype": "float", + "fieldname": "closing_balance", + "width": 150, + }, + ] -def get_data(filters): - leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name') + +def get_data(filters: Filters) -> List: + leave_types = frappe.db.get_list("Leave Type", pluck="name", order_by="name") conditions = get_conditions(filters) user = frappe.session.user - department_approver_map = get_department_leave_approver_map(filters.get('department')) + department_approver_map = get_department_leave_approver_map(filters.get("department")) - active_employees = frappe.get_list('Employee', + active_employees = frappe.get_list( + "Employee", filters=conditions, - fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver']) + fields=["name", "employee_name", "department", "user_id", "leave_approver"], + ) data = [] for leave_type in leave_types: if len(active_employees) > 1: - data.append({ - 'leave_type': leave_type - }) + data.append({"leave_type": leave_type}) else: - row = frappe._dict({ - 'leave_type': leave_type - }) + row = frappe._dict({"leave_type": leave_type}) for employee in active_employees: - leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver) + leave_approvers = department_approver_map.get(employee.department_name, []).append( + employee.leave_approver + ) - if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \ - or ("HR Manager" in frappe.get_roles(user)): + if ( + (leave_approvers and len(leave_approvers) and user in leave_approvers) + or (user in ["Administrator", employee.user_id]) + or ("HR Manager" in frappe.get_roles(user)) + ): if len(active_employees) > 1: row = frappe._dict() - row.employee = employee.name, + row.employee = employee.name row.employee_name = employee.employee_name - leaves_taken = get_leaves_for_period(employee.name, leave_type, - filters.from_date, filters.to_date) * -1 + leaves_taken = ( + get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1 + ) - new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type) - - - opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition + new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves( + filters.from_date, filters.to_date, employee.name, leave_type + ) + opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves) row.leaves_allocated = new_allocation - row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0 + row.leaves_expired = expired_leaves row.opening_balance = opening row.leaves_taken = leaves_taken # 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.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken) row.indent = 1 data.append(row) return data -def get_conditions(filters): - conditions={ - 'status': 'Active', + +def get_opening_balance( + employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float +) -> float: + # allocation boundary condition + # opening balance is the closing leave balance 1 day before the filter start date + opening_balance_date = add_days(filters.from_date, -1) + allocation = get_previous_allocation(filters.from_date, leave_type, employee) + + if ( + allocation + and allocation.get("to_date") + and opening_balance_date + and getdate(allocation.get("to_date")) == getdate(opening_balance_date) + ): + # if opening balance date is same as the previous allocation's expiry + # then opening balance should only consider carry forwarded leaves + opening_balance = carry_forwarded_leaves + else: + # else directly get leave balance on the previous day + opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date) + + return opening_balance + + +def get_conditions(filters: Filters) -> Dict: + conditions = { + "status": "Active", } - 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("company"): + conditions["company"] = filters.get("company") - if filters.get('department'): - conditions['department'] = filters.get('department') + if filters.get("department"): + conditions["department"] = filters.get("department") return conditions -def get_department_leave_approver_map(department=None): +def get_department_leave_approver_map(department: Optional[str] = None): # get current department and all its child - department_list = frappe.get_list('Department', - filters={ - 'disabled': 0 - }, - or_filters={ - 'name': department, - 'parent_department': department - }, - fields=['name'], - pluck='name' - ) + department_list = frappe.get_list( + "Department", + filters={"disabled": 0}, + or_filters={"name": department, "parent_department": department}, + 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=True, + ) approvers = {} @@ -171,73 +206,100 @@ def get_department_leave_approver_map(department=None): return approvers -def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): - - from frappe.utils import getdate +def get_allocated_and_expired_leaves( + from_date: str, to_date: str, employee: str, leave_type: str +) -> Tuple[float, float, float]: new_allocation = 0 expired_leaves = 0 + carry_forwarded_leaves = 0 - records= frappe.db.sql(""" - SELECT - employee, leave_type, from_date, to_date, leaves, transaction_name, - transaction_type, is_carry_forward, is_expired - FROM `tabLeave Ledger Entry` - WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 - AND transaction_type = 'Leave Allocation' - AND (from_date between %(from_date)s AND %(to_date)s - OR to_date between %(from_date)s AND %(to_date)s - OR (from_date < %(from_date)s AND to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + records = get_leave_ledger_entries(from_date, to_date, employee, leave_type) for record in records: + # new allocation records with `is_expired=1` are created when leave expires + # these new records should not be considered, else it leads to negative leave balance + if record.is_expired: + continue + if record.to_date < getdate(to_date): + # leave allocations ending before to_date, reduce leaves taken within that period + # since they are already used, they won't expire expired_leaves += record.leaves + expired_leaves += get_leaves_for_period(employee, leave_type, record.from_date, record.to_date) if record.from_date >= getdate(from_date): - new_allocation += record.leaves + if record.is_carry_forward: + carry_forwarded_leaves += record.leaves + else: + new_allocation += record.leaves - return new_allocation, expired_leaves + return new_allocation, expired_leaves, carry_forwarded_leaves -def get_chart_data(data): + +def get_leave_ledger_entries( + from_date: str, to_date: str, employee: str, leave_type: str +) -> List[Dict]: + ledger = frappe.qb.DocType("Leave Ledger Entry") + records = ( + frappe.qb.from_(ledger) + .select( + ledger.employee, + ledger.leave_type, + ledger.from_date, + ledger.to_date, + ledger.leaves, + ledger.transaction_name, + ledger.transaction_type, + ledger.is_carry_forward, + ledger.is_expired, + ) + .where( + (ledger.docstatus == 1) + & (ledger.transaction_type == "Leave Allocation") + & (ledger.employee == employee) + & (ledger.leave_type == leave_type) + & ( + (ledger.from_date[from_date:to_date]) + | (ledger.to_date[from_date:to_date]) + | ((ledger.from_date < from_date) & (ledger.to_date > to_date)) + ) + ) + ).run(as_dict=True) + + return records + + +def get_chart_data(data: List) -> Dict: labels = [] datasets = [] employee_data = data - if data and data[0].get('employee_name'): + 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'] + "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']): +def get_dataset_for_chart(employee_data: List, datasets: List, labels: List) -> List: + 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 - })) + 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]}) + datasets.append({"name": leave.leave_type, "values": [leave.closing_balance]}) diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py new file mode 100644 index 00000000000..dc0f4d2c944 --- /dev/null +++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py @@ -0,0 +1,209 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +import unittest + +import frappe +from frappe.utils import add_days, add_months, flt, get_year_ending, get_year_start, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.doctype.leave_application.test_leave_application import ( + get_first_sunday, + make_allocation_record, +) +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation +from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type +from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) + +test_records = frappe.get_test_records("Leave Type") + + +class TestEmployeeLeaveBalance(unittest.TestCase): + def setUp(self): + for dt in [ + "Leave Application", + "Leave Allocation", + "Salary Slip", + "Leave Ledger Entry", + "Leave Type", + ]: + frappe.db.delete(dt) + + frappe.set_user("Administrator") + + self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company") + + self.date = getdate() + self.year_start = getdate(get_year_start(self.date)) + self.mid_year = add_months(self.year_start, 6) + self.year_end = getdate(get_year_ending(self.date)) + + self.holiday_list = make_holiday_list( + "_Test Emp Balance Holiday List", self.year_start, self.year_end + ) + + def tearDown(self): + frappe.db.rollback() + + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") + def test_employee_leave_balance(self): + frappe.get_doc(test_records[0]).insert() + + # 5 leaves + allocation1 = make_allocation_record( + employee=self.employee_id, + from_date=add_days(self.year_start, -11), + to_date=add_days(self.year_start, -1), + leaves=5, + ) + # 30 leaves + allocation2 = make_allocation_record( + employee=self.employee_id, from_date=self.year_start, to_date=self.year_end + ) + # expires 5 leaves + process_expired_allocation() + + # 4 days leave + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application( + self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) + leave_application.reload() + + filters = frappe._dict( + { + "from_date": allocation1.from_date, + "to_date": allocation2.to_date, + "employee": self.employee_id, + } + ) + + report = execute(filters) + + expected_data = [ + { + "leave_type": "_Test Leave Type", + "employee": self.employee_id, + "employee_name": "test_emp_leave_balance@example.com", + "leaves_allocated": flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated), + "leaves_expired": flt(allocation1.new_leaves_allocated), + "opening_balance": flt(0), + "leaves_taken": flt(leave_application.total_leave_days), + "closing_balance": flt(allocation2.new_leaves_allocated - leave_application.total_leave_days), + "indent": 1, + } + ] + + self.assertEqual(report[1], expected_data) + + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") + def test_opening_balance_on_alloc_boundary_dates(self): + frappe.get_doc(test_records[0]).insert() + + # 30 leaves allocated + allocation1 = make_allocation_record( + employee=self.employee_id, from_date=self.year_start, to_date=self.year_end + ) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application( + self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) + leave_application.reload() + + # Case 1: opening balance for first alloc boundary + filters = frappe._dict( + {"from_date": self.year_start, "to_date": self.year_end, "employee": self.employee_id} + ) + report = execute(filters) + self.assertEqual(report[1][0].opening_balance, 0) + + # Case 2: opening balance after leave application date + filters = frappe._dict( + { + "from_date": add_days(leave_application.to_date, 1), + "to_date": self.year_end, + "employee": self.employee_id, + } + ) + report = execute(filters) + self.assertEqual( + report[1][0].opening_balance, + (allocation1.new_leaves_allocated - leave_application.total_leave_days), + ) + + # Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date + # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 + filters = frappe._dict( + { + "from_date": add_days(self.year_end, -3), + "to_date": self.year_end, + "employee": self.employee_id, + } + ) + report = execute(filters) + self.assertEqual( + report[1][0].opening_balance, + (allocation1.new_leaves_allocated - leave_application.total_leave_days), + ) + + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") + def test_opening_balance_considers_carry_forwarded_leaves(self): + leave_type = create_leave_type(leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1) + leave_type.insert() + + # 30 leaves allocated for first half of the year + allocation1 = make_allocation_record( + employee=self.employee_id, + from_date=self.year_start, + to_date=self.mid_year, + leave_type=leave_type.name, + ) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application( + self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name + ) + leave_application.reload() + # 30 leaves allocated for second half of the year + carry forward leaves (26) from the previous allocation + allocation2 = make_allocation_record( + employee=self.employee_id, + from_date=add_days(self.mid_year, 1), + to_date=self.year_end, + carry_forward=True, + leave_type=leave_type.name, + ) + + # Case 1: carry forwarded leaves considered in opening balance for second alloc + filters = frappe._dict( + { + "from_date": add_days(self.mid_year, 1), + "to_date": self.year_end, + "employee": self.employee_id, + } + ) + report = execute(filters) + # available leaves from old alloc + opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days + self.assertEqual(report[1][0].opening_balance, opening_balance) + + # Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc + filters = frappe._dict( + { + "from_date": add_days(self.mid_year, 2), + "to_date": self.year_end, + "employee": self.employee_id, + } + ) + report = execute(filters) + # available leaves from old alloc + opening_balance = allocation2.new_leaves_allocated + ( + allocation1.new_leaves_allocated - leave_application.total_leave_days + ) + self.assertEqual(report[1][0].opening_balance, opening_balance) diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 936184a9c0d..f0087eb1b9e 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -19,11 +19,12 @@ def execute(filters=None): return columns, data + def get_columns(leave_types): columns = [ _("Employee") + ":Link.Employee:150", _("Employee Name") + "::200", - _("Department") +"::150" + _("Department") + "::150", ] for leave_type in leave_types: @@ -31,6 +32,7 @@ def get_columns(leave_types): return columns + def get_conditions(filters): conditions = { "status": "Active", @@ -43,15 +45,18 @@ def get_conditions(filters): return conditions + def get_data(filters, leave_types): user = frappe.session.user conditions = get_conditions(filters) - active_employees = frappe.get_all("Employee", + active_employees = frappe.get_all( + "Employee", filters=conditions, - fields=["name", "employee_name", "department", "user_id", "leave_approver"]) + fields=["name", "employee_name", "department", "user_id", "leave_approver"], + ) - department_approver_map = get_department_leave_approver_map(filters.get('department')) + department_approver_map = get_department_leave_approver_map(filters.get("department")) data = [] for employee in active_employees: @@ -59,14 +64,18 @@ def get_data(filters, leave_types): if employee.leave_approver: leave_approvers.append(employee.leave_approver) - if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)): + if ( + (len(leave_approvers) and user in leave_approvers) + or (user in ["Administrator", employee.user_id]) + or ("HR Manager" in frappe.get_roles(user)) + ): row = [employee.name, employee.employee_name, employee.department] available_leave = get_leave_details(employee.name, filters.date) for leave_type in leave_types: remaining = 0 if leave_type in available_leave["leave_allocation"]: # opening balance - remaining = available_leave["leave_allocation"][leave_type]['remaining_leaves'] + remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"] row += [remaining] diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py new file mode 100644 index 00000000000..34b665fa9f0 --- /dev/null +++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py @@ -0,0 +1,148 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +import unittest + +import frappe +from frappe.utils import add_days, flt, get_year_ending, get_year_start, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.doctype.leave_application.test_leave_application import ( + get_first_sunday, + make_allocation_record, +) +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation +from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) + +test_records = frappe.get_test_records("Leave Type") + + +class TestEmployeeLeaveBalance(unittest.TestCase): + def setUp(self): + for dt in [ + "Leave Application", + "Leave Allocation", + "Salary Slip", + "Leave Ledger Entry", + "Leave Type", + ]: + frappe.db.delete(dt) + + frappe.set_user("Administrator") + + self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company") + self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company") + + self.date = getdate() + self.year_start = getdate(get_year_start(self.date)) + self.year_end = getdate(get_year_ending(self.date)) + + self.holiday_list = make_holiday_list( + "_Test Emp Balance Holiday List", self.year_start, self.year_end + ) + + def tearDown(self): + frappe.db.rollback() + + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") + def test_employee_leave_balance_summary(self): + frappe.get_doc(test_records[0]).insert() + + # 5 leaves + allocation1 = make_allocation_record( + employee=self.employee_id, + from_date=add_days(self.year_start, -11), + to_date=add_days(self.year_start, -1), + leaves=5, + ) + # 30 leaves + allocation2 = make_allocation_record( + employee=self.employee_id, from_date=self.year_start, to_date=self.year_end + ) + + # 2 days leave within the first allocation + leave_application1 = make_leave_application( + self.employee_id, + add_days(self.year_start, -11), + add_days(self.year_start, -10), + "_Test Leave Type", + ) + leave_application1.reload() + + # expires 3 leaves + process_expired_allocation() + + # 4 days leave within the second allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application2 = make_leave_application( + self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) + leave_application2.reload() + + filters = frappe._dict( + { + "date": add_days(leave_application2.to_date, 1), + "company": "_Test Company", + "employee": self.employee_id, + } + ) + + report = execute(filters) + + expected_data = [ + [ + self.employee_id, + "test_emp_leave_balance@example.com", + frappe.db.get_value("Employee", self.employee_id, "department"), + flt( + allocation1.new_leaves_allocated # allocated = 5 + + allocation2.new_leaves_allocated # allocated = 30 + - leave_application1.total_leave_days # leaves taken in the 1st alloc = 2 + - ( + allocation1.new_leaves_allocated - leave_application1.total_leave_days + ) # leaves expired from 1st alloc = 3 + - leave_application2.total_leave_days # leaves taken in the 2nd alloc = 4 + ), + ] + ] + + self.assertEqual(report[1], expected_data) + + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") + def test_get_leave_balance_near_alloc_expiry(self): + frappe.get_doc(test_records[0]).insert() + + # 30 leaves allocated + allocation = make_allocation_record( + employee=self.employee_id, from_date=self.year_start, to_date=self.year_end + ) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application( + self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) + leave_application.reload() + + # Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date + # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 + filters = frappe._dict( + {"date": add_days(self.year_end, -3), "company": "_Test Company", "employee": self.employee_id} + ) + report = execute(filters) + + expected_data = [ + [ + self.employee_id, + "test_emp_leave_balance@example.com", + frappe.db.get_value("Employee", self.employee_id, "department"), + flt(allocation.new_leaves_allocated - leave_application.total_leave_days), + ] + ] + + self.assertEqual(report[1], expected_data) diff --git a/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py b/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py index 00a4a7c29f5..f13fabf06e6 100644 --- a/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py +++ b/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py @@ -21,16 +21,21 @@ def get_columns(): _("Name") + ":Data:200", _("Date") + ":Date:100", _("Status") + ":Data:70", - _("Holiday") + ":Data:200" + _("Holiday") + ":Data:200", ] + def get_employees(filters): - holiday_filter = [["holiday_date", ">=", filters.from_date], ["holiday_date", "<=", filters.to_date]] + holiday_filter = [ + ["holiday_date", ">=", filters.from_date], + ["holiday_date", "<=", filters.to_date], + ] if filters.holiday_list: holiday_filter.append(["parent", "=", filters.holiday_list]) - holidays = frappe.get_all("Holiday", fields=["holiday_date", "description"], - filters=holiday_filter) + holidays = frappe.get_all( + "Holiday", fields=["holiday_date", "description"], filters=holiday_filter + ) holiday_names = {} holidays_list = [] @@ -39,18 +44,23 @@ def get_employees(filters): holidays_list.append(holiday.holiday_date) holiday_names[holiday.holiday_date] = holiday.description - if(holidays_list): + if holidays_list: cond = " attendance_date in %(holidays_list)s" if filters.holiday_list: - cond += """ and (employee in (select employee from tabEmployee where holiday_list = %(holidays)s))""" + cond += ( + """ and (employee in (select employee from tabEmployee where holiday_list = %(holidays)s))""" + ) - employee_list = frappe.db.sql("""select + employee_list = frappe.db.sql( + """select employee, employee_name, attendance_date, status from tabAttendance - where %s"""% cond.format(', '.join(["%s"] * len(holidays_list))), - {'holidays_list':holidays_list, - 'holidays':filters.holiday_list}, as_list=True) + where %s""" + % cond.format(", ".join(["%s"] * len(holidays_list))), + {"holidays_list": holidays_list, "holidays": filters.holiday_list}, + as_list=True, + ) for employee_data in employee_list: employee_data.append(holiday_names[employee_data[2]]) diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py index 4e043379404..c6f5bf05891 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py @@ -15,21 +15,15 @@ status_map = { "Weekly Off": "WO", "On Leave": "L", "Present": "P", - "Work From Home": "WFH" - } + "Work From Home": "WFH", +} + +day_abbr = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] -day_abbr = [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" -] def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if filters.hide_year_field == 1: filters.year = 2020 @@ -44,25 +38,29 @@ def execute(filters=None): emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company) holiday_list = [] for parameter in group_by_parameters: - h_list = [emp_map[parameter][d]["holiday_list"] for d in emp_map[parameter] if emp_map[parameter][d]["holiday_list"]] + h_list = [ + emp_map[parameter][d]["holiday_list"] + for d in emp_map[parameter] + if emp_map[parameter][d]["holiday_list"] + ] holiday_list += h_list else: emp_map = get_employee_details(filters.group_by, filters.company) holiday_list = [emp_map[d]["holiday_list"] for d in emp_map if emp_map[d]["holiday_list"]] - - default_holiday_list = frappe.get_cached_value('Company', filters.get("company"), "default_holiday_list") + default_holiday_list = frappe.get_cached_value( + "Company", filters.get("company"), "default_holiday_list" + ) holiday_list.append(default_holiday_list) holiday_list = list(set(holiday_list)) holiday_map = get_holiday(holiday_list, filters["month"]) data = [] - leave_list = None + leave_types = None if filters.summarized_view: - leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True) - leave_list = [d[0] + ":Float:120" for d in leave_types] - columns.extend(leave_list) + leave_types = frappe.get_all("Leave Type", pluck="name") + columns.extend([leave_type + ":Float:120" for leave_type in leave_types]) columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"]) if filters.group_by: @@ -70,20 +68,39 @@ def execute(filters=None): for parameter in group_by_parameters: emp_map_set = set([key for key in emp_map[parameter].keys()]) att_map_set = set([key for key in att_map.keys()]) - if (att_map_set & emp_map_set): - parameter_row = [""+ parameter + ""] + ['' for day in range(filters["total_days_in_month"] + 2)] + if att_map_set & emp_map_set: + parameter_row = ["" + parameter + ""] + [ + "" for day in range(filters["total_days_in_month"] + 2) + ] data.append(parameter_row) - record, emp_att_data = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list) + record, emp_att_data = add_data( + emp_map[parameter], + att_map, + filters, + holiday_map, + conditions, + default_holiday_list, + leave_types=leave_types, + ) emp_att_map.update(emp_att_data) data += record else: - record, emp_att_map = add_data(emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list) + record, emp_att_map = add_data( + emp_map, + att_map, + filters, + holiday_map, + conditions, + default_holiday_list, + leave_types=leave_types, + ) data += record chart_data = get_chart_data(emp_att_map, days) return columns, data, None, chart_data + def get_chart_data(emp_att_map, days): labels = [] datasets = [ @@ -92,12 +109,10 @@ def get_chart_data(emp_att_map, days): {"name": "Leave", "values": []}, ] for idx, day in enumerate(days, start=0): - p = day.replace("::65", "") labels.append(day.replace("::65", "")) total_absent_on_day = 0 total_leave_on_day = 0 total_present_on_day = 0 - total_holiday = 0 for emp in emp_att_map.keys(): if emp_att_map[emp][idx]: if emp_att_map[emp][idx] == "A": @@ -110,25 +125,20 @@ def get_chart_data(emp_att_map, days): if emp_att_map[emp][idx] == "L": total_leave_on_day += 1 - datasets[0]["values"].append(total_absent_on_day) datasets[1]["values"].append(total_present_on_day) datasets[2]["values"].append(total_leave_on_day) - - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - } - } + chart = {"data": {"labels": labels, "datasets": datasets}} chart["type"] = "line" return chart -def add_data(employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=None): +def add_data( + employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_types=None +): record = [] emp_att_map = {} for emp in employee_map: @@ -141,7 +151,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho row += [" "] row += [emp, emp_det.employee_name] - total_p = total_a = total_l = total_h = total_um= 0.0 + total_p = total_a = total_l = total_h = total_um = 0.0 emp_status_map = [] for day in range(filters["total_days_in_month"]): status = None @@ -152,7 +162,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho if emp_holiday_list in holiday_map: for idx, ele in enumerate(holiday_map[emp_holiday_list]): - if day+1 == holiday_map[emp_holiday_list][idx][0]: + if day + 1 == holiday_map[emp_holiday_list][idx][0]: if holiday_map[emp_holiday_list][idx][1]: status = "Weekly Off" else: @@ -162,7 +172,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho abbr = status_map.get(status, "") emp_status_map.append(abbr) - if filters.summarized_view: + if filters.summarized_view: if status == "Present" or status == "Work From Home": total_p += 1 elif status == "Absent": @@ -189,12 +199,21 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho filters.update({"employee": emp}) if filters.summarized_view: - leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\ - where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1) + leave_details = frappe.db.sql( + """select leave_type, status, count(*) as count from `tabAttendance`\ + where leave_type is not NULL %s group by leave_type, status""" + % conditions, + filters, + as_dict=1, + ) - time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \ + time_default_counts = frappe.db.sql( + """select (select count(*) from `tabAttendance` where \ late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \ - early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters) + early_exit = 1 %s) as early_exit_count""" + % (conditions, conditions), + filters, + ) leaves = {} for d in leave_details: @@ -205,44 +224,54 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho else: leaves[d.leave_type] = d.count - for d in leave_list: + for d in leave_types: if d in leaves: row.append(leaves[d]) else: row.append("0.0") - row.extend([time_default_counts[0][0],time_default_counts[0][1]]) + row.extend([time_default_counts[0][0], time_default_counts[0][1]]) emp_att_map[emp] = emp_status_map record.append(row) return record, emp_att_map + def get_columns(filters): columns = [] if filters.group_by: - columns = [_(filters.group_by)+ ":Link/Branch:120"] + columns = [_(filters.group_by) + ":Link/Branch:120"] - columns += [ - _("Employee") + ":Link/Employee:120", _("Employee Name") + ":Data/:120" - ] + columns += [_("Employee") + ":Link/Employee:120", _("Employee Name") + ":Data/:120"] days = [] for day in range(filters["total_days_in_month"]): - date = str(filters.year) + "-" + str(filters.month)+ "-" + str(day+1) + date = str(filters.year) + "-" + str(filters.month) + "-" + str(day + 1) day_name = day_abbr[getdate(date).weekday()] - days.append(cstr(day+1)+ " " +day_name +"::65") + days.append(cstr(day + 1) + " " + day_name + "::65") if not filters.summarized_view: columns += days if filters.summarized_view: - columns += [_("Total Present") + ":Float:120", _("Total Leaves") + ":Float:120", _("Total Absent") + ":Float:120", _("Total Holidays") + ":Float:120", _("Unmarked Days")+ ":Float:120"] + columns += [ + _("Total Present") + ":Float:120", + _("Total Leaves") + ":Float:120", + _("Total Absent") + ":Float:120", + _("Total Holidays") + ":Float:120", + _("Unmarked Days") + ":Float:120", + ] return columns, days + def get_attendance_list(conditions, filters): - attendance_list = frappe.db.sql("""select employee, day(attendance_date) as day_of_month, - status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" % - conditions, filters, as_dict=1) + attendance_list = frappe.db.sql( + """select employee, day(attendance_date) as day_of_month, + status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" + % conditions, + filters, + as_dict=1, + ) if not attendance_list: msgprint(_("No attendance record found"), alert=True, indicator="orange") @@ -254,6 +283,7 @@ def get_attendance_list(conditions, filters): return att_map + def get_conditions(filters): if not (filters.get("month") and filters.get("year")): msgprint(_("Please select month and year"), raise_exception=1) @@ -262,29 +292,35 @@ def get_conditions(filters): conditions = " and month(attendance_date) = %(month)s and year(attendance_date) = %(year)s" - if filters.get("company"): conditions += " and company = %(company)s" - if filters.get("employee"): conditions += " and employee = %(employee)s" + if filters.get("company"): + conditions += " and company = %(company)s" + if filters.get("employee"): + conditions += " and employee = %(employee)s" return conditions, filters + def get_employee_details(group_by, company): emp_map = {} query = """select name, employee_name, designation, department, branch, company, - holiday_list from `tabEmployee` where company = %s """ % frappe.db.escape(company) + holiday_list from `tabEmployee` where company = %s """ % frappe.db.escape( + company + ) if group_by: group_by = group_by.lower() query += " order by " + group_by + " ASC" - employee_details = frappe.db.sql(query , as_dict=1) + employee_details = frappe.db.sql(query, as_dict=1) group_by_parameters = [] if group_by: - group_by_parameters = list(set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, ""))) + group_by_parameters = list( + set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, "")) + ) for parameter in group_by_parameters: - emp_map[parameter] = {} - + emp_map[parameter] = {} for d in employee_details: if group_by and len(group_by_parameters): @@ -299,18 +335,28 @@ def get_employee_details(group_by, company): else: return emp_map, group_by_parameters + def get_holiday(holiday_list, month): holiday_map = frappe._dict() for d in holiday_list: if d: - holiday_map.setdefault(d, frappe.db.sql('''select day(holiday_date), weekly_off from `tabHoliday` - where parent=%s and month(holiday_date)=%s''', (d, month))) + holiday_map.setdefault( + d, + frappe.db.sql( + """select day(holiday_date), weekly_off from `tabHoliday` + where parent=%s and month(holiday_date)=%s""", + (d, month), + ), + ) return holiday_map + @frappe.whitelist() def get_attendance_years(): - year_list = frappe.db.sql_list("""select distinct YEAR(attendance_date) from tabAttendance ORDER BY YEAR(attendance_date) DESC""") + year_list = frappe.db.sql_list( + """select distinct YEAR(attendance_date) from tabAttendance ORDER BY YEAR(attendance_date) DESC""" + ) if not year_list: year_list = [getdate().year] diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py new file mode 100644 index 00000000000..91da08eee50 --- /dev/null +++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py @@ -0,0 +1,46 @@ +import frappe +from dateutil.relativedelta import relativedelta +from frappe.tests.utils import FrappeTestCase +from frappe.utils import now_datetime + +from erpnext.hr.doctype.attendance.attendance import mark_attendance +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute + + +class TestMonthlyAttendanceSheet(FrappeTestCase): + def setUp(self): + self.employee = make_employee("test_employee@example.com") + frappe.db.delete("Attendance", {"employee": self.employee}) + + def test_monthly_attendance_sheet_report(self): + now = now_datetime() + previous_month = now.month - 1 + previous_month_first = now.replace(day=1).replace(month=previous_month).date() + + company = frappe.db.get_value("Employee", self.employee, "company") + + # mark different attendance status on first 3 days of previous month + mark_attendance(self.employee, previous_month_first, "Absent") + mark_attendance(self.employee, previous_month_first + relativedelta(days=1), "Present") + mark_attendance(self.employee, previous_month_first + relativedelta(days=2), "On Leave") + + filters = frappe._dict( + { + "month": previous_month, + "year": now.year, + "company": company, + } + ) + report = execute(filters=filters) + employees = report[1][0] + datasets = report[3]["data"]["datasets"] + absent = datasets[0]["values"] + present = datasets[1]["values"] + leaves = datasets[2]["values"] + + # ensure correct attendance is reflect on the report + self.assertIn(self.employee, employees) + self.assertEqual(absent[0], 1) + self.assertEqual(present[1], 1) + self.assertEqual(leaves[2], 1) diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py index 6383a9bbac9..b6caf400dd0 100644 --- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py +++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py @@ -8,7 +8,8 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} filters = frappe._dict(filters) columns = get_columns() @@ -25,67 +26,53 @@ def get_columns(): "fieldtype": "Link", "fieldname": "staffing_plan", "options": "Staffing Plan", - "width": 150 + "width": 150, }, { "label": _("Job Opening"), "fieldtype": "Link", "fieldname": "job_opening", "options": "Job Opening", - "width": 105 + "width": 105, }, { "label": _("Job Applicant"), "fieldtype": "Link", "fieldname": "job_applicant", "options": "Job Applicant", - "width": 150 - }, - { - "label": _("Applicant name"), - "fieldtype": "data", - "fieldname": "applicant_name", - "width": 130 + "width": 150, }, + {"label": _("Applicant name"), "fieldtype": "data", "fieldname": "applicant_name", "width": 130}, { "label": _("Application Status"), "fieldtype": "Data", "fieldname": "application_status", - "width": 150 + "width": 150, }, { "label": _("Job Offer"), "fieldtype": "Link", "fieldname": "job_offer", "options": "job Offer", - "width": 150 - }, - { - "label": _("Designation"), - "fieldtype": "Data", - "fieldname": "designation", - "width": 100 - }, - { - "label": _("Offer Date"), - "fieldtype": "date", - "fieldname": "offer_date", - "width": 100 + "width": 150, }, + {"label": _("Designation"), "fieldtype": "Data", "fieldname": "designation", "width": 100}, + {"label": _("Offer Date"), "fieldtype": "date", "fieldname": "offer_date", "width": 100}, { "label": _("Job Offer status"), "fieldtype": "Data", "fieldname": "job_offer_status", - "width": 150 - } + "width": 150, + }, ] + def get_data(filters): data = [] staffing_plan_details = get_staffing_plan(filters) - staffing_plan_list = list(set([details["name"] for details in staffing_plan_details])) - sp_jo_map , jo_list = get_job_opening(staffing_plan_list) - jo_ja_map , ja_list = get_job_applicant(jo_list) + staffing_plan_list = list(set([details["name"] for details in staffing_plan_details])) + sp_jo_map, jo_list = get_job_opening(staffing_plan_list) + jo_ja_map, ja_list = get_job_applicant(jo_list) ja_joff_map = get_job_offer(ja_list) for sp in sp_jo_map.keys(): @@ -100,37 +87,40 @@ def get_parent_row(sp_jo_map, sp, jo_ja_map, ja_joff_map): if sp in sp_jo_map.keys(): for jo in sp_jo_map[sp]: row = { - "staffing_plan" : sp, - "job_opening" : jo["name"], + "staffing_plan": sp, + "job_opening": jo["name"], } data.append(row) - child_row = get_child_row( jo["name"], jo_ja_map, ja_joff_map) + child_row = get_child_row(jo["name"], jo_ja_map, ja_joff_map) data += child_row return data + def get_child_row(jo, jo_ja_map, ja_joff_map): data = [] if jo in jo_ja_map.keys(): for ja in jo_ja_map[jo]: row = { - "indent":1, + "indent": 1, "job_applicant": ja.name, "applicant_name": ja.applicant_name, "application_status": ja.status, } if ja.name in ja_joff_map.keys(): - jo_detail =ja_joff_map[ja.name][0] + jo_detail = ja_joff_map[ja.name][0] row["job_offer"] = jo_detail.name row["job_offer_status"] = jo_detail.status - row["offer_date"]= jo_detail.offer_date.strftime("%d-%m-%Y") + row["offer_date"] = jo_detail.offer_date.strftime("%d-%m-%Y") row["designation"] = jo_detail.designation data.append(row) return data + def get_staffing_plan(filters): - staffing_plan = frappe.db.sql(""" + staffing_plan = frappe.db.sql( + """ select sp.name, sp.department, spd.designation, spd.vacancies, spd.current_count, spd.parent, sp.to_date from @@ -139,13 +129,20 @@ def get_staffing_plan(filters): spd.parent = sp.name And sp.to_date > '{0}' - """.format(filters.on_date), as_dict = 1) + """.format( + filters.on_date + ), + as_dict=1, + ) return staffing_plan + def get_job_opening(sp_list): - job_openings = frappe.get_all("Job Opening", filters = [["staffing_plan", "IN", sp_list]], fields =["name", "staffing_plan"]) + job_openings = frappe.get_all( + "Job Opening", filters=[["staffing_plan", "IN", sp_list]], fields=["name", "staffing_plan"] + ) sp_jo_map = {} jo_list = [] @@ -160,12 +157,17 @@ def get_job_opening(sp_list): return sp_jo_map, jo_list + def get_job_applicant(jo_list): jo_ja_map = {} - ja_list =[] + ja_list = [] - applicants = frappe.get_all("Job Applicant", filters = [["job_title", "IN", jo_list]], fields =["name", "job_title","applicant_name", 'status']) + applicants = frappe.get_all( + "Job Applicant", + filters=[["job_title", "IN", jo_list]], + fields=["name", "job_title", "applicant_name", "status"], + ) for applicant in applicants: if applicant.job_title not in jo_ja_map.keys(): @@ -175,12 +177,17 @@ def get_job_applicant(jo_list): ja_list.append(applicant.name) - return jo_ja_map , ja_list + return jo_ja_map, ja_list + def get_job_offer(ja_list): ja_joff_map = {} - offers = frappe.get_all("Job Offer", filters = [["job_applicant", "IN", ja_list]], fields =["name", "job_applicant", "status", 'offer_date', 'designation']) + offers = frappe.get_all( + "Job Offer", + filters=[["job_applicant", "IN", ja_list]], + fields=["name", "job_applicant", "status", "offer_date", "designation"], + ) for offer in offers: if offer.job_applicant not in ja_joff_map.keys(): diff --git a/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py index 8672e68cf4b..da6dace72b5 100644 --- a/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py +++ b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py @@ -17,12 +17,14 @@ from erpnext.hr.report.vehicle_expenses.vehicle_expenses import execute class TestVehicleExpenses(unittest.TestCase): @classmethod def setUpClass(self): - frappe.db.sql('delete from `tabVehicle Log`') + frappe.db.sql("delete from `tabVehicle Log`") - employee_id = frappe.db.sql('''select name from `tabEmployee` where name="testdriver@example.com"''') + 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.employee_id = make_employee("testdriver@example.com", company="_Test Company") self.license_plate = get_vehicle(self.employee_id) @@ -31,36 +33,35 @@ class TestVehicleExpenses(unittest.TestCase): 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] - } + 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 - }] + 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 + "filter_based_on": "Date Range", + "from_date": fiscal_year.year_start_date, + "to_date": fiscal_year.year_end_date, } report = execute(filters) @@ -68,9 +69,9 @@ class TestVehicleExpenses(unittest.TestCase): # clean up vehicle_log.cancel() - frappe.delete_doc('Expense Claim', expense_claim.name) - frappe.delete_doc('Vehicle Log', vehicle_log.name) + 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) + 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.py b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py index 17d1e9d46a0..fc5510ddad8 100644 --- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py +++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py @@ -18,83 +18,44 @@ def execute(filters=None): return columns, data, None, chart + def get_columns(): return [ { - 'fieldname': 'vehicle', - 'fieldtype': 'Link', - 'label': _('Vehicle'), - 'options': 'Vehicle', - 'width': 150 + "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': 'make', - 'fieldtype': 'Data', - 'label': _('Make'), - 'width': 100 + "fieldname": "employee", + "fieldtype": "Link", + "label": _("Employee"), + "options": "Employee", + "width": 150, }, - { - '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 - } ] @@ -102,7 +63,8 @@ def get_vehicle_log_data(filters): start_date, end_date = get_period_dates(filters) conditions, values = get_conditions(filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT vhcl.license_plate as vehicle, vhcl.make, vhcl.model, vhcl.location, log.name as log_name, log.odometer, @@ -116,58 +78,70 @@ def get_vehicle_log_data(filters): and log.docstatus = 1 and date between %(start_date)s and %(end_date)s {0} - ORDER BY date""".format(conditions), values, as_dict=1) + ORDER BY date""".format( + conditions + ), + values, + as_dict=1, + ) for row in data: - row['service_expense'] = get_service_expense(row.log_name) + row["service_expense"] = get_service_expense(row.log_name) return data def get_conditions(filters): - conditions = '' + conditions = "" start_date, end_date = get_period_dates(filters) - values = { - 'start_date': start_date, - 'end_date': end_date - } + values = {"start_date": start_date, "end_date": end_date} if filters.employee: - conditions += ' and log.employee = %(employee)s' - values['employee'] = 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 + 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) + 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(""" + 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) + """, + 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') + 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 = [], [] @@ -184,29 +158,20 @@ def get_chart_data(data, filters): 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] + fuel_exp_data = [row[1] for row in fuel_data] + service_exp_data = [row[1] for row in service_data] datasets = [] if fuel_exp_data: - datasets.append({ - 'name': _('Fuel Expenses'), - 'values': fuel_exp_data - }) + datasets.append({"name": _("Fuel Expenses"), "values": fuel_exp_data}) if service_exp_data: - datasets.append({ - 'name': _('Service Expenses'), - 'values': service_exp_data - }) + datasets.append({"name": _("Service Expenses"), "values": service_exp_data}) chart = { - 'data': { - 'labels': labels, - 'datasets': datasets - }, - 'type': 'line', - 'fieldtype': 'Currency' + "data": {"labels": labels, "datasets": datasets}, + "type": "line", + "fieldtype": "Currency", } return chart diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 46bcadcf536..f1c1608cdd0 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -26,20 +26,22 @@ from erpnext.hr.doctype.employee.employee import ( ) -class DuplicateDeclarationError(frappe.ValidationError): pass +class DuplicateDeclarationError(frappe.ValidationError): + pass class EmployeeBoardingController(Document): - ''' - Create the project and the task for the boarding process - Assign to the concerned person and roles as per the onboarding/separation template - ''' + """ + Create the project and the task for the boarding process + Assign to the concerned person and roles as per the onboarding/separation template + """ + def validate(self): validate_active_employee(self.employee) # remove the task if linked before submitting the form if self.amended_from: for activity in self.activities: - activity.task = '' + activity.task = "" def on_submit(self): # create the project for the given employee onboarding @@ -49,13 +51,17 @@ class EmployeeBoardingController(Document): else: project_name += self.employee - project = frappe.get_doc({ + project = frappe.get_doc( + { "doctype": "Project", "project_name": project_name, - "expected_start_date": self.date_of_joining if self.doctype == "Employee Onboarding" else self.resignation_letter_date, + "expected_start_date": self.date_of_joining + if self.doctype == "Employee Onboarding" + else self.resignation_letter_date, "department": self.department, - "company": self.company - }).insert(ignore_permissions=True, ignore_mandatory=True) + "company": self.company, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) self.db_set("project", project.name) self.db_set("boarding_status", "Pending") @@ -68,20 +74,23 @@ class EmployeeBoardingController(Document): if activity.task: continue - task = frappe.get_doc({ - "doctype": "Task", - "project": self.project, - "subject": activity.activity_name + " : " + self.employee_name, - "description": activity.description, - "department": self.department, - "company": self.company, - "task_weight": activity.task_weight - }).insert(ignore_permissions=True) + task = frappe.get_doc( + { + "doctype": "Task", + "project": self.project, + "subject": activity.activity_name + " : " + self.employee_name, + "description": activity.description, + "department": self.department, + "company": self.company, + "task_weight": activity.task_weight, + } + ).insert(ignore_permissions=True) activity.db_set("task", task.name) users = [activity.user] if activity.user else [] if activity.role: - user_list = frappe.db.sql_list(''' + user_list = frappe.db.sql_list( + """ SELECT DISTINCT(has_role.parent) FROM @@ -92,7 +101,9 @@ class EmployeeBoardingController(Document): has_role.parenttype = 'User' AND user.enabled = 1 AND has_role.role = %s - ''', activity.role) + """, + activity.role, + ) users = unique(users + user_list) if "Administrator" in users: @@ -105,11 +116,11 @@ class EmployeeBoardingController(Document): def assign_task_to_users(self, task, users): for user in users: args = { - 'assign_to': [user], - 'doctype': task.doctype, - 'name': task.name, - 'description': task.description or task.subject, - 'notify': self.notify_users_by_email + "assign_to": [user], + "doctype": task.doctype, + "name": task.name, + "description": task.description or task.subject, + "notify": self.notify_users_by_email, } assign_to.add(args) @@ -118,41 +129,56 @@ class EmployeeBoardingController(Document): for task in frappe.get_all("Task", filters={"project": self.project}): frappe.delete_doc("Task", task.name, force=1) frappe.delete_doc("Project", self.project, force=1) - self.db_set('project', '') + self.db_set("project", "") for activity in self.activities: activity.db_set("task", "") @frappe.whitelist() def get_onboarding_details(parent, parenttype): - return frappe.get_all("Employee Boarding Activity", - fields=["activity_name", "role", "user", "required_for_employee_creation", "description", "task_weight"], + return frappe.get_all( + "Employee Boarding Activity", + fields=[ + "activity_name", + "role", + "user", + "required_for_employee_creation", + "description", + "task_weight", + ], filters={"parent": parent, "parenttype": parenttype}, - order_by= "idx") + order_by="idx", + ) + @frappe.whitelist() def get_boarding_status(project): - status = 'Pending' + status = "Pending" if project: - doc = frappe.get_doc('Project', project) + doc = frappe.get_doc("Project", project) if flt(doc.percent_complete) > 0.0 and flt(doc.percent_complete) < 100.0: - status = 'In Process' + status = "In Process" elif flt(doc.percent_complete) == 100.0: - status = 'Completed' + status = "Completed" return status + def set_employee_name(doc): if doc.employee and not doc.employee_name: doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name") + def update_employee_work_history(employee, details, date=None, cancel=False): if not employee.internal_work_history and not cancel: - employee.append("internal_work_history", { - "branch": employee.branch, - "designation": employee.designation, - "department": employee.department, - "from_date": employee.date_of_joining - }) + employee.append( + "internal_work_history", + { + "branch": employee.branch, + "designation": employee.designation, + "department": employee.department, + "from_date": employee.date_of_joining, + }, + ) internal_work_history = {} for item in details: @@ -163,7 +189,7 @@ def update_employee_work_history(employee, details, date=None, cancel=False): new_data = item.new if not cancel else item.current if fieldtype == "Date" and new_data: new_data = getdate(new_data) - elif fieldtype =="Datetime" and new_data: + elif fieldtype == "Datetime" and new_data: new_data = get_datetime(new_data) setattr(employee, item.fieldname, new_data) if item.fieldname in ["department", "designation", "branch"]: @@ -178,6 +204,7 @@ def update_employee_work_history(employee, details, date=None, cancel=False): return employee + def delete_employee_work_history(details, employee, date): filters = {} for d in details: @@ -201,12 +228,25 @@ def delete_employee_work_history(details, employee, date): def get_employee_fields_label(): fields = [] for df in frappe.get_meta("Employee").get("fields"): - if df.fieldname in ["salutation", "user_id", "employee_number", "employment_type", - "holiday_list", "branch", "department", "designation", "grade", - "notice_number_of_days", "reports_to", "leave_policy", "company_email"]: - fields.append({"value": df.fieldname, "label": df.label}) + if df.fieldname in [ + "salutation", + "user_id", + "employee_number", + "employment_type", + "holiday_list", + "branch", + "department", + "designation", + "grade", + "notice_number_of_days", + "reports_to", + "leave_policy", + "company_email", + ]: + fields.append({"value": df.fieldname, "label": df.label}) return fields + @frappe.whitelist() def get_employee_field_property(employee, fieldname): if employee and fieldname: @@ -217,17 +257,15 @@ def get_employee_field_property(employee, fieldname): value = formatdate(value) elif field.fieldtype == "Datetime": value = format_datetime(value) - return { - "value" : value, - "datatype" : field.fieldtype, - "label" : field.label, - "options" : options - } + return {"value": value, "datatype": field.fieldtype, "label": field.label, "options": options} else: return False + def validate_dates(doc, from_date, to_date): - date_of_joining, relieving_date = frappe.db.get_value("Employee", doc.employee, ["date_of_joining", "relieving_date"]) + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", doc.employee, ["date_of_joining", "relieving_date"] + ) if getdate(from_date) > getdate(to_date): frappe.throw(_("To date can not be less than from date")) elif getdate(from_date) > getdate(nowdate()): @@ -237,7 +275,8 @@ def validate_dates(doc, from_date, to_date): elif relieving_date and getdate(to_date) > getdate(relieving_date): frappe.throw(_("To date can not greater than employee's relieving date")) -def validate_overlap(doc, from_date, to_date, company = None): + +def validate_overlap(doc, from_date, to_date, company=None): query = """ select name from `tab{0}` @@ -247,15 +286,19 @@ def validate_overlap(doc, from_date, to_date, company = None): if not doc.name: # hack! if name is null, it could cause problems with != - doc.name = "New "+doc.doctype + doc.name = "New " + doc.doctype - overlap_doc = frappe.db.sql(query.format(doc.doctype),{ + overlap_doc = frappe.db.sql( + query.format(doc.doctype), + { "employee": doc.get("employee"), "from_date": from_date, "to_date": to_date, "name": doc.name, - "company": company - }, as_dict = 1) + "company": company, + }, + as_dict=1, + ) if overlap_doc: if doc.get("employee"): @@ -264,6 +307,7 @@ def validate_overlap(doc, from_date, to_date, company = None): exists_for = company throw_overlap_error(doc, exists_for, overlap_doc[0].name, from_date, to_date) + def get_doc_condition(doctype): if doctype == "Compensatory Leave Request": return "and employee = %(employee)s and docstatus < 2 \ @@ -275,23 +319,36 @@ def get_doc_condition(doctype): or to_date between %(from_date)s and %(to_date)s \ or (from_date < %(from_date)s and to_date > %(to_date)s))" + def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date): - msg = _("A {0} exists between {1} and {2} (").format(doc.doctype, - formatdate(from_date), formatdate(to_date)) \ - + """ {1}""".format(doc.doctype, overlap_doc) \ + msg = ( + _("A {0} exists between {1} and {2} (").format( + doc.doctype, formatdate(from_date), formatdate(to_date) + ) + + """ {1}""".format(doc.doctype, overlap_doc) + _(") for {0}").format(exists_for) + ) frappe.throw(msg) + def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee): - existing_record = frappe.db.exists(doctype, { - "payroll_period": payroll_period, - "employee": employee, - 'docstatus': ['<', 2], - 'name': ['!=', docname] - }) + existing_record = frappe.db.exists( + doctype, + { + "payroll_period": payroll_period, + "employee": employee, + "docstatus": ["<", 2], + "name": ["!=", docname], + }, + ) if existing_record: - frappe.throw(_("{0} already exists for employee {1} and period {2}") - .format(doctype, employee, payroll_period), DuplicateDeclarationError) + frappe.throw( + _("{0} already exists for employee {1} and period {2}").format( + doctype, employee, payroll_period + ), + DuplicateDeclarationError, + ) + def validate_tax_declaration(declarations): subcategories = [] @@ -300,61 +357,79 @@ def validate_tax_declaration(declarations): frappe.throw(_("More than one selection for {0} not allowed").format(d.exemption_sub_category)) subcategories.append(d.exemption_sub_category) + def get_total_exemption_amount(declarations): exemptions = frappe._dict() for d in declarations: exemptions.setdefault(d.exemption_category, frappe._dict()) category_max_amount = exemptions.get(d.exemption_category).max_amount if not category_max_amount: - category_max_amount = frappe.db.get_value("Employee Tax Exemption Category", d.exemption_category, "max_amount") + category_max_amount = frappe.db.get_value( + "Employee Tax Exemption Category", d.exemption_category, "max_amount" + ) exemptions.get(d.exemption_category).max_amount = category_max_amount - sub_category_exemption_amount = d.max_amount \ - if (d.max_amount and flt(d.amount) > flt(d.max_amount)) else d.amount + sub_category_exemption_amount = ( + d.max_amount if (d.max_amount and flt(d.amount) > flt(d.max_amount)) else d.amount + ) exemptions.get(d.exemption_category).setdefault("total_exemption_amount", 0.0) exemptions.get(d.exemption_category).total_exemption_amount += flt(sub_category_exemption_amount) - if category_max_amount and exemptions.get(d.exemption_category).total_exemption_amount > category_max_amount: + if ( + category_max_amount + and exemptions.get(d.exemption_category).total_exemption_amount > category_max_amount + ): exemptions.get(d.exemption_category).total_exemption_amount = category_max_amount 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(""" + leave_period = frappe.db.sql( + """ select name, from_date, to_date from `tabLeave Period` where company=%(company)s and is_active=1 and (from_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s or (from_date < %(from_date)s and to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "company": company - }, as_dict=1) + """, + {"from_date": from_date, "to_date": to_date, "company": company}, + as_dict=1, + ) if leave_period: return leave_period + def generate_leave_encashment(): - ''' Generates a draft leave encashment on allocation expiry ''' + """Generates a draft leave encashment on allocation expiry""" from erpnext.hr.doctype.leave_encashment.leave_encashment import create_leave_encashment - if frappe.db.get_single_value('HR Settings', 'auto_leave_encashment'): - leave_type = frappe.get_all('Leave Type', filters={'allow_encashment': 1}, fields=['name']) - leave_type=[l['name'] for l in leave_type] + if frappe.db.get_single_value("HR Settings", "auto_leave_encashment"): + leave_type = frappe.get_all("Leave Type", filters={"allow_encashment": 1}, fields=["name"]) + leave_type = [l["name"] for l in leave_type] - leave_allocation = frappe.get_all("Leave Allocation", filters={ - 'to_date': add_days(today(), -1), - 'leave_type': ('in', leave_type) - }, fields=['employee', 'leave_period', 'leave_type', 'to_date', 'total_leaves_allocated', 'new_leaves_allocated']) + leave_allocation = frappe.get_all( + "Leave Allocation", + filters={"to_date": add_days(today(), -1), "leave_type": ("in", leave_type)}, + fields=[ + "employee", + "leave_period", + "leave_type", + "to_date", + "total_leaves_allocated", + "new_leaves_allocated", + ], + ) create_leave_encashment(leave_allocation=leave_allocation) + def allocate_earned_leaves(ignore_duplicates=False): - '''Allocate earned leaves to Employees''' + """Allocate earned leaves to Employees""" e_leave_types = get_earned_leaves() today = getdate() @@ -367,37 +442,63 @@ def allocate_earned_leaves(ignore_duplicates=False): if not allocation.leave_policy_assignment and not allocation.leave_policy: continue - leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value( - "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"]) + leave_policy = ( + allocation.leave_policy + if allocation.leave_policy + else frappe.db.get_value( + "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"] + ) + ) - annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={ - 'parent': leave_policy, - 'leave_type': e_leave_type.name - }, fieldname=['annual_allocation']) + annual_allocation = frappe.db.get_value( + "Leave Policy Detail", + filters={"parent": leave_policy, "leave_type": e_leave_type.name}, + fieldname=["annual_allocation"], + ) - from_date=allocation.from_date + from_date = allocation.from_date if e_leave_type.based_on_date_of_joining: - from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") + from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining): - update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates) + if check_effective_date( + from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining + ): + update_previous_leave_allocation( + allocation, annual_allocation, e_leave_type, ignore_duplicates + ) -def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False): - earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding) - allocation = frappe.get_doc('Leave Allocation', allocation.name) - new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) +def update_previous_leave_allocation( + allocation, annual_allocation, e_leave_type, ignore_duplicates=False +): + earned_leaves = get_monthly_earned_leave( + annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding + ) - if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: - new_allocation = e_leave_type.max_leaves_allowed + allocation = frappe.get_doc("Leave Allocation", allocation.name) + new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) - if new_allocation != allocation.total_leaves_allocated: - today_date = today() + if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: + new_allocation = e_leave_type.max_leaves_allowed - if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation): - allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + if new_allocation != allocation.total_leaves_allocated: + today_date = today() + + if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation): + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + + if e_leave_type.based_on_date_of_joining: + text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format( + frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) + ) + else: + text = _("allocated {0} leave(s) via scheduler on {1}").format( + frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) + ) + + allocation.add_comment(comment_type="Info", text=text) def get_monthly_earned_leave(annual_leaves, frequency, rounding): @@ -425,8 +526,9 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) - leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, - annual_allocation, leave_type_details, date_of_joining) + leaves_for_passed_months = assignment.get_leaves_for_passed_months( + allocation.leave_type, annual_allocation, leave_type_details, date_of_joining + ) # exclude carry-forwarded leaves while checking for leave allocation for passed months num_allocations = allocation.total_leaves_allocated @@ -439,26 +541,39 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): def get_leave_allocations(date, leave_type): - return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy + return frappe.db.sql( + """select name, employee, from_date, to_date, leave_policy_assignment, leave_policy from `tabLeave Allocation` where %s between from_date and to_date and docstatus=1 and leave_type=%s""", - (date, leave_type), as_dict=1) + (date, leave_type), + as_dict=1, + ) def get_earned_leaves(): - return frappe.get_all("Leave Type", - fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"], - filters={'is_earned_leave' : 1}) + return frappe.get_all( + "Leave Type", + fields=[ + "name", + "max_leaves_allowed", + "earned_leave_frequency", + "rounding", + "based_on_date_of_joining", + ], + filters={"is_earned_leave": 1}, + ) + def create_additional_leave_ledger_entry(allocation, leaves, date): - ''' Create leave ledger entry for leave types ''' + """Create leave ledger entry for leave types""" allocation.new_leaves_allocated = leaves allocation.from_date = date allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() + def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining): import calendar @@ -467,10 +582,12 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining from_date = get_datetime(from_date) to_date = get_datetime(to_date) rd = relativedelta.relativedelta(to_date, from_date) - #last day of month - last_day = calendar.monthrange(to_date.year, to_date.month)[1] + # last day of month + last_day = calendar.monthrange(to_date.year, to_date.month)[1] - if (from_date.day == to_date.day and based_on_date_of_joining) or (not based_on_date_of_joining and to_date.day == last_day): + if (from_date.day == to_date.day and based_on_date_of_joining) or ( + not based_on_date_of_joining and to_date.day == last_day + ): if frequency == "Monthly": return True elif frequency == "Quarterly" and rd.months % 3: @@ -487,16 +604,21 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining def get_salary_assignment(employee, date): - assignment = frappe.db.sql(""" + assignment = frappe.db.sql( + """ select * from `tabSalary Structure Assignment` where employee=%(employee)s and docstatus = 1 - and %(on_date)s >= from_date order by from_date desc limit 1""", { - 'employee': employee, - 'on_date': date, - }, as_dict=1) + and %(on_date)s >= from_date order by from_date desc limit 1""", + { + "employee": employee, + "on_date": date, + }, + as_dict=1, + ) return assignment[0] if assignment else None + def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): total_given_benefit_amount = 0 query = """ @@ -514,17 +636,22 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): if component: query += "and sd.salary_component = %(component)s" - sum_of_given_benefit = frappe.db.sql(query, { - 'employee': employee, - 'start_date': payroll_period.start_date, - 'end_date': payroll_period.end_date, - 'component': component - }, as_dict=True) + sum_of_given_benefit = frappe.db.sql( + query, + { + "employee": employee, + "start_date": payroll_period.start_date, + "end_date": payroll_period.end_date, + "component": component, + }, + as_dict=True, + ) if sum_of_given_benefit and flt(sum_of_given_benefit[0].total_amount) > 0: total_given_benefit_amount = sum_of_given_benefit[0].total_amount return total_given_benefit_amount + def get_holiday_dates_for_employee(employee, start_date, end_date): """return a list of holiday dates for the given employee between start_date and end_date""" # return only date @@ -533,50 +660,48 @@ def get_holiday_dates_for_employee(employee, start_date, end_date): return [cstr(h.holiday_date) for h in holidays] -def get_holidays_for_employee(employee, start_date, end_date, raise_exception=True, only_non_weekly=False): +def get_holidays_for_employee( + employee, start_date, end_date, raise_exception=True, only_non_weekly=False +): """Get Holidays for a given employee - `employee` (str) - `start_date` (str or datetime) - `end_date` (str or datetime) - `raise_exception` (bool) - `only_non_weekly` (bool) + `employee` (str) + `start_date` (str or datetime) + `end_date` (str or datetime) + `raise_exception` (bool) + `only_non_weekly` (bool) - return: list of dicts with `holiday_date` and `description` + return: list of dicts with `holiday_date` and `description` """ holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception) if not holiday_list: return [] - filters = { - 'parent': holiday_list, - 'holiday_date': ('between', [start_date, end_date]) - } + filters = {"parent": holiday_list, "holiday_date": ("between", [start_date, end_date])} if only_non_weekly: - filters['weekly_off'] = False + filters["weekly_off"] = False - holidays = frappe.get_all( - 'Holiday', - fields=['description', 'holiday_date'], - filters=filters - ) + holidays = frappe.get_all("Holiday", fields=["description", "holiday_date"], filters=filters) return holidays + @erpnext.allow_regional def calculate_annual_eligible_hra_exemption(doc): # Don't delete this method, used for localization # Indian HRA Exemption Calculation return {} + @erpnext.allow_regional def calculate_hra_exemption_for_period(doc): # Don't delete this method, used for localization # Indian HRA Exemption Calculation return {} + def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, component=False): total_claimed_amount = 0 query = """ @@ -591,24 +716,29 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co if component: query += "and earning_component = %(component)s" - sum_of_claimed_amount = frappe.db.sql(query, { - 'employee': employee, - 'start_date': payroll_period.start_date, - 'end_date': payroll_period.end_date, - 'component': component - }, as_dict=True) + sum_of_claimed_amount = frappe.db.sql( + query, + { + "employee": employee, + "start_date": payroll_period.start_date, + "end_date": payroll_period.end_date, + "component": component, + }, + as_dict=True, + ) if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0: total_claimed_amount = sum_of_claimed_amount[0].total_amount return total_claimed_amount + def share_doc_with_approver(doc, user): # if approver does not have permissions, share if not frappe.has_permission(doc=doc, ptype="submit", user=user): - frappe.share.add(doc.doctype, doc.name, user, submit=1, - flags={"ignore_share_permission": True}) + frappe.share.add(doc.doctype, doc.name, user, submit=1, flags={"ignore_share_permission": True}) - frappe.msgprint(_("Shared with the user {0} with {1} access").format( - user, frappe.bold("submit"), alert=True)) + frappe.msgprint( + _("Shared with the user {0} with {1} access").format(user, frappe.bold("submit"), alert=True) + ) # remove shared doc if approver changes doc_before_save = doc.get_doc_before_save() @@ -616,14 +746,19 @@ def share_doc_with_approver(doc, user): approvers = { "Leave Application": "leave_approver", "Expense Claim": "expense_approver", - "Shift Request": "approver" + "Shift Request": "approver", } approver = approvers.get(doc.doctype) if doc_before_save.get(approver) != doc.get(approver): frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver)) + def validate_active_employee(employee): if frappe.db.get_value("Employee", employee, "status") == "Inactive": - frappe.throw(_("Transactions cannot be created for an Inactive Employee {0}.").format( - get_link_to_form("Employee", employee)), InactiveEmployeeStatusError) + frappe.throw( + _("Transactions cannot be created for an Inactive Employee {0}.").format( + get_link_to_form("Employee", employee) + ), + InactiveEmployeeStatusError, + ) diff --git a/erpnext/hr/web_form/job_application/job_application.py b/erpnext/hr/web_form/job_application/job_application.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/hr/web_form/job_application/job_application.py +++ b/erpnext/hr/web_form/job_application/job_application.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/hub_node/__init__.py b/erpnext/hub_node/__init__.py index 2bfbfd1088e..4ff2e2c9dd0 100644 --- a/erpnext/hub_node/__init__.py +++ b/erpnext/hub_node/__init__.py @@ -7,12 +7,13 @@ import frappe @frappe.whitelist() def enable_hub(): - hub_settings = frappe.get_doc('Marketplace Settings') + hub_settings = frappe.get_doc("Marketplace Settings") hub_settings.register() frappe.db.commit() return hub_settings + @frappe.whitelist() def sync(): - hub_settings = frappe.get_doc('Marketplace Settings') + hub_settings = frappe.get_doc("Marketplace Settings") hub_settings.sync() diff --git a/erpnext/hub_node/api.py b/erpnext/hub_node/api.py index 1bf7b8c3b52..410c508de06 100644 --- a/erpnext/hub_node/api.py +++ b/erpnext/hub_node/api.py @@ -1,4 +1,3 @@ - import json import frappe @@ -14,24 +13,24 @@ current_user = frappe.session.user def register_marketplace(company, company_description): validate_registerer() - settings = frappe.get_single('Marketplace Settings') + settings = frappe.get_single("Marketplace Settings") message = settings.register_seller(company, company_description) - if message.get('hub_seller_name'): + if message.get("hub_seller_name"): settings.registered = 1 - settings.hub_seller_name = message.get('hub_seller_name') + settings.hub_seller_name = message.get("hub_seller_name") settings.save() settings.add_hub_user(frappe.session.user) - return { 'ok': 1 } + return {"ok": 1} @frappe.whitelist() def register_users(user_list): user_list = json.loads(user_list) - settings = frappe.get_single('Marketplace Settings') + settings = frappe.get_single("Marketplace Settings") for user in user_list: settings.add_hub_user(user) @@ -40,14 +39,16 @@ def register_users(user_list): def validate_registerer(): - if current_user == 'Administrator': - frappe.throw(_('Please login as another user to register on Marketplace')) + if current_user == "Administrator": + frappe.throw(_("Please login as another user to register on Marketplace")) - valid_roles = ['System Manager', 'Item Manager'] + valid_roles = ["System Manager", "Item Manager"] if not frappe.utils.is_subset(valid_roles, frappe.get_roles()): - frappe.throw(_('Only users with {0} role can register on Marketplace').format(', '.join(valid_roles)), - frappe.PermissionError) + frappe.throw( + _("Only users with {0} role can register on Marketplace").format(", ".join(valid_roles)), + frappe.PermissionError, + ) @frappe.whitelist() @@ -57,9 +58,7 @@ def call_hub_method(method, params=None): if isinstance(params, string_types): params = json.loads(params) - params.update({ - 'cmd': 'hub.hub.api.' + method - }) + params.update({"cmd": "hub.hub.api." + method}) response = connection.post_request(params) return response @@ -67,81 +66,81 @@ def call_hub_method(method, params=None): def map_fields(items): field_mappings = get_field_mappings() - table_fields = [d.fieldname for d in frappe.get_meta('Item').get_table_fields()] + table_fields = [d.fieldname for d in frappe.get_meta("Item").get_table_fields()] - hub_seller_name = frappe.db.get_value('Marketplace Settings', 'Marketplace Settings', 'hub_seller_name') + hub_seller_name = frappe.db.get_value( + "Marketplace Settings", "Marketplace Settings", "hub_seller_name" + ) for item in items: for fieldname in table_fields: item.pop(fieldname, None) for mapping in field_mappings: - local_fieldname = mapping.get('local_fieldname') - remote_fieldname = mapping.get('remote_fieldname') + local_fieldname = mapping.get("local_fieldname") + remote_fieldname = mapping.get("remote_fieldname") value = item.get(local_fieldname) item.pop(local_fieldname, None) item[remote_fieldname] = value - item['doctype'] = 'Hub Item' - item['hub_seller'] = hub_seller_name - item.pop('attachments', None) + item["doctype"] = "Hub Item" + item["hub_seller"] = hub_seller_name + item.pop("attachments", None) return items @frappe.whitelist() -def get_valid_items(search_value=''): +def get_valid_items(search_value=""): items = frappe.get_list( - 'Item', + "Item", fields=["*"], - filters={ - 'disabled': 0, - 'item_name': ['like', '%' + search_value + '%'], - 'publish_in_hub': 0 - }, - order_by="modified desc" + filters={"disabled": 0, "item_name": ["like", "%" + search_value + "%"], "publish_in_hub": 0}, + order_by="modified desc", ) valid_items = filter(lambda x: x.image and x.description, items) def prepare_item(item): item.source_type = "local" - item.attachments = get_attachments('Item', item.item_code) + item.attachments = get_attachments("Item", item.item_code) return item valid_items = map(prepare_item, valid_items) return valid_items + @frappe.whitelist() def update_item(ref_doc, data): data = json.loads(data) - data.update(dict(doctype='Hub Item', name=ref_doc)) + data.update(dict(doctype="Hub Item", name=ref_doc)) try: connection = get_hub_connection() connection.update(data) except Exception as e: - frappe.log_error(message=e, title='Hub Sync Error') + frappe.log_error(message=e, title="Hub Sync Error") + @frappe.whitelist() def publish_selected_items(items_to_publish): items_to_publish = json.loads(items_to_publish) items_to_update = [] if not len(items_to_publish): - frappe.throw(_('No items to publish')) + frappe.throw(_("No items to publish")) for item in items_to_publish: - item_code = item.get('item_code') - frappe.db.set_value('Item', item_code, 'publish_in_hub', 1) + item_code = item.get("item_code") + frappe.db.set_value("Item", item_code, "publish_in_hub", 1) hub_dict = { - 'doctype': 'Hub Tracked Item', - 'item_code': item_code, - 'published': 1, - 'hub_category': item.get('hub_category'), - 'image_list': item.get('image_list') + "doctype": "Hub Tracked Item", + "item_code": item_code, + "published": 1, + "hub_category": item.get("hub_category"), + "image_list": item.get("image_list"), } frappe.get_doc(hub_dict).insert(ignore_if_duplicate=True) @@ -157,65 +156,67 @@ def publish_selected_items(items_to_publish): item_sync_postprocess() except Exception as e: - frappe.log_error(message=e, title='Hub Sync Error') + frappe.log_error(message=e, title="Hub Sync Error") + @frappe.whitelist() def unpublish_item(item_code, hub_item_name): - ''' Remove item listing from the marketplace ''' + """Remove item listing from the marketplace""" - response = call_hub_method('unpublish_item', { - 'hub_item_name': hub_item_name - }) + response = call_hub_method("unpublish_item", {"hub_item_name": hub_item_name}) if response: - frappe.db.set_value('Item', item_code, 'publish_in_hub', 0) - frappe.delete_doc('Hub Tracked Item', item_code) + frappe.db.set_value("Item", item_code, "publish_in_hub", 0) + frappe.delete_doc("Hub Tracked Item", item_code) else: - frappe.throw(_('Unable to update remote activity')) + frappe.throw(_("Unable to update remote activity")) + @frappe.whitelist() def get_unregistered_users(): - settings = frappe.get_single('Marketplace Settings') - registered_users = [user.user for user in settings.users] + ['Administrator', 'Guest'] - all_users = [user.name for user in frappe.db.get_all('User', filters={'enabled': 1})] + settings = frappe.get_single("Marketplace Settings") + registered_users = [user.user for user in settings.users] + ["Administrator", "Guest"] + all_users = [user.name for user in frappe.db.get_all("User", filters={"enabled": 1})] unregistered_users = [user for user in all_users if user not in registered_users] return unregistered_users def item_sync_preprocess(intended_item_publish_count): - response = call_hub_method('pre_items_publish', { - 'intended_item_publish_count': intended_item_publish_count - }) + response = call_hub_method( + "pre_items_publish", {"intended_item_publish_count": intended_item_publish_count} + ) if response: frappe.db.set_value("Marketplace Settings", "Marketplace Settings", "sync_in_progress", 1) return response else: - frappe.throw(_('Unable to update remote activity')) + frappe.throw(_("Unable to update remote activity")) def item_sync_postprocess(): - response = call_hub_method('post_items_publish', {}) + response = call_hub_method("post_items_publish", {}) if response: - frappe.db.set_value('Marketplace Settings', 'Marketplace Settings', 'last_sync_datetime', frappe.utils.now()) + frappe.db.set_value( + "Marketplace Settings", "Marketplace Settings", "last_sync_datetime", frappe.utils.now() + ) else: - frappe.throw(_('Unable to update remote activity')) + frappe.throw(_("Unable to update remote activity")) - frappe.db.set_value('Marketplace Settings', 'Marketplace Settings', 'sync_in_progress', 0) + frappe.db.set_value("Marketplace Settings", "Marketplace Settings", "sync_in_progress", 0) def convert_relative_image_urls_to_absolute(items): from six.moves.urllib.parse import urljoin for item in items: - file_path = item['image'] + file_path = item["image"] - if file_path.startswith('/files/'): - item['image'] = urljoin(frappe.utils.get_url(), file_path) + if file_path.startswith("/files/"): + item["image"] = urljoin(frappe.utils.get_url(), file_path) def get_hub_connection(): - settings = frappe.get_single('Marketplace Settings') + settings = frappe.get_single("Marketplace Settings") marketplace_url = settings.marketplace_url hub_user = settings.get_hub_user(frappe.session.user) diff --git a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py index af2ff37797e..e8b68cbc0e7 100644 --- a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py +++ b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py @@ -10,82 +10,82 @@ from frappe.utils import cint class MarketplaceSettings(Document): - def register_seller(self, company, company_description): - country, currency, company_logo = frappe.db.get_value('Company', company, - ['country', 'default_currency', 'company_logo']) + country, currency, company_logo = frappe.db.get_value( + "Company", company, ["country", "default_currency", "company_logo"] + ) company_details = { - 'company': company, - 'country': country, - 'currency': currency, - 'company_description': company_description, - 'company_logo': company_logo, - 'site_name': frappe.utils.get_url() + "company": company, + "country": country, + "currency": currency, + "company_description": company_description, + "company_logo": company_logo, + "site_name": frappe.utils.get_url(), } hub_connection = self.get_connection() - response = hub_connection.post_request({ - 'cmd': 'hub.hub.api.add_hub_seller', - 'company_details': json.dumps(company_details) - }) + response = hub_connection.post_request( + {"cmd": "hub.hub.api.add_hub_seller", "company_details": json.dumps(company_details)} + ) return response - def add_hub_user(self, user_email): - '''Create a Hub User and User record on hub server + """Create a Hub User and User record on hub server and if successfull append it to Hub User table - ''' + """ if not self.registered: return hub_connection = self.get_connection() - first_name, last_name = frappe.db.get_value('User', user_email, ['first_name', 'last_name']) + first_name, last_name = frappe.db.get_value("User", user_email, ["first_name", "last_name"]) - hub_user = hub_connection.post_request({ - 'cmd': 'hub.hub.api.add_hub_user', - 'user_email': user_email, - 'first_name': first_name, - 'last_name': last_name, - 'hub_seller': self.hub_seller_name - }) + hub_user = hub_connection.post_request( + { + "cmd": "hub.hub.api.add_hub_user", + "user_email": user_email, + "first_name": first_name, + "last_name": last_name, + "hub_seller": self.hub_seller_name, + } + ) - self.append('users', { - 'user': hub_user.get('user_email'), - 'hub_user_name': hub_user.get('hub_user_name'), - 'password': hub_user.get('password') - }) + self.append( + "users", + { + "user": hub_user.get("user_email"), + "hub_user_name": hub_user.get("hub_user_name"), + "password": hub_user.get("password"), + }, + ) self.save() def get_hub_user(self, user): - '''Return the Hub User doc from the `users` table if password is set''' + """Return the Hub User doc from the `users` table if password is set""" - filtered_users = list(filter( - lambda x: x.user == user and x.password, - self.users - )) + filtered_users = list(filter(lambda x: x.user == user and x.password, self.users)) if filtered_users: return filtered_users[0] - def get_connection(self): return FrappeClient(self.marketplace_url) - def unregister(self): """Disable the User on hubmarket.org""" + @frappe.whitelist() def is_marketplace_enabled(): - if not hasattr(frappe.local, 'is_marketplace_enabled'): - frappe.local.is_marketplace_enabled = cint(frappe.db.get_single_value('Marketplace Settings', - 'disable_marketplace')) + if not hasattr(frappe.local, "is_marketplace_enabled"): + frappe.local.is_marketplace_enabled = cint( + frappe.db.get_single_value("Marketplace Settings", "disable_marketplace") + ) return frappe.local.is_marketplace_enabled diff --git a/erpnext/hub_node/legacy.py b/erpnext/hub_node/legacy.py index b19167bfefe..8d95c0706c2 100644 --- a/erpnext/hub_node/legacy.py +++ b/erpnext/hub_node/legacy.py @@ -1,4 +1,3 @@ - import json import frappe @@ -11,9 +10,10 @@ from frappe.utils.nestedset import get_root_of def get_list(doctype, start, limit, fields, filters, order_by): pass + def get_hub_connection(): - if frappe.db.exists('Data Migration Connector', 'Hub Connector'): - hub_connector = frappe.get_doc('Data Migration Connector', 'Hub Connector') + if frappe.db.exists("Data Migration Connector", "Hub Connector"): + hub_connector = frappe.get_doc("Data Migration Connector", "Hub Connector") hub_connection = hub_connector.get_connection() return hub_connection.connection @@ -21,10 +21,11 @@ def get_hub_connection(): hub_connection = FrappeClient(frappe.conf.hub_url) return hub_connection + def make_opportunity(buyer_name, email_id): buyer_name = "HUB-" + buyer_name - if not frappe.db.exists('Lead', {'email_id': email_id}): + if not frappe.db.exists("Lead", {"email_id": email_id}): lead = frappe.new_doc("Lead") lead.lead_name = buyer_name lead.email_id = email_id @@ -32,9 +33,10 @@ def make_opportunity(buyer_name, email_id): o = frappe.new_doc("Opportunity") o.opportunity_from = "Lead" - o.lead = frappe.get_all("Lead", filters={"email_id": email_id}, fields = ["name"])[0]["name"] + o.lead = frappe.get_all("Lead", filters={"email_id": email_id}, fields=["name"])[0]["name"] o.save(ignore_permissions=True) + @frappe.whitelist() def make_rfq_and_send_opportunity(item, supplier): supplier = make_supplier(supplier) @@ -43,105 +45,110 @@ def make_rfq_and_send_opportunity(item, supplier): rfq = make_rfq(item, supplier, contact) status = send_opportunity(contact) - return { - 'rfq': rfq, - 'hub_document_created': status - } + return {"rfq": rfq, "hub_document_created": status} + def make_supplier(supplier): # make supplier if not already exists supplier = frappe._dict(json.loads(supplier)) - if not frappe.db.exists('Supplier', {'supplier_name': supplier.supplier_name}): - supplier_doc = frappe.get_doc({ - 'doctype': 'Supplier', - 'supplier_name': supplier.supplier_name, - 'supplier_group': supplier.supplier_group, - 'supplier_email': supplier.supplier_email - }).insert() + if not frappe.db.exists("Supplier", {"supplier_name": supplier.supplier_name}): + supplier_doc = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": supplier.supplier_name, + "supplier_group": supplier.supplier_group, + "supplier_email": supplier.supplier_email, + } + ).insert() else: - supplier_doc = frappe.get_doc('Supplier', supplier.supplier_name) + supplier_doc = frappe.get_doc("Supplier", supplier.supplier_name) return supplier_doc + def make_contact(supplier): - contact_name = get_default_contact('Supplier', supplier.supplier_name) + contact_name = get_default_contact("Supplier", supplier.supplier_name) # make contact if not already exists if not contact_name: - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': supplier.supplier_name, - 'is_primary_contact': 1, - 'links': [ - {'link_doctype': 'Supplier', 'link_name': supplier.supplier_name} - ] - }) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": supplier.supplier_name, + "is_primary_contact": 1, + "links": [{"link_doctype": "Supplier", "link_name": supplier.supplier_name}], + } + ) contact.add_email(supplier.supplier_email, is_primary=True) contact.insert() else: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) return contact + def make_item(item): # make item if not already exists item = frappe._dict(json.loads(item)) - if not frappe.db.exists('Item', {'item_code': item.item_code}): - item_doc = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': item.item_code, - 'item_group': item.item_group, - 'is_item_from_hub': 1 - }).insert() + if not frappe.db.exists("Item", {"item_code": item.item_code}): + item_doc = frappe.get_doc( + { + "doctype": "Item", + "item_code": item.item_code, + "item_group": item.item_group, + "is_item_from_hub": 1, + } + ).insert() else: - item_doc = frappe.get_doc('Item', item.item_code) + item_doc = frappe.get_doc("Item", item.item_code) return item_doc + def make_rfq(item, supplier, contact): # make rfq - rfq = frappe.get_doc({ - 'doctype': 'Request for Quotation', - 'transaction_date': nowdate(), - 'status': 'Draft', - 'company': frappe.db.get_single_value('Marketplace Settings', 'company'), - 'message_for_supplier': 'Please supply the specified items at the best possible rates', - 'suppliers': [ - { 'supplier': supplier.name, 'contact': contact.name } - ], - 'items': [ - { - 'item_code': item.item_code, - 'qty': 1, - 'schedule_date': nowdate(), - 'warehouse': item.default_warehouse or get_root_of("Warehouse"), - 'description': item.description, - 'uom': item.stock_uom - } - ] - }).insert() + rfq = frappe.get_doc( + { + "doctype": "Request for Quotation", + "transaction_date": nowdate(), + "status": "Draft", + "company": frappe.db.get_single_value("Marketplace Settings", "company"), + "message_for_supplier": "Please supply the specified items at the best possible rates", + "suppliers": [{"supplier": supplier.name, "contact": contact.name}], + "items": [ + { + "item_code": item.item_code, + "qty": 1, + "schedule_date": nowdate(), + "warehouse": item.default_warehouse or get_root_of("Warehouse"), + "description": item.description, + "uom": item.stock_uom, + } + ], + } + ).insert() rfq.save() rfq.submit() return rfq + def send_opportunity(contact): # Make Hub Message on Hub with lead data doc = { - 'doctype': 'Lead', - 'lead_name': frappe.db.get_single_value('Marketplace Settings', 'company'), - 'email_id': frappe.db.get_single_value('Marketplace Settings', 'user') + "doctype": "Lead", + "lead_name": frappe.db.get_single_value("Marketplace Settings", "company"), + "email_id": frappe.db.get_single_value("Marketplace Settings", "user"), } - args = frappe._dict(dict( - doctype='Hub Message', - reference_doctype='Lead', - data=json.dumps(doc), - user=contact.email_id - )) + args = frappe._dict( + dict( + doctype="Hub Message", reference_doctype="Lead", data=json.dumps(doc), user=contact.email_id + ) + ) connection = get_hub_connection() - response = connection.insert('Hub Message', args) + response = connection.insert("Hub Message", args) return response.ok diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py index 9512c8fa195..03eed801fcb 100644 --- a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py @@ -13,10 +13,19 @@ from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applic @frappe.whitelist() @cache_source -def get_data(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None, heatmap_year = None): +def get_data( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, +): if chart_name: - chart = frappe.get_doc('Dashboard Chart', chart_name) + chart = frappe.get_doc("Dashboard Chart", chart_name) else: chart = frappe._dict(frappe.parse_json(chart)) @@ -30,28 +39,44 @@ def get_data(chart_name = None, chart = None, no_cache = None, filters = None, f labels = [] values = [] - if filters.get('company'): + if filters.get("company"): conditions = "AND company = %(company)s" loan_security_details = get_loan_security_details() - unpledges = frappe._dict(frappe.db.sql(""" + unpledges = frappe._dict( + frappe.db.sql( + """ SELECT u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY u.loan_security - """.format(conditions=conditions), filters, as_list=1)) + """.format( + conditions=conditions + ), + filters, + as_list=1, + ) + ) - pledges = frappe._dict(frappe.db.sql(""" + pledges = frappe._dict( + frappe.db.sql( + """ SELECT p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY p.loan_security - """.format(conditions=conditions), filters, as_list=1)) + """.format( + conditions=conditions + ), + filters, + as_list=1, + ) + ) for security, qty in iteritems(pledges): current_pledges.setdefault(security, qty) @@ -61,19 +86,15 @@ def get_data(chart_name = None, chart = None, no_cache = None, filters = None, f count = 0 for security, qty in iteritems(sorted_pledges): - values.append(qty * loan_security_details.get(security, {}).get('latest_price', 0)) + values.append(qty * loan_security_details.get(security, {}).get("latest_price", 0)) labels.append(security) - count +=1 + count += 1 ## Just need top 10 securities if count == 10: break return { - 'labels': labels, - 'datasets': [{ - 'name': 'Top 10 Securities', - 'chartType': 'bar', - 'values': values - }] + "labels": labels, + "datasets": [{"name": "Top 10 Securities", "chartType": "bar", "values": values}], } diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index f3914d51286..03ec4014eec 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -21,7 +21,7 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled class Loan(AccountsController): def validate(self): - if self.applicant_type == 'Employee' and self.repay_from_salary: + if self.applicant_type == "Employee" and self.repay_from_salary: validate_employee_currency_with_company_currency(self.applicant, self.company) self.set_loan_amount() self.validate_loan_amount() @@ -31,27 +31,40 @@ class Loan(AccountsController): self.validate_repay_from_salary() if self.is_term_loan: - validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, - self.repayment_periods, self.is_term_loan) + validate_repayment_method( + self.repayment_method, + self.loan_amount, + self.monthly_repayment_amount, + self.repayment_periods, + self.is_term_loan, + ) self.make_repayment_schedule() self.set_repayment_period() self.calculate_totals() def validate_accounts(self): - for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']: - company = frappe.get_value("Account", self.get(fieldname), 'company') + for fieldname in [ + "payment_account", + "loan_account", + "interest_income_account", + "penalty_income_account", + ]: + company = frappe.get_value("Account", self.get(fieldname), "company") if company != self.company: - frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)), - frappe.bold(self.company))) + frappe.throw( + _("Account {0} does not belongs to company {1}").format( + frappe.bold(self.get(fieldname)), frappe.bold(self.company) + ) + ) def on_submit(self): self.link_loan_security_pledge() def on_cancel(self): self.unlink_loan_security_pledge() - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] def set_missing_fields(self): if not self.company: @@ -64,15 +77,25 @@ class Loan(AccountsController): self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest") if self.repayment_method == "Repay Over Number of Periods": - self.monthly_repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods) + self.monthly_repayment_amount = get_monthly_repayment_amount( + self.loan_amount, self.rate_of_interest, self.repayment_periods + ) def check_sanctioned_amount_limit(self): - sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + sanctioned_amount_limit = get_sanctioned_amount_limit( + self.applicant_type, self.applicant, self.company + ) if sanctioned_amount_limit: total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) - if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): - frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt( + sanctioned_amount_limit + ): + frappe.throw( + _("Sanctioned Amount limit crossed for {0} {1}").format( + self.applicant_type, frappe.bold(self.applicant) + ) + ) def validate_repay_from_salary(self): if not self.is_term_loan and self.repay_from_salary: @@ -85,8 +108,8 @@ class Loan(AccountsController): self.repayment_schedule = [] payment_date = self.repayment_start_date balance_amount = self.loan_amount - while(balance_amount > 0): - interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12*100)) + while balance_amount > 0: + interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100)) principal_amount = self.monthly_repayment_amount - interest_amount balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) if balance_amount < 0: @@ -94,13 +117,16 @@ class Loan(AccountsController): balance_amount = 0.0 total_payment = principal_amount + interest_amount - self.append("repayment_schedule", { - "payment_date": payment_date, - "principal_amount": principal_amount, - "interest_amount": interest_amount, - "total_payment": total_payment, - "balance_loan_amount": balance_amount - }) + self.append( + "repayment_schedule", + { + "payment_date": payment_date, + "principal_amount": principal_amount, + "interest_amount": interest_amount, + "total_payment": total_payment, + "balance_loan_amount": balance_amount, + }, + ) next_payment_date = add_single_month(payment_date) payment_date = next_payment_date @@ -118,14 +144,13 @@ class Loan(AccountsController): if self.is_term_loan: for data in self.repayment_schedule: self.total_payment += data.total_payment - self.total_interest_payable +=data.interest_amount + self.total_interest_payable += data.interest_amount else: self.total_payment = self.loan_amount def set_loan_amount(self): if self.loan_application and not self.loan_amount: - self.loan_amount = frappe.db.get_value('Loan Application', self.loan_application, 'loan_amount') - + self.loan_amount = frappe.db.get_value("Loan Application", self.loan_application, "loan_amount") def validate_loan_amount(self): if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: @@ -137,30 +162,36 @@ class Loan(AccountsController): def link_loan_security_pledge(self): if self.is_secured_loan and self.loan_application: - maximum_loan_value = frappe.db.get_value('Loan Security Pledge', - { - 'loan_application': self.loan_application, - 'status': 'Requested' - }, - 'sum(maximum_loan_value)' + maximum_loan_value = frappe.db.get_value( + "Loan Security Pledge", + {"loan_application": self.loan_application, "status": "Requested"}, + "sum(maximum_loan_value)", ) if maximum_loan_value: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLoan Security Pledge` SET loan = %s, pledge_time = %s, status = 'Pledged' WHERE status = 'Requested' and loan_application = %s - """, (self.name, now_datetime(), self.loan_application)) + """, + (self.name, now_datetime(), self.loan_application), + ) - self.db_set('maximum_loan_amount', maximum_loan_value) + self.db_set("maximum_loan_amount", maximum_loan_value) def unlink_loan_security_pledge(self): - pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name}) + pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name}) pledge_list = [d.name for d in pledges] if pledge_list: - frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET + frappe.db.sql( + """UPDATE `tabLoan Security Pledge` SET loan = '', status = 'Unpledged' - where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) #nosec + where name in (%s) """ + % (", ".join(["%s"] * len(pledge_list))), + tuple(pledge_list), + ) # nosec + def update_total_amount_paid(doc): total_amount_paid = 0 @@ -169,24 +200,51 @@ def update_total_amount_paid(doc): total_amount_paid += data.total_payment frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) + def get_total_loan_amount(applicant_type, applicant, company): pending_amount = 0 - loan_details = frappe.db.get_all("Loan", - filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1, - "status": ("!=", "Closed")}, - fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid", - "written_off_amount"]) + loan_details = frappe.db.get_all( + "Loan", + filters={ + "applicant_type": applicant_type, + "company": company, + "applicant": applicant, + "docstatus": 1, + "status": ("!=", "Closed"), + }, + fields=[ + "status", + "total_payment", + "disbursed_amount", + "total_interest_payable", + "total_principal_paid", + "written_off_amount", + ], + ) - interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type, - "company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)")) + interest_amount = flt( + frappe.db.get_value( + "Loan Interest Accrual", + {"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1}, + "sum(interest_amount - paid_interest_amount)", + ) + ) for loan in loan_details: if loan.status in ("Disbursed", "Loan Closure Requested"): - pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + pending_amount += ( + flt(loan.total_payment) + - flt(loan.total_interest_payable) + - flt(loan.total_principal_paid) + - flt(loan.written_off_amount) + ) elif loan.status == "Partially Disbursed": - pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + pending_amount += ( + flt(loan.disbursed_amount) + - flt(loan.total_interest_payable) + - flt(loan.total_principal_paid) + - flt(loan.written_off_amount) + ) elif loan.status == "Sanctioned": pending_amount += flt(loan.total_payment) @@ -194,12 +252,18 @@ def get_total_loan_amount(applicant_type, applicant, company): return pending_amount -def get_sanctioned_amount_limit(applicant_type, applicant, company): - return frappe.db.get_value('Sanctioned Loan Amount', - {'applicant_type': applicant_type, 'company': company, 'applicant': applicant}, - 'sanctioned_amount_limit') -def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan): +def get_sanctioned_amount_limit(applicant_type, applicant, company): + return frappe.db.get_value( + "Sanctioned Loan Amount", + {"applicant_type": applicant_type, "company": company, "applicant": applicant}, + "sanctioned_amount_limit", + ) + + +def validate_repayment_method( + repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan +): if is_term_loan and not repayment_method: frappe.throw(_("Repayment Method is mandatory for term loans")) @@ -213,27 +277,34 @@ def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_a if monthly_repayment_amount > loan_amount: frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount")) + def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods): if rate_of_interest: - monthly_interest_rate = flt(rate_of_interest) / (12 *100) - monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate * - (1 + monthly_interest_rate)**repayment_periods) \ - / ((1 + monthly_interest_rate)**repayment_periods - 1)) + monthly_interest_rate = flt(rate_of_interest) / (12 * 100) + monthly_repayment_amount = math.ceil( + (loan_amount * monthly_interest_rate * (1 + monthly_interest_rate) ** repayment_periods) + / ((1 + monthly_interest_rate) ** repayment_periods - 1) + ) else: monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) return monthly_repayment_amount + @frappe.whitelist() def request_loan_closure(loan, posting_date=None): if not posting_date: posting_date = getdate() amounts = calculate_amounts(loan, posting_date) - pending_amount = amounts['pending_principal_amount'] + amounts['unaccrued_interest'] + \ - amounts['interest_amount'] + amounts['penalty_amount'] + pending_amount = ( + amounts["pending_principal_amount"] + + amounts["unaccrued_interest"] + + amounts["interest_amount"] + + amounts["penalty_amount"] + ) - loan_type = frappe.get_value('Loan', loan, 'loan_type') - write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount') + loan_type = frappe.get_value("Loan", loan, "loan_type") + write_off_limit = frappe.get_value("Loan Type", loan_type, "write_off_amount") if pending_amount and abs(pending_amount) < write_off_limit: # Auto create loan write off and update status as loan closure requested @@ -242,7 +313,8 @@ def request_loan_closure(loan, posting_date=None): elif pending_amount > 0: frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) - frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') + frappe.db.set_value("Loan", loan, "status", "Loan Closure Requested") + @frappe.whitelist() def get_loan_application(loan_application): @@ -250,10 +322,12 @@ def get_loan_application(loan_application): if loan: return loan.as_dict() + def close_loan(loan, total_amount_paid): frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid) frappe.db.set_value("Loan", loan, "status", "Closed") + @frappe.whitelist() def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amount=0, as_dict=0): disbursement_entry = frappe.new_doc("Loan Disbursement") @@ -270,6 +344,7 @@ def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amo else: return disbursement_entry + @frappe.whitelist() def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as_dict=0): repayment_entry = frappe.new_doc("Loan Repayment") @@ -285,27 +360,28 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as else: return repayment_entry + @frappe.whitelist() def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict=0): if not company: - company = frappe.get_value('Loan', loan, 'company') + company = frappe.get_value("Loan", loan, "company") if not posting_date: posting_date = getdate() amounts = calculate_amounts(loan, posting_date) - pending_amount = amounts['pending_principal_amount'] + pending_amount = amounts["pending_principal_amount"] if amount and (amount > pending_amount): - frappe.throw(_('Write Off amount cannot be greater than pending loan amount')) + frappe.throw(_("Write Off amount cannot be greater than pending loan amount")) if not amount: amount = pending_amount # get default write off account from company master - write_off_account = frappe.get_value('Company', company, 'write_off_account') + write_off_account = frappe.get_value("Company", company, "write_off_account") - write_off = frappe.new_doc('Loan Write Off') + write_off = frappe.new_doc("Loan Write Off") write_off.loan = loan write_off.posting_date = posting_date write_off.write_off_account = write_off_account @@ -317,26 +393,35 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict else: return write_off + @frappe.whitelist() -def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0): +def unpledge_security( + loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0 +): # if no security_map is passed it will be considered as full unpledge if security_map and isinstance(security_map, string_types): security_map = json.loads(security_map) if loan: pledge_qty_map = security_map or get_pledged_security_qty(loan) - loan_doc = frappe.get_doc('Loan', loan) - unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, - loan_doc.applicant_type, loan_doc.applicant) + loan_doc = frappe.get_doc("Loan", loan) + unpledge_request = create_loan_security_unpledge( + pledge_qty_map, loan_doc.name, loan_doc.company, loan_doc.applicant_type, loan_doc.applicant + ) # will unpledge qty based on loan security pledge elif loan_security_pledge: security_map = {} - pledge_doc = frappe.get_doc('Loan Security Pledge', loan_security_pledge) + pledge_doc = frappe.get_doc("Loan Security Pledge", loan_security_pledge) for security in pledge_doc.securities: security_map.setdefault(security.loan_security, security.qty) - unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan, - pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant) + unpledge_request = create_loan_security_unpledge( + security_map, + pledge_doc.loan, + pledge_doc.company, + pledge_doc.applicant_type, + pledge_doc.applicant, + ) if save: unpledge_request.save() @@ -346,16 +431,17 @@ def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, a if approve: if unpledge_request.docstatus == 1: - unpledge_request.status = 'Approved' + unpledge_request.status = "Approved" unpledge_request.save() else: - frappe.throw(_('Only submittted unpledge requests can be approved')) + frappe.throw(_("Only submittted unpledge requests can be approved")) if as_dict: return unpledge_request else: return unpledge_request + def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant): unpledge_request = frappe.new_doc("Loan Security Unpledge") unpledge_request.applicant_type = applicant_type @@ -365,17 +451,16 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a for security, qty in unpledge_map.items(): if qty: - unpledge_request.append('securities', { - "loan_security": security, - "qty": qty - }) + unpledge_request.append("securities", {"loan_security": security, "qty": qty}) return unpledge_request + def validate_employee_currency_with_company_currency(applicant, company): from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import ( get_employee_currency, ) + if not applicant: frappe.throw(_("Please select Applicant")) if not company: @@ -383,18 +468,20 @@ def validate_employee_currency_with_company_currency(applicant, company): employee_currency = get_employee_currency(applicant) company_currency = erpnext.get_company_currency(company) if employee_currency != company_currency: - frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") - .format(applicant, employee_currency)) + frappe.throw( + _( + "Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}" + ).format(applicant, employee_currency) + ) + @frappe.whitelist() def get_shortfall_applicants(): - loans = frappe.get_all('Loan Security Shortfall', {'status': 'Pending'}, pluck='loan') - applicants = set(frappe.get_all('Loan', {'name': ('in', loans)}, pluck='name')) + loans = frappe.get_all("Loan Security Shortfall", {"status": "Pending"}, pluck="loan") + applicants = set(frappe.get_all("Loan", {"name": ("in", loans)}, pluck="name")) + + return {"value": len(applicants), "fieldtype": "Int"} - return { - "value": len(applicants), - "fieldtype": "Int" - } def add_single_month(date): if getdate(date) == get_last_day(date): @@ -402,29 +489,46 @@ def add_single_month(date): else: return add_months(date, 1) + @frappe.whitelist() def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0): - loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant', - 'loan_account', 'payment_account', 'posting_date', 'company', 'name', - 'total_payment', 'total_principal_paid'], as_dict=1) + loan_details = frappe.db.get_value( + "Loan", + loan, + [ + "applicant_type", + "applicant", + "loan_account", + "payment_account", + "posting_date", + "company", + "name", + "total_payment", + "total_principal_paid", + ], + as_dict=1, + ) - loan_details.doctype = 'Loan' + loan_details.doctype = "Loan" loan_details[loan_details.applicant_type.lower()] = loan_details.applicant if not amount: amount = flt(loan_details.total_principal_paid - loan_details.total_payment) if amount < 0: - frappe.throw(_('No excess amount pending for refund')) + frappe.throw(_("No excess amount pending for refund")) - refund_jv = get_payment_entry(loan_details, { - "party_type": loan_details.applicant_type, - "party_account": loan_details.loan_account, - "amount_field_party": 'debit_in_account_currency', - "amount_field_bank": 'credit_in_account_currency', - "amount": amount, - "bank_account": loan_details.payment_account - }) + refund_jv = get_payment_entry( + loan_details, + { + "party_type": loan_details.applicant_type, + "party_account": loan_details.loan_account, + "amount_field_party": "debit_in_account_currency", + "amount_field_bank": "credit_in_account_currency", + "amount": amount, + "bank_account": loan_details.payment_account, + }, + ) if reference_number: refund_jv.cheque_no = reference_number @@ -435,4 +539,4 @@ def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, s if submit: refund_jv.submit() - return refund_jv \ No newline at end of file + return refund_jv diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py index 0374eda4991..971d5450eaa 100644 --- a/erpnext/loan_management/doctype/loan/loan_dashboard.py +++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py @@ -1,18 +1,19 @@ - - def get_data(): return { - 'fieldname': 'loan', - 'non_standard_fieldnames': { - 'Loan Disbursement': 'against_loan', - 'Loan Repayment': 'against_loan', + "fieldname": "loan", + "non_standard_fieldnames": { + "Loan Disbursement": "against_loan", + "Loan Repayment": "against_loan", }, - 'transactions': [ + "transactions": [ + {"items": ["Loan Security Pledge", "Loan Security Shortfall", "Loan Disbursement"]}, { - 'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement'] + "items": [ + "Loan Repayment", + "Loan Interest Accrual", + "Loan Write Off", + "Loan Security Unpledge", + ] }, - { - 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge'] - } - ] + ], } diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 5ebb2e1bdce..e2b0870c322 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -39,31 +39,69 @@ from erpnext.selling.doctype.customer.test_customer import get_customer_dict class TestLoan(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Personal Loan", 500000, 8.4, + create_loan_type( + "Personal Loan", + 500000, + 8.4, is_term_loan=1, - mode_of_payment='Cash', - disbursement_account='Disbursement Account - _TC', - payment_account='Payment Account - _TC', - loan_account='Loan Account - _TC', - interest_income_account='Interest Income Account - _TC', - penalty_income_account='Penalty Income Account - _TC') + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + ) - create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC', - 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type( + "Stock Loan", + 2000000, + 13.5, + 25, + 1, + 5, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', - 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type( + "Demand Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) create_loan_security_type() create_loan_security() - create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) - create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price( + "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) + create_loan_security_price( + "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) self.applicant1 = make_employee("robert_loan@loan.com") - make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company") + make_salary_structure( + "Test Salary Structure Loan", + "Monthly", + employee=self.applicant1, + currency="INR", + company="_Test Company", + ) if not frappe.db.exists("Customer", "_Test Loan Customer"): - frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) if not frappe.db.exists("Customer", "_Test Loan Customer 1"): frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True) @@ -74,7 +112,7 @@ class TestLoan(unittest.TestCase): create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) def test_loan(self): - loan = frappe.get_doc("Loan", {"applicant":self.applicant1}) + loan = frappe.get_doc("Loan", {"applicant": self.applicant1}) self.assertEqual(loan.monthly_repayment_amount, 15052) self.assertEqual(flt(loan.total_interest_payable, 0), 21034) self.assertEqual(flt(loan.total_payment, 0), 301034) @@ -83,7 +121,11 @@ class TestLoan(unittest.TestCase): self.assertEqual(len(schedule), 20) - for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227080], [19, 14941, 105, 0], [17, 14740, 312, 29785]]: + for idx, principal_amount, interest_amount, balance_loan_amount in [ + [3, 13369, 1683, 227080], + [19, 14941, 105, 0], + [17, 14740, 312, 29785], + ]: self.assertEqual(flt(schedule[idx].principal_amount, 0), principal_amount) self.assertEqual(flt(schedule[idx].interest_amount, 0), interest_amount) self.assertEqual(flt(schedule[idx].balance_loan_amount, 0), balance_loan_amount) @@ -98,30 +140,35 @@ class TestLoan(unittest.TestCase): def test_loan_with_security(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00, - }] + pledge = [ + { + "loan_security": "Test Security 1", + "qty": 4000.00, + } + ] - loan_application = create_loan_application('_Test Company', self.applicant2, - 'Stock Loan', pledge, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledge, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", - 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) self.assertEqual(loan.loan_amount, 1000000) def test_loan_disbursement(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Stock Loan', pledge, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledge, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) self.assertEqual(loan.loan_amount, 1000000) loan.submit() @@ -130,14 +177,16 @@ class TestLoan(unittest.TestCase): loan_disbursement_entry2 = make_loan_disbursement_entry(loan.name, 500000) loan = frappe.get_doc("Loan", loan.name) - gl_entries1 = frappe.db.get_all("GL Entry", + gl_entries1 = frappe.db.get_all( + "GL Entry", fields=["name"], - filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry1.name} + filters={"voucher_type": "Loan Disbursement", "voucher_no": loan_disbursement_entry1.name}, ) - gl_entries2 = frappe.db.get_all("GL Entry", + gl_entries2 = frappe.db.get_all( + "GL Entry", fields=["name"], - filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry2.name} + filters={"voucher_type": "Loan Disbursement", "voucher_no": loan_disbursement_entry2.name}, ) self.assertEqual(loan.status, "Disbursed") @@ -151,73 +200,93 @@ class TestLoan(unittest.TestCase): frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'") frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'") - if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer", - "applicant": "_Test Loan Customer 1", "company": "_Test Company"}): - frappe.get_doc({ - "doctype": "Sanctioned Loan Amount", + if not frappe.db.get_value( + "Sanctioned Loan Amount", + filters={ "applicant_type": "Customer", "applicant": "_Test Loan Customer 1", - "sanctioned_amount_limit": 1500000, - "company": "_Test Company" - }).insert(ignore_permissions=True) + "company": "_Test Company", + }, + ): + frappe.get_doc( + { + "doctype": "Sanctioned Loan Amount", + "applicant_type": "Customer", + "applicant": "_Test Loan Customer 1", + "sanctioned_amount_limit": 1500000, + "company": "_Test Company", + } + ).insert(ignore_permissions=True) # Make First Loan - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant3, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant3, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() # Make second loan greater than the sanctioned amount - loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge, - do_not_save=True) + loan_application = create_loan_application( + "_Test Company", self.applicant3, "Demand Loan", pledge, do_not_save=True + ) self.assertRaises(frappe.ValidationError, loan_application.save) def test_regular_loan_repayment(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = flt((loan.loan_amount * loan.rate_of_interest * no_of_days) - / (days_in_year(get_datetime(first_date).year) * 100), 2) + accrued_interest_amount = flt( + (loan.loan_amount * loan.rate_of_interest * no_of_days) + / (days_in_year(get_datetime(first_date).year) * 100), + 2, + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, add_days(last_date, 10), 111119 + ) repayment_entry.save() repayment_entry.submit() penalty_amount = (accrued_interest_amount * 5 * 25) / 100 - self.assertEqual(flt(repayment_entry.penalty_amount,0), flt(penalty_amount, 0)) + self.assertEqual(flt(repayment_entry.penalty_amount, 0), flt(penalty_amount, 0)) - amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount']) + amounts = frappe.db.get_all( + "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount"] + ) loan.load_from_db() - total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount'] - self.assertEqual(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable) - self.assertEqual(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid - - penalty_amount - total_interest_paid, 0)) + total_interest_paid = amounts[0]["paid_interest_amount"] + amounts[1]["paid_interest_amount"] + self.assertEqual(amounts[1]["paid_interest_amount"], repayment_entry.interest_payable) + self.assertEqual( + flt(loan.total_principal_paid, 0), + flt(repayment_entry.amount_paid - penalty_amount - total_interest_paid, 0), + ) # Check Repayment Entry cancel repayment_entry.load_from_db() @@ -228,21 +297,22 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.total_principal_paid, 0) def test_loan_closure(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 @@ -251,20 +321,27 @@ class TestLoan(unittest.TestCase): # 5 days as well though in grace period no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount), + ) repayment_entry.submit() - amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + amount = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"] + ) - self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0)) self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0) request_loan_closure(loan.name) @@ -272,78 +349,101 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, "Loan Closure Requested") def test_loan_repayment_for_term_loan(self): - pledges = [{ - "loan_security": "Test Security 2", - "qty": 4000.00 - }, - { - "loan_security": "Test Security 1", - "qty": 2000.00 - }] + pledges = [ + {"loan_security": "Test Security 2", "qty": 4000.00}, + {"loan_security": "Test Security 1", "qty": 2000.00}, + ] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Stock Loan', pledges, - "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application, - posting_date=add_months(nowdate(), -1)) + loan = create_loan_with_security( + self.applicant2, + "Stock Loan", + "Repay Over Number of Periods", + 12, + loan_application, + posting_date=add_months(nowdate(), -1), + ) loan.submit() - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + make_loan_disbursement_entry( + loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1) + ) process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75 + ) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amounts = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount", "paid_principal_amount"] + ) self.assertEqual(amounts[0], 11250.00) self.assertEqual(amounts[1], 78303.00) def test_repayment_schedule_update(self): - loan = create_loan(self.applicant2, "Personal Loan", 200000, "Repay Over Number of Periods", 4, - applicant_type='Customer', repayment_start_date='2021-04-30', posting_date='2021-04-01') + loan = create_loan( + self.applicant2, + "Personal Loan", + 200000, + "Repay Over Number of Periods", + 4, + applicant_type="Customer", + repayment_start_date="2021-04-30", + posting_date="2021-04-01", + ) loan.submit() - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date='2021-04-01') + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date="2021-04-01") - process_loan_interest_accrual_for_term_loans(posting_date='2021-05-01') - process_loan_interest_accrual_for_term_loans(posting_date='2021-06-01') + process_loan_interest_accrual_for_term_loans(posting_date="2021-05-01") + process_loan_interest_accrual_for_term_loans(posting_date="2021-06-01") - repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2021-06-05', 120000) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, "2021-06-05", 120000) repayment_entry.submit() loan.load_from_db() - self.assertEqual(flt(loan.get('repayment_schedule')[3].principal_amount, 2), 41369.83) - self.assertEqual(flt(loan.get('repayment_schedule')[3].interest_amount, 2), 289.59) - self.assertEqual(flt(loan.get('repayment_schedule')[3].total_payment, 2), 41659.41) - self.assertEqual(flt(loan.get('repayment_schedule')[3].balance_loan_amount, 2), 0) + self.assertEqual(flt(loan.get("repayment_schedule")[3].principal_amount, 2), 41369.83) + self.assertEqual(flt(loan.get("repayment_schedule")[3].interest_amount, 2), 289.59) + self.assertEqual(flt(loan.get("repayment_schedule")[3].total_payment, 2), 41659.41) + self.assertEqual(flt(loan.get("repayment_schedule")[3].balance_loan_amount, 2), 0) def test_security_shortfall(self): - pledges = [{ - "loan_security": "Test Security 2", - "qty": 8000.00, - "haircut": 50, - }] + pledges = [ + { + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + } + ] - loan_application = create_loan_application('_Test Company', self.applicant2, - 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) loan.submit() make_loan_disbursement_entry(loan.name, loan.loan_amount) - frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 - where loan_security='Test Security 2'""") + frappe.db.sql( + """UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""" + ) create_process_loan_security_shortfall() loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) @@ -353,8 +453,10 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan_security_shortfall.security_value, 800000.00) self.assertEqual(loan_security_shortfall.shortfall_amount, 600000.00) - frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 - where loan_security='Test Security 2'""") + frappe.db.sql( + """ UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""" + ) create_process_loan_security_shortfall() loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) @@ -362,33 +464,40 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan_security_shortfall.shortfall_amount, 0) def test_loan_security_unpledge(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount), + ) repayment_entry.submit() request_loan_closure(loan.name) @@ -397,98 +506,108 @@ class TestLoan(unittest.TestCase): unpledge_request = unpledge_security(loan=loan.name, save=1) unpledge_request.submit() - unpledge_request.status = 'Approved' + unpledge_request.status = "Approved" unpledge_request.save() loan.load_from_db() pledged_qty = get_pledged_security_qty(loan.name) - self.assertEqual(loan.status, 'Closed') + self.assertEqual(loan.status, "Closed") self.assertEqual(sum(pledged_qty.values()), 0) amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(amounts['pending_principal_amount'], 0) - self.assertEqual(amounts['payable_principal_amount'], 0.0) - self.assertEqual(amounts['interest_amount'], 0) + self.assertEqual(amounts["pending_principal_amount"], 0) + self.assertEqual(amounts["payable_principal_amount"], 0.0) + self.assertEqual(amounts["interest_amount"], 0) def test_partial_loan_security_unpledge(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 2000.00 - }, - { - "loan_security": "Test Security 2", - "qty": 4000.00 - }] + pledge = [ + {"loan_security": "Test Security 1", "qty": 2000.00}, + {"loan_security": "Test Security 2", "qty": 4000.00}, + ] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, add_days(last_date, 5), 600000 + ) repayment_entry.submit() - unpledge_map = {'Test Security 2': 2000} + unpledge_map = {"Test Security 2": 2000} - unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_request = unpledge_security(loan=loan.name, security_map=unpledge_map, save=1) unpledge_request.submit() - unpledge_request.status = 'Approved' + unpledge_request.status = "Approved" unpledge_request.save() unpledge_request.submit() unpledge_request.load_from_db() self.assertEqual(unpledge_request.docstatus, 1) def test_sanctioned_loan_security_unpledge(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - unpledge_map = {'Test Security 1': 4000} - unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_map = {"Test Security 1": 4000} + unpledge_request = unpledge_security(loan=loan.name, security_map=unpledge_map, save=1) unpledge_request.submit() - unpledge_request.status = 'Approved' + unpledge_request.status = "Approved" unpledge_request.save() unpledge_request.submit() def test_disbursal_check_with_shortfall(self): - pledges = [{ - "loan_security": "Test Security 2", - "qty": 8000.00, - "haircut": 50, - }] + pledges = [ + { + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + } + ] - loan_application = create_loan_application('_Test Company', self.applicant2, - 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) loan.submit() - #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + # Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge make_loan_disbursement_entry(loan.name, 700000) - frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 - where loan_security='Test Security 2'""") + frappe.db.sql( + """UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""" + ) create_process_loan_security_shortfall() loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) @@ -496,422 +615,505 @@ class TestLoan(unittest.TestCase): self.assertEqual(get_disbursal_amount(loan.name), 0) - frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 - where loan_security='Test Security 2'""") + frappe.db.sql( + """ UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""" + ) def test_disbursal_check_without_shortfall(self): - pledges = [{ - "loan_security": "Test Security 2", - "qty": 8000.00, - "haircut": 50, - }] + pledges = [ + { + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + } + ] - loan_application = create_loan_application('_Test Company', self.applicant2, - 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) loan.submit() - #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + # Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge make_loan_disbursement_entry(loan.name, 700000) self.assertEqual(get_disbursal_amount(loan.name), 300000) def test_pending_loan_amount_after_closure_request(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount), + ) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amounts = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount", "paid_principal_amount"] + ) request_loan_closure(loan.name) loan.load_from_db() self.assertEqual(loan.status, "Loan Closure Requested") amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(amounts['pending_principal_amount'], 0.0) + self.assertEqual(amounts["pending_principal_amount"], 0.0) def test_partial_unaccrued_interest_payment(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5.5 # get partial unaccrued interest amount - paid_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + paid_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - paid_amount) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, add_days(last_date, 5), paid_amount + ) repayment_entry.submit() repayment_entry.load_from_db() - partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) \ - / (days_in_year(get_datetime(first_date).year) * 100) + partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) - interest_amount = flt(amounts['interest_amount'] + partial_accrued_interest_amount, 2) + interest_amount = flt(amounts["interest_amount"] + partial_accrued_interest_amount, 2) self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) def test_penalty(self): loan, amounts = create_loan_scenario_for_penalty(self) # 30 days - grace period penalty_days = 30 - 4 - penalty_applicable_amount = flt(amounts['interest_amount']/2) + penalty_applicable_amount = flt(amounts["interest_amount"] / 2) penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2) - process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30') + process = process_loan_interest_accrual_for_demand_loans(posting_date="2019-11-30") - calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', - {'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount') + calculated_penalty_amount = frappe.db.get_value( + "Loan Interest Accrual", + {"process_loan_interest_accrual": process, "loan": loan.name}, + "penalty_amount", + ) self.assertEqual(loan.loan_amount, 1000000) self.assertEqual(calculated_penalty_amount, penalty_amount) def test_penalty_repayment(self): loan, dummy = create_loan_scenario_for_penalty(self) - amounts = calculate_amounts(loan.name, '2019-11-30 00:00:00') + amounts = calculate_amounts(loan.name, "2019-11-30 00:00:00") first_penalty = 10000 - second_penalty = amounts['penalty_amount'] - 10000 + second_penalty = amounts["penalty_amount"] - 10000 - repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:00', 10000) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, "2019-11-30 00:00:00", 10000 + ) repayment_entry.submit() - amounts = calculate_amounts(loan.name, '2019-11-30 00:00:01') - self.assertEqual(amounts['penalty_amount'], second_penalty) + amounts = calculate_amounts(loan.name, "2019-11-30 00:00:01") + self.assertEqual(amounts["penalty_amount"], second_penalty) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:01', second_penalty) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, "2019-11-30 00:00:01", second_penalty + ) repayment_entry.submit() - amounts = calculate_amounts(loan.name, '2019-11-30 00:00:02') - self.assertEqual(amounts['penalty_amount'], 0) + amounts = calculate_amounts(loan.name, "2019-11-30 00:00:02") + self.assertEqual(amounts["penalty_amount"], 0) def test_loan_write_off_limit(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) # repay 50 less so that it can be automatically written off - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - flt(loan.loan_amount + accrued_interest_amount - 50)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount - 50), + ) repayment_entry.submit() - amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + amount = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"] + ) - self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0)) self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0) amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(flt(amounts['pending_principal_amount'], 0), 50) + self.assertEqual(flt(amounts["pending_principal_amount"], 0), 50) request_loan_closure(loan.name) loan.load_from_db() self.assertEqual(loan.status, "Loan Closure Requested") def test_loan_repayment_against_partially_disbursed_loan(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" - make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date) + make_loan_disbursement_entry(loan.name, loan.loan_amount / 2, disbursement_date=first_date) loan.load_from_db() self.assertEqual(loan.status, "Partially Disbursed") - create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - flt(loan.loan_amount/3)) + create_repayment_entry( + loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount / 3) + ) def test_loan_amount_write_off(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) # repay 100 less so that it can be automatically written off - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - flt(loan.loan_amount + accrued_interest_amount - 100)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount - 100), + ) repayment_entry.submit() - amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + amount = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"] + ) - self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0)) self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0) amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(flt(amounts['pending_principal_amount'], 0), 100) + self.assertEqual(flt(amounts["pending_principal_amount"], 0), 100) - we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount']) + we = make_loan_write_off(loan.name, amount=amounts["pending_principal_amount"]) we.submit() amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(flt(amounts['pending_principal_amount'], 0), 0) + self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0) + def create_loan_scenario_for_penalty(doc): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', doc.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application("_Test Company", doc.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan(doc.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + doc.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 1)) - paid_amount = amounts['interest_amount']/2 + paid_amount = amounts["interest_amount"] / 2 - repayment_entry = create_repayment_entry(loan.name, doc.applicant2, add_days(last_date, 5), - paid_amount) + repayment_entry = create_repayment_entry( + loan.name, doc.applicant2, add_days(last_date, 5), paid_amount + ) repayment_entry.submit() return loan, amounts + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): - frappe.get_doc({ - "doctype": "Account", - "account_name": "Loans and Advances (Assets)", - "company": "_Test Company", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Current Assets - _TC", - "account_type": "Bank", - "is_group": 1 - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Loans and Advances (Assets)", + "company": "_Test Company", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Current Assets - _TC", + "account_type": "Bank", + "is_group": 1, + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Loan Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Loan Account", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Loans and Advances (Assets) - _TC", - "account_type": "Bank", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "Loan Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Loans and Advances (Assets) - _TC", + "account_type": "Bank", + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Payment Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Payment Account", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Bank Accounts - _TC", - "account_type": "Bank", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "Payment Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Disbursement Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Disbursement Account", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Bank Accounts - _TC", - "account_type": "Bank", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "Disbursement Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Interest Income Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "root_type": "Income", - "account_name": "Interest Income Account", - "report_type": "Profit and Loss", - "currency": "INR", - "parent_account": "Direct Income - _TC", - "account_type": "Income Account", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "root_type": "Income", + "account_name": "Interest Income Account", + "report_type": "Profit and Loss", + "currency": "INR", + "parent_account": "Direct Income - _TC", + "account_type": "Income Account", + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Penalty Income Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Penalty Income Account", - "root_type": "Income", - "report_type": "Profit and Loss", - "currency": "INR", - "parent_account": "Direct Income - _TC", - "account_type": "Income Account", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "Penalty Income Account", + "root_type": "Income", + "report_type": "Profit and Loss", + "currency": "INR", + "parent_account": "Direct Income - _TC", + "account_type": "Income Account", + } + ).insert(ignore_permissions=True) -def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None, - mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, - repayment_method=None, repayment_periods=None): + +def create_loan_type( + loan_name, + maximum_loan_amount, + rate_of_interest, + penalty_interest_rate=None, + is_term_loan=None, + grace_period_in_days=None, + mode_of_payment=None, + disbursement_account=None, + payment_account=None, + loan_account=None, + interest_income_account=None, + penalty_income_account=None, + repayment_method=None, + repayment_periods=None, +): if not frappe.db.exists("Loan Type", loan_name): - loan_type = frappe.get_doc({ - "doctype": "Loan Type", - "company": "_Test Company", - "loan_name": loan_name, - "is_term_loan": is_term_loan, - "maximum_loan_amount": maximum_loan_amount, - "rate_of_interest": rate_of_interest, - "penalty_interest_rate": penalty_interest_rate, - "grace_period_in_days": grace_period_in_days, - "mode_of_payment": mode_of_payment, - "disbursement_account": disbursement_account, - "payment_account": payment_account, - "loan_account": loan_account, - "interest_income_account": interest_income_account, - "penalty_income_account": penalty_income_account, - "repayment_method": repayment_method, - "repayment_periods": repayment_periods, - "write_off_amount": 100 - }).insert() + loan_type = frappe.get_doc( + { + "doctype": "Loan Type", + "company": "_Test Company", + "loan_name": loan_name, + "is_term_loan": is_term_loan, + "maximum_loan_amount": maximum_loan_amount, + "rate_of_interest": rate_of_interest, + "penalty_interest_rate": penalty_interest_rate, + "grace_period_in_days": grace_period_in_days, + "mode_of_payment": mode_of_payment, + "disbursement_account": disbursement_account, + "payment_account": payment_account, + "loan_account": loan_account, + "interest_income_account": interest_income_account, + "penalty_income_account": penalty_income_account, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "write_off_amount": 100, + } + ).insert() loan_type.submit() + def create_loan_security_type(): if not frappe.db.exists("Loan Security Type", "Stock"): - frappe.get_doc({ - "doctype": "Loan Security Type", - "loan_security_type": "Stock", - "unit_of_measure": "Nos", - "haircut": 50.00, - "loan_to_value_ratio": 50 - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Loan Security Type", + "loan_security_type": "Stock", + "unit_of_measure": "Nos", + "haircut": 50.00, + "loan_to_value_ratio": 50, + } + ).insert(ignore_permissions=True) + def create_loan_security(): if not frappe.db.exists("Loan Security", "Test Security 1"): - frappe.get_doc({ - "doctype": "Loan Security", - "loan_security_type": "Stock", - "loan_security_code": "532779", - "loan_security_name": "Test Security 1", - "unit_of_measure": "Nos", - "haircut": 50.00, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Loan Security", + "loan_security_type": "Stock", + "loan_security_code": "532779", + "loan_security_name": "Test Security 1", + "unit_of_measure": "Nos", + "haircut": 50.00, + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Loan Security", "Test Security 2"): - frappe.get_doc({ - "doctype": "Loan Security", - "loan_security_type": "Stock", - "loan_security_code": "531335", - "loan_security_name": "Test Security 2", - "unit_of_measure": "Nos", - "haircut": 50.00, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Loan Security", + "loan_security_type": "Stock", + "loan_security_code": "531335", + "loan_security_name": "Test Security 2", + "unit_of_measure": "Nos", + "haircut": 50.00, + } + ).insert(ignore_permissions=True) + def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None): lsp = frappe.new_doc("Loan Security Pledge") - lsp.applicant_type = 'Customer' + lsp.applicant_type = "Customer" lsp.applicant = applicant lsp.company = "_Test Company" lsp.loan_application = loan_application @@ -920,64 +1122,82 @@ def create_loan_security_pledge(applicant, pledges, loan_application=None, loan= lsp.loan = loan for pledge in pledges: - lsp.append('securities', { - "loan_security": pledge['loan_security'], - "qty": pledge['qty'] - }) + lsp.append("securities", {"loan_security": pledge["loan_security"], "qty": pledge["qty"]}) lsp.save() lsp.submit() return lsp + def make_loan_disbursement_entry(loan, amount, disbursement_date=None): - loan_disbursement_entry = frappe.get_doc({ - "doctype": "Loan Disbursement", - "against_loan": loan, - "disbursement_date": disbursement_date, - "company": "_Test Company", - "disbursed_amount": amount, - "cost_center": 'Main - _TC' - }).insert(ignore_permissions=True) + loan_disbursement_entry = frappe.get_doc( + { + "doctype": "Loan Disbursement", + "against_loan": loan, + "disbursement_date": disbursement_date, + "company": "_Test Company", + "disbursed_amount": amount, + "cost_center": "Main - _TC", + } + ).insert(ignore_permissions=True) loan_disbursement_entry.save() loan_disbursement_entry.submit() return loan_disbursement_entry + def create_loan_security_price(loan_security, loan_security_price, uom, from_date, to_date): - if not frappe.db.get_value("Loan Security Price",{"loan_security": loan_security, - "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, 'name'): + if not frappe.db.get_value( + "Loan Security Price", + {"loan_security": loan_security, "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, + "name", + ): + + lsp = frappe.get_doc( + { + "doctype": "Loan Security Price", + "loan_security": loan_security, + "loan_security_price": loan_security_price, + "uom": uom, + "valid_from": from_date, + "valid_upto": to_date, + } + ).insert(ignore_permissions=True) - lsp = frappe.get_doc({ - "doctype": "Loan Security Price", - "loan_security": loan_security, - "loan_security_price": loan_security_price, - "uom": uom, - "valid_from":from_date, - "valid_upto": to_date - }).insert(ignore_permissions=True) def create_repayment_entry(loan, applicant, posting_date, paid_amount): - lr = frappe.get_doc({ - "doctype": "Loan Repayment", - "against_loan": loan, - "company": "_Test Company", - "posting_date": posting_date or nowdate(), - "applicant": applicant, - "amount_paid": paid_amount, - "loan_type": "Stock Loan" - }).insert(ignore_permissions=True) + lr = frappe.get_doc( + { + "doctype": "Loan Repayment", + "against_loan": loan, + "company": "_Test Company", + "posting_date": posting_date or nowdate(), + "applicant": applicant, + "amount_paid": paid_amount, + "loan_type": "Stock Loan", + } + ).insert(ignore_permissions=True) return lr -def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, - repayment_periods=None, posting_date=None, do_not_save=False): - loan_application = frappe.new_doc('Loan Application') - loan_application.applicant_type = 'Customer' + +def create_loan_application( + company, + applicant, + loan_type, + proposed_pledges, + repayment_method=None, + repayment_periods=None, + posting_date=None, + do_not_save=False, +): + loan_application = frappe.new_doc("Loan Application") + loan_application.applicant_type = "Customer" loan_application.company = company loan_application.applicant = applicant loan_application.loan_type = loan_type @@ -989,7 +1209,7 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep loan_application.repayment_periods = repayment_periods for pledge in proposed_pledges: - loan_application.append('proposed_pledges', pledge) + loan_application.append("proposed_pledges", pledge) if do_not_save: return loan_application @@ -997,75 +1217,99 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep loan_application.save() loan_application.submit() - loan_application.status = 'Approved' + loan_application.status = "Approved" loan_application.save() return loan_application.name -def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods, - applicant_type=None, repayment_start_date=None, posting_date=None): +def create_loan( + applicant, + loan_type, + loan_amount, + repayment_method, + repayment_periods, + applicant_type=None, + repayment_start_date=None, + posting_date=None, +): - loan = frappe.get_doc({ - "doctype": "Loan", - "applicant_type": applicant_type or "Employee", - "company": "_Test Company", - "applicant": applicant, - "loan_type": loan_type, - "loan_amount": loan_amount, - "repayment_method": repayment_method, - "repayment_periods": repayment_periods, - "repayment_start_date": repayment_start_date or nowdate(), - "is_term_loan": 1, - "posting_date": posting_date or nowdate() - }) + loan = frappe.get_doc( + { + "doctype": "Loan", + "applicant_type": applicant_type or "Employee", + "company": "_Test Company", + "applicant": applicant, + "loan_type": loan_type, + "loan_amount": loan_amount, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "repayment_start_date": repayment_start_date or nowdate(), + "is_term_loan": 1, + "posting_date": posting_date or nowdate(), + } + ) loan.save() return loan -def create_loan_with_security(applicant, loan_type, repayment_method, repayment_periods, loan_application, posting_date=None, repayment_start_date=None): - loan = frappe.get_doc({ - "doctype": "Loan", - "company": "_Test Company", - "applicant_type": "Customer", - "posting_date": posting_date or nowdate(), - "loan_application": loan_application, - "applicant": applicant, - "loan_type": loan_type, - "is_term_loan": 1, - "is_secured_loan": 1, - "repayment_method": repayment_method, - "repayment_periods": repayment_periods, - "repayment_start_date": repayment_start_date or nowdate(), - "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), - "payment_account": 'Payment Account - _TC', - "loan_account": 'Loan Account - _TC', - "interest_income_account": 'Interest Income Account - _TC', - "penalty_income_account": 'Penalty Income Account - _TC', - }) + +def create_loan_with_security( + applicant, + loan_type, + repayment_method, + repayment_periods, + loan_application, + posting_date=None, + repayment_start_date=None, +): + loan = frappe.get_doc( + { + "doctype": "Loan", + "company": "_Test Company", + "applicant_type": "Customer", + "posting_date": posting_date or nowdate(), + "loan_application": loan_application, + "applicant": applicant, + "loan_type": loan_type, + "is_term_loan": 1, + "is_secured_loan": 1, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "repayment_start_date": repayment_start_date or nowdate(), + "mode_of_payment": frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name"), + "payment_account": "Payment Account - _TC", + "loan_account": "Loan Account - _TC", + "interest_income_account": "Interest Income Account - _TC", + "penalty_income_account": "Penalty Income Account - _TC", + } + ) loan.save() return loan + def create_demand_loan(applicant, loan_type, loan_application, posting_date=None): - loan = frappe.get_doc({ - "doctype": "Loan", - "company": "_Test Company", - "applicant_type": "Customer", - "posting_date": posting_date or nowdate(), - 'loan_application': loan_application, - "applicant": applicant, - "loan_type": loan_type, - "is_term_loan": 0, - "is_secured_loan": 1, - "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), - "payment_account": 'Payment Account - _TC', - "loan_account": 'Loan Account - _TC', - "interest_income_account": 'Interest Income Account - _TC', - "penalty_income_account": 'Penalty Income Account - _TC', - }) + loan = frappe.get_doc( + { + "doctype": "Loan", + "company": "_Test Company", + "applicant_type": "Customer", + "posting_date": posting_date or nowdate(), + "loan_application": loan_application, + "applicant": applicant, + "loan_type": loan_type, + "is_term_loan": 0, + "is_secured_loan": 1, + "mode_of_payment": frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name"), + "payment_account": "Payment Account - _TC", + "loan_account": "Loan Account - _TC", + "interest_income_account": "Interest Income Account - _TC", + "penalty_income_account": "Penalty Income Account - _TC", + } + ) loan.save() diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index 2abd8ecbe48..41d8c2a9e28 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -30,8 +30,13 @@ class LoanApplication(Document): self.validate_loan_amount() if self.is_term_loan: - validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, - self.repayment_periods, self.is_term_loan) + validate_repayment_method( + self.repayment_method, + self.loan_amount, + self.repayment_amount, + self.repayment_periods, + self.is_term_loan, + ) self.validate_loan_type() @@ -47,21 +52,35 @@ class LoanApplication(Document): if not self.loan_amount: frappe.throw(_("Loan Amount is mandatory")) - maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount') + maximum_loan_limit = frappe.db.get_value("Loan Type", self.loan_type, "maximum_loan_amount") if maximum_loan_limit and self.loan_amount > maximum_loan_limit: - frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)) + frappe.throw( + _("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit) + ) if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: - frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) + frappe.throw( + _("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format( + self.maximum_loan_amount + ) + ) def check_sanctioned_amount_limit(self): - sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + sanctioned_amount_limit = get_sanctioned_amount_limit( + self.applicant_type, self.applicant, self.company + ) if sanctioned_amount_limit: total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) - if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): - frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt( + sanctioned_amount_limit + ): + frappe.throw( + _("Sanctioned Amount limit crossed for {0} {1}").format( + self.applicant_type, frappe.bold(self.applicant) + ) + ) def set_pledge_amount(self): for proposed_pledge in self.proposed_pledges: @@ -72,26 +91,31 @@ class LoanApplication(Document): proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security) if not proposed_pledge.qty: - proposed_pledge.qty = cint(proposed_pledge.amount/proposed_pledge.loan_security_price) + proposed_pledge.qty = cint(proposed_pledge.amount / proposed_pledge.loan_security_price) proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price - proposed_pledge.post_haircut_amount = cint(proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut/100)) + proposed_pledge.post_haircut_amount = cint( + proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut / 100) + ) def get_repayment_details(self): if self.is_term_loan: if self.repayment_method == "Repay Over Number of Periods": - self.repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods) + self.repayment_amount = get_monthly_repayment_amount( + self.loan_amount, self.rate_of_interest, self.repayment_periods + ) if self.repayment_method == "Repay Fixed Amount per Period": - monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) + monthly_interest_rate = flt(self.rate_of_interest) / (12 * 100) if monthly_interest_rate: - min_repayment_amount = self.loan_amount*monthly_interest_rate + min_repayment_amount = self.loan_amount * monthly_interest_rate if self.repayment_amount - min_repayment_amount <= 0: - frappe.throw(_("Repayment Amount must be greater than " \ - + str(flt(min_repayment_amount, 2)))) - self.repayment_periods = math.ceil((math.log(self.repayment_amount) - - math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate))) + frappe.throw(_("Repayment Amount must be greater than " + str(flt(min_repayment_amount, 2)))) + self.repayment_periods = math.ceil( + (math.log(self.repayment_amount) - math.log(self.repayment_amount - min_repayment_amount)) + / (math.log(1 + monthly_interest_rate)) + ) else: self.repayment_periods = self.loan_amount / self.repayment_amount @@ -104,8 +128,8 @@ class LoanApplication(Document): self.total_payable_amount = 0 self.total_payable_interest = 0 - while(balance_amount > 0): - interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) + while balance_amount > 0: + interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12 * 100)) balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount) self.total_payable_interest += interest_amount @@ -124,12 +148,21 @@ class LoanApplication(Document): if not self.loan_amount and self.is_secured_loan and self.proposed_pledges: self.loan_amount = self.maximum_loan_amount + @frappe.whitelist() def create_loan(source_name, target_doc=None, submit=0): def update_accounts(source_doc, target_doc, source_parent): - account_details = frappe.get_all("Loan Type", - fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"], - filters = {'name': source_doc.loan_type})[0] + account_details = frappe.get_all( + "Loan Type", + fields=[ + "mode_of_payment", + "payment_account", + "loan_account", + "interest_income_account", + "penalty_income_account", + ], + filters={"name": source_doc.loan_type}, + )[0] if source_doc.is_secured_loan: target_doc.maximum_loan_amount = 0 @@ -141,22 +174,25 @@ def create_loan(source_name, target_doc=None, submit=0): target_doc.penalty_income_account = account_details.penalty_income_account target_doc.loan_application = source_name - - doclist = get_mapped_doc("Loan Application", source_name, { - "Loan Application": { - "doctype": "Loan", - "validation": { - "docstatus": ["=", 1] - }, - "postprocess": update_accounts - } - }, target_doc) + doclist = get_mapped_doc( + "Loan Application", + source_name, + { + "Loan Application": { + "doctype": "Loan", + "validation": {"docstatus": ["=", 1]}, + "postprocess": update_accounts, + } + }, + target_doc, + ) if submit: doclist.submit() return doclist + @frappe.whitelist() def create_pledge(loan_application, loan=None): loan_application_doc = frappe.get_doc("Loan Application", loan_application) @@ -172,12 +208,15 @@ def create_pledge(loan_application, loan=None): for pledge in loan_application_doc.proposed_pledges: - lsp.append('securities', { - "loan_security": pledge.loan_security, - "qty": pledge.qty, - "loan_security_price": pledge.loan_security_price, - "haircut": pledge.haircut - }) + lsp.append( + "securities", + { + "loan_security": pledge.loan_security, + "qty": pledge.qty, + "loan_security_price": pledge.loan_security_price, + "haircut": pledge.haircut, + }, + ) lsp.save() lsp.submit() @@ -187,15 +226,14 @@ def create_pledge(loan_application, loan=None): return lsp.name -#This is a sandbox method to get the proposed pledges + +# This is a sandbox method to get the proposed pledges @frappe.whitelist() def get_proposed_pledge(securities): if isinstance(securities, string_types): securities = json.loads(securities) - proposed_pledges = { - 'securities': [] - } + proposed_pledges = {"securities": []} maximum_loan_amount = 0 for security in securities: @@ -206,15 +244,15 @@ def get_proposed_pledge(securities): security.loan_security_price = get_loan_security_price(security.loan_security) if not security.qty: - security.qty = cint(security.amount/security.loan_security_price) + security.qty = cint(security.amount / security.loan_security_price) security.amount = security.qty * security.loan_security_price - security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut/100)) + security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut / 100)) maximum_loan_amount += security.post_haircut_amount - proposed_pledges['securities'].append(security) + proposed_pledges["securities"].append(security) - proposed_pledges['maximum_loan_amount'] = maximum_loan_amount + proposed_pledges["maximum_loan_amount"] = maximum_loan_amount return proposed_pledges diff --git a/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py index 01ef9f9d78a..1d90e9bb114 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'loan_application', - 'transactions': [ - { - 'items': ['Loan', 'Loan Security Pledge'] - }, - ], - } + return { + "fieldname": "loan_application", + "transactions": [ + {"items": ["Loan", "Loan Security Pledge"]}, + ], + } diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py index 640709c095f..2a4bb882a8e 100644 --- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -15,27 +15,45 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( class TestLoanApplication(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) + create_loan_type( + "Home Loan", + 500000, + 9.2, + 0, + 1, + 0, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + "Repay Over Number of Periods", + 18, + ) self.applicant = make_employee("kate_loan@loan.com", "_Test Company") - make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR') + make_salary_structure( + "Test Salary Structure Loan", "Monthly", employee=self.applicant, currency="INR" + ) self.create_loan_application() def create_loan_application(self): loan_application = frappe.new_doc("Loan Application") - loan_application.update({ - "applicant": self.applicant, - "loan_type": "Home Loan", - "rate_of_interest": 9.2, - "loan_amount": 250000, - "repayment_method": "Repay Over Number of Periods", - "repayment_periods": 18, - "company": "_Test Company" - }) + loan_application.update( + { + "applicant": self.applicant, + "loan_type": "Home Loan", + "rate_of_interest": 9.2, + "loan_amount": 250000, + "repayment_method": "Repay Over Number of Periods", + "repayment_periods": 18, + "company": "_Test Company", + } + ) loan_application.insert() def test_loan_totals(self): - loan_application = frappe.get_doc("Loan Application", {"applicant":self.applicant}) + loan_application = frappe.get_doc("Loan Application", {"applicant": self.applicant}) self.assertEqual(loan_application.total_payable_interest, 18599) self.assertEqual(loan_application.total_payable_amount, 268599) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json index 7811d56a758..50926d77268 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -14,11 +14,15 @@ "applicant", "section_break_7", "disbursement_date", + "clearance_date", "column_break_8", "disbursed_amount", "accounting_dimensions_section", "cost_center", - "customer_details_section", + "accounting_details", + "disbursement_account", + "column_break_16", + "loan_account", "bank_account", "disbursement_references_section", "reference_date", @@ -106,11 +110,6 @@ "fieldtype": "Section Break", "label": "Disbursement Details" }, - { - "fieldname": "customer_details_section", - "fieldtype": "Section Break", - "label": "Customer Details" - }, { "fetch_from": "against_loan.applicant_type", "fieldname": "applicant_type", @@ -149,15 +148,48 @@ "fieldname": "reference_number", "fieldtype": "Data", "label": "Reference Number" + }, + { + "fieldname": "clearance_date", + "fieldtype": "Date", + "label": "Clearance Date", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_details", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fetch_from": "against_loan.disbursement_account", + "fieldname": "disbursement_account", + "fieldtype": "Link", + "label": "Disbursement Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fetch_from": "against_loan.loan_account", + "fieldname": "loan_account", + "fieldtype": "Link", + "label": "Loan Account", + "options": "Account", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:09:32.175355", + "modified": "2022-02-17 18:23:44.157598", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -194,5 +226,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index df3aadfb18d..10174e531a1 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -18,7 +18,6 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ class LoanDisbursement(AccountsController): - def validate(self): self.set_missing_values() self.validate_disbursal_amount() @@ -30,7 +29,7 @@ class LoanDisbursement(AccountsController): def on_cancel(self): self.set_status_and_amounts(cancel=1) self.make_gl_entries(cancel=1) - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] def set_missing_values(self): if not self.disbursement_date: @@ -42,9 +41,6 @@ class LoanDisbursement(AccountsController): if not self.posting_date: self.posting_date = self.disbursement_date or nowdate() - if not self.bank_account and self.applicant_type == "Customer": - self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account") - def validate_disbursal_amount(self): possible_disbursal_amount = get_disbursal_amount(self.against_loan) @@ -52,21 +48,36 @@ class LoanDisbursement(AccountsController): frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) def set_status_and_amounts(self, cancel=0): - loan_details = frappe.get_all("Loan", - fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable", - "status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0] + loan_details = frappe.get_all( + "Loan", + fields=[ + "loan_amount", + "disbursed_amount", + "total_payment", + "total_principal_paid", + "total_interest_payable", + "status", + "is_term_loan", + "is_secured_loan", + ], + filters={"name": self.against_loan}, + )[0] if cancel: disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details) else: disbursed_amount, status, total_payment = self.get_values_on_submit(loan_details) - frappe.db.set_value("Loan", self.against_loan, { - "disbursement_date": self.disbursement_date, - "disbursed_amount": disbursed_amount, - "status": status, - "total_payment": total_payment - }) + frappe.db.set_value( + "Loan", + self.against_loan, + { + "disbursement_date": self.disbursement_date, + "disbursed_amount": disbursed_amount, + "status": status, + "total_payment": total_payment, + }, + ) def get_values_on_cancel(self, loan_details): disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount @@ -94,8 +105,11 @@ class LoanDisbursement(AccountsController): total_payment = loan_details.total_payment if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan: - process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), - loan=self.against_loan, accrual_type="Disbursement") + process_loan_interest_accrual_for_demand_loans( + posting_date=add_days(self.disbursement_date, -1), + loan=self.against_loan, + accrual_type="Disbursement", + ) if disbursed_amount > loan_details.loan_amount: topup_amount = disbursed_amount - loan_details.loan_amount @@ -117,75 +131,98 @@ class LoanDisbursement(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] - loan_details = frappe.get_doc("Loan", self.against_loan) gle_map.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.disbursement_account, - "debit": self.disbursed_amount, - "debit_in_account_currency": self.disbursed_amount, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, - "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, - "posting_date": self.disbursement_date - }) + self.get_gl_dict( + { + "account": self.loan_account, + "against": self.disbursement_account, + "debit": self.disbursed_amount, + "debit_in_account_currency": self.disbursed_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Disbursement against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": self.disbursement_date, + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": loan_details.disbursement_account, - "against": loan_details.loan_account, - "credit": self.disbursed_amount, - "credit_in_account_currency": self.disbursed_amount, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, - "cost_center": self.cost_center, - "posting_date": self.disbursement_date - }) + self.get_gl_dict( + { + "account": self.disbursement_account, + "against": self.loan_account, + "credit": self.disbursed_amount, + "credit_in_account_currency": self.disbursed_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Disbursement against loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": self.disbursement_date, + } + ) ) if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) + def get_total_pledged_security_value(loan): update_time = get_datetime() - loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", - fields=["loan_security", "loan_security_price"], - filters = { - "valid_from": ("<=", update_time), - "valid_upto": (">=", update_time) - }, as_list=1)) + loan_security_price_map = frappe._dict( + frappe.get_all( + "Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters={"valid_from": ("<=", update_time), "valid_upto": (">=", update_time)}, + as_list=1, + ) + ) - hair_cut_map = frappe._dict(frappe.get_all('Loan Security', - fields=["name", "haircut"], as_list=1)) + hair_cut_map = frappe._dict( + frappe.get_all("Loan Security", fields=["name", "haircut"], as_list=1) + ) security_value = 0.0 pledged_securities = get_pledged_security_qty(loan) for security, qty in pledged_securities.items(): after_haircut_percentage = 100 - hair_cut_map.get(security) - security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage)/100 + security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage) / 100 return security_value + @frappe.whitelist() def get_disbursal_amount(loan, on_current_security_price=0): from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( get_pending_principal_amount, ) - loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment", - "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan", - "maximum_loan_amount", "written_off_amount"], as_dict=1) + loan_details = frappe.get_value( + "Loan", + loan, + [ + "loan_amount", + "disbursed_amount", + "total_payment", + "total_principal_paid", + "total_interest_payable", + "status", + "is_term_loan", + "is_secured_loan", + "maximum_loan_amount", + "written_off_amount", + ], + as_dict=1, + ) - if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, - 'status': 'Pending'}): + if loan_details.is_secured_loan and frappe.get_all( + "Loan Security Shortfall", filters={"loan": loan, "status": "Pending"} + ): return 0 pending_principal_amount = get_pending_principal_amount(loan_details) @@ -202,10 +239,14 @@ def get_disbursal_amount(loan, on_current_security_price=0): disbursal_amount = flt(security_value) - flt(pending_principal_amount) - if loan_details.is_term_loan and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount: + if ( + loan_details.is_term_loan + and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount + ): disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount return disbursal_amount + def get_maximum_amount_as_per_pledged_security(loan): - return flt(frappe.db.get_value('Loan Security Pledge', {'loan': loan}, 'sum(maximum_loan_value)')) + return flt(frappe.db.get_value("Loan Security Pledge", {"loan": loan}, "sum(maximum_loan_value)")) diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 10be750b449..4daa2edb28a 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -40,34 +40,50 @@ from erpnext.selling.doctype.customer.test_customer import get_customer_dict class TestLoanDisbursement(unittest.TestCase): - def setUp(self): create_loan_accounts() - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', - 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type( + "Demand Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) create_loan_security_type() create_loan_security() - create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) - create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price( + "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) + create_loan_security_price( + "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) if not frappe.db.exists("Customer", "_Test Loan Customer"): - frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) - self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") def test_loan_topup(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan( + self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) + ) loan.submit() @@ -76,18 +92,22 @@ class TestLoanDisbursement(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime().year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime().year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 1)) # Should not be able to create loan disbursement entry before repayment - self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name, - 500000, first_date) + self.assertRaises( + frappe.ValidationError, make_loan_disbursement_entry, loan.name, 500000, first_date + ) - repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89) + repayment_entry = create_repayment_entry( + loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89 + ) repayment_entry.submit() loan.reload() @@ -96,49 +116,48 @@ class TestLoanDisbursement(unittest.TestCase): make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) # check for disbursement accrual - loan_interest_accrual = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name, - 'accrual_type': 'Disbursement'}) + loan_interest_accrual = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name, "accrual_type": "Disbursement"} + ) self.assertTrue(loan_interest_accrual) def test_loan_topup_with_additional_pledge(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" # Disbursed 10,00,000 amount make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 1)) - previous_interest = amounts['interest_amount'] + previous_interest = amounts["interest_amount"] - pledge1 = [{ - "loan_security": "Test Security 1", - "qty": 2000.00 - }] + pledge1 = [{"loan_security": "Test Security 1", "qty": 2000.00}] create_loan_security_pledge(self.applicant, pledge1, loan=loan.name) # Topup 500000 make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1)) - process_loan_interest_accrual_for_demand_loans(posting_date = add_days(last_date, 15)) + process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 15)) amounts = calculate_amounts(loan.name, add_days(last_date, 15)) - per_day_interest = get_per_day_interest(1500000, 13.5, '2019-10-30') + per_day_interest = get_per_day_interest(1500000, 13.5, "2019-10-30") interest = per_day_interest * 15 - self.assertEqual(amounts['pending_principal_amount'], 1500000) - self.assertEqual(amounts['interest_amount'], flt(interest + previous_interest, 2)) + self.assertEqual(amounts["pending_principal_amount"], 1500000) + self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2)) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 1c800a06da0..0c4b051fba2 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -33,45 +33,51 @@ class LoanInterestAccrual(AccountsController): self.update_is_accrued() self.make_gl_entries(cancel=1) - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] def update_is_accrued(self): - frappe.db.set_value('Repayment Schedule', self.repayment_schedule_name, 'is_accrued', 0) + frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0) def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] if self.interest_amount: gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "against": self.interest_income_account, - "debit": self.interest_amount, - "debit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) + self.get_gl_dict( + { + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": self.interest_income_account, + "debit": self.interest_amount, + "debit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( + self.last_accrual_date, self.posting_date, self.loan + ), + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date, + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": self.interest_income_account, - "against": self.loan_account, - "credit": self.interest_amount, - "credit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) + self.get_gl_dict( + { + "account": self.interest_income_account, + "against": self.loan_account, + "credit": self.interest_amount, + "credit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( + self.last_accrual_date, self.posting_date, self.loan + ), + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date, + } + ) ) if gle_map: @@ -81,7 +87,9 @@ class LoanInterestAccrual(AccountsController): # For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and # rate of interest is 13.5 then first loan interest accural will be on '01-10-2019' # which means interest will be accrued for 30 days which should be equal to 11095.89 -def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type): +def calculate_accrual_amount_for_demand_loans( + loan, posting_date, process_loan_interest, accrual_type +): from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( calculate_amounts, get_pending_principal_amount, @@ -95,51 +103,76 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i pending_principal_amount = get_pending_principal_amount(loan) - interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) + interest_per_day = get_per_day_interest( + pending_principal_amount, loan.rate_of_interest, posting_date + ) payable_interest = interest_per_day * no_of_days - pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure') + pending_amounts = calculate_amounts(loan.name, posting_date, payment_type="Loan Closure") - args = frappe._dict({ - 'loan': loan.name, - 'applicant_type': loan.applicant_type, - 'applicant': loan.applicant, - 'interest_income_account': loan.interest_income_account, - 'loan_account': loan.loan_account, - 'pending_principal_amount': pending_principal_amount, - 'interest_amount': payable_interest, - 'total_pending_interest_amount': pending_amounts['interest_amount'], - 'penalty_amount': pending_amounts['penalty_amount'], - 'process_loan_interest': process_loan_interest, - 'posting_date': posting_date, - 'accrual_type': accrual_type - }) + args = frappe._dict( + { + "loan": loan.name, + "applicant_type": loan.applicant_type, + "applicant": loan.applicant, + "interest_income_account": loan.interest_income_account, + "loan_account": loan.loan_account, + "pending_principal_amount": pending_principal_amount, + "interest_amount": payable_interest, + "total_pending_interest_amount": pending_amounts["interest_amount"], + "penalty_amount": pending_amounts["penalty_amount"], + "process_loan_interest": process_loan_interest, + "posting_date": posting_date, + "accrual_type": accrual_type, + } + ) if flt(payable_interest, precision) > 0.0: make_loan_interest_accrual_entry(args) -def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"): - query_filters = { - "status": ('in', ['Disbursed', 'Partially Disbursed']), - "docstatus": 1 - } + +def make_accrual_interest_entry_for_demand_loans( + posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular" +): + query_filters = {"status": ("in", ["Disbursed", "Partially Disbursed"]), "docstatus": 1} if loan_type: - query_filters.update({ - "loan_type": loan_type - }) + query_filters.update({"loan_type": loan_type}) if not open_loans: - open_loans = frappe.get_all("Loan", - fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "loan_amount", - "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", - "rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"], - filters=query_filters) + open_loans = frappe.get_all( + "Loan", + fields=[ + "name", + "total_payment", + "total_amount_paid", + "loan_account", + "interest_income_account", + "loan_amount", + "is_term_loan", + "status", + "disbursement_date", + "disbursed_amount", + "applicant_type", + "applicant", + "rate_of_interest", + "total_interest_payable", + "written_off_amount", + "total_principal_paid", + "repayment_start_date", + ], + filters=query_filters, + ) for loan in open_loans: - calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type) + calculate_accrual_amount_for_demand_loans( + loan, posting_date, process_loan_interest, accrual_type + ) -def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"): + +def make_accrual_interest_entry_for_term_loans( + posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular" +): curr_date = posting_date or add_days(nowdate(), 1) term_loans = get_term_loans(curr_date, term_loan, loan_type) @@ -148,37 +181,44 @@ def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_intere for loan in term_loans: accrued_entries.append(loan.payment_entry) - args = frappe._dict({ - 'loan': loan.name, - 'applicant_type': loan.applicant_type, - 'applicant': loan.applicant, - 'interest_income_account': loan.interest_income_account, - 'loan_account': loan.loan_account, - 'interest_amount': loan.interest_amount, - 'payable_principal': loan.principal_amount, - 'process_loan_interest': process_loan_interest, - 'repayment_schedule_name': loan.payment_entry, - 'posting_date': posting_date, - 'accrual_type': accrual_type - }) + args = frappe._dict( + { + "loan": loan.name, + "applicant_type": loan.applicant_type, + "applicant": loan.applicant, + "interest_income_account": loan.interest_income_account, + "loan_account": loan.loan_account, + "interest_amount": loan.interest_amount, + "payable_principal": loan.principal_amount, + "process_loan_interest": process_loan_interest, + "repayment_schedule_name": loan.payment_entry, + "posting_date": posting_date, + "accrual_type": accrual_type, + } + ) make_loan_interest_accrual_entry(args) if accrued_entries: - frappe.db.sql("""UPDATE `tabRepayment Schedule` - SET is_accrued = 1 where name in (%s)""" #nosec - % ", ".join(['%s']*len(accrued_entries)), tuple(accrued_entries)) + frappe.db.sql( + """UPDATE `tabRepayment Schedule` + SET is_accrued = 1 where name in (%s)""" # nosec + % ", ".join(["%s"] * len(accrued_entries)), + tuple(accrued_entries), + ) + def get_term_loans(date, term_loan=None, loan_type=None): - condition = '' + condition = "" if term_loan: - condition +=' AND l.name = %s' % frappe.db.escape(term_loan) + condition += " AND l.name = %s" % frappe.db.escape(term_loan) if loan_type: - condition += ' AND l.loan_type = %s' % frappe.db.escape(loan_type) + condition += " AND l.loan_type = %s" % frappe.db.escape(loan_type) - term_loans = frappe.db.sql("""SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account, + term_loans = frappe.db.sql( + """SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account, l.interest_income_account, l.is_term_loan, l.disbursement_date, l.applicant_type, l.applicant, l.rate_of_interest, l.total_interest_payable, l.repayment_start_date, rs.name as payment_entry, rs.payment_date, rs.principal_amount, rs.interest_amount, rs.is_accrued , rs.balance_loan_amount @@ -189,10 +229,16 @@ def get_term_loans(date, term_loan=None, loan_type=None): AND rs.payment_date <= %s AND rs.is_accrued=0 {0} AND l.status = 'Disbursed' - ORDER BY rs.payment_date""".format(condition), (getdate(date)), as_dict=1) + ORDER BY rs.payment_date""".format( + condition + ), + (getdate(date)), + as_dict=1, + ) return term_loans + def make_loan_interest_accrual_entry(args): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -204,7 +250,9 @@ def make_loan_interest_accrual_entry(args): loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) loan_interest_accrual.interest_amount = flt(args.interest_amount, precision) - loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision) + loan_interest_accrual.total_pending_interest_amount = flt( + args.total_pending_interest_amount, precision + ) loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision) loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest @@ -223,15 +271,20 @@ def get_no_of_days_for_interest_accural(loan, posting_date): return no_of_days + def get_last_accrual_date(loan): - last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` - WHERE loan = %s and docstatus = 1""", (loan)) + last_posting_date = frappe.db.sql( + """ SELECT MAX(posting_date) from `tabLoan Interest Accrual` + WHERE loan = %s and docstatus = 1""", + (loan), + ) if last_posting_date[0][0]: # interest for last interest accrual date is already booked, so add 1 day return add_days(last_posting_date[0][0], 1) else: - return frappe.db.get_value('Loan', loan, 'disbursement_date') + return frappe.db.get_value("Loan", loan, "disbursement_date") + def days_in_year(year): days = 365 @@ -241,8 +294,11 @@ def days_in_year(year): return days + def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): if not posting_date: posting_date = getdate() - return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)) + return flt( + (principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) + ) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index e8c77506fcb..fd59393b827 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -30,78 +30,98 @@ class TestLoanInterestAccrual(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', - 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type( + "Demand Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) create_loan_security_type() create_loan_security() - create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price( + "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) if not frappe.db.exists("Customer", "_Test Loan Customer"): - frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) - self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") def test_loan_interest_accural(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, - posting_date=get_first_day(nowdate())) + loan = create_demand_loan( + self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) + ) loan.submit() - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) + loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) self.assertEqual(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0)) def test_accumulated_amounts(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, - posting_date=get_first_day(nowdate())) + loan = create_demand_loan( + self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) + ) loan.submit() - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) + loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) self.assertEqual(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0)) - next_start_date = '2019-10-31' - next_end_date = '2019-11-29' + next_start_date = "2019-10-31" + next_end_date = "2019-11-29" no_of_days = date_diff(next_end_date, next_start_date) + 1 process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date) - new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0) - loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name, - 'process_loan_interest_accrual': process}) - self.assertEqual(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount) + loan_interest_accrual = frappe.get_doc( + "Loan Interest Accrual", {"loan": loan.name, "process_loan_interest_accrual": process} + ) + self.assertEqual( + flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount + ) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 93ef2170420..480e010b49a 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "LM-REP-.####", - "creation": "2019-09-03 14:44:39.977266", + "creation": "2022-01-25 10:30:02.767941", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -13,6 +13,7 @@ "column_break_3", "company", "posting_date", + "clearance_date", "rate_of_interest", "payroll_payable_account", "is_term_loan", @@ -37,7 +38,12 @@ "total_penalty_paid", "total_interest_paid", "repayment_details", - "amended_from" + "amended_from", + "accounting_details_section", + "payment_account", + "penalty_income_account", + "column_break_36", + "loan_account" ], "fields": [ { @@ -260,12 +266,52 @@ "fieldname": "repay_from_salary", "fieldtype": "Check", "label": "Repay From Salary" + }, + { + "fieldname": "clearance_date", + "fieldtype": "Date", + "label": "Clearance Date", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fetch_from": "against_loan.payment_account", + "fieldname": "payment_account", + "fieldtype": "Link", + "label": "Repayment Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fetch_from": "against_loan.loan_account", + "fieldname": "loan_account", + "fieldtype": "Link", + "label": "Loan Account", + "options": "Account", + "read_only": 1 + }, + { + "fetch_from": "against_loan.penalty_income_account", + "fieldname": "penalty_income_account", + "fieldtype": "Link", + "hidden": 1, + "label": "Penalty Income Account", + "options": "Account" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-01-06 01:51:06.707782", + "modified": "2022-02-18 19:10:07.742298", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index a6e526a0490..6a6eb591629 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -23,7 +23,6 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ class LoanRepayment(AccountsController): - def validate(self): amounts = calculate_amounts(self.against_loan, self.posting_date) self.set_missing_values(amounts) @@ -43,7 +42,7 @@ class LoanRepayment(AccountsController): self.check_future_accruals() self.update_repayment_schedule(cancel=1) self.mark_as_unpaid() - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] self.make_gl_entries(cancel=1) def set_missing_values(self, amounts): @@ -56,32 +55,38 @@ class LoanRepayment(AccountsController): self.cost_center = erpnext.get_default_cost_center(self.company) if not self.interest_payable: - self.interest_payable = flt(amounts['interest_amount'], precision) + self.interest_payable = flt(amounts["interest_amount"], precision) if not self.penalty_amount: - self.penalty_amount = flt(amounts['penalty_amount'], precision) + self.penalty_amount = flt(amounts["penalty_amount"], precision) if not self.pending_principal_amount: - self.pending_principal_amount = flt(amounts['pending_principal_amount'], precision) + self.pending_principal_amount = flt(amounts["pending_principal_amount"], precision) if not self.payable_principal_amount and self.is_term_loan: - self.payable_principal_amount = flt(amounts['payable_principal_amount'], precision) + self.payable_principal_amount = flt(amounts["payable_principal_amount"], precision) if not self.payable_amount: - self.payable_amount = flt(amounts['payable_amount'], precision) + self.payable_amount = flt(amounts["payable_amount"], precision) - shortfall_amount = flt(frappe.db.get_value('Loan Security Shortfall', {'loan': self.against_loan, 'status': 'Pending'}, - 'shortfall_amount')) + shortfall_amount = flt( + frappe.db.get_value( + "Loan Security Shortfall", {"loan": self.against_loan, "status": "Pending"}, "shortfall_amount" + ) + ) if shortfall_amount: self.shortfall_amount = shortfall_amount - if amounts.get('due_date'): - self.due_date = amounts.get('due_date') + if amounts.get("due_date"): + self.due_date = amounts.get("due_date") def check_future_entries(self): - future_repayment_date = frappe.db.get_value("Loan Repayment", {"posting_date": (">", self.posting_date), - "docstatus": 1, "against_loan": self.against_loan}, 'posting_date') + future_repayment_date = frappe.db.get_value( + "Loan Repayment", + {"posting_date": (">", self.posting_date), "docstatus": 1, "against_loan": self.against_loan}, + "posting_date", + ) if future_repayment_date: frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date))) @@ -100,106 +105,167 @@ class LoanRepayment(AccountsController): last_accrual_date = get_last_accrual_date(self.against_loan) # get posting date upto which interest has to be accrued - per_day_interest = get_per_day_interest(self.pending_principal_amount, - self.rate_of_interest, self.posting_date) + per_day_interest = get_per_day_interest( + self.pending_principal_amount, self.rate_of_interest, self.posting_date + ) - no_of_days = flt(flt(self.total_interest_paid - self.interest_payable, - precision)/per_day_interest, 0) - 1 + no_of_days = ( + flt(flt(self.total_interest_paid - self.interest_payable, precision) / per_day_interest, 0) + - 1 + ) posting_date = add_days(last_accrual_date, no_of_days) # book excess interest paid - process = process_loan_interest_accrual_for_demand_loans(posting_date=posting_date, - loan=self.against_loan, accrual_type="Repayment") + process = process_loan_interest_accrual_for_demand_loans( + posting_date=posting_date, loan=self.against_loan, accrual_type="Repayment" + ) # get loan interest accrual to update paid amount - lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': - process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + lia = frappe.db.get_value( + "Loan Interest Accrual", + {"process_loan_interest_accrual": process}, + ["name", "interest_amount", "payable_principal_amount"], + as_dict=1, + ) if lia: - self.append('repayment_details', { - 'loan_interest_accrual': lia.name, - 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision), - 'paid_principal_amount': 0.0, - 'accrual_type': 'Repayment' - }) + self.append( + "repayment_details", + { + "loan_interest_accrual": lia.name, + "paid_interest_amount": flt(self.total_interest_paid - self.interest_payable, precision), + "paid_principal_amount": 0.0, + "accrual_type": "Repayment", + }, + ) def update_paid_amount(self): - loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', - 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable', - 'written_off_amount'], as_dict=1) + loan = frappe.get_value( + "Loan", + self.against_loan, + [ + "total_amount_paid", + "total_principal_paid", + "status", + "is_secured_loan", + "total_payment", + "loan_amount", + "disbursed_amount", + "total_interest_payable", + "written_off_amount", + ], + as_dict=1, + ) - loan.update({ - 'total_amount_paid': loan.total_amount_paid + self.amount_paid, - 'total_principal_paid': loan.total_principal_paid + self.principal_amount_paid - }) + loan.update( + { + "total_amount_paid": loan.total_amount_paid + self.amount_paid, + "total_principal_paid": loan.total_principal_paid + self.principal_amount_paid, + } + ) pending_principal_amount = get_pending_principal_amount(loan) if not loan.is_secured_loan and pending_principal_amount <= 0: - loan.update({'status': 'Loan Closure Requested'}) + loan.update({"status": "Loan Closure Requested"}) for payment in self.repayment_details: - frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` + frappe.db.sql( + """ UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` + %s, paid_interest_amount = `paid_interest_amount` + %s WHERE name = %s""", - (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual)) + ( + flt(payment.paid_principal_amount), + flt(payment.paid_interest_amount), + payment.loan_interest_accrual, + ), + ) - frappe.db.sql(""" UPDATE `tabLoan` + frappe.db.sql( + """ UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s, status = %s - WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status, - self.against_loan)) + WHERE name = %s """, + (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan), + ) update_shortfall_status(self.against_loan, self.principal_amount_paid) def mark_as_unpaid(self): - loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', - 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable', - 'written_off_amount'], as_dict=1) + loan = frappe.get_value( + "Loan", + self.against_loan, + [ + "total_amount_paid", + "total_principal_paid", + "status", + "is_secured_loan", + "total_payment", + "loan_amount", + "disbursed_amount", + "total_interest_payable", + "written_off_amount", + ], + as_dict=1, + ) no_of_repayments = len(self.repayment_details) - loan.update({ - 'total_amount_paid': loan.total_amount_paid - self.amount_paid, - 'total_principal_paid': loan.total_principal_paid - self.principal_amount_paid - }) + loan.update( + { + "total_amount_paid": loan.total_amount_paid - self.amount_paid, + "total_principal_paid": loan.total_principal_paid - self.principal_amount_paid, + } + ) - if loan.status == 'Loan Closure Requested': + if loan.status == "Loan Closure Requested": if loan.disbursed_amount >= loan.loan_amount: - loan['status'] = 'Disbursed' + loan["status"] = "Disbursed" else: - loan['status'] = 'Partially Disbursed' + loan["status"] = "Partially Disbursed" for payment in self.repayment_details: - frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` + frappe.db.sql( + """ UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` - %s, paid_interest_amount = `paid_interest_amount` - %s WHERE name = %s""", - (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual)) + (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual), + ) # Cancel repayment interest accrual # checking idx as a preventive measure, repayment accrual will always be the last entry - if payment.accrual_type == 'Repayment' and payment.idx == no_of_repayments: - lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual) + if payment.accrual_type == "Repayment" and payment.idx == no_of_repayments: + lia_doc = frappe.get_doc("Loan Interest Accrual", payment.loan_interest_accrual) lia_doc.cancel() - frappe.db.sql(""" UPDATE `tabLoan` + frappe.db.sql( + """ UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s, status = %s - WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan)) + WHERE name = %s """, + (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan), + ) def check_future_accruals(self): - future_accrual_date = frappe.db.get_value("Loan Interest Accrual", {"posting_date": (">", self.posting_date), - "docstatus": 1, "loan": self.against_loan}, 'posting_date') + future_accrual_date = frappe.db.get_value( + "Loan Interest Accrual", + {"posting_date": (">", self.posting_date), "docstatus": 1, "loan": self.against_loan}, + "posting_date", + ) if future_accrual_date: - frappe.throw("Cannot cancel. Interest accruals already processed till {0}".format(get_datetime(future_accrual_date))) + frappe.throw( + "Cannot cancel. Interest accruals already processed till {0}".format( + get_datetime(future_accrual_date) + ) + ) def update_repayment_schedule(self, cancel=0): if self.is_term_loan and self.principal_amount_paid > self.payable_principal_amount: regenerate_repayment_schedule(self.against_loan, cancel) def allocate_amounts(self, repayment_details): - self.set('repayment_details', []) + self.set("repayment_details", []) self.principal_amount_paid = 0 self.total_penalty_paid = 0 interest_paid = self.amount_paid @@ -232,15 +298,15 @@ class LoanRepayment(AccountsController): idx = 1 if interest_paid > 0: - for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])): + for lia, amounts in iteritems(repayment_details.get("pending_accrual_entries", [])): interest_amount = 0 - if amounts['interest_amount'] <= interest_paid: - interest_amount = amounts['interest_amount'] + if amounts["interest_amount"] <= interest_paid: + interest_amount = amounts["interest_amount"] self.total_interest_paid += interest_amount interest_paid -= interest_amount elif interest_paid: - if interest_paid >= amounts['interest_amount']: - interest_amount = amounts['interest_amount'] + if interest_paid >= amounts["interest_amount"]: + interest_amount = amounts["interest_amount"] self.total_interest_paid += interest_amount interest_paid = 0 else: @@ -249,27 +315,32 @@ class LoanRepayment(AccountsController): interest_paid = 0 if interest_amount: - self.append('repayment_details', { - 'loan_interest_accrual': lia, - 'paid_interest_amount': interest_amount, - 'paid_principal_amount': 0 - }) + self.append( + "repayment_details", + { + "loan_interest_accrual": lia, + "paid_interest_amount": interest_amount, + "paid_principal_amount": 0, + }, + ) updated_entries[lia] = idx idx += 1 return interest_paid, updated_entries - def allocate_principal_amount_for_term_loans(self, interest_paid, repayment_details, updated_entries): + def allocate_principal_amount_for_term_loans( + self, interest_paid, repayment_details, updated_entries + ): if interest_paid > 0: - for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])): + for lia, amounts in iteritems(repayment_details.get("pending_accrual_entries", [])): paid_principal = 0 - if amounts['payable_principal_amount'] <= interest_paid: - paid_principal = amounts['payable_principal_amount'] + if amounts["payable_principal_amount"] <= interest_paid: + paid_principal = amounts["payable_principal_amount"] self.principal_amount_paid += paid_principal interest_paid -= paid_principal elif interest_paid: - if interest_paid >= amounts['payable_principal_amount']: - paid_principal = amounts['payable_principal_amount'] + if interest_paid >= amounts["payable_principal_amount"]: + paid_principal = amounts["payable_principal_amount"] self.principal_amount_paid += paid_principal interest_paid = 0 else: @@ -279,30 +350,34 @@ class LoanRepayment(AccountsController): if updated_entries.get(lia): idx = updated_entries.get(lia) - self.get('repayment_details')[idx-1].paid_principal_amount += paid_principal + self.get("repayment_details")[idx - 1].paid_principal_amount += paid_principal else: - self.append('repayment_details', { - 'loan_interest_accrual': lia, - 'paid_interest_amount': 0, - 'paid_principal_amount': paid_principal - }) + self.append( + "repayment_details", + { + "loan_interest_accrual": lia, + "paid_interest_amount": 0, + "paid_principal_amount": paid_principal, + }, + ) if interest_paid > 0: self.principal_amount_paid += interest_paid def allocate_excess_payment_for_demand_loans(self, interest_paid, repayment_details): - if repayment_details['unaccrued_interest'] and interest_paid > 0: + if repayment_details["unaccrued_interest"] and interest_paid > 0: # no of days for which to accrue interest # Interest can only be accrued for an entire day and not partial - if interest_paid > repayment_details['unaccrued_interest']: - interest_paid -= repayment_details['unaccrued_interest'] - self.total_interest_paid += repayment_details['unaccrued_interest'] + if interest_paid > repayment_details["unaccrued_interest"]: + interest_paid -= repayment_details["unaccrued_interest"] + self.total_interest_paid += repayment_details["unaccrued_interest"] else: # get no of days for which interest can be paid - per_day_interest = get_per_day_interest(self.pending_principal_amount, - self.rate_of_interest, self.posting_date) + per_day_interest = get_per_day_interest( + self.pending_principal_amount, self.rate_of_interest, self.posting_date + ) - no_of_days = cint(interest_paid/per_day_interest) + no_of_days = cint(interest_paid / per_day_interest) self.total_interest_paid += no_of_days * per_day_interest interest_paid -= no_of_days * per_day_interest @@ -311,11 +386,11 @@ class LoanRepayment(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] - loan_details = frappe.get_doc("Loan", self.against_loan) if self.shortfall_amount and self.amount_paid > self.shortfall_amount: - remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount, - self.against_loan) + remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format( + self.shortfall_amount, self.against_loan + ) elif self.shortfall_amount: remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount) else: @@ -324,96 +399,117 @@ class LoanRepayment(AccountsController): if self.repay_from_salary: payment_account = self.payroll_payable_account else: - payment_account = loan_details.payment_account + payment_account = self.payment_account if self.total_penalty_paid: gle_map.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.payment_account, - "debit": self.total_penalty_paid, - "debit_in_account_currency": self.total_penalty_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, - "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": self.loan_account, + "against": payment_account, + "debit": self.total_penalty_paid, + "debit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date), + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": loan_details.penalty_income_account, - "against": loan_details.loan_account, - "credit": self.total_penalty_paid, - "credit_in_account_currency": self.total_penalty_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": self.penalty_income_account, + "against": self.loan_account, + "credit": self.total_penalty_paid, + "credit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": payment_account, - "against": loan_details.loan_account + ", " + loan_details.interest_income_account - + ", " + loan_details.penalty_income_account, - "debit": self.amount_paid, - "debit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date), - "party_type": loan_details.applicant_type if self.repay_from_salary else '', - "party": loan_details.applicant if self.repay_from_salary else '' - }) + self.get_gl_dict( + { + "account": payment_account, + "against": self.loan_account + ", " + self.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": remarks, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + "party_type": self.applicant_type if self.repay_from_salary else "", + "party": self.applicant if self.repay_from_salary else "", + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, - "against": payment_account, - "credit": self.amount_paid, - "credit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": payment_account, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": remarks, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + } + ) ) if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) -def create_repayment_entry(loan, applicant, company, posting_date, loan_type, - payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None, - payroll_payable_account=None): - lr = frappe.get_doc({ - "doctype": "Loan Repayment", - "against_loan": loan, - "payment_type": payment_type, - "company": company, - "posting_date": posting_date, - "applicant": applicant, - "penalty_amount": penalty_amount, - "interest_payable": interest_payable, - "payable_principal_amount": payable_principal_amount, - "amount_paid": amount_paid, - "loan_type": loan_type, - "payroll_payable_account": payroll_payable_account - }).insert() +def create_repayment_entry( + loan, + applicant, + company, + posting_date, + loan_type, + payment_type, + interest_payable, + payable_principal_amount, + amount_paid, + penalty_amount=None, + payroll_payable_account=None, +): + + lr = frappe.get_doc( + { + "doctype": "Loan Repayment", + "against_loan": loan, + "payment_type": payment_type, + "company": company, + "posting_date": posting_date, + "applicant": applicant, + "penalty_amount": penalty_amount, + "interest_payable": interest_payable, + "payable_principal_amount": payable_principal_amount, + "amount_paid": amount_paid, + "loan_type": loan_type, + "payroll_payable_account": payroll_payable_account, + } + ).insert() return lr + def get_accrued_interest_entries(against_loan, posting_date=None): if not posting_date: posting_date = getdate() @@ -433,35 +529,43 @@ def get_accrued_interest_entries(against_loan, posting_date=None): AND docstatus = 1 ORDER BY posting_date - """, (against_loan, posting_date), as_dict=1) + """, + (against_loan, posting_date), + as_dict=1, + ) return unpaid_accrued_entries + def get_penalty_details(against_loan): - penalty_details = frappe.db.sql(""" + penalty_details = frappe.db.sql( + """ SELECT posting_date, (penalty_amount - total_penalty_paid) as pending_penalty_amount FROM `tabLoan Repayment` where posting_date >= (SELECT MAX(posting_date) from `tabLoan Repayment` where against_loan = %s) and docstatus = 1 and against_loan = %s - """, (against_loan, against_loan)) + """, + (against_loan, against_loan), + ) if penalty_details: return penalty_details[0][0], flt(penalty_details[0][1]) else: return None, 0 + def regenerate_repayment_schedule(loan, cancel=0): from erpnext.loan_management.doctype.loan.loan import ( add_single_month, get_monthly_repayment_amount, ) - loan_doc = frappe.get_doc('Loan', loan) + loan_doc = frappe.get_doc("Loan", loan) next_accrual_date = None accrued_entries = 0 last_repayment_amount = 0 last_balance_amount = 0 - for term in reversed(loan_doc.get('repayment_schedule')): + for term in reversed(loan_doc.get("repayment_schedule")): if not term.is_accrued: next_accrual_date = term.payment_date loan_doc.remove(term) @@ -476,20 +580,24 @@ def regenerate_repayment_schedule(loan, cancel=0): balance_amount = get_pending_principal_amount(loan_doc) - if loan_doc.repayment_method == 'Repay Fixed Amount per Period': - monthly_repayment_amount = flt(balance_amount/len(loan_doc.get('repayment_schedule')) - accrued_entries) + if loan_doc.repayment_method == "Repay Fixed Amount per Period": + monthly_repayment_amount = flt( + balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries + ) else: - if not cancel: - monthly_repayment_amount = get_monthly_repayment_amount(balance_amount, - loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries) + repayment_period = loan_doc.repayment_periods - accrued_entries + if not cancel and repayment_period > 0: + monthly_repayment_amount = get_monthly_repayment_amount( + balance_amount, loan_doc.rate_of_interest, repayment_period + ) else: monthly_repayment_amount = last_repayment_amount balance_amount = last_balance_amount payment_date = next_accrual_date - while(balance_amount > 0): - interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12*100)) + while balance_amount > 0: + interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12 * 100)) principal_amount = monthly_repayment_amount - interest_amount balance_amount = flt(balance_amount + interest_amount - monthly_repayment_amount) if balance_amount < 0: @@ -497,31 +605,45 @@ def regenerate_repayment_schedule(loan, cancel=0): balance_amount = 0.0 total_payment = principal_amount + interest_amount - loan_doc.append("repayment_schedule", { - "payment_date": payment_date, - "principal_amount": principal_amount, - "interest_amount": interest_amount, - "total_payment": total_payment, - "balance_loan_amount": balance_amount - }) + loan_doc.append( + "repayment_schedule", + { + "payment_date": payment_date, + "principal_amount": principal_amount, + "interest_amount": interest_amount, + "total_payment": total_payment, + "balance_loan_amount": balance_amount, + }, + ) next_payment_date = add_single_month(payment_date) payment_date = next_payment_date loan_doc.save() + def get_pending_principal_amount(loan): - if loan.status in ('Disbursed', 'Closed') or loan.disbursed_amount >= loan.loan_amount: - pending_principal_amount = flt(loan.total_payment) - flt(loan.total_principal_paid) \ - - flt(loan.total_interest_payable) - flt(loan.written_off_amount) + if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount: + pending_principal_amount = ( + flt(loan.total_payment) + - flt(loan.total_principal_paid) + - flt(loan.total_interest_payable) + - flt(loan.written_off_amount) + ) else: - pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_principal_paid) \ - - flt(loan.total_interest_payable) - flt(loan.written_off_amount) + pending_principal_amount = ( + flt(loan.disbursed_amount) + - flt(loan.total_principal_paid) + - flt(loan.total_interest_payable) + - flt(loan.written_off_amount) + ) return pending_principal_amount + # This function returns the amounts that are payable at the time of loan repayment based on posting date # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable + def get_amounts(amounts, against_loan, posting_date): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -535,8 +657,8 @@ def get_amounts(amounts, against_loan, posting_date): total_pending_interest = 0 penalty_amount = 0 payable_principal_amount = 0 - final_due_date = '' - due_date = '' + final_due_date = "" + due_date = "" for entry in accrued_interest_entries: # Loan repayment due date is one day after the loan interest is accrued @@ -552,16 +674,25 @@ def get_amounts(amounts, against_loan, posting_date): no_of_late_days = date_diff(posting_date, due_date_after_grace_period) + 1 - if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular': - penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days) + if ( + no_of_late_days > 0 + and (not against_loan_doc.repay_from_salary) + and entry.accrual_type == "Regular" + ): + penalty_amount += ( + entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days + ) total_pending_interest += entry.interest_amount payable_principal_amount += entry.payable_principal_amount - pending_accrual_entries.setdefault(entry.name, { - 'interest_amount': flt(entry.interest_amount, precision), - 'payable_principal_amount': flt(entry.payable_principal_amount, precision) - }) + pending_accrual_entries.setdefault( + entry.name, + { + "interest_amount": flt(entry.interest_amount, precision), + "payable_principal_amount": flt(entry.payable_principal_amount, precision), + }, + ) if due_date and not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) @@ -577,14 +708,18 @@ def get_amounts(amounts, against_loan, posting_date): if pending_days > 0: principal_amount = flt(pending_principal_amount, precision) - per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date) - unaccrued_interest += (pending_days * per_day_interest) + per_day_interest = get_per_day_interest( + principal_amount, loan_type_details.rate_of_interest, posting_date + ) + unaccrued_interest += pending_days * per_day_interest amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) amounts["interest_amount"] = flt(total_pending_interest, precision) amounts["penalty_amount"] = flt(penalty_amount + pending_penalty_amount, precision) - amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) + amounts["payable_amount"] = flt( + payable_principal_amount + total_pending_interest + penalty_amount, precision + ) amounts["pending_accrual_entries"] = pending_accrual_entries amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) @@ -593,24 +728,28 @@ def get_amounts(amounts, against_loan, posting_date): return amounts + @frappe.whitelist() -def calculate_amounts(against_loan, posting_date, payment_type=''): +def calculate_amounts(against_loan, posting_date, payment_type=""): amounts = { - 'penalty_amount': 0.0, - 'interest_amount': 0.0, - 'pending_principal_amount': 0.0, - 'payable_principal_amount': 0.0, - 'payable_amount': 0.0, - 'unaccrued_interest': 0.0, - 'due_date': '' + "penalty_amount": 0.0, + "interest_amount": 0.0, + "pending_principal_amount": 0.0, + "payable_principal_amount": 0.0, + "payable_amount": 0.0, + "unaccrued_interest": 0.0, + "due_date": "", } amounts = get_amounts(amounts, against_loan, posting_date) # update values for closure - if payment_type == 'Loan Closure': - amounts['payable_principal_amount'] = amounts['pending_principal_amount'] - amounts['interest_amount'] += amounts['unaccrued_interest'] - amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] + if payment_type == "Loan Closure": + amounts["payable_principal_amount"] = amounts["pending_principal_amount"] + amounts["interest_amount"] += amounts["unaccrued_interest"] + amounts["payable_amount"] = amounts["payable_principal_amount"] + amounts["interest_amount"] + amounts["payable_amount"] = ( + amounts["payable_principal_amount"] + amounts["interest_amount"] + amounts["penalty_amount"] + ) return amounts diff --git a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py index 8d5c525ac31..1d96885e155 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py +++ b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py @@ -1,14 +1,8 @@ - - def get_data(): return { - 'fieldname': 'loan_security', - 'transactions': [ - { - 'items': ['Loan Application', 'Loan Security Price'] - }, - { - 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] - } - ] + "fieldname": "loan_security", + "transactions": [ + {"items": ["Loan Application", "Loan Security Price"]}, + {"items": ["Loan Security Pledge", "Loan Security Unpledge"]}, + ], } diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index 7d02645609b..f0d59542753 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -40,22 +40,28 @@ class LoanSecurityPledge(Document): if security.loan_security not in security_list: security_list.append(security.loan_security) else: - frappe.throw(_('Loan Security {0} added multiple times').format(frappe.bold( - security.loan_security))) + frappe.throw( + _("Loan Security {0} added multiple times").format(frappe.bold(security.loan_security)) + ) def validate_loan_security_type(self): - existing_pledge = '' + existing_pledge = "" if self.loan: - existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name']) + existing_pledge = frappe.db.get_value( + "Loan Security Pledge", {"loan": self.loan, "docstatus": 1}, ["name"] + ) if existing_pledge: - loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type']) + loan_security_type = frappe.db.get_value( + "Pledge", {"parent": existing_pledge}, ["loan_security_type"] + ) else: loan_security_type = self.securities[0].loan_security_type - ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", - fields=["name", "loan_to_value_ratio"], as_list=1)) + ltv_ratio_map = frappe._dict( + frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1) + ) ltv_ratio = ltv_ratio_map.get(loan_security_type) @@ -63,7 +69,6 @@ class LoanSecurityPledge(Document): if ltv_ratio_map.get(security.loan_security_type) != ltv_ratio: frappe.throw(_("Loan Securities with different LTV ratio cannot be pledged against one loan")) - def set_pledge_amount(self): total_security_value = 0 maximum_loan_value = 0 @@ -77,10 +82,10 @@ class LoanSecurityPledge(Document): pledge.loan_security_price = get_loan_security_price(pledge.loan_security) if not pledge.qty: - pledge.qty = cint(pledge.amount/pledge.loan_security_price) + pledge.qty = cint(pledge.amount / pledge.loan_security_price) pledge.amount = pledge.qty * pledge.loan_security_price - pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut/100)) + pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut / 100)) total_security_value += pledge.amount maximum_loan_value += pledge.post_haircut_amount @@ -88,12 +93,19 @@ class LoanSecurityPledge(Document): self.total_security_value = total_security_value self.maximum_loan_value = maximum_loan_value + def update_loan(loan, maximum_value_against_pledge, cancel=0): - maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount']) + maximum_loan_value = frappe.db.get_value("Loan", {"name": loan}, ["maximum_loan_amount"]) if cancel: - frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s - WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan)) + frappe.db.sql( + """ UPDATE `tabLoan` SET maximum_loan_amount=%s + WHERE name=%s""", + (maximum_loan_value - maximum_value_against_pledge, loan), + ) else: - frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 - WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) + frappe.db.sql( + """ UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 + WHERE name=%s""", + (maximum_loan_value + maximum_value_against_pledge, loan), + ) diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py index fca9dd6bcb9..45c4459ac3f 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py @@ -17,23 +17,37 @@ class LoanSecurityPrice(Document): if self.valid_from > self.valid_upto: frappe.throw(_("Valid From Time must be lesser than Valid Upto Time.")) - existing_loan_security = frappe.db.sql(""" SELECT name from `tabLoan Security Price` + existing_loan_security = frappe.db.sql( + """ SELECT name from `tabLoan Security Price` WHERE loan_security = %s AND name != %s AND (valid_from BETWEEN %s and %s OR valid_upto BETWEEN %s and %s) """, - (self.loan_security, self.name, self.valid_from, self.valid_upto, self.valid_from, self.valid_upto)) + ( + self.loan_security, + self.name, + self.valid_from, + self.valid_upto, + self.valid_from, + self.valid_upto, + ), + ) if existing_loan_security: frappe.throw(_("Loan Security Price overlapping with {0}").format(existing_loan_security[0][0])) + @frappe.whitelist() def get_loan_security_price(loan_security, valid_time=None): if not valid_time: valid_time = get_datetime() - loan_security_price = frappe.db.get_value("Loan Security Price", { - 'loan_security': loan_security, - 'valid_from': ("<=",valid_time), - 'valid_upto': (">=", valid_time) - }, 'loan_security_price') + loan_security_price = frappe.db.get_value( + "Loan Security Price", + { + "loan_security": loan_security, + "valid_from": ("<=", valid_time), + "valid_upto": (">=", valid_time), + }, + "loan_security_price", + ) if not loan_security_price: frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 20e451b81ea..b901e626b43 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -14,27 +14,42 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled class LoanSecurityShortfall(Document): pass + def update_shortfall_status(loan, security_value, on_cancel=0): - loan_security_shortfall = frappe.db.get_value("Loan Security Shortfall", - {"loan": loan, "status": "Pending"}, ['name', 'shortfall_amount'], as_dict=1) + loan_security_shortfall = frappe.db.get_value( + "Loan Security Shortfall", + {"loan": loan, "status": "Pending"}, + ["name", "shortfall_amount"], + as_dict=1, + ) if not loan_security_shortfall: return if security_value >= loan_security_shortfall.shortfall_amount: - frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, { - "status": "Completed", - "shortfall_amount": loan_security_shortfall.shortfall_amount, - "shortfall_percentage": 0 - }) + frappe.db.set_value( + "Loan Security Shortfall", + loan_security_shortfall.name, + { + "status": "Completed", + "shortfall_amount": loan_security_shortfall.shortfall_amount, + "shortfall_percentage": 0, + }, + ) else: - frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, - "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value) + frappe.db.set_value( + "Loan Security Shortfall", + loan_security_shortfall.name, + "shortfall_amount", + loan_security_shortfall.shortfall_amount - security_value, + ) @frappe.whitelist() def add_security(loan): - loan_details = frappe.db.get_value("Loan", loan, ['applicant', 'company', 'applicant_type'], as_dict=1) + loan_details = frappe.db.get_value( + "Loan", loan, ["applicant", "company", "applicant_type"], as_dict=1 + ) loan_security_pledge = frappe.new_doc("Loan Security Pledge") loan_security_pledge.loan = loan @@ -44,33 +59,51 @@ def add_security(loan): return loan_security_pledge.as_dict() + def check_for_ltv_shortfall(process_loan_security_shortfall): update_time = get_datetime() - loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", - fields=["loan_security", "loan_security_price"], - filters = { - "valid_from": ("<=", update_time), - "valid_upto": (">=", update_time) - }, as_list=1)) + loan_security_price_map = frappe._dict( + frappe.get_all( + "Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters={"valid_from": ("<=", update_time), "valid_upto": (">=", update_time)}, + as_list=1, + ) + ) - loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', - 'total_interest_payable', 'disbursed_amount', 'status'], - filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) + loans = frappe.get_all( + "Loan", + fields=[ + "name", + "loan_amount", + "total_principal_paid", + "total_payment", + "total_interest_payable", + "disbursed_amount", + "status", + ], + filters={"status": ("in", ["Disbursed", "Partially Disbursed"]), "is_secured_loan": 1}, + ) - loan_shortfall_map = frappe._dict(frappe.get_all("Loan Security Shortfall", - fields=["loan", "name"], filters={"status": "Pending"}, as_list=1)) + loan_shortfall_map = frappe._dict( + frappe.get_all( + "Loan Security Shortfall", fields=["loan", "name"], filters={"status": "Pending"}, as_list=1 + ) + ) loan_security_map = {} for loan in loans: - if loan.status == 'Disbursed': - outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + if loan.status == "Disbursed": + outstanding_amount = ( + flt(loan.total_payment) - flt(loan.total_interest_payable) - flt(loan.total_principal_paid) + ) else: - outstanding_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + outstanding_amount = ( + flt(loan.disbursed_amount) - flt(loan.total_interest_payable) - flt(loan.total_principal_paid) + ) pledged_securities = get_pledged_security_qty(loan.name) ltv_ratio = 0.0 @@ -81,21 +114,36 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): ltv_ratio = get_ltv_ratio(security) security_value += flt(loan_security_price_map.get(security)) * flt(qty) - current_ratio = (outstanding_amount/security_value) * 100 if security_value else 0 + current_ratio = (outstanding_amount / security_value) * 100 if security_value else 0 if current_ratio > ltv_ratio: shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100) - create_loan_security_shortfall(loan.name, outstanding_amount, security_value, shortfall_amount, - current_ratio, process_loan_security_shortfall) + create_loan_security_shortfall( + loan.name, + outstanding_amount, + security_value, + shortfall_amount, + current_ratio, + process_loan_security_shortfall, + ) elif loan_shortfall_map.get(loan.name): shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100) if shortfall_amount <= 0: shortfall = loan_shortfall_map.get(loan.name) update_pending_shortfall(shortfall) -def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, shortfall_ratio, - process_loan_security_shortfall): - existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") + +def create_loan_security_shortfall( + loan, + loan_amount, + security_value, + shortfall_amount, + shortfall_ratio, + process_loan_security_shortfall, +): + existing_shortfall = frappe.db.get_value( + "Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name" + ) if existing_shortfall: ltv_shortfall = frappe.get_doc("Loan Security Shortfall", existing_shortfall) @@ -111,16 +159,17 @@ def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_ ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall ltv_shortfall.save() + def get_ltv_ratio(loan_security): - loan_security_type = frappe.db.get_value('Loan Security', loan_security, 'loan_security_type') - ltv_ratio = frappe.db.get_value('Loan Security Type', loan_security_type, 'loan_to_value_ratio') + loan_security_type = frappe.db.get_value("Loan Security", loan_security, "loan_security_type") + ltv_ratio = frappe.db.get_value("Loan Security Type", loan_security_type, "loan_to_value_ratio") return ltv_ratio + def update_pending_shortfall(shortfall): # Get all pending loan security shortfall - frappe.db.set_value("Loan Security Shortfall", shortfall, - { - "status": "Completed", - "shortfall_amount": 0, - "shortfall_percentage": 0 - }) + frappe.db.set_value( + "Loan Security Shortfall", + shortfall, + {"status": "Completed", "shortfall_amount": 0, "shortfall_percentage": 0}, + ) diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py index daa9958808f..8fc4520ccd2 100644 --- a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py @@ -1,14 +1,8 @@ - - def get_data(): return { - 'fieldname': 'loan_security_type', - 'transactions': [ - { - 'items': ['Loan Security', 'Loan Security Price'] - }, - { - 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] - } - ] + "fieldname": "loan_security_type", + "transactions": [ + {"items": ["Loan Security", "Loan Security Price"]}, + {"items": ["Loan Security Pledge", "Loan Security Unpledge"]}, + ], } diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index b716288fc59..731b65e9a29 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -16,7 +16,7 @@ class LoanSecurityUnpledge(Document): def on_cancel(self): self.update_loan_status(cancel=1) - self.db_set('status', 'Requested') + self.db_set("status", "Requested") def validate_duplicate_securities(self): security_list = [] @@ -24,8 +24,11 @@ class LoanSecurityUnpledge(Document): if d.loan_security not in security_list: security_list.append(d.loan_security) else: - frappe.throw(_("Row {0}: Loan Security {1} added multiple times").format( - d.idx, frappe.bold(d.loan_security))) + frappe.throw( + _("Row {0}: Loan Security {1} added multiple times").format( + d.idx, frappe.bold(d.loan_security) + ) + ) def validate_unpledge_qty(self): from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( @@ -37,18 +40,33 @@ class LoanSecurityUnpledge(Document): pledge_qty_map = get_pledged_security_qty(self.loan) - ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", - fields=["name", "loan_to_value_ratio"], as_list=1)) + ltv_ratio_map = frappe._dict( + frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1) + ) - loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", - fields=["loan_security", "loan_security_price"], - filters = { - "valid_from": ("<=", get_datetime()), - "valid_upto": (">=", get_datetime()) - }, as_list=1)) + loan_security_price_map = frappe._dict( + frappe.get_all( + "Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters={"valid_from": ("<=", get_datetime()), "valid_upto": (">=", get_datetime())}, + as_list=1, + ) + ) - loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', 'loan_amount', - 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1) + loan_details = frappe.get_value( + "Loan", + self.loan, + [ + "total_payment", + "total_principal_paid", + "loan_amount", + "total_interest_payable", + "written_off_amount", + "disbursed_amount", + "status", + ], + as_dict=1, + ) pending_principal_amount = get_pending_principal_amount(loan_details) @@ -59,8 +77,13 @@ class LoanSecurityUnpledge(Document): for security in self.securities: pledged_qty = pledge_qty_map.get(security.loan_security, 0) if security.qty > pledged_qty: - msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(security.idx, pledged_qty, security.uom, - frappe.bold(security.loan_security), frappe.bold(self.loan)) + msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format( + security.idx, + pledged_qty, + security.uom, + frappe.bold(security.loan_security), + frappe.bold(self.loan), + ) msg += "
" msg += _("You are trying to unpledge more.") frappe.throw(msg, title=_("Loan Security Unpledge Error")) @@ -79,14 +102,14 @@ class LoanSecurityUnpledge(Document): if not security_value and flt(pending_principal_amount, 2) > 0: self._throw(security_value, pending_principal_amount, ltv_ratio) - if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: + if security_value and flt(pending_principal_amount / security_value) * 100 > ltv_ratio: self._throw(security_value, pending_principal_amount, ltv_ratio) def _throw(self, security_value, pending_principal_amount, ltv_ratio): msg = _("Loan Security Value after unpledge is {0}").format(frappe.bold(security_value)) - msg += '
' + msg += "
" msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2))) - msg += '
' + msg += "
" msg += _("Loan To Security Value ratio must always be {0}").format(frappe.bold(ltv_ratio)) frappe.throw(msg, title=_("Loan To Value ratio breach")) @@ -96,13 +119,13 @@ class LoanSecurityUnpledge(Document): def approve(self): if self.status == "Approved" and not self.unpledge_time: self.update_loan_status() - self.db_set('unpledge_time', get_datetime()) + self.db_set("unpledge_time", get_datetime()) def update_loan_status(self, cancel=0): if cancel: - loan_status = frappe.get_value('Loan', self.loan, 'status') - if loan_status == 'Closed': - frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested') + loan_status = frappe.get_value("Loan", self.loan, "status") + if loan_status == "Closed": + frappe.db.set_value("Loan", self.loan, "status", "Loan Closure Requested") else: pledged_qty = 0 current_pledges = get_pledged_security_qty(self.loan) @@ -111,34 +134,41 @@ class LoanSecurityUnpledge(Document): pledged_qty += qty if not pledged_qty: - frappe.db.set_value('Loan', self.loan, - { - 'status': 'Closed', - 'closure_date': getdate() - }) + frappe.db.set_value("Loan", self.loan, {"status": "Closed", "closure_date": getdate()}) + @frappe.whitelist() def get_pledged_security_qty(loan): current_pledges = {} - unpledges = frappe._dict(frappe.db.sql(""" + unpledges = frappe._dict( + frappe.db.sql( + """ SELECT u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE up.loan = %s AND u.parent = up.name AND up.status = 'Approved' GROUP BY u.loan_security - """, (loan))) + """, + (loan), + ) + ) - pledges = frappe._dict(frappe.db.sql(""" + pledges = frappe._dict( + frappe.db.sql( + """ SELECT p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE lp.loan = %s AND p.parent = lp.name AND lp.status = 'Pledged' GROUP BY p.loan_security - """, (loan))) + """, + (loan), + ) + ) for security, qty in iteritems(pledges): current_pledges.setdefault(security, qty) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/loan_type/loan_type.py index 592229cf994..51ee05b277e 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.py +++ b/erpnext/loan_management/doctype/loan_type/loan_type.py @@ -12,12 +12,20 @@ class LoanType(Document): self.validate_accounts() def validate_accounts(self): - for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']: - company = frappe.get_value("Account", self.get(fieldname), 'company') + for fieldname in [ + "payment_account", + "loan_account", + "interest_income_account", + "penalty_income_account", + ]: + company = frappe.get_value("Account", self.get(fieldname), "company") if company and company != self.company: - frappe.throw(_("Account {0} does not belong to company {1}").format(frappe.bold(self.get(fieldname)), - frappe.bold(self.company))) + frappe.throw( + _("Account {0} does not belong to company {1}").format( + frappe.bold(self.get(fieldname)), frappe.bold(self.company) + ) + ) - if self.get('loan_account') == self.get('payment_account'): - frappe.throw(_('Loan Account and Payment Account cannot be same')) + if self.get("loan_account") == self.get("payment_account"): + frappe.throw(_("Loan Account and Payment Account cannot be same")) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py index 19026da2f66..e2467c64558 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py +++ b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py @@ -1,14 +1,5 @@ - - def get_data(): return { - 'fieldname': 'loan_type', - 'transactions': [ - { - 'items': ['Loan Repayment', 'Loan'] - }, - { - 'items': ['Loan Application'] - } - ] + "fieldname": "loan_type", + "transactions": [{"items": ["Loan Repayment", "Loan"]}, {"items": ["Loan Application"]}], } diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 35be587f87f..e19fd15fc84 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -22,11 +22,16 @@ class LoanWriteOff(AccountsController): def validate_write_off_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 - total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, - ['total_payment', 'total_principal_paid','total_interest_payable', 'written_off_amount']) + total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value( + "Loan", + self.loan, + ["total_payment", "total_principal_paid", "total_interest_payable", "written_off_amount"], + ) - pending_principal_amount = flt(flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount), - precision) + pending_principal_amount = flt( + flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount), + precision, + ) if self.write_off_amount > pending_principal_amount: frappe.throw(_("Write off amount cannot be greater than pending principal amount")) @@ -37,52 +42,55 @@ class LoanWriteOff(AccountsController): def on_cancel(self): self.update_outstanding_amount(cancel=1) - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] self.make_gl_entries(cancel=1) def update_outstanding_amount(self, cancel=0): - written_off_amount = frappe.db.get_value('Loan', self.loan, 'written_off_amount') + written_off_amount = frappe.db.get_value("Loan", self.loan, "written_off_amount") if cancel: written_off_amount -= self.write_off_amount else: written_off_amount += self.write_off_amount - frappe.db.set_value('Loan', self.loan, 'written_off_amount', written_off_amount) - + frappe.db.set_value("Loan", self.loan, "written_off_amount", written_off_amount) def make_gl_entries(self, cancel=0): gl_entries = [] loan_details = frappe.get_doc("Loan", self.loan) gl_entries.append( - self.get_gl_dict({ - "account": self.write_off_account, - "against": loan_details.loan_account, - "debit": self.write_off_amount, - "debit_in_account_currency": self.write_off_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": self.write_off_account, + "against": loan_details.loan_account, + "debit": self.write_off_amount, + "debit_in_account_currency": self.write_off_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + } + ) ) gl_entries.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, - "against": self.write_off_account, - "credit": self.write_off_amount, - "credit_in_account_currency": self.write_off_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, + "against": self.write_off_account, + "credit": self.write_off_amount, + "credit_in_account_currency": self.write_off_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + } + ) ) make_gl_entries(gl_entries, cancel=cancel, merge_entries=False) diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index 4c34ccd983e..81464a36c3d 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -17,24 +17,36 @@ class ProcessLoanInterestAccrual(Document): open_loans = [] if self.loan: - loan_doc = frappe.get_doc('Loan', self.loan) + loan_doc = frappe.get_doc("Loan", self.loan) if loan_doc: open_loans.append(loan_doc) - if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': - make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, - open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type) + if (not self.loan or not loan_doc.is_term_loan) and self.process_type != "Term Loans": + make_accrual_interest_entry_for_demand_loans( + self.posting_date, + self.name, + open_loans=open_loans, + loan_type=self.loan_type, + accrual_type=self.accrual_type, + ) - if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': - make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, - loan_type=self.loan_type, accrual_type=self.accrual_type) + if (not self.loan or loan_doc.is_term_loan) and self.process_type != "Demand Loans": + make_accrual_interest_entry_for_term_loans( + self.posting_date, + self.name, + term_loan=self.loan, + loan_type=self.loan_type, + accrual_type=self.accrual_type, + ) -def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"): - loan_process = frappe.new_doc('Process Loan Interest Accrual') +def process_loan_interest_accrual_for_demand_loans( + posting_date=None, loan_type=None, loan=None, accrual_type="Regular" +): + loan_process = frappe.new_doc("Process Loan Interest Accrual") loan_process.posting_date = posting_date or nowdate() loan_process.loan_type = loan_type - loan_process.process_type = 'Demand Loans' + loan_process.process_type = "Demand Loans" loan_process.loan = loan loan_process.accrual_type = accrual_type @@ -42,25 +54,26 @@ def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type= return loan_process.name + def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): if not term_loan_accrual_pending(posting_date or nowdate()): return - loan_process = frappe.new_doc('Process Loan Interest Accrual') + loan_process = frappe.new_doc("Process Loan Interest Accrual") loan_process.posting_date = posting_date or nowdate() loan_process.loan_type = loan_type - loan_process.process_type = 'Term Loans' + loan_process.process_type = "Term Loans" loan_process.loan = loan loan_process.submit() return loan_process.name + def term_loan_accrual_pending(date): - pending_accrual = frappe.db.get_value('Repayment Schedule', { - 'payment_date': ('<=', date), - 'is_accrued': 0 - }) + pending_accrual = frappe.db.get_value( + "Repayment Schedule", {"payment_date": ("<=", date), "is_accrued": 0} + ) return pending_accrual diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py index 4195960890c..ac85df761ce 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): return { - 'fieldname': 'process_loan_interest_accrual', - 'transactions': [ - { - 'items': ['Loan Interest Accrual'] - } - ] + "fieldname": "process_loan_interest_accrual", + "transactions": [{"items": ["Loan Interest Accrual"]}], } diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py index ba9fb0c449f..fffc5d4876b 100644 --- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py @@ -13,16 +13,18 @@ from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_short class ProcessLoanSecurityShortfall(Document): def onload(self): - self.set_onload('update_time', get_datetime()) + self.set_onload("update_time", get_datetime()) def on_submit(self): check_for_ltv_shortfall(self.name) + def create_process_loan_security_shortfall(): if check_for_secured_loans(): process = frappe.new_doc("Process Loan Security Shortfall") process.update_time = get_datetime() process.submit() + def check_for_secured_loans(): - return frappe.db.count('Loan', {'docstatus': 1, 'is_secured_loan': 1}) + return frappe.db.count("Loan", {"docstatus": 1, "is_secured_loan": 1}) diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py index fa9d18b6fab..4d7b1630bad 100644 --- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): return { - 'fieldname': 'process_loan_security_shortfall', - 'transactions': [ - { - 'items': ['Loan Security Shortfall'] - } - ] + "fieldname": "process_loan_security_shortfall", + "transactions": [{"items": ["Loan Security Shortfall"]}], } diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py index 6063b7bad8b..e7487cb34d3 100644 --- a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py @@ -9,9 +9,13 @@ from frappe.model.document import Document class SanctionedLoanAmount(Document): def validate(self): - sanctioned_doc = frappe.db.exists('Sanctioned Loan Amount', {'applicant': self.applicant, 'company': self.company}) + sanctioned_doc = frappe.db.exists( + "Sanctioned Loan Amount", {"applicant": self.applicant, "company": self.company} + ) if sanctioned_doc and sanctioned_doc != self.name: - frappe.throw(_("Sanctioned Loan Amount already exists for {0} against company {1}").format( - frappe.bold(self.applicant), frappe.bold(self.company) - )) + frappe.throw( + _("Sanctioned Loan Amount already exists for {0} against company {1}").format( + frappe.bold(self.applicant), frappe.bold(self.company) + ) + ) diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index 8ebca39061c..75c4b2877b1 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -18,84 +18,166 @@ def execute(filters=None): def get_columns(filters): columns = [ - {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, - {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, - {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160}, - {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100}, - {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150}, + { + "label": _("Applicant Type"), + "fieldname": "applicant_type", + "options": "DocType", + "width": 100, + }, + { + "label": _("Applicant Name"), + "fieldname": "applicant_name", + "fieldtype": "Dynamic Link", + "options": "applicant_type", + "width": 150, + }, + { + "label": _("Loan Security"), + "fieldname": "loan_security", + "fieldtype": "Link", + "options": "Loan Security", + "width": 160, + }, + { + "label": _("Loan Security Code"), + "fieldname": "loan_security_code", + "fieldtype": "Data", + "width": 100, + }, + { + "label": _("Loan Security Name"), + "fieldname": "loan_security_name", + "fieldtype": "Data", + "width": 150, + }, {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100}, - {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, + { + "label": _("Loan Security Type"), + "fieldname": "loan_security_type", + "fieldtype": "Link", + "options": "Loan Security Type", + "width": 120, + }, {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, - {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, - {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100}, - {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, - {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, - {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + { + "label": _("Latest Price"), + "fieldname": "latest_price", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("Price Valid Upto"), + "fieldname": "price_valid_upto", + "fieldtype": "Datetime", + "width": 100, + }, + { + "label": _("Current Value"), + "fieldname": "current_value", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("% Of Applicant Portfolio"), + "fieldname": "portfolio_percent", + "fieldtype": "Percentage", + "width": 100, + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "options": "Currency", + "hidden": 1, + "width": 100, + }, ] return columns + def get_data(filters): data = [] loan_security_details = get_loan_security_details() - pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, - loan_security_details) + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty( + filters, loan_security_details + ) - currency = erpnext.get_company_currency(filters.get('company')) + currency = erpnext.get_company_currency(filters.get("company")) for key, qty in iteritems(pledge_values): if qty: row = {} - current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) - valid_upto = loan_security_details.get(key[1], {}).get('valid_upto') + current_value = flt(qty * loan_security_details.get(key[1], {}).get("latest_price", 0)) + valid_upto = loan_security_details.get(key[1], {}).get("valid_upto") row.update(loan_security_details.get(key[1])) - row.update({ - 'applicant_type': applicant_type_map.get(key[0]), - 'applicant_name': key[0], - 'total_qty': qty, - 'current_value': current_value, - 'price_valid_upto': valid_upto, - 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \ + row.update( + { + "applicant_type": applicant_type_map.get(key[0]), + "applicant_name": key[0], + "total_qty": qty, + "current_value": current_value, + "price_valid_upto": valid_upto, + "portfolio_percent": flt(current_value * 100 / total_value_map.get(key[0]), 2) + if total_value_map.get(key[0]) else 0.0, - 'currency': currency - }) + "currency": currency, + } + ) data.append(row) return data + def get_loan_security_details(): security_detail_map = {} loan_security_price_map = {} lsp_validity_map = {} - loan_security_prices = frappe.db.sql(""" + loan_security_prices = frappe.db.sql( + """ SELECT loan_security, loan_security_price, valid_upto FROM `tabLoan Security Price` t1 WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2 WHERE t1.loan_security = t2.loan_security) - """, as_dict=1) + """, + as_dict=1, + ) for security in loan_security_prices: loan_security_price_map.setdefault(security.loan_security, security.loan_security_price) lsp_validity_map.setdefault(security.loan_security, security.valid_upto) - loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security', - 'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type', - 'disabled']) + loan_security_details = frappe.get_all( + "Loan Security", + fields=[ + "name as loan_security", + "loan_security_code", + "loan_security_name", + "haircut", + "loan_security_type", + "disabled", + ], + ) for security in loan_security_details: - security.update({ - 'latest_price': flt(loan_security_price_map.get(security.loan_security)), - 'valid_upto': lsp_validity_map.get(security.loan_security) - }) + security.update( + { + "latest_price": flt(loan_security_price_map.get(security.loan_security)), + "valid_upto": lsp_validity_map.get(security.loan_security), + } + ) security_detail_map.setdefault(security.loan_security, security) return security_detail_map + def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): current_pledges = {} total_value_map = {} @@ -103,39 +185,53 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): applicant_wise_unpledges = {} conditions = "" - if filters.get('company'): + if filters.get("company"): conditions = "AND company = %(company)s" - unpledges = frappe.db.sql(""" + unpledges = frappe.db.sql( + """ SELECT up.applicant, u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY up.applicant, u.loan_security - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) for unpledge in unpledges: applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty) - pledges = frappe.db.sql(""" + pledges = frappe.db.sql( + """ SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY lp.applicant, p.loan_security - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) for security in pledges: current_pledges.setdefault((security.applicant, security.loan_security), security.qty) total_value_map.setdefault(security.applicant, 0.0) applicant_type_map.setdefault(security.applicant, security.applicant_type) - current_pledges[(security.applicant, security.loan_security)] -= \ - applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0) + current_pledges[(security.applicant, security.loan_security)] -= applicant_wise_unpledges.get( + (security.applicant, security.loan_security), 0.0 + ) - total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \ - * loan_security_details.get(security.loan_security, {}).get('latest_price', 0) + total_value_map[security.applicant] += current_pledges.get( + (security.applicant, security.loan_security) + ) * loan_security_details.get(security.loan_security, {}).get("latest_price", 0) return current_pledges, total_value_map, applicant_type_map diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index 7c512679567..9186ce61743 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -17,41 +17,148 @@ def execute(filters=None): data = get_active_loan_details(filters) return columns, data + def get_columns(filters): columns = [ {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160}, - {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, - {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, - {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100}, - {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Penalty Amount"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, - {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100}, - {"label": _("Loan To Value Ratio"), "fieldname": "loan_to_value", "fieldtype": "Percent", "width": 100}, - {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + { + "label": _("Applicant Type"), + "fieldname": "applicant_type", + "options": "DocType", + "width": 100, + }, + { + "label": _("Applicant Name"), + "fieldname": "applicant_name", + "fieldtype": "Dynamic Link", + "options": "applicant_type", + "width": 150, + }, + { + "label": _("Loan Type"), + "fieldname": "loan_type", + "fieldtype": "Link", + "options": "Loan Type", + "width": 100, + }, + { + "label": _("Sanctioned Amount"), + "fieldname": "sanctioned_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Disbursed Amount"), + "fieldname": "disbursed_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Penalty Amount"), + "fieldname": "penalty", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Accrued Interest"), + "fieldname": "accrued_interest", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Total Repayment"), + "fieldname": "total_repayment", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Principal Outstanding"), + "fieldname": "principal_outstanding", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Interest Outstanding"), + "fieldname": "interest_outstanding", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Total Outstanding"), + "fieldname": "total_outstanding", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Undue Booked Interest"), + "fieldname": "undue_interest", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Interest %"), + "fieldname": "rate_of_interest", + "fieldtype": "Percent", + "width": 100, + }, + { + "label": _("Penalty Interest %"), + "fieldname": "penalty_interest", + "fieldtype": "Percent", + "width": 100, + }, + { + "label": _("Loan To Value Ratio"), + "fieldname": "loan_to_value", + "fieldtype": "Percent", + "width": 100, + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "options": "Currency", + "hidden": 1, + "width": 100, + }, ] return columns + def get_active_loan_details(filters): filter_obj = {"status": ("!=", "Closed")} - if filters.get('company'): - filter_obj.update({'company': filters.get('company')}) + if filters.get("company"): + filter_obj.update({"company": filters.get("company")}) - loan_details = frappe.get_all("Loan", - fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type", - "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid", - "total_interest_payable", "written_off_amount", "status"], - filters=filter_obj) + loan_details = frappe.get_all( + "Loan", + fields=[ + "name as loan", + "applicant_type", + "applicant as applicant_name", + "loan_type", + "disbursed_amount", + "rate_of_interest", + "total_payment", + "total_principal_paid", + "total_interest_payable", + "written_off_amount", + "status", + ], + filters=filter_obj, + ) loan_list = [d.loan for d in loan_details] @@ -62,70 +169,105 @@ def get_active_loan_details(filters): penal_interest_rate_map = get_penal_interest_rate_map() payments = get_payments(loan_list) accrual_map = get_interest_accruals(loan_list) - currency = erpnext.get_company_currency(filters.get('company')) + currency = erpnext.get_company_currency(filters.get("company")) for loan in loan_details: - total_payment = loan.total_payment if loan.status == 'Disbursed' else loan.disbursed_amount + total_payment = loan.total_payment if loan.status == "Disbursed" else loan.disbursed_amount - loan.update({ - "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)), - "principal_outstanding": flt(total_payment) - flt(loan.total_principal_paid) \ - - flt(loan.total_interest_payable) - flt(loan.written_off_amount), - "total_repayment": flt(payments.get(loan.loan)), - "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), - "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), - "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), - "penalty_interest": penal_interest_rate_map.get(loan.loan_type), - "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")), - "loan_to_value": 0.0, - "currency": currency - }) + loan.update( + { + "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)), + "principal_outstanding": flt(total_payment) + - flt(loan.total_principal_paid) + - flt(loan.total_interest_payable) + - flt(loan.written_off_amount), + "total_repayment": flt(payments.get(loan.loan)), + "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), + "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), + "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), + "penalty_interest": penal_interest_rate_map.get(loan.loan_type), + "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")), + "loan_to_value": 0.0, + "currency": currency, + } + ) - loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \ - + loan['penalty'] + loan["total_outstanding"] = ( + loan["principal_outstanding"] + loan["interest_outstanding"] + loan["penalty"] + ) if loan_wise_security_value.get(loan.loan): - loan['loan_to_value'] = flt((loan['principal_outstanding'] * 100) / loan_wise_security_value.get(loan.loan)) + loan["loan_to_value"] = flt( + (loan["principal_outstanding"] * 100) / loan_wise_security_value.get(loan.loan) + ) return loan_details + def get_sanctioned_amount_map(): - return frappe._dict(frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], - as_list=1)) + return frappe._dict( + frappe.get_all( + "Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], as_list=1 + ) + ) + def get_payments(loans): - return frappe._dict(frappe.get_all("Loan Repayment", fields=["against_loan", "sum(amount_paid)"], - filters={"against_loan": ("in", loans)}, group_by="against_loan", as_list=1)) + return frappe._dict( + frappe.get_all( + "Loan Repayment", + fields=["against_loan", "sum(amount_paid)"], + filters={"against_loan": ("in", loans)}, + group_by="against_loan", + as_list=1, + ) + ) + def get_interest_accruals(loans): accrual_map = {} - interest_accruals = frappe.get_all("Loan Interest Accrual", - fields=["loan", "interest_amount", "posting_date", "penalty_amount", - "paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc") + interest_accruals = frappe.get_all( + "Loan Interest Accrual", + fields=[ + "loan", + "interest_amount", + "posting_date", + "penalty_amount", + "paid_interest_amount", + "accrual_type", + ], + filters={"loan": ("in", loans)}, + order_by="posting_date desc", + ) for entry in interest_accruals: - accrual_map.setdefault(entry.loan, { - "accrued_interest": 0.0, - "undue_interest": 0.0, - "interest_outstanding": 0.0, - "last_accrual_date": '', - "due_date": '' - }) + accrual_map.setdefault( + entry.loan, + { + "accrued_interest": 0.0, + "undue_interest": 0.0, + "interest_outstanding": 0.0, + "last_accrual_date": "", + "due_date": "", + }, + ) - if entry.accrual_type == 'Regular': - if not accrual_map[entry.loan]['due_date']: - accrual_map[entry.loan]['due_date'] = add_days(entry.posting_date, 1) - if not accrual_map[entry.loan]['last_accrual_date']: - accrual_map[entry.loan]['last_accrual_date'] = entry.posting_date + if entry.accrual_type == "Regular": + if not accrual_map[entry.loan]["due_date"]: + accrual_map[entry.loan]["due_date"] = add_days(entry.posting_date, 1) + if not accrual_map[entry.loan]["last_accrual_date"]: + accrual_map[entry.loan]["last_accrual_date"] = entry.posting_date - due_date = accrual_map[entry.loan]['due_date'] - last_accrual_date = accrual_map[entry.loan]['last_accrual_date'] + due_date = accrual_map[entry.loan]["due_date"] + last_accrual_date = accrual_map[entry.loan]["last_accrual_date"] if due_date and getdate(entry.posting_date) < getdate(due_date): - accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount + accrual_map[entry.loan]["interest_outstanding"] += ( + entry.interest_amount - entry.paid_interest_amount + ) else: - accrual_map[entry.loan]['undue_interest'] += entry.interest_amount - entry.paid_interest_amount + accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount @@ -134,8 +276,12 @@ def get_interest_accruals(loans): return accrual_map + def get_penal_interest_rate_map(): - return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1)) + return frappe._dict( + frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1) + ) + def get_loan_wise_pledges(filters): loan_wise_unpledges = {} @@ -143,37 +289,51 @@ def get_loan_wise_pledges(filters): conditions = "" - if filters.get('company'): + if filters.get("company"): conditions = "AND company = %(company)s" - unpledges = frappe.db.sql(""" + unpledges = frappe.db.sql( + """ SELECT up.loan, u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY up.loan, u.loan_security - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) for unpledge in unpledges: loan_wise_unpledges.setdefault((unpledge.loan, unpledge.loan_security), unpledge.qty) - pledges = frappe.db.sql(""" + pledges = frappe.db.sql( + """ SELECT lp.loan, p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY lp.loan, p.loan_security - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) for security in pledges: current_pledges.setdefault((security.loan, security.loan_security), security.qty) - current_pledges[(security.loan, security.loan_security)] -= \ - loan_wise_unpledges.get((security.loan, security.loan_security), 0.0) + current_pledges[(security.loan, security.loan_security)] -= loan_wise_unpledges.get( + (security.loan, security.loan_security), 0.0 + ) return current_pledges + def get_loan_wise_security_value(filters, current_pledges): loan_security_details = get_loan_security_details() loan_wise_security_value = {} @@ -181,7 +341,8 @@ def get_loan_wise_security_value(filters, current_pledges): for key in current_pledges: qty = current_pledges.get(key) loan_wise_security_value.setdefault(key[0], 0.0) - loan_wise_security_value[key[0]] += \ - flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) + loan_wise_security_value[key[0]] += flt( + qty * loan_security_details.get(key[1], {}).get("latest_price", 0) + ) return loan_wise_security_value diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py index 68fd3d8e8b5..253b994ae04 100644 --- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py @@ -11,101 +11,96 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(): return [ - { - "label": _("Posting Date"), - "fieldtype": "Date", - "fieldname": "posting_date", - "width": 100 - }, - { - "label": _("Loan Repayment"), - "fieldtype": "Link", - "fieldname": "loan_repayment", - "options": "Loan Repayment", - "width": 100 - }, - { - "label": _("Against Loan"), - "fieldtype": "Link", - "fieldname": "against_loan", - "options": "Loan", - "width": 200 - }, - { - "label": _("Applicant"), - "fieldtype": "Data", - "fieldname": "applicant", - "width": 150 - }, - { - "label": _("Payment Type"), - "fieldtype": "Data", - "fieldname": "payment_type", - "width": 150 - }, - { - "label": _("Principal Amount"), - "fieldtype": "Currency", - "fieldname": "principal_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Interest Amount"), - "fieldtype": "Currency", - "fieldname": "interest", - "options": "currency", - "width": 100 - }, - { - "label": _("Penalty Amount"), - "fieldtype": "Currency", - "fieldname": "penalty", - "options": "currency", - "width": 100 - }, - { - "label": _("Payable Amount"), - "fieldtype": "Currency", - "fieldname": "payable_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Paid Amount"), - "fieldtype": "Currency", - "fieldname": "paid_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Currency"), - "fieldtype": "Link", - "fieldname": "currency", - "options": "Currency", - "width": 100 - } - ] + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 100}, + { + "label": _("Loan Repayment"), + "fieldtype": "Link", + "fieldname": "loan_repayment", + "options": "Loan Repayment", + "width": 100, + }, + { + "label": _("Against Loan"), + "fieldtype": "Link", + "fieldname": "against_loan", + "options": "Loan", + "width": 200, + }, + {"label": _("Applicant"), "fieldtype": "Data", "fieldname": "applicant", "width": 150}, + {"label": _("Payment Type"), "fieldtype": "Data", "fieldname": "payment_type", "width": 150}, + { + "label": _("Principal Amount"), + "fieldtype": "Currency", + "fieldname": "principal_amount", + "options": "currency", + "width": 100, + }, + { + "label": _("Interest Amount"), + "fieldtype": "Currency", + "fieldname": "interest", + "options": "currency", + "width": 100, + }, + { + "label": _("Penalty Amount"), + "fieldtype": "Currency", + "fieldname": "penalty", + "options": "currency", + "width": 100, + }, + { + "label": _("Payable Amount"), + "fieldtype": "Currency", + "fieldname": "payable_amount", + "options": "currency", + "width": 100, + }, + { + "label": _("Paid Amount"), + "fieldtype": "Currency", + "fieldname": "paid_amount", + "options": "currency", + "width": 100, + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "width": 100, + }, + ] + def get_data(filters): data = [] query_filters = { "docstatus": 1, - "company": filters.get('company'), + "company": filters.get("company"), } - if filters.get('applicant'): - query_filters.update({ - "applicant": filters.get('applicant') - }) + if filters.get("applicant"): + query_filters.update({"applicant": filters.get("applicant")}) - loan_repayments = frappe.get_all("Loan Repayment", - filters = query_filters, - fields=["posting_date", "applicant", "name", "against_loan", "payable_amount", - "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"] + loan_repayments = frappe.get_all( + "Loan Repayment", + filters=query_filters, + fields=[ + "posting_date", + "applicant", + "name", + "against_loan", + "payable_amount", + "pending_principal_amount", + "interest_payable", + "penalty_amount", + "amount_paid", + ], ) default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") @@ -122,7 +117,7 @@ def get_data(filters): "penalty": repayment.penalty_amount, "payable_amount": repayment.payable_amount, "paid_amount": repayment.amount_paid, - "currency": default_currency + "currency": default_currency, } data.append(row) diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index e3d99952902..4a459413876 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -18,46 +18,110 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): columns = [ - {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160}, - {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100}, - {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150}, + { + "label": _("Loan Security"), + "fieldname": "loan_security", + "fieldtype": "Link", + "options": "Loan Security", + "width": 160, + }, + { + "label": _("Loan Security Code"), + "fieldname": "loan_security_code", + "fieldtype": "Data", + "width": 100, + }, + { + "label": _("Loan Security Name"), + "fieldname": "loan_security_name", + "fieldtype": "Data", + "width": 150, + }, {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100}, - {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, + { + "label": _("Loan Security Type"), + "fieldname": "loan_security_type", + "fieldtype": "Link", + "options": "Loan Security Type", + "width": 120, + }, {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, - {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, - {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100}, - {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, - {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, - {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100}, - {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + { + "label": _("Latest Price"), + "fieldname": "latest_price", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("Price Valid Upto"), + "fieldname": "price_valid_upto", + "fieldtype": "Datetime", + "width": 100, + }, + { + "label": _("Current Value"), + "fieldname": "current_value", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("% Of Total Portfolio"), + "fieldname": "portfolio_percent", + "fieldtype": "Percentage", + "width": 100, + }, + { + "label": _("Pledged Applicant Count"), + "fieldname": "pledged_applicant_count", + "fieldtype": "Percentage", + "width": 100, + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "options": "Currency", + "hidden": 1, + "width": 100, + }, ] return columns + def get_data(filters): data = [] loan_security_details = get_loan_security_details() - current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details) - currency = erpnext.get_company_currency(filters.get('company')) + current_pledges, total_portfolio_value = get_company_wise_loan_security_details( + filters, loan_security_details + ) + currency = erpnext.get_company_currency(filters.get("company")) for security, value in iteritems(current_pledges): - if value.get('qty'): + if value.get("qty"): row = {} - current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0)) - valid_upto = loan_security_details.get(security, {}).get('valid_upto') + current_value = flt( + value.get("qty", 0) * loan_security_details.get(security, {}).get("latest_price", 0) + ) + valid_upto = loan_security_details.get(security, {}).get("valid_upto") row.update(loan_security_details.get(security)) - row.update({ - 'total_qty': value.get('qty'), - 'current_value': current_value, - 'price_valid_upto': valid_upto, - 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), - 'pledged_applicant_count': value.get('applicant_count'), - 'currency': currency - }) + row.update( + { + "total_qty": value.get("qty"), + "current_value": current_value, + "price_valid_upto": valid_upto, + "portfolio_percent": flt(current_value * 100 / total_portfolio_value, 2), + "pledged_applicant_count": value.get("applicant_count"), + "currency": currency, + } + ) data.append(row) @@ -65,21 +129,19 @@ def get_data(filters): def get_company_wise_loan_security_details(filters, loan_security_details): - pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, - loan_security_details) + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty( + filters, loan_security_details + ) total_portfolio_value = 0 security_wise_map = {} for key, qty in iteritems(pledge_values): - security_wise_map.setdefault(key[1], { - 'qty': 0.0, - 'applicant_count': 0.0 - }) + security_wise_map.setdefault(key[1], {"qty": 0.0, "applicant_count": 0.0}) - security_wise_map[key[1]]['qty'] += qty + security_wise_map[key[1]]["qty"] += qty if qty: - security_wise_map[key[1]]['applicant_count'] += 1 + security_wise_map[key[1]]["applicant_count"] += 1 - total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) + total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get("latest_price", 0)) return security_wise_map, total_portfolio_value diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.py b/erpnext/loan_management/report/loan_security_status/loan_security_status.py index b7e716880e9..9a5a18001ea 100644 --- a/erpnext/loan_management/report/loan_security_status/loan_security_status.py +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.py @@ -11,66 +11,41 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): - columns= [ + columns = [ { "label": _("Loan Security Pledge"), "fieldtype": "Link", "fieldname": "loan_security_pledge", "options": "Loan Security Pledge", - "width": 200 - }, - { - "label": _("Loan"), - "fieldtype": "Link", - "fieldname": "loan", - "options": "Loan", - "width": 200 - }, - { - "label": _("Applicant"), - "fieldtype": "Data", - "fieldname": "applicant", - "width": 200 - }, - { - "label": _("Status"), - "fieldtype": "Data", - "fieldname": "status", - "width": 100 - }, - { - "label": _("Pledge Time"), - "fieldtype": "Data", - "fieldname": "pledge_time", - "width": 150 + "width": 200, }, + {"label": _("Loan"), "fieldtype": "Link", "fieldname": "loan", "options": "Loan", "width": 200}, + {"label": _("Applicant"), "fieldtype": "Data", "fieldname": "applicant", "width": 200}, + {"label": _("Status"), "fieldtype": "Data", "fieldname": "status", "width": 100}, + {"label": _("Pledge Time"), "fieldtype": "Data", "fieldname": "pledge_time", "width": 150}, { "label": _("Loan Security"), "fieldtype": "Link", "fieldname": "loan_security", "options": "Loan Security", - "width": 150 - }, - { - "label": _("Quantity"), - "fieldtype": "Float", - "fieldname": "qty", - "width": 100 + "width": 150, }, + {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "qty", "width": 100}, { "label": _("Loan Security Price"), "fieldtype": "Currency", "fieldname": "loan_security_price", "options": "currency", - "width": 200 + "width": 200, }, { "label": _("Loan Security Value"), "fieldtype": "Currency", "fieldname": "loan_security_value", "options": "currency", - "width": 200 + "width": 200, }, { "label": _("Currency"), @@ -78,18 +53,20 @@ def get_columns(filters): "fieldname": "currency", "options": "Currency", "width": 50, - "hidden": 1 - } + "hidden": 1, + }, ] return columns + def get_data(filters): data = [] conditions = get_conditions(filters) - loan_security_pledges = frappe.db.sql(""" + loan_security_pledges = frappe.db.sql( + """ SELECT p.name, p.applicant, p.loan, p.status, p.pledge_time, c.loan_security, c.qty, c.loan_security_price, c.amount @@ -100,7 +77,12 @@ def get_data(filters): AND c.parent = p.name AND p.company = %(company)s {conditions} - """.format(conditions = conditions), (filters), as_dict=1) #nosec + """.format( + conditions=conditions + ), + (filters), + as_dict=1, + ) # nosec default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") @@ -121,6 +103,7 @@ def get_data(filters): return data + def get_conditions(filters): conditions = [] diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 07d928c221f..256f66071f3 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -16,17 +16,17 @@ class MaintenanceSchedule(TransactionBase): def generate_schedule(self): if self.docstatus != 0: return - self.set('schedules', []) + self.set("schedules", []) count = 1 - for d in self.get('items'): + for d in self.get("items"): self.validate_maintenance_detail() s_list = [] s_list = self.create_schedule_list(d.start_date, d.end_date, d.no_of_visits, d.sales_person) for i in range(d.no_of_visits): - child = self.append('schedules') + child = self.append("schedules") child.item_code = d.item_code child.item_name = d.item_name - child.scheduled_date = s_list[i].strftime('%Y-%m-%d') + child.scheduled_date = s_list[i].strftime("%Y-%m-%d") if d.serial_no: child.serial_no = d.serial_no child.idx = count @@ -37,18 +37,14 @@ class MaintenanceSchedule(TransactionBase): @frappe.whitelist() def validate_end_date_visits(self): - days_in_period = { - "Weekly": 7, - "Monthly": 30, - "Quarterly": 91, - "Half Yearly": 182, - "Yearly": 365 - } + days_in_period = {"Weekly": 7, "Monthly": 30, "Quarterly": 91, "Half Yearly": 182, "Yearly": 365} for item in self.items: if item.periodicity and item.periodicity != "Random" 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]) + 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]) @@ -61,20 +57,23 @@ class MaintenanceSchedule(TransactionBase): 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]) + 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]) - + item.end_date = add_days( + item.start_date, item.no_of_visits * days_in_period[item.periodicity] + ) def on_submit(self): - if not self.get('schedules'): + if not self.get("schedules"): throw(_("Please click on 'Generate Schedule' to get schedule")) self.check_serial_no_added() self.validate_schedule() email_map = {} - for d in self.get('items'): + for d in self.get("items"): if d.serial_no: serial_nos = get_valid_serial_nos(d.serial_no) self.validate_serial_no(d.item_code, serial_nos, d.start_date) @@ -90,29 +89,37 @@ class MaintenanceSchedule(TransactionBase): if no_email_sp: frappe.msgprint( - _("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) - ) + _( + "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 + scheduled_date = frappe.db.sql( + """select scheduled_date from `tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and - parent=%s""", (d.sales_person, d.item_code, self.name), as_dict=1) + parent=%s""", + (d.sales_person, d.item_code, self.name), + as_dict=1, + ) for key in scheduled_date: - description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) - event = frappe.get_doc({ - "doctype": "Event", - "owner": email_map.get(d.sales_person, self.owner), - "subject": description, - "description": description, - "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", - "event_type": "Private", - }) + description = frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format( + self.name, d.item_code, self.customer + ) + event = frappe.get_doc( + { + "doctype": "Event", + "owner": email_map.get(d.sales_person, self.owner), + "subject": description, + "description": description, + "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", + "event_type": "Private", + } + ) event.add_participant(self.doctype, self.name) event.insert(ignore_permissions=1) - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person): schedule_list = [] @@ -121,11 +128,12 @@ class MaintenanceSchedule(TransactionBase): add_by = date_diff / no_of_visit for visit in range(cint(no_of_visit)): - if (getdate(start_date_copy) < getdate(end_date)): + if getdate(start_date_copy) < getdate(end_date): start_date_copy = add_days(start_date_copy, add_by) if len(schedule_list) < no_of_visit: - schedule_date = self.validate_schedule_date_for_holiday_list(getdate(start_date_copy), - sales_person) + schedule_date = self.validate_schedule_date_for_holiday_list( + getdate(start_date_copy), sales_person + ) if schedule_date > getdate(end_date): schedule_date = getdate(end_date) schedule_list.append(schedule_date) @@ -139,9 +147,11 @@ 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) + holidays = frappe.db.sql_list( + """select holiday_date from `tabHoliday` where parent=%s""", holiday_list + ) if not validated and holidays: @@ -157,25 +167,28 @@ class MaintenanceSchedule(TransactionBase): def validate_dates_with_periodicity(self): for d in self.get("items"): - if d.start_date and d.end_date and d.periodicity and d.periodicity!="Random": + if d.start_date and d.end_date and d.periodicity and d.periodicity != "Random": date_diff = (getdate(d.end_date) - getdate(d.start_date)).days + 1 days_in_period = { "Weekly": 7, "Monthly": 30, "Quarterly": 90, "Half Yearly": 180, - "Yearly": 365 + "Yearly": 365, } 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}") - .format(d.idx, d.periodicity, 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}" + ).format(d.idx, d.periodicity, days_in_period[d.periodicity]) + ) def validate_maintenance_detail(self): - if not self.get('items'): + if not self.get("items"): throw(_("Please enter Maintaince Details first")) - for d in self.get('items'): + for d in self.get("items"): if not d.item_code: throw(_("Please select item code")) elif not d.start_date or not d.end_date: @@ -189,11 +202,14 @@ class MaintenanceSchedule(TransactionBase): throw(_("Start date should be less than end date for Item {0}").format(d.item_code)) def validate_sales_order(self): - for d in self.get('items'): + for d in self.get("items"): if d.sales_order: - chk = frappe.db.sql("""select ms.name from `tabMaintenance Schedule` ms, + chk = frappe.db.sql( + """select ms.name from `tabMaintenance Schedule` ms, `tabMaintenance Schedule Item` msi where msi.parent=ms.name and - msi.sales_order=%s and ms.docstatus=1""", d.sales_order) + msi.sales_order=%s and ms.docstatus=1""", + d.sales_order, + ) if chk: throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order)) @@ -209,7 +225,7 @@ class MaintenanceSchedule(TransactionBase): self.generate_schedule() def on_update(self): - frappe.db.set(self, 'status', 'Draft') + frappe.db.set(self, "status", "Draft") def update_amc_date(self, serial_nos, amc_expiry_date=None): for serial_no in serial_nos: @@ -219,65 +235,96 @@ class MaintenanceSchedule(TransactionBase): def validate_serial_no(self, item_code, serial_nos, amc_start_date): for serial_no in serial_nos: - sr_details = frappe.db.get_value("Serial No", serial_no, - ["warranty_expiry_date", "amc_expiry_date", "warehouse", "delivery_date", "item_code"], as_dict=1) + sr_details = frappe.db.get_value( + "Serial No", + serial_no, + ["warranty_expiry_date", "amc_expiry_date", "warehouse", "delivery_date", "item_code"], + as_dict=1, + ) if not sr_details: frappe.throw(_("Serial No {0} not found").format(serial_no)) if sr_details.get("item_code") != item_code: - frappe.throw(_("Serial No {0} does not belong to Item {1}") - .format(frappe.bold(serial_no), frappe.bold(item_code)), title="Invalid") + frappe.throw( + _("Serial No {0} does not belong to Item {1}").format( + frappe.bold(serial_no), frappe.bold(item_code) + ), + title="Invalid", + ) - if sr_details.warranty_expiry_date \ - and getdate(sr_details.warranty_expiry_date) >= getdate(amc_start_date): - throw(_("Serial No {0} is under warranty upto {1}") - .format(serial_no, sr_details.warranty_expiry_date)) + if sr_details.warranty_expiry_date and getdate(sr_details.warranty_expiry_date) >= getdate( + amc_start_date + ): + throw( + _("Serial No {0} is under warranty upto {1}").format( + serial_no, sr_details.warranty_expiry_date + ) + ) - if sr_details.amc_expiry_date and getdate(sr_details.amc_expiry_date) >= getdate(amc_start_date): - throw(_("Serial No {0} is under maintenance contract upto {1}") - .format(serial_no, sr_details.amc_expiry_date)) + if sr_details.amc_expiry_date and getdate(sr_details.amc_expiry_date) >= getdate( + amc_start_date + ): + throw( + _("Serial No {0} is under maintenance contract upto {1}").format( + serial_no, sr_details.amc_expiry_date + ) + ) - 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)) + 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 + ) + ) def validate_schedule(self): - item_lst1 =[] - item_lst2 =[] - for d in self.get('items'): + item_lst1 = [] + item_lst2 = [] + for d in self.get("items"): if d.item_code not in item_lst1: item_lst1.append(d.item_code) - for m in self.get('schedules'): + for m in self.get("schedules"): if m.item_code not in item_lst2: item_lst2.append(m.item_code) if len(item_lst1) != len(item_lst2): - throw(_("Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule'")) + throw( + _( + "Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule'" + ) + ) else: for x in item_lst1: if x not in item_lst2: throw(_("Please click on 'Generate Schedule'")) def check_serial_no_added(self): - serial_present =[] - for d in self.get('items'): + serial_present = [] + for d in self.get("items"): if d.serial_no: serial_present.append(d.item_code) - for m in self.get('schedules'): + for m in self.get("schedules"): if serial_present: if m.item_code in serial_present and not m.serial_no: - throw(_("Please click on 'Generate Schedule' to fetch Serial No added for Item {0}").format(m.item_code)) + throw( + _("Please click on 'Generate Schedule' to fetch Serial No added for Item {0}").format( + m.item_code + ) + ) def on_cancel(self): - for d in self.get('items'): + for d in self.get("items"): if d.serial_no: serial_nos = get_valid_serial_nos(d.serial_no) self.update_amc_date(serial_nos) - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") delete_events(self.doctype, self.name) def on_trash(self): @@ -301,23 +348,26 @@ class MaintenanceSchedule(TransactionBase): 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"): + if schedule.item_name == item_name and s_date == formatdate( + schedule.scheduled_date, "dd-mm-yyyy" + ): return schedule.name + @frappe.whitelist() def get_serial_nos_from_schedule(item_code, schedule=None): serial_nos = [] if schedule: - serial_nos = frappe.db.get_value('Maintenance Schedule Item', { - 'parent': schedule, - 'item_code': item_code - }, 'serial_no') + serial_nos = frappe.db.get_value( + "Maintenance Schedule Item", {"parent": schedule, "item_code": item_code}, "serial_no" + ) if serial_nos: serial_nos = get_serial_nos(serial_nos) return serial_nos + @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 @@ -331,27 +381,26 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No if len(serial_nos) == 1: target.serial_no = serial_nos[0] else: - target.serial_no = '' + target.serial_no = "" - doclist = get_mapped_doc("Maintenance Schedule", source_name, { - "Maintenance Schedule": { - "doctype": "Maintenance Visit", - "field_map": { - "name": "maintenance_schedule" + doclist = get_mapped_doc( + "Maintenance Schedule", + source_name, + { + "Maintenance Schedule": { + "doctype": "Maintenance Visit", + "field_map": {"name": "maintenance_schedule"}, + "validation": {"docstatus": ["=", 1]}, + "postprocess": update_status_and_detail, }, - "validation": { - "docstatus": ["=", 1] + "Maintenance Schedule Item": { + "doctype": "Maintenance Visit Purpose", + "condition": lambda doc: doc.item_name == item_name, + "field_map": {"sales_person": "service_person"}, + "postprocess": update_serial, }, - "postprocess": update_status_and_detail }, - "Maintenance Schedule Item": { - "doctype": "Maintenance Visit Purpose", - "condition": lambda doc: doc.item_name == item_name, - "field_map": { - "sales_person": "service_person" - }, - "postprocess": update_serial - } - }, target_doc) + target_doc, + ) return doclist diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index 6e727e53efd..a98cd10e320 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_i # test_records = frappe.get_test_records('Maintenance Schedule') + class TestMaintenanceSchedule(unittest.TestCase): def test_events_should_be_created_and_deleted(self): ms = make_maintenance_schedule() @@ -42,15 +43,15 @@ class TestMaintenanceSchedule(unittest.TestCase): 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 = ms.get_pending_data(data_type="items") + items = items.split("\n") items.pop(0) - expected_items = ['_Test Item'] + 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 = 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")) @@ -59,33 +60,38 @@ class TestMaintenanceSchedule(unittest.TestCase): 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]) + s_id = ms.get_pending_data(data_type="id", item_name=i.item_name, s_date=expected_dates[1]) # Check if item is mapped in visit. - test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id) + test_map_visit = make_maintenance_visit(source_name=ms.name, item_name="_Test Item", s_id=s_id) self.assertEqual(len(test_map_visit.purposes), 1) self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item") - visit = frappe.new_doc('Maintenance Visit') + visit = frappe.new_doc("Maintenance Visit") visit = test_map_visit 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.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) + ms = frappe.get_doc("Maintenance Schedule", ms.name) - #checks if visit status is back updated in schedule + # checks if visit status is back updated in schedule self.assertTrue(ms.schedules[1].completion_status, "Partially Completed") self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date)) - #checks if visit status is updated on cancel + # checks if visit status is updated on cancel visit.cancel() ms.reload() self.assertTrue(ms.schedules[1].completion_status, "Pending") @@ -117,22 +123,24 @@ class TestMaintenanceSchedule(unittest.TestCase): frappe.db.rollback() + def make_serial_item_with_serial(item_code): serial_item_doc = create_item(item_code, is_stock_item=1) if not serial_item_doc.has_serial_no or not serial_item_doc.serial_no_series: serial_item_doc.has_serial_no = 1 serial_item_doc.serial_no_series = "TEST.###" serial_item_doc.save(ignore_permissions=True) - active_serials = frappe.db.get_all('Serial No', {"status": "Active", "item_code": item_code}) + active_serials = frappe.db.get_all("Serial No", {"status": "Active", "item_code": item_code}) if len(active_serials) < 2: make_serialized_item(item_code=item_code) + def get_events(ms): - return frappe.get_all("Event Participants", filters={ - "reference_doctype": ms.doctype, - "reference_docname": ms.name, - "parenttype": "Event" - }) + return frappe.get_all( + "Event Participants", + filters={"reference_doctype": ms.doctype, "reference_docname": ms.name, "parenttype": "Event"}, + ) + def make_maintenance_schedule(**args): ms = frappe.new_doc("Maintenance Schedule") @@ -140,14 +148,17 @@ def make_maintenance_schedule(**args): ms.customer = "_Test Customer" ms.transaction_date = today() - ms.append("items", { - "item_code": args.get("item_code") or "_Test Item", - "start_date": today(), - "periodicity": "Weekly", - "no_of_visits": 4, - "serial_no": args.get("serial_no"), - "sales_person": "Sales Team", - }) + ms.append( + "items", + { + "item_code": args.get("item_code") or "_Test Item", + "start_date": today(), + "periodicity": "Weekly", + "no_of_visits": 4, + "serial_no": args.get("serial_no"), + "sales_person": "Sales Team", + }, + ) ms.insert(ignore_permissions=True) return ms diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index f4a0d4d399c..939df5c07c5 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -12,6 +12,9 @@ frappe.ui.form.on('Maintenance Visit', { // filters for serial no based on item code if (frm.doc.maintenance_type === "Scheduled") { let item_code = frm.doc.purposes[0].item_code; + if (!item_code) { + return; + } frappe.call({ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule", args: { diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py index 6fe2466be22..29a17849fd9 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py @@ -14,7 +14,7 @@ class MaintenanceVisit(TransactionBase): return _("To {0}").format(self.customer_name) def validate_serial_no(self): - for d in self.get('purposes'): + for d in self.get("purposes"): 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)) @@ -24,13 +24,19 @@ class MaintenanceVisit(TransactionBase): 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') + 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(format_date(start_date), format_date(end_date))) - + 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(format_date(start_date), format_date(end_date)) + ) def validate(self): self.validate_serial_no() @@ -44,73 +50,87 @@ class MaintenanceVisit(TransactionBase): status = self.completion_status actual_date = self.mntc_date if self.maintenance_schedule_detail: - frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status) - frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date) + frappe.db.set_value( + "Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status + ) + frappe.db.set_value( + "Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date + ) def update_customer_issue(self, flag): if not self.maintenance_schedule: - for d in self.get('purposes'): - if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' : - if flag==1: + 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' + 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)) + 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 '' + 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' + status = "Open" mntc_date = None service_person = None work_done = None - 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 = 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""" check_for_docname = None - for d in self.get('purposes'): + for d in self.get("purposes"): if d.prevdoc_docname: check_for_docname = d.prevdoc_docname - #check_for_doctype = d.prevdoc_doctype + # check_for_doctype = d.prevdoc_doctype if check_for_docname: - check = frappe.db.sql("select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.name!=%s and t2.prevdoc_docname=%s and t1.docstatus = 1 and (t1.mntc_date > %s or (t1.mntc_date = %s and t1.mntc_time > %s))", (self.name, check_for_docname, self.mntc_date, self.mntc_date, self.mntc_time)) + check = frappe.db.sql( + "select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.name!=%s and t2.prevdoc_docname=%s and t1.docstatus = 1 and (t1.mntc_date > %s or (t1.mntc_date = %s and t1.mntc_time > %s))", + (self.name, check_for_docname, self.mntc_date, self.mntc_date, self.mntc_time), + ) if check: check_lst = [x[0] for x in check] - check_lst =','.join(check_lst) - frappe.throw(_("Cancel Material Visits {0} before cancelling this Maintenance Visit").format(check_lst)) + check_lst = ",".join(check_lst) + frappe.throw( + _("Cancel Material Visits {0} before cancelling this Maintenance Visit").format(check_lst) + ) raise Exception else: self.update_customer_issue(0) def on_submit(self): self.update_customer_issue(1) - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") self.update_status_and_actual_date() def on_cancel(self): self.check_if_last_visit() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") self.update_status_and_actual_date(cancel=True) def on_update(self): diff --git a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py index a0c1a338e49..bdbed7c0965 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Maintenance Visit') + class TestMaintenanceVisit(unittest.TestCase): pass diff --git a/erpnext/manufacturing/dashboard_fixtures.py b/erpnext/manufacturing/dashboard_fixtures.py index 1bc12ff35eb..9e64f4dc196 100644 --- a/erpnext/manufacturing/dashboard_fixtures.py +++ b/erpnext/manufacturing/dashboard_fixtures.py @@ -11,33 +11,39 @@ import erpnext def get_data(): - return frappe._dict({ - "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards(), - }) + return frappe._dict( + { + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards(), + } + ) + def get_dashboards(): - return [{ - "name": "Manufacturing", - "dashboard_name": "Manufacturing", - "charts": [ - { "chart": "Produced Quantity", "width": "Half" }, - { "chart": "Completed Operation", "width": "Half" }, - { "chart": "Work Order Analysis", "width": "Half" }, - { "chart": "Quality Inspection Analysis", "width": "Half" }, - { "chart": "Pending Work Order", "width": "Half" }, - { "chart": "Last Month Downtime Analysis", "width": "Half" }, - { "chart": "Work Order Qty Analysis", "width": "Full" }, - { "chart": "Job Card Analysis", "width": "Full" } - ], - "cards": [ - { "card": "Monthly Total Work Order" }, - { "card": "Monthly Completed Work Order" }, - { "card": "Ongoing Job Card" }, - { "card": "Monthly Quality Inspection"} - ] - }] + return [ + { + "name": "Manufacturing", + "dashboard_name": "Manufacturing", + "charts": [ + {"chart": "Produced Quantity", "width": "Half"}, + {"chart": "Completed Operation", "width": "Half"}, + {"chart": "Work Order Analysis", "width": "Half"}, + {"chart": "Quality Inspection Analysis", "width": "Half"}, + {"chart": "Pending Work Order", "width": "Half"}, + {"chart": "Last Month Downtime Analysis", "width": "Half"}, + {"chart": "Work Order Qty Analysis", "width": "Full"}, + {"chart": "Job Card Analysis", "width": "Full"}, + ], + "cards": [ + {"card": "Monthly Total Work Order"}, + {"card": "Monthly Completed Work Order"}, + {"card": "Ongoing Job Card"}, + {"card": "Monthly Quality Inspection"}, + ], + } + ] + def get_charts(): company = erpnext.get_default_company() @@ -45,200 +51,198 @@ def get_charts(): if not company: company = frappe.db.get_value("Company", {"is_group": 0}, "name") - return [{ - "doctype": "Dashboard Chart", - "based_on": "modified", - "chart_type": "Sum", - "chart_name": _("Produced Quantity"), - "name": "Produced Quantity", - "document_type": "Work Order", - "filters_json": json.dumps([['Work Order', 'docstatus', '=', 1, False]]), - "group_by_type": "Count", - "time_interval": "Monthly", - "timespan": "Last Year", - "owner": "Administrator", - "type": "Line", - "value_based_on": "produced_qty", - "is_public": 1, - "timeseries": 1 - }, { - "doctype": "Dashboard Chart", - "based_on": "creation", - "chart_type": "Sum", - "chart_name": _("Completed Operation"), - "name": "Completed Operation", - "document_type": "Work Order Operation", - "filters_json": json.dumps([['Work Order Operation', 'docstatus', '=', 1, False]]), - "group_by_type": "Count", - "time_interval": "Quarterly", - "timespan": "Last Year", - "owner": "Administrator", - "type": "Line", - "value_based_on": "completed_qty", - "is_public": 1, - "timeseries": 1 - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Work Order Analysis"), - "name": "Work Order Analysis", - "timespan": "Last Year", - "report_name": "Work Order Summary", - "owner": "Administrator", - "filters_json": json.dumps({"company": company, "charts_based_on": "Status"}), - "type": "Donut", - "is_public": 1, - "is_custom": 1, - "custom_options": json.dumps({ - "axisOptions": { - "shortenYAxisNumbers": 1 - }, - "height": 300 - }), - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Quality Inspection Analysis"), - "name": "Quality Inspection Analysis", - "timespan": "Last Year", - "report_name": "Quality Inspection Summary", - "owner": "Administrator", - "filters_json": json.dumps({}), - "type": "Donut", - "is_public": 1, - "is_custom": 1, - "custom_options": json.dumps({ - "axisOptions": { - "shortenYAxisNumbers": 1 - }, - "height": 300 - }), - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Pending Work Order"), - "name": "Pending Work Order", - "timespan": "Last Year", - "report_name": "Work Order Summary", - "filters_json": json.dumps({"company": company, "charts_based_on": "Age"}), - "owner": "Administrator", - "type": "Donut", - "is_public": 1, - "is_custom": 1, - "custom_options": json.dumps({ - "axisOptions": { - "shortenYAxisNumbers": 1 - }, - "height": 300 - }), - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Last Month Downtime Analysis"), - "name": "Last Month Downtime Analysis", - "timespan": "Last Year", - "filters_json": json.dumps({}), - "report_name": "Downtime Analysis", - "owner": "Administrator", - "is_public": 1, - "is_custom": 1, - "type": "Bar" - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Work Order Qty Analysis"), - "name": "Work Order Qty Analysis", - "timespan": "Last Year", - "report_name": "Work Order Summary", - "filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}), - "owner": "Administrator", - "type": "Bar", - "is_public": 1, - "is_custom": 1, - "custom_options": json.dumps({ - "barOptions": { "stacked": 1 } - }), - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Job Card Analysis"), - "name": "Job Card Analysis", - "timespan": "Last Year", - "report_name": "Job Card Summary", - "owner": "Administrator", - "is_public": 1, - "is_custom": 1, - "filters_json": json.dumps({"company": company, "docstatus": 1, "range":"Monthly"}), - "custom_options": json.dumps({ - "barOptions": { "stacked": 1 } - }), - "type": "Bar" - }] + return [ + { + "doctype": "Dashboard Chart", + "based_on": "modified", + "chart_type": "Sum", + "chart_name": _("Produced Quantity"), + "name": "Produced Quantity", + "document_type": "Work Order", + "filters_json": json.dumps([["Work Order", "docstatus", "=", 1, False]]), + "group_by_type": "Count", + "time_interval": "Monthly", + "timespan": "Last Year", + "owner": "Administrator", + "type": "Line", + "value_based_on": "produced_qty", + "is_public": 1, + "timeseries": 1, + }, + { + "doctype": "Dashboard Chart", + "based_on": "creation", + "chart_type": "Sum", + "chart_name": _("Completed Operation"), + "name": "Completed Operation", + "document_type": "Work Order Operation", + "filters_json": json.dumps([["Work Order Operation", "docstatus", "=", 1, False]]), + "group_by_type": "Count", + "time_interval": "Quarterly", + "timespan": "Last Year", + "owner": "Administrator", + "type": "Line", + "value_based_on": "completed_qty", + "is_public": 1, + "timeseries": 1, + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Work Order Analysis"), + "name": "Work Order Analysis", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "owner": "Administrator", + "filters_json": json.dumps({"company": company, "charts_based_on": "Status"}), + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}), + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Quality Inspection Analysis"), + "name": "Quality Inspection Analysis", + "timespan": "Last Year", + "report_name": "Quality Inspection Summary", + "owner": "Administrator", + "filters_json": json.dumps({}), + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}), + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Pending Work Order"), + "name": "Pending Work Order", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "filters_json": json.dumps({"company": company, "charts_based_on": "Age"}), + "owner": "Administrator", + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}), + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Last Month Downtime Analysis"), + "name": "Last Month Downtime Analysis", + "timespan": "Last Year", + "filters_json": json.dumps({}), + "report_name": "Downtime Analysis", + "owner": "Administrator", + "is_public": 1, + "is_custom": 1, + "type": "Bar", + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Work Order Qty Analysis"), + "name": "Work Order Qty Analysis", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}), + "owner": "Administrator", + "type": "Bar", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({"barOptions": {"stacked": 1}}), + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Job Card Analysis"), + "name": "Job Card Analysis", + "timespan": "Last Year", + "report_name": "Job Card Summary", + "owner": "Administrator", + "is_public": 1, + "is_custom": 1, + "filters_json": json.dumps({"company": company, "docstatus": 1, "range": "Monthly"}), + "custom_options": json.dumps({"barOptions": {"stacked": 1}}), + "type": "Bar", + }, + ] + def get_number_cards(): start_date = add_months(nowdate(), -1) end_date = nowdate() - return [{ - "doctype": "Number Card", - "document_type": "Work Order", - "name": "Monthly Total Work Order", - "filters_json": json.dumps([ - ['Work Order', 'docstatus', '=', 1], - ['Work Order', 'creation', 'between', [start_date, end_date]] - ]), - "function": "Count", - "is_public": 1, - "label": _("Monthly Total Work Orders"), - "show_percentage_stats": 1, - "stats_time_interval": "Weekly" - }, - { - "doctype": "Number Card", - "document_type": "Work Order", - "name": "Monthly Completed Work Order", - "filters_json": json.dumps([ - ['Work Order', 'status', '=', 'Completed'], - ['Work Order', 'docstatus', '=', 1], - ['Work Order', 'creation', 'between', [start_date, end_date]] - ]), - "function": "Count", - "is_public": 1, - "label": _("Monthly Completed Work Orders"), - "show_percentage_stats": 1, - "stats_time_interval": "Weekly" - }, - { - "doctype": "Number Card", - "document_type": "Job Card", - "name": "Ongoing Job Card", - "filters_json": json.dumps([ - ['Job Card', 'status','!=','Completed'], - ['Job Card', 'docstatus', '=', 1] - ]), - "function": "Count", - "is_public": 1, - "label": _("Ongoing Job Cards"), - "show_percentage_stats": 1, - "stats_time_interval": "Weekly" - }, - { - "doctype": "Number Card", - "document_type": "Quality Inspection", - "name": "Monthly Quality Inspection", - "filters_json": json.dumps([ - ['Quality Inspection', 'docstatus', '=', 1], - ['Quality Inspection', 'creation', 'between', [start_date, end_date]] - ]), - "function": "Count", - "is_public": 1, - "label": _("Monthly Quality Inspections"), - "show_percentage_stats": 1, - "stats_time_interval": "Weekly" - }] + return [ + { + "doctype": "Number Card", + "document_type": "Work Order", + "name": "Monthly Total Work Order", + "filters_json": json.dumps( + [ + ["Work Order", "docstatus", "=", 1], + ["Work Order", "creation", "between", [start_date, end_date]], + ] + ), + "function": "Count", + "is_public": 1, + "label": _("Monthly Total Work Orders"), + "show_percentage_stats": 1, + "stats_time_interval": "Weekly", + }, + { + "doctype": "Number Card", + "document_type": "Work Order", + "name": "Monthly Completed Work Order", + "filters_json": json.dumps( + [ + ["Work Order", "status", "=", "Completed"], + ["Work Order", "docstatus", "=", 1], + ["Work Order", "creation", "between", [start_date, end_date]], + ] + ), + "function": "Count", + "is_public": 1, + "label": _("Monthly Completed Work Orders"), + "show_percentage_stats": 1, + "stats_time_interval": "Weekly", + }, + { + "doctype": "Number Card", + "document_type": "Job Card", + "name": "Ongoing Job Card", + "filters_json": json.dumps( + [["Job Card", "status", "!=", "Completed"], ["Job Card", "docstatus", "=", 1]] + ), + "function": "Count", + "is_public": 1, + "label": _("Ongoing Job Cards"), + "show_percentage_stats": 1, + "stats_time_interval": "Weekly", + }, + { + "doctype": "Number Card", + "document_type": "Quality Inspection", + "name": "Monthly Quality Inspection", + "filters_json": json.dumps( + [ + ["Quality Inspection", "docstatus", "=", 1], + ["Quality Inspection", "creation", "between", [start_date, end_date]], + ] + ), + "function": "Count", + "is_public": 1, + "label": _("Monthly Quality Inspections"), + "show_percentage_stats": 1, + "stats_time_interval": "Weekly", + }, + ] diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 5340c51131f..ff2140199de 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -29,7 +29,9 @@ class BlanketOrder(Document): def update_ordered_qty(self): ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order" - item_ordered_qty = frappe._dict(frappe.db.sql(""" + item_ordered_qty = frappe._dict( + frappe.db.sql( + """ select trans_item.item_code, sum(trans_item.stock_qty) as qty from `tab{0} Item` trans_item, `tab{0}` trans where trans.name = trans_item.parent @@ -37,18 +39,24 @@ class BlanketOrder(Document): and trans.docstatus=1 and trans.status not in ('Closed', 'Stopped') group by trans_item.item_code - """.format(ref_doctype), self.name)) + """.format( + ref_doctype + ), + self.name, + ) + ) for d in self.items: d.db_set("ordered_qty", item_ordered_qty.get(d.item_code, 0)) + @frappe.whitelist() def make_order(source_name): doctype = frappe.flags.args.doctype def update_doc(source_doc, target_doc, source_parent): - if doctype == 'Quotation': - target_doc.quotation_to = 'Customer' + if doctype == "Quotation": + target_doc.quotation_to = "Customer" target_doc.party_name = source_doc.customer def update_item(source, target, source_parent): @@ -62,18 +70,16 @@ def make_order(source_name): target.against_blanket_order = 1 target.blanket_order = source_name - target_doc = get_mapped_doc("Blanket Order", source_name, { - "Blanket Order": { - "doctype": doctype, - "postprocess": update_doc - }, - "Blanket Order Item": { - "doctype": doctype + " Item", - "field_map": { - "rate": "blanket_order_rate", - "parent": "blanket_order" + target_doc = get_mapped_doc( + "Blanket Order", + source_name, + { + "Blanket Order": {"doctype": doctype, "postprocess": update_doc}, + "Blanket Order Item": { + "doctype": doctype + " Item", + "field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"}, + "postprocess": update_item, }, - "postprocess": update_item - } - }) + }, + ) return target_doc diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py index 2556f2f163d..31062342d03 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): return { - 'fieldname': 'blanket_order', - 'transactions': [ - { - 'items': ['Purchase Order', 'Sales Order', 'Quotation'] - } - ] + "fieldname": "blanket_order", + "transactions": [{"items": ["Purchase Order", "Sales Order", "Quotation"]}], } diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index eff2344e85c..2f1f3ae0f52 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -1,22 +1,22 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months, today from erpnext import get_company_currency -from erpnext.tests.utils import ERPNextTestCase from .blanket_order import make_order -class TestBlanketOrder(ERPNextTestCase): +class TestBlanketOrder(FrappeTestCase): def setUp(self): frappe.flags.args = frappe._dict() def test_sales_order_creation(self): bo = make_blanket_order(blanket_order_type="Selling") - frappe.flags.args.doctype = 'Sales Order' + frappe.flags.args.doctype = "Sales Order" so = make_order(bo.name) so.currency = get_company_currency(so.company) so.delivery_date = today() @@ -33,16 +33,15 @@ class TestBlanketOrder(ERPNextTestCase): self.assertEqual(so.items[0].qty, bo.items[0].ordered_qty) # test the quantity - frappe.flags.args.doctype = 'Sales Order' + frappe.flags.args.doctype = "Sales Order" so1 = make_order(bo.name) so1.currency = get_company_currency(so1.company) - self.assertEqual(so1.items[0].qty, (bo.items[0].qty-bo.items[0].ordered_qty)) - + self.assertEqual(so1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) def test_purchase_order_creation(self): bo = make_blanket_order(blanket_order_type="Purchasing") - frappe.flags.args.doctype = 'Purchase Order' + frappe.flags.args.doctype = "Purchase Order" po = make_order(bo.name) po.currency = get_company_currency(po.company) po.schedule_date = today() @@ -59,11 +58,10 @@ class TestBlanketOrder(ERPNextTestCase): self.assertEqual(po.items[0].qty, bo.items[0].ordered_qty) # test the quantity - frappe.flags.args.doctype = 'Purchase Order' + frappe.flags.args.doctype = "Purchase Order" po1 = make_order(bo.name) po1.currency = get_company_currency(po1.company) - self.assertEqual(po1.items[0].qty, (bo.items[0].qty-bo.items[0].ordered_qty)) - + self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) def make_blanket_order(**args): @@ -80,11 +78,14 @@ def make_blanket_order(**args): bo.from_date = today() bo.to_date = add_months(bo.from_date, months=12) - bo.append("items", { - "item_code": args.item_code or "_Test Item", - "qty": args.quantity or 1000, - "rate": args.rate or 100 - }) + bo.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "qty": args.quantity or 1000, + "rate": args.rate or 100, + }, + ) bo.insert() bo.submit() diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b97dcab632f..f8fcd073951 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import functools +import re from collections import deque from operator import itemgetter from typing import List @@ -18,9 +19,7 @@ from erpnext.setup.utils import get_exchange_rate from erpnext.stock.doctype.item.item import get_item_details from erpnext.stock.get_item_details import get_conversion_factor, get_price_list_rate -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class BOMTree: @@ -30,10 +29,12 @@ class BOMTree: # ref: https://docs.python.org/3/reference/datamodel.html#slots __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] - def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: + def __init__( + self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1 + ) -> None: self.name = name # name of node, BOM number if is_bom else item_code self.child_items: List["BOMTree"] = [] # list of child items - self.is_bom = is_bom # true if the node is a BOM and not a leaf item + self.is_bom = is_bom # true if the node is a BOM and not a leaf item self.item_code: str = None # item_code associated with node self.qty = qty # required unit quantity to make one unit of parent item. self.exploded_qty = exploded_qty # total exploded qty required for making root of tree. @@ -61,12 +62,12 @@ class BOMTree: """Get level order traversal of tree. E.g. for following tree the traversal will return list of nodes in order from top to bottom. BOM: - - SubAssy1 - - item1 - - item2 - - SubAssy2 - - item3 - - item4 + - SubAssy1 + - item1 + - item2 + - SubAssy2 + - item3 + - item4 returns = [SubAssy1, item1, item2, SubAssy2, item3, item4] """ @@ -95,52 +96,84 @@ class BOMTree: rep += child.__repr__(level=level + 1) return rep + class BOM(WebsiteGenerator): website = frappe._dict( # page_title_field = "item_name", - condition_field = "show_in_website", - template = "templates/generators/bom.html" + condition_field="show_in_website", + template="templates/generators/bom.html", ) def autoname(self): - names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item) + # ignore amended documents while calculating current index + existing_boms = frappe.get_all( + "BOM", filters={"item": self.item, "amended_from": ["is", "not set"]}, pluck="name" + ) - if names: - # name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1 - - # split by item - names = [name.split(self.item, 1) for name in names] - names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)] - - # split by (-) if cancelled - if names: - names = [cint(name.split('-')[-1]) for name in names] - idx = max(names) + 1 - else: - idx = 1 + if existing_boms: + index = self.get_next_version_index(existing_boms) else: - idx = 1 + index = 1 + + prefix = self.doctype + suffix = "%.3i" % index # convert index to string (1 -> "001") + bom_name = f"{prefix}-{self.item}-{suffix}" + + if len(bom_name) <= 140: + name = bom_name + else: + # since max characters for name is 140, remove enough characters from the + # item name to fit the prefix, suffix and the separators + truncated_length = 140 - (len(prefix) + len(suffix) + 2) + truncated_item_name = self.item[:truncated_length] + # if a partial word is found after truncate, remove the extra characters + truncated_item_name = truncated_item_name.rsplit(" ", 1)[0] + name = f"{prefix}-{truncated_item_name}-{suffix}" - name = 'BOM-' + self.item + ('-%.3i' % idx) if frappe.db.exists("BOM", name): conflicting_bom = frappe.get_doc("BOM", name) if conflicting_bom.item != self.item: - msg = (_("A BOM with name {0} already exists for item {1}.") - .format(frappe.bold(name), frappe.bold(conflicting_bom.item))) + msg = _("A BOM with name {0} already exists for item {1}.").format( + frappe.bold(name), frappe.bold(conflicting_bom.item) + ) - frappe.throw(_("{0}{1} Did you rename the item? Please contact Administrator / Tech support") - .format(msg, "
")) + frappe.throw( + _("{0}{1} Did you rename the item? Please contact Administrator / Tech support").format( + msg, "
" + ) + ) self.name = name + @staticmethod + def get_next_version_index(existing_boms: List[str]) -> int: + # split by "/" and "-" + delimiters = ["/", "-"] + pattern = "|".join(map(re.escape, delimiters)) + bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms] + + # filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001 + valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) + + # extract the current index from the BOM parts + if valid_bom_parts: + # handle cancelled and submitted documents + indexes = [cint(part[-1]) for part in valid_bom_parts] + index = max(indexes) + 1 + else: + index = 1 + + return index + def validate(self): - self.route = frappe.scrub(self.name).replace('_', '-') + self.route = frappe.scrub(self.name).replace("_", "-") if not self.company: frappe.throw(_("Please select a Company first."), title=_("Mandatory")) self.clear_operations() + self.clear_inspection() self.validate_main_item() self.validate_currency() self.set_conversion_rate() @@ -155,13 +188,13 @@ class BOM(WebsiteGenerator): self.calculate_cost() self.update_stock_qty() self.validate_scrap_items() - self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) + self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) def get_context(self, context): - context.parents = [{'name': 'boms', 'title': _('All BOMs') }] + context.parents = [{"name": "boms", "title": _("All BOMs")}] def on_update(self): - frappe.cache().hdel('bom_children', self.name) + frappe.cache().hdel("bom_children", self.name) self.check_recursion() def on_submit(self): @@ -191,31 +224,47 @@ class BOM(WebsiteGenerator): def get_routing(self): if self.routing: self.set("operations", []) - fields = ["sequence_id", "operation", "workstation", "description", - "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"] + fields = [ + "sequence_id", + "operation", + "workstation", + "description", + "time_in_mins", + "batch_size", + "operating_cost", + "idx", + "hour_rate", + "set_cost_based_on_bom_qty", + ] - for row in frappe.get_all("BOM Operation", fields = fields, - filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"): - child = self.append('operations', row) - child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2) + for row in frappe.get_all( + "BOM Operation", + fields=fields, + filters={"parenttype": "Routing", "parent": self.routing}, + order_by="sequence_id, idx", + ): + child = self.append("operations", row) + child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate")) def set_bom_material_details(self): for item in self.get("items"): self.validate_bom_currency(item) - ret = self.get_bom_material_detail({ - "company": self.company, - "item_code": item.item_code, - "item_name": item.item_name, - "bom_no": item.bom_no, - "stock_qty": item.stock_qty, - "include_item_in_manufacturing": item.include_item_in_manufacturing, - "qty": item.qty, - "uom": item.uom, - "stock_uom": item.stock_uom, - "conversion_factor": item.conversion_factor, - "sourced_by_supplier": item.sourced_by_supplier - }) + ret = self.get_bom_material_detail( + { + "company": self.company, + "item_code": item.item_code, + "item_name": item.item_name, + "bom_no": item.bom_no, + "stock_qty": item.stock_qty, + "include_item_in_manufacturing": item.include_item_in_manufacturing, + "qty": item.qty, + "uom": item.uom, + "stock_uom": item.stock_uom, + "conversion_factor": item.conversion_factor, + "sourced_by_supplier": item.sourced_by_supplier, + } + ) for r in ret: if not item.get(r): item.set(r, ret[r]) @@ -226,7 +275,7 @@ class BOM(WebsiteGenerator): "item_code": item.item_code, "company": self.company, "scrap_items": True, - "bom_no": '', + "bom_no": "", } ret = self.get_bom_material_detail(args) for key, value in ret.items(): @@ -235,72 +284,90 @@ class BOM(WebsiteGenerator): @frappe.whitelist() def get_bom_material_detail(self, args=None): - """ Get raw material details like uom, desc and rate""" + """Get raw material details like uom, desc and rate""" if not args: - args = frappe.form_dict.get('args') + args = frappe.form_dict.get("args") if isinstance(args, str): import json + args = json.loads(args) - item = self.get_item_det(args['item_code']) + item = self.get_item_det(args["item_code"]) - args['bom_no'] = args['bom_no'] or item and cstr(item['default_bom']) or '' - args['transfer_for_manufacture'] = (cstr(args.get('include_item_in_manufacturing', '')) or - item and item.include_item_in_manufacturing or 0) + args["bom_no"] = args["bom_no"] or item and cstr(item["default_bom"]) or "" + args["transfer_for_manufacture"] = ( + cstr(args.get("include_item_in_manufacturing", "")) + or item + and item.include_item_in_manufacturing + or 0 + ) args.update(item) rate = self.get_rm_rate(args) ret_item = { - 'item_name' : item and args['item_name'] or '', - 'description' : item and args['description'] or '', - 'image' : item and args['image'] or '', - 'stock_uom' : item and args['stock_uom'] or '', - 'uom' : item and args['stock_uom'] or '', - 'conversion_factor': 1, - 'bom_no' : args['bom_no'], - 'rate' : rate, - 'qty' : args.get("qty") or args.get("stock_qty") or 1, - 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, - 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), - 'include_item_in_manufacturing': cint(args.get('transfer_for_manufacture')), - 'sourced_by_supplier' : args.get('sourced_by_supplier', 0) + "item_name": item and args["item_name"] or "", + "description": item and args["description"] or "", + "image": item and args["image"] or "", + "stock_uom": item and args["stock_uom"] or "", + "uom": item and args["stock_uom"] or "", + "conversion_factor": 1, + "bom_no": args["bom_no"], + "rate": rate, + "qty": args.get("qty") or args.get("stock_qty") or 1, + "stock_qty": args.get("qty") or args.get("stock_qty") or 1, + "base_rate": flt(rate) * (flt(self.conversion_rate) or 1), + "include_item_in_manufacturing": cint(args.get("transfer_for_manufacture")), + "sourced_by_supplier": args.get("sourced_by_supplier", 0), } return ret_item def validate_bom_currency(self, item): - if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency: - frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}") - .format(item.idx, item.bom_no, self.currency)) + if ( + item.get("bom_no") + and frappe.db.get_value("BOM", item.get("bom_no"), "currency") != self.currency + ): + frappe.throw( + _("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}").format( + item.idx, item.bom_no, self.currency + ) + ) def get_rm_rate(self, arg): - """ Get raw material rate as per selected method, if bom exists takes bom cost """ + """Get raw material rate as per selected method, if bom exists takes bom cost""" rate = 0 if not self.rm_cost_as_per: self.rm_cost_as_per = "Valuation Rate" - if arg.get('scrap_items'): + if arg.get("scrap_items"): rate = get_valuation_rate(arg) elif arg: - #Customer Provided parts and Supplier sourced parts will have zero rate - if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item') and not arg.get('sourced_by_supplier'): - if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom: - rate = flt(self.get_bom_unitcost(arg['bom_no'])) * (arg.get("conversion_factor") or 1) + # Customer Provided parts and Supplier sourced parts will have zero rate + if not frappe.db.get_value( + "Item", arg["item_code"], "is_customer_provided_item" + ) and not arg.get("sourced_by_supplier"): + if arg.get("bom_no") and self.set_rate_of_sub_assembly_item_based_on_bom: + rate = flt(self.get_bom_unitcost(arg["bom_no"])) * (arg.get("conversion_factor") or 1) else: rate = get_bom_item_rate(arg, self) if not rate: if self.rm_cost_as_per == "Price List": - frappe.msgprint(_("Price not found for item {0} in price list {1}") - .format(arg["item_code"], self.buying_price_list), alert=True) + frappe.msgprint( + _("Price not found for item {0} in price list {1}").format( + arg["item_code"], self.buying_price_list + ), + alert=True, + ) else: - frappe.msgprint(_("{0} not found for item {1}") - .format(self.rm_cost_as_per, arg["item_code"]), alert=True) + frappe.msgprint( + _("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]), alert=True + ) return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) @frappe.whitelist() - def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True): + def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate=True, save=True): if self.docstatus == 2: return @@ -310,16 +377,18 @@ class BOM(WebsiteGenerator): if not d.item_code: continue - rate = self.get_rm_rate({ - "company": self.company, - "item_code": d.item_code, - "bom_no": d.bom_no, - "qty": d.qty, - "uom": d.uom, - "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier - }) + rate = self.get_rm_rate( + { + "company": self.company, + "item_code": d.item_code, + "bom_no": d.bom_no, + "qty": d.qty, + "uom": d.uom, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "sourced_by_supplier": d.sourced_by_supplier, + } + ) if rate: d.rate = rate @@ -340,8 +409,11 @@ class BOM(WebsiteGenerator): # update parent BOMs if self.total_cost != existing_bom_cost and update_parent: - parent_boms = frappe.db.sql_list("""select distinct parent from `tabBOM Item` - where bom_no = %s and docstatus=1 and parenttype='BOM'""", self.name) + parent_boms = frappe.db.sql_list( + """select distinct parent from `tabBOM Item` + where bom_no = %s and docstatus=1 and parenttype='BOM'""", + self.name, + ) for bom in parent_boms: frappe.get_doc("BOM", bom).update_cost(from_child_bom=True) @@ -353,41 +425,54 @@ class BOM(WebsiteGenerator): if self.total_cost: cost = self.total_cost / self.quantity - frappe.db.sql("""update `tabBOM Item` set rate=%s, amount=stock_qty*%s + frappe.db.sql( + """update `tabBOM Item` set rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""", - (cost, cost, self.name)) + (cost, cost, self.name), + ) def get_bom_unitcost(self, bom_no): - bom = frappe.db.sql("""select name, base_total_cost/quantity as unit_cost from `tabBOM` - where is_active = 1 and name = %s""", bom_no, as_dict=1) - return bom and bom[0]['unit_cost'] or 0 + bom = frappe.db.sql( + """select name, base_total_cost/quantity as unit_cost from `tabBOM` + where is_active = 1 and name = %s""", + bom_no, + as_dict=1, + ) + return bom and bom[0]["unit_cost"] or 0 def manage_default_bom(self): - """ Uncheck others if current one is selected as default or - check the current one as default if it the only bom for the selected item, - update default bom in item master + """Uncheck others if current one is selected as default or + check the current one as default if it the only bom for the selected item, + update default bom in item master """ if self.is_default and self.is_active: from frappe.model.utils import set_default + set_default(self, "item") item = frappe.get_doc("Item", self.item) if item.default_bom != self.name: - frappe.db.set_value('Item', self.item, 'default_bom', self.name) - elif not frappe.db.exists(dict(doctype='BOM', docstatus=1, item=self.item, is_default=1)) \ - and self.is_active: + frappe.db.set_value("Item", self.item, "default_bom", self.name) + elif ( + not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1)) + and self.is_active + ): frappe.db.set(self, "is_default", 1) else: frappe.db.set(self, "is_default", 0) item = frappe.get_doc("Item", self.item) if item.default_bom == self.name: - frappe.db.set_value('Item', self.item, 'default_bom', None) + frappe.db.set_value("Item", self.item, "default_bom", None) def clear_operations(self): if not self.with_operations: - self.set('operations', []) + self.set("operations", []) + + def clear_inspection(self): + if not self.inspection_required: + self.quality_inspection_template = None def validate_main_item(self): - """ Validate main FG item""" + """Validate main FG item""" item = self.get_item_det(self.item) if not item: frappe.throw(_("Item {0} does not exist in the system or has expired").format(self.item)) @@ -395,30 +480,34 @@ class BOM(WebsiteGenerator): ret = frappe.db.get_value("Item", self.item, ["description", "stock_uom", "item_name"]) self.description = ret[0] self.uom = ret[1] - self.item_name= ret[2] + self.item_name = ret[2] if not self.quantity: frappe.throw(_("Quantity should be greater than 0")) def validate_currency(self): - if self.rm_cost_as_per == 'Price List': - price_list_currency = frappe.db.get_value('Price List', self.buying_price_list, 'currency') + if self.rm_cost_as_per == "Price List": + price_list_currency = frappe.db.get_value("Price List", self.buying_price_list, "currency") if price_list_currency not in (self.currency, self.company_currency()): - frappe.throw(_("Currency of the price list {0} must be {1} or {2}") - .format(self.buying_price_list, self.currency, self.company_currency())) + frappe.throw( + _("Currency of the price list {0} must be {1} or {2}").format( + self.buying_price_list, self.currency, self.company_currency() + ) + ) def update_stock_qty(self): - for m in self.get('items'): + for m in self.get("items"): if not m.conversion_factor: - m.conversion_factor = flt(get_conversion_factor(m.item_code, m.uom)['conversion_factor']) + m.conversion_factor = flt(get_conversion_factor(m.item_code, m.uom)["conversion_factor"]) if m.uom and m.qty: - m.stock_qty = flt(m.conversion_factor)*flt(m.qty) + m.stock_qty = flt(m.conversion_factor) * flt(m.qty) if not m.uom and m.stock_uom: m.uom = m.stock_uom m.qty = m.stock_qty def validate_uom_is_interger(self): from erpnext.utilities.transaction_base import validate_uom_is_integer + validate_uom_is_integer(self, "uom", "qty", "BOM Item") validate_uom_is_integer(self, "stock_uom", "stock_qty", "BOM Item") @@ -426,23 +515,26 @@ class BOM(WebsiteGenerator): if self.currency == self.company_currency(): self.conversion_rate = 1 elif self.conversion_rate == 1 or flt(self.conversion_rate) <= 0: - self.conversion_rate = get_exchange_rate(self.currency, self.company_currency(), args="for_buying") + self.conversion_rate = get_exchange_rate( + self.currency, self.company_currency(), args="for_buying" + ) def set_plc_conversion_rate(self): if self.rm_cost_as_per in ["Valuation Rate", "Last Purchase Rate"]: self.plc_conversion_rate = 1 elif not self.plc_conversion_rate and self.price_list_currency: - self.plc_conversion_rate = get_exchange_rate(self.price_list_currency, - self.company_currency(), args="for_buying") + self.plc_conversion_rate = get_exchange_rate( + self.price_list_currency, self.company_currency(), args="for_buying" + ) def validate_materials(self): - """ Validate raw material entries """ + """Validate raw material entries""" - if not self.get('items'): + if not self.get("items"): frappe.throw(_("Raw Materials cannot be blank.")) check_list = [] - for m in self.get('items'): + for m in self.get("items"): if m.bom_no: validate_bom_no(m.item_code, m.bom_no) if flt(m.qty) <= 0: @@ -450,13 +542,20 @@ class BOM(WebsiteGenerator): check_list.append(m) def check_recursion(self, bom_list=None): - """ Check whether recursion occurs in any bom""" + """Check whether recursion occurs in any bom""" + def _throw_error(bom_name): frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name)) bom_list = self.traverse_tree() - child_items = frappe.get_all('BOM Item', fields=["bom_no", "item_code"], - filters={'parent': ('in', bom_list), 'parenttype': 'BOM'}) or [] + child_items = ( + frappe.get_all( + "BOM Item", + fields=["bom_no", "item_code"], + filters={"parent": ("in", bom_list), "parenttype": "BOM"}, + ) + or [] + ) child_bom = {d.bom_no for d in child_items} child_items_codes = {d.item_code for d in child_items} @@ -467,19 +566,26 @@ class BOM(WebsiteGenerator): if self.item in child_items_codes: _throw_error(self.item) - bom_nos = frappe.get_all('BOM Item', fields=["parent"], - filters={'bom_no': self.name, 'parenttype': 'BOM'}) or [] + bom_nos = ( + frappe.get_all( + "BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"} + ) + or [] + ) if self.name in {d.parent for d in bom_nos}: _throw_error(self.name) def traverse_tree(self, bom_list=None): def _get_children(bom_no): - children = frappe.cache().hget('bom_children', bom_no) + children = frappe.cache().hget("bom_children", bom_no) if children is None: - children = frappe.db.sql_list("""SELECT `bom_no` FROM `tabBOM Item` - WHERE `parent`=%s AND `bom_no`!='' AND `parenttype`='BOM'""", bom_no) - frappe.cache().hset('bom_children', bom_no, children) + children = frappe.db.sql_list( + """SELECT `bom_no` FROM `tabBOM Item` + WHERE `parent`=%s AND `bom_no`!='' AND `parenttype`='BOM'""", + bom_no, + ) + frappe.cache().hset("bom_children", bom_no, children) return children count = 0 @@ -489,7 +595,7 @@ class BOM(WebsiteGenerator): if self.name not in bom_list: bom_list.append(self.name) - while(count < len(bom_list)): + while count < len(bom_list): for child_bom in _get_children(bom_list[count]): if child_bom not in bom_list: bom_list.append(child_bom) @@ -497,19 +603,21 @@ class BOM(WebsiteGenerator): bom_list.reverse() return bom_list - def calculate_cost(self, update_hour_rate = False): + def calculate_cost(self, update_hour_rate=False): """Calculate bom totals""" self.calculate_op_cost(update_hour_rate) self.calculate_rm_cost() self.calculate_sm_cost() self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost - self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost + self.base_total_cost = ( + self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost + ) - def calculate_op_cost(self, update_hour_rate = False): + def calculate_op_cost(self, update_hour_rate=False): """Update workstation rate and calculates totals""" self.operating_cost = 0 self.base_operating_cost = 0 - for d in self.get('operations'): + for d in self.get("operations"): if d.workstation: self.update_rate_and_time(d, update_hour_rate) @@ -522,13 +630,14 @@ class BOM(WebsiteGenerator): self.operating_cost += flt(operating_cost) self.base_operating_cost += flt(base_operating_cost) - def update_rate_and_time(self, row, update_hour_rate = False): + def update_rate_and_time(self, row, update_hour_rate=False): if not row.hour_rate or update_hour_rate: hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) if hour_rate: - row.hour_rate = (hour_rate / flt(self.conversion_rate) - if self.conversion_rate and hour_rate else hour_rate) + row.hour_rate = ( + hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate + ) if row.hour_rate and row.time_in_mins: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) @@ -545,12 +654,13 @@ class BOM(WebsiteGenerator): total_rm_cost = 0 base_total_rm_cost = 0 - for d in self.get('items'): + for d in self.get("items"): d.base_rate = flt(d.rate) * flt(self.conversion_rate) d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")) d.base_amount = d.amount * flt(self.conversion_rate) - d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) \ - / flt(self.quantity, self.precision("quantity")) + d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt( + self.quantity, self.precision("quantity") + ) total_rm_cost += d.amount base_total_rm_cost += d.base_amount @@ -563,49 +673,49 @@ class BOM(WebsiteGenerator): total_sm_cost = 0 base_total_sm_cost = 0 - for d in self.get('scrap_items'): - d.base_rate = flt(d.rate, d.precision("rate")) * flt(self.conversion_rate, self.precision("conversion_rate")) + for d in self.get("scrap_items"): + d.base_rate = flt(d.rate, d.precision("rate")) * flt( + self.conversion_rate, self.precision("conversion_rate") + ) d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")) - d.base_amount = flt(d.amount, d.precision("amount")) * flt(self.conversion_rate, self.precision("conversion_rate")) + d.base_amount = flt(d.amount, d.precision("amount")) * flt( + self.conversion_rate, self.precision("conversion_rate") + ) total_sm_cost += d.amount base_total_sm_cost += d.base_amount self.scrap_material_cost = total_sm_cost self.base_scrap_material_cost = base_total_sm_cost - def update_new_bom(self, old_bom, new_bom, rate): - for d in self.get("items"): - if d.bom_no != old_bom: continue - - d.bom_no = new_bom - d.rate = rate - d.amount = (d.stock_qty or d.qty) * rate - def update_exploded_items(self, save=True): - """ Update Flat BOM, following will be correct data""" + """Update Flat BOM, following will be correct data""" self.get_exploded_items() self.add_exploded_items(save=save) def get_exploded_items(self): - """ Get all raw materials including items from child bom""" + """Get all raw materials including items from child bom""" self.cur_exploded_items = {} - for d in self.get('items'): + for d in self.get("items"): if d.bom_no: self.get_child_exploded_items(d.bom_no, d.stock_qty) elif d.item_code: - self.add_to_cur_exploded_items(frappe._dict({ - 'item_code' : d.item_code, - 'item_name' : d.item_name, - 'operation' : d.operation, - 'source_warehouse': d.source_warehouse, - 'description' : d.description, - 'image' : d.image, - 'stock_uom' : d.stock_uom, - 'stock_qty' : flt(d.stock_qty), - 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), - 'include_item_in_manufacturing': d.include_item_in_manufacturing, - 'sourced_by_supplier': d.sourced_by_supplier - })) + self.add_to_cur_exploded_items( + frappe._dict( + { + "item_code": d.item_code, + "item_name": d.item_name, + "operation": d.operation, + "source_warehouse": d.source_warehouse, + "description": d.description, + "image": d.image, + "stock_uom": d.stock_uom, + "stock_qty": flt(d.stock_qty), + "rate": flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), + "include_item_in_manufacturing": d.include_item_in_manufacturing, + "sourced_by_supplier": d.sourced_by_supplier, + } + ) + ) def company_currency(self): return erpnext.get_company_currency(self.company) @@ -617,9 +727,10 @@ class BOM(WebsiteGenerator): self.cur_exploded_items[args.item_code] = args def get_child_exploded_items(self, bom_no, stock_qty): - """ Add all items from Flat BOM of child BOM""" + """Add all items from Flat BOM of child BOM""" # Did not use qty_consumed_per_unit in the query, as it leads to rounding loss - child_fb_items = frappe.db.sql(""" + child_fb_items = frappe.db.sql( + """ SELECT bom_item.item_code, bom_item.item_name, @@ -637,31 +748,38 @@ class BOM(WebsiteGenerator): bom_item.parent = bom.name AND bom.name = %s AND bom.docstatus = 1 - """, bom_no, as_dict = 1) + """, + bom_no, + as_dict=1, + ) for d in child_fb_items: - self.add_to_cur_exploded_items(frappe._dict({ - 'item_code' : d['item_code'], - 'item_name' : d['item_name'], - 'source_warehouse' : d['source_warehouse'], - 'operation' : d['operation'], - 'description' : d['description'], - 'stock_uom' : d['stock_uom'], - 'stock_qty' : d['qty_consumed_per_unit'] * stock_qty, - 'rate' : flt(d['rate']), - 'include_item_in_manufacturing': d.get('include_item_in_manufacturing', 0), - 'sourced_by_supplier': d.get('sourced_by_supplier', 0) - })) + self.add_to_cur_exploded_items( + frappe._dict( + { + "item_code": d["item_code"], + "item_name": d["item_name"], + "source_warehouse": d["source_warehouse"], + "operation": d["operation"], + "description": d["description"], + "stock_uom": d["stock_uom"], + "stock_qty": d["qty_consumed_per_unit"] * stock_qty, + "rate": flt(d["rate"]), + "include_item_in_manufacturing": d.get("include_item_in_manufacturing", 0), + "sourced_by_supplier": d.get("sourced_by_supplier", 0), + } + ) + ) def add_exploded_items(self, save=True): "Add items to Flat BOM table" - self.set('exploded_items', []) + self.set("exploded_items", []) if save: frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name) for d in sorted(self.cur_exploded_items, key=itemgetter(0)): - ch = self.append('exploded_items', {}) + ch = self.append("exploded_items", {}) for i in self.cur_exploded_items[d].keys(): ch.set(i, self.cur_exploded_items[d][i]) ch.amount = flt(ch.stock_qty) * flt(ch.rate) @@ -673,10 +791,13 @@ class BOM(WebsiteGenerator): def validate_bom_links(self): if not self.is_active: - act_pbom = frappe.db.sql("""select distinct bom_item.parent from `tabBOM Item` bom_item + act_pbom = frappe.db.sql( + """select distinct bom_item.parent from `tabBOM Item` bom_item where bom_item.bom_no = %s and bom_item.docstatus = 1 and bom_item.parenttype='BOM' and exists (select * from `tabBOM` where name = bom_item.parent - and docstatus = 1 and is_active = 1)""", self.name) + and docstatus = 1 and is_active = 1)""", + self.name, + ) if act_pbom and act_pbom[0][0]: frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs")) @@ -685,20 +806,23 @@ class BOM(WebsiteGenerator): if not self.with_operations: self.transfer_material_against = "Work Order" if not self.transfer_material_against and not self.is_new(): - frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value")) + frappe.throw( + _("Setting {} is required").format(self.meta.get_label("transfer_material_against")), + title=_("Missing value"), + ) def set_routing_operations(self): if self.routing and self.with_operations and not self.operations: self.get_routing() def validate_operations(self): - if self.with_operations and not self.get('operations') and self.docstatus == 1: + if self.with_operations and not self.get("operations") and self.docstatus == 1: frappe.throw(_("Operations cannot be left blank")) if self.with_operations: for d in self.operations: if not d.description: - d.description = frappe.db.get_value('Operation', d.operation, 'description') + d.description = frappe.db.get_value("Operation", d.operation, "description") if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 @@ -706,24 +830,29 @@ class BOM(WebsiteGenerator): for item in self.scrap_items: msg = "" if item.item_code == self.item and not item.is_process_loss: - msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked.') \ - .format(frappe.bold(item.item_code)) + msg = _( + "Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked." + ).format(frappe.bold(item.item_code)) elif item.item_code != self.item and item.is_process_loss: - msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked') \ - .format(frappe.bold(item.item_code)) + msg = _( + "Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked" + ).format(frappe.bold(item.item_code)) must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") if item.is_process_loss and must_be_whole_number: - msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM.") \ - .format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) + msg = _( + "Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM." + ).format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) if item.is_process_loss and (item.stock_qty >= self.quantity): - msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.") \ - .format(frappe.bold(item.item_code)) + msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.").format( + frappe.bold(item.item_code) + ) if item.is_process_loss and (item.rate > 0): - msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked.") \ - .format(frappe.bold(item.item_code)) + msg = _( + "Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked." + ).format(frappe.bold(item.item_code)) if msg: frappe.throw(msg, title=_("Note")) @@ -732,42 +861,48 @@ class BOM(WebsiteGenerator): """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) + def get_bom_item_rate(args, bom_doc): - if bom_doc.rm_cost_as_per == 'Valuation Rate': + if bom_doc.rm_cost_as_per == "Valuation Rate": rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) - elif bom_doc.rm_cost_as_per == 'Last Purchase Rate': - rate = ( flt(args.get('last_purchase_rate')) \ - or frappe.db.get_value("Item", args['item_code'], "last_purchase_rate")) \ - * (args.get("conversion_factor") or 1) + elif bom_doc.rm_cost_as_per == "Last Purchase Rate": + rate = ( + flt(args.get("last_purchase_rate")) + or flt(frappe.db.get_value("Item", args["item_code"], "last_purchase_rate")) + ) * (args.get("conversion_factor") or 1) elif bom_doc.rm_cost_as_per == "Price List": if not bom_doc.buying_price_list: frappe.throw(_("Please select Price List")) - bom_args = frappe._dict({ - "doctype": "BOM", - "price_list": bom_doc.buying_price_list, - "qty": args.get("qty") or 1, - "uom": args.get("uom") or args.get("stock_uom"), - "stock_uom": args.get("stock_uom"), - "transaction_type": "buying", - "company": bom_doc.company, - "currency": bom_doc.currency, - "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function - "conversion_factor": args.get("conversion_factor") or 1, - "plc_conversion_rate": 1, - "ignore_party": True, - "ignore_conversion_rate": True - }) + bom_args = frappe._dict( + { + "doctype": "BOM", + "price_list": bom_doc.buying_price_list, + "qty": args.get("qty") or 1, + "uom": args.get("uom") or args.get("stock_uom"), + "stock_uom": args.get("stock_uom"), + "transaction_type": "buying", + "company": bom_doc.company, + "currency": bom_doc.currency, + "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function + "conversion_factor": args.get("conversion_factor") or 1, + "plc_conversion_rate": 1, + "ignore_party": True, + "ignore_conversion_rate": True, + } + ) item_doc = frappe.get_cached_doc("Item", args.get("item_code")) price_list_data = get_price_list_rate(bom_args, item_doc) rate = price_list_data.price_list_rate - return rate + return flt(rate) + def get_valuation_rate(args): - """ Get weighted average of valuation rate from all warehouses """ + """Get weighted average of valuation rate from all warehouses""" total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 - item_bins = frappe.db.sql(""" + item_bins = frappe.db.sql( + """ select bin.actual_qty, bin.stock_value from @@ -776,33 +911,48 @@ def get_valuation_rate(args): bin.item_code=%(item)s and bin.warehouse = warehouse.name and warehouse.company=%(company)s""", - {"item": args['item_code'], "company": args['company']}, as_dict=1) + {"item": args["item_code"], "company": args["company"]}, + as_dict=1, + ) for d in item_bins: total_qty += flt(d.actual_qty) total_value += flt(d.stock_value) if total_qty: - valuation_rate = total_value / total_qty + valuation_rate = total_value / total_qty if valuation_rate <= 0: - last_valuation_rate = frappe.db.sql("""select valuation_rate + last_valuation_rate = frappe.db.sql( + """select valuation_rate from `tabStock Ledger Entry` where item_code = %s and valuation_rate > 0 and is_cancelled = 0 - order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) + order by posting_date desc, posting_time desc, creation desc limit 1""", + args["item_code"], + ) valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 if not valuation_rate: - valuation_rate = frappe.db.get_value("Item", args['item_code'], "valuation_rate") + valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate") return flt(valuation_rate) + def get_list_context(context): context.title = _("Bill of Materials") # context.introduction = _('Boms') -def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_items=0, include_non_stock_items=False, fetch_qty_in_stock_uom=True): + +def get_bom_items_as_dict( + bom, + company, + qty=1, + fetch_exploded=1, + fetch_scrap_items=0, + include_non_stock_items=False, + fetch_qty_in_stock_uom=True, +): item_dict = {} # Did not use qty_consumed_per_unit in the query, as it leads to rounding loss @@ -839,30 +989,40 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite is_stock_item = 0 if include_non_stock_items else 1 if cint(fetch_exploded): - query = query.format(table="BOM Explosion Item", + query = query.format( + table="BOM Explosion Item", where_conditions="", is_stock_item=is_stock_item, qty_field="stock_qty", - select_columns = """, bom_item.source_warehouse, bom_item.operation, + select_columns=""", bom_item.source_warehouse, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier, - (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""") - - items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) - elif fetch_scrap_items: - query = query.format( - table="BOM Scrap Item", where_conditions="", - select_columns=", bom_item.idx, item.description, is_process_loss", - is_stock_item=is_stock_item, qty_field="stock_qty" + (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""", ) - items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) + items = frappe.db.sql( + query, {"parent": bom, "qty": qty, "bom": bom, "company": company}, as_dict=True + ) + elif fetch_scrap_items: + query = query.format( + table="BOM Scrap Item", + where_conditions="", + select_columns=", item.description, is_process_loss", + is_stock_item=is_stock_item, + qty_field="stock_qty", + ) + + items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True) else: - query = query.format(table="BOM Item", where_conditions="", is_stock_item=is_stock_item, + query = query.format( + table="BOM Item", + where_conditions="", + 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.description, bom_item.base_rate as rate """) - items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) + select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, + 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) for item in items: if item.item_code in item_dict: @@ -871,21 +1031,28 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite item_dict[item.item_code] = item for item, item_details in item_dict.items(): - for d in [["Account", "expense_account", "stock_adjustment_account"], - ["Cost Center", "cost_center", "cost_center"], ["Warehouse", "default_warehouse", ""]]: - company_in_record = frappe.db.get_value(d[0], item_details.get(d[1]), "company") - if not item_details.get(d[1]) or (company_in_record and company != company_in_record): - item_dict[item][d[1]] = frappe.get_cached_value('Company', company, d[2]) if d[2] else None + for d in [ + ["Account", "expense_account", "stock_adjustment_account"], + ["Cost Center", "cost_center", "cost_center"], + ["Warehouse", "default_warehouse", ""], + ]: + company_in_record = frappe.db.get_value(d[0], item_details.get(d[1]), "company") + if not item_details.get(d[1]) or (company_in_record and company != company_in_record): + item_dict[item][d[1]] = frappe.get_cached_value("Company", company, d[2]) if d[2] else None return item_dict + @frappe.whitelist() def get_bom_items(bom, company, qty=1, fetch_exploded=1): - items = get_bom_items_as_dict(bom, company, qty, fetch_exploded, include_non_stock_items=True).values() + items = get_bom_items_as_dict( + bom, company, qty, fetch_exploded, include_non_stock_items=True + ).values() items = list(items) - items.sort(key = functools.cmp_to_key(lambda a, b: a.item_code > b.item_code and 1 or -1)) + items.sort(key=functools.cmp_to_key(lambda a, b: a.item_code > b.item_code and 1 or -1)) return items + def validate_bom_no(item, bom_no): """Validate BOM No of sub-contracted items""" bom = frappe.get_doc("BOM", bom_no) @@ -897,21 +1064,24 @@ def validate_bom_no(item, bom_no): if item: rm_item_exists = False for d in bom.items: - if (d.item_code.lower() == item.lower()): + if d.item_code.lower() == item.lower(): rm_item_exists = True for d in bom.scrap_items: - if (d.item_code.lower() == item.lower()): + if d.item_code.lower() == item.lower(): rm_item_exists = True - if bom.item.lower() == item.lower() or \ - bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower(): - rm_item_exists = True + if ( + bom.item.lower() == item.lower() + or bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower() + ): + rm_item_exists = True if not rm_item_exists: frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) + @frappe.whitelist() def get_children(doctype, parent=None, is_root=False, **filters): - if not parent or parent=="BOM": - frappe.msgprint(_('Please select a BOM')) + if not parent or parent == "BOM": + frappe.msgprint(_("Please select a BOM")) return if parent: @@ -921,38 +1091,45 @@ def get_children(doctype, parent=None, is_root=False, **filters): bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent) frappe.has_permission("BOM", doc=bom_doc, throw=True) - bom_items = frappe.get_all('BOM Item', - fields=['item_code', 'bom_no as value', 'stock_qty'], - filters=[['parent', '=', frappe.form_dict.parent]], - order_by='idx') + bom_items = frappe.get_all( + "BOM Item", + fields=["item_code", "bom_no as value", "stock_qty"], + filters=[["parent", "=", frappe.form_dict.parent]], + order_by="idx", + ) - item_names = tuple(d.get('item_code') for d in bom_items) + item_names = tuple(d.get("item_code") for d in bom_items) - items = frappe.get_list('Item', - fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'], - filters=[['name', 'in', item_names]]) # to get only required item dicts + items = frappe.get_list( + "Item", + fields=["image", "description", "name", "stock_uom", "item_name", "is_sub_contracted_item"], + filters=[["name", "in", item_names]], + ) # to get only required item dicts for bom_item in bom_items: # extend bom_item dict with respective item dict bom_item.update( # returns an item dict from items list which matches with item_code - next(item for item in items if item.get('name') - == bom_item.get('item_code')) + next(item for item in items if item.get("name") == bom_item.get("item_code")) ) bom_item.parent_bom_qty = bom_doc.quantity - bom_item.expandable = 0 if bom_item.value in ('', None) else 1 + bom_item.expandable = 0 if bom_item.value in ("", None) else 1 bom_item.image = frappe.db.escape(bom_item.image) return bom_items + def get_boms_in_bottom_up_order(bom_no=None): def _get_parent(bom_no): - return frappe.db.sql_list(""" + return frappe.db.sql_list( + """ select distinct bom_item.parent from `tabBOM Item` bom_item where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM' and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1) - """, bom_no) + """, + bom_no, + ) count = 0 bom_list = [] @@ -960,12 +1137,14 @@ def get_boms_in_bottom_up_order(bom_no=None): bom_list.append(bom_no) else: # get all leaf BOMs - bom_list = frappe.db.sql_list("""select name from `tabBOM` bom + bom_list = frappe.db.sql_list( + """select name from `tabBOM` bom where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item` - where parent=bom.name and ifnull(bom_no, '')!='')""") + where parent=bom.name and ifnull(bom_no, '')!='')""" + ) - while(count < len(bom_list)): + while count < len(bom_list): for child_bom in _get_parent(bom_list[count]): if child_bom not in bom_list: bom_list.append(child_bom) @@ -973,69 +1152,92 @@ def get_boms_in_bottom_up_order(bom_no=None): return bom_list + def add_additional_cost(stock_entry, work_order): # Add non stock items cost in the additional cost stock_entry.additional_costs = [] - expenses_included_in_valuation = frappe.get_cached_value("Company", work_order.company, - "expenses_included_in_valuation") + expenses_included_in_valuation = frappe.get_cached_value( + "Company", work_order.company, "expenses_included_in_valuation" + ) add_non_stock_items_cost(stock_entry, work_order, expenses_included_in_valuation) add_operations_cost(stock_entry, work_order, expenses_included_in_valuation) + def add_non_stock_items_cost(stock_entry, work_order, expense_account): - bom = frappe.get_doc('BOM', work_order.bom_no) - table = 'exploded_items' if work_order.get('use_multi_level_bom') else 'items' + bom = frappe.get_doc("BOM", work_order.bom_no) + table = "exploded_items" if work_order.get("use_multi_level_bom") else "items" items = {} for d in bom.get(table): items.setdefault(d.item_code, d.amount) - non_stock_items = frappe.get_all('Item', - fields="name", filters={'name': ('in', list(items.keys())), 'ifnull(is_stock_item, 0)': 0}, as_list=1) + non_stock_items = frappe.get_all( + "Item", + fields="name", + filters={"name": ("in", list(items.keys())), "ifnull(is_stock_item, 0)": 0}, + as_list=1, + ) non_stock_items_cost = 0.0 for name in non_stock_items: - non_stock_items_cost += flt(items.get(name[0])) * flt(stock_entry.fg_completed_qty) / flt(bom.quantity) + non_stock_items_cost += ( + flt(items.get(name[0])) * flt(stock_entry.fg_completed_qty) / flt(bom.quantity) + ) if non_stock_items_cost: - stock_entry.append('additional_costs', { - 'expense_account': expense_account, - 'description': _("Non stock items"), - 'amount': non_stock_items_cost - }) + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": _("Non stock items"), + "amount": non_stock_items_cost, + }, + ) + def add_operations_cost(stock_entry, work_order=None, expense_account=None): from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit + operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no) if operating_cost_per_unit: - stock_entry.append('additional_costs', { - "expense_account": expense_account, - "description": _("Operating Cost as per Work Order / BOM"), - "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty) - }) + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": _("Operating Cost as per Work Order / BOM"), + "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty), + }, + ) if work_order and work_order.additional_operating_cost and work_order.qty: - additional_operating_cost_per_unit = \ - flt(work_order.additional_operating_cost) / flt(work_order.qty) + additional_operating_cost_per_unit = flt(work_order.additional_operating_cost) / flt( + work_order.qty + ) if additional_operating_cost_per_unit: - stock_entry.append('additional_costs', { - "expense_account": expense_account, - "description": "Additional Operating Cost", - "amount": additional_operating_cost_per_unit * flt(stock_entry.fg_completed_qty) - }) + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": "Additional Operating Cost", + "amount": additional_operating_cost_per_unit * flt(stock_entry.fg_completed_qty), + }, + ) + @frappe.whitelist() def get_bom_diff(bom1, bom2): from frappe.model import table_fields if bom1 == bom2: - frappe.throw(_("BOM 1 {0} and BOM 2 {1} should not be same") - .format(frappe.bold(bom1), frappe.bold(bom2))) + frappe.throw( + _("BOM 1 {0} and BOM 2 {1} should not be same").format(frappe.bold(bom1), frappe.bold(bom2)) + ) - doc1 = frappe.get_doc('BOM', bom1) - doc2 = frappe.get_doc('BOM', bom2) + doc1 = frappe.get_doc("BOM", bom1) + doc2 = frappe.get_doc("BOM", bom2) out = get_diff(doc1, doc2) out.row_changed = [] @@ -1045,10 +1247,10 @@ def get_bom_diff(bom1, bom2): meta = doc1.meta identifiers = { - 'operations': 'operation', - 'items': 'item_code', - 'scrap_items': 'item_code', - 'exploded_items': 'item_code' + "operations": "operation", + "items": "item_code", + "scrap_items": "item_code", + "exploded_items": "item_code", } for df in meta.fields: @@ -1079,6 +1281,7 @@ def get_bom_diff(bom1, bom2): return out + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): @@ -1088,25 +1291,28 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): order_by = "idx desc, name, item_name" fields = ["name", "item_group", "item_name", "description"] - fields.extend([field for field in searchfields - if not field in ["name", "item_group", "description"]]) + fields.extend( + [field for field in searchfields if not field in ["name", "item_group", "description"]] + ) - searchfields = searchfields + [field for field in [searchfield or "name", "item_code", "item_group", "item_name"] - if not field in searchfields] + searchfields = searchfields + [ + field + for field in [searchfield or "name", "item_code", "item_group", "item_name"] + if not field in searchfields + ] - query_filters = { - "disabled": 0, - "ifnull(end_of_life, '5050-50-50')": (">", today()) - } + query_filters = {"disabled": 0, "ifnull(end_of_life, '5050-50-50')": (">", today())} or_cond_filters = {} if txt: for s_field in searchfields: or_cond_filters[s_field] = ("like", "%{0}%".format(txt)) - barcodes = frappe.get_all("Item Barcode", + barcodes = frappe.get_all( + "Item Barcode", fields=["distinct parent as item_code"], - filters = {"barcode": ("like", "%{0}%".format(txt))}) + filters={"barcode": ("like", "%{0}%".format(txt))}, + ) barcodes = [d.item_code for d in barcodes] if barcodes: @@ -1120,10 +1326,17 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("is_stock_item"): query_filters["is_stock_item"] = 1 - return frappe.get_list("Item", - fields = fields, filters=query_filters, - or_filters = or_cond_filters, order_by=order_by, - limit_start=start, limit_page_length=page_len, as_list=1) + return frappe.get_list( + "Item", + fields=fields, + filters=query_filters, + or_filters=or_cond_filters, + order_by=order_by, + limit_start=start, + limit_page_length=page_len, + as_list=1, + ) + @frappe.whitelist() def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): @@ -1134,28 +1347,31 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): doc.quantity = 1 item_data = get_item_details(item) - doc.update({ - "item_name": item_data.item_name, - "description": item_data.description, - "uom": item_data.stock_uom, - "allow_alternative_item": item_data.allow_alternative_item - }) + doc.update( + { + "item_name": item_data.item_name, + "description": item_data.description, + "uom": item_data.stock_uom, + "allow_alternative_item": item_data.allow_alternative_item, + } + ) add_variant_item(variant_items, doc, source_name) - doc = get_mapped_doc('BOM', source_name, { - 'BOM': { - 'doctype': 'BOM', - 'validation': { - 'docstatus': ['=', 1] - } + doc = get_mapped_doc( + "BOM", + source_name, + { + "BOM": {"doctype": "BOM", "validation": {"docstatus": ["=", 1]}}, + "BOM Item": { + "doctype": "BOM Item", + # stop get_mapped_doc copying parent bom_no to children + "field_no_map": ["bom_no"], + "condition": lambda doc: doc.has_variants == 0, + }, }, - 'BOM Item': { - 'doctype': 'BOM Item', - # stop get_mapped_doc copying parent bom_no to children - 'field_no_map': ['bom_no'], - 'condition': lambda doc: doc.has_variants == 0 - }, - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doc diff --git a/erpnext/manufacturing/doctype/bom/bom_dashboard.py b/erpnext/manufacturing/doctype/bom/bom_dashboard.py index 9b8f6bff095..d8a810bd0ad 100644 --- a/erpnext/manufacturing/doctype/bom/bom_dashboard.py +++ b/erpnext/manufacturing/doctype/bom/bom_dashboard.py @@ -1,30 +1,30 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'bom_no', - 'non_standard_fieldnames': { - 'Item': 'default_bom', - 'Purchase Order': 'bom', - 'Purchase Receipt': 'bom', - 'Purchase Invoice': 'bom' + "fieldname": "bom_no", + "non_standard_fieldnames": { + "Item": "default_bom", + "Purchase Order": "bom", + "Purchase Receipt": "bom", + "Purchase Invoice": "bom", }, - 'transactions': [ + "transactions": [ + {"label": _("Stock"), "items": ["Item", "Stock Entry", "Quality Inspection"]}, + {"label": _("Manufacture"), "items": ["BOM", "Work Order", "Job Card"]}, { - 'label': _('Stock'), - 'items': ['Item', 'Stock Entry', 'Quality Inspection'] + "label": _("Subcontract"), + "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"], }, - { - 'label': _('Manufacture'), - 'items': ['BOM', 'Work Order', 'Job Card'] - }, - { - 'label': _('Subcontract'), - 'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] - } ], - 'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt", - "Purchase Invoice", "Job Card", "Stock Entry", "BOM"] + "disable_create_buttons": [ + "Item", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + "Job Card", + "Stock Entry", + "BOM", + ], } diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index bfafacdfb57..455e3f9d9c3 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -6,7 +6,7 @@ from collections import deque from functools import partial import frappe -from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -17,27 +17,28 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.tests.test_subcontracting import set_backflush_based_on -from erpnext.tests.utils import ERPNextTestCase -test_records = frappe.get_test_records('BOM') +test_records = frappe.get_test_records("BOM") +test_dependencies = ["Item", "Quality Inspection Template"] -class TestBOM(ERPNextTestCase): - def setUp(self): - if not frappe.get_value('Item', '_Test Item'): - make_test_records('Item') +class TestBOM(FrappeTestCase): def test_get_items(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict - items_dict = get_bom_items_as_dict(bom=get_default_bom(), - company="_Test Company", qty=1, fetch_exploded=0) + + items_dict = get_bom_items_as_dict( + bom=get_default_bom(), company="_Test Company", qty=1, fetch_exploded=0 + ) self.assertTrue(test_records[2]["items"][0]["item_code"] in items_dict) self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict) self.assertEqual(len(items_dict.values()), 2) def test_get_items_exploded(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict - items_dict = get_bom_items_as_dict(bom=get_default_bom(), - company="_Test Company", qty=1, fetch_exploded=1) + + items_dict = get_bom_items_as_dict( + bom=get_default_bom(), company="_Test Company", qty=1, fetch_exploded=1 + ) self.assertTrue(test_records[2]["items"][0]["item_code"] in items_dict) self.assertFalse(test_records[2]["items"][1]["item_code"] in items_dict) self.assertTrue(test_records[0]["items"][0]["item_code"] in items_dict) @@ -46,13 +47,14 @@ class TestBOM(ERPNextTestCase): def test_get_items_list(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items + self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3) def test_default_bom(self): def _get_default_bom_in_item(): return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom")) - bom = frappe.get_doc("BOM", {"item":"_Test FG Item 2", "is_default": 1}) + bom = frappe.get_doc("BOM", {"item": "_Test FG Item 2", "is_default": 1}) self.assertEqual(_get_default_bom_in_item(), bom.name) bom.is_active = 0 @@ -60,28 +62,33 @@ class TestBOM(ERPNextTestCase): self.assertEqual(_get_default_bom_in_item(), "") bom.is_active = 1 - bom.is_default=1 + bom.is_default = 1 bom.save() self.assertTrue(_get_default_bom_in_item(), bom.name) def test_update_bom_cost_in_all_boms(self): # get current rate for '_Test Item 2' - rm_rate = frappe.db.sql("""select rate from `tabBOM Item` + rm_rate = frappe.db.sql( + """select rate from `tabBOM Item` where parent='BOM-_Test Item Home Desktop Manufactured-001' - and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""") + and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""" + ) rm_rate = rm_rate[0][0] if rm_rate else 0 # Reset item valuation rate - reset_item_valuation_rate(item_code='_Test Item 2', qty=200, rate=rm_rate + 10) + reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10) # update cost of all BOMs based on latest valuation rate update_cost() # check if new valuation rate updated in all BOMs - for d in frappe.db.sql("""select rate from `tabBOM Item` - where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""", as_dict=1): - self.assertEqual(d.rate, rm_rate + 10) + for d in frappe.db.sql( + """select rate from `tabBOM Item` + where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""", + as_dict=1, + ): + self.assertEqual(d.rate, rm_rate + 10) def test_bom_cost(self): bom = frappe.copy_doc(test_records[2]) @@ -96,7 +103,9 @@ class TestBOM(ERPNextTestCase): for row in bom.items: raw_material_cost += row.amount - base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) + base_raw_material_cost = raw_material_cost * flt( + bom.conversion_rate, bom.precision("conversion_rate") + ) base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) # test amounts in selected currency, almostEqual checks for 7 digits by default @@ -124,14 +133,15 @@ class TestBOM(ERPNextTestCase): for op_row in bom.operations: self.assertAlmostEqual(op_row.cost_per_unit, op_row.operating_cost / 2) - self.assertAlmostEqual(bom.operating_cost, op_cost/2) + self.assertAlmostEqual(bom.operating_cost, op_cost / 2) bom.delete() def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)): - frappe.db.sql("delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", - item_code) + frappe.db.sql( + "delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", item_code + ) item_price = frappe.new_doc("Item Price") item_price.price_list = "_Test Price List" item_price.item_code = item_code @@ -146,7 +156,7 @@ class TestBOM(ERPNextTestCase): bom.items[0].conversion_factor = 5 bom.insert() - bom.update_cost(update_hour_rate = False) + bom.update_cost(update_hour_rate=False) # test amounts in selected currency self.assertEqual(bom.items[0].rate, 300) @@ -171,11 +181,12 @@ class TestBOM(ERPNextTestCase): bom.insert() reset_item_valuation_rate( - item_code='_Test Item', - warehouse_list=frappe.get_all("Warehouse", - {"is_group":0, "company": bom.company}, pluck="name"), + item_code="_Test Item", + warehouse_list=frappe.get_all( + "Warehouse", {"is_group": 0, "company": bom.company}, pluck="name" + ), qty=200, - rate=200 + rate=200, ) bom.update_cost() @@ -184,77 +195,72 @@ class TestBOM(ERPNextTestCase): def test_subcontractor_sourced_item(self): item_code = "_Test Subcontracted FG Item 1" - set_backflush_based_on('Material Transferred for Subcontract') + set_backflush_based_on("Material Transferred for Subcontract") - if not frappe.db.exists('Item', item_code): - make_item(item_code, { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'stock_uom': 'Nos' - }) + if not frappe.db.exists("Item", item_code): + make_item(item_code, {"is_stock_item": 1, "is_sub_contracted_item": 1, "stock_uom": "Nos"}) - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - 'stock_uom': 'Nos' - }) + if not frappe.db.exists("Item", "Test Extra Item 1"): + make_item("Test Extra Item 1", {"is_stock_item": 1, "stock_uom": "Nos"}) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - 'stock_uom': 'Nos' - }) + if not frappe.db.exists("Item", "Test Extra Item 2"): + make_item("Test Extra Item 2", {"is_stock_item": 1, "stock_uom": "Nos"}) - if not frappe.db.exists('Item', "Test Extra Item 3"): - make_item("Test Extra Item 3", { - 'is_stock_item': 1, - 'stock_uom': 'Nos' - }) - bom = frappe.get_doc({ - 'doctype': 'BOM', - 'is_default': 1, - 'item': item_code, - 'currency': 'USD', - 'quantity': 1, - 'company': '_Test Company' - }) + if not frappe.db.exists("Item", "Test Extra Item 3"): + make_item("Test Extra Item 3", {"is_stock_item": 1, "stock_uom": "Nos"}) + bom = frappe.get_doc( + { + "doctype": "BOM", + "is_default": 1, + "item": item_code, + "currency": "USD", + "quantity": 1, + "company": "_Test Company", + } + ) for item in ["Test Extra Item 1", "Test Extra Item 2"]: - item_doc = frappe.get_doc('Item', item) + item_doc = frappe.get_doc("Item", item) - bom.append('items', { - 'item_code': item, - 'qty': 1, - 'uom': item_doc.stock_uom, - 'stock_uom': item_doc.stock_uom, - 'rate': item_doc.valuation_rate - }) + bom.append( + "items", + { + "item_code": item, + "qty": 1, + "uom": item_doc.stock_uom, + "stock_uom": item_doc.stock_uom, + "rate": item_doc.valuation_rate, + }, + ) - bom.append('items', { - 'item_code': "Test Extra Item 3", - 'qty': 1, - 'uom': item_doc.stock_uom, - 'stock_uom': item_doc.stock_uom, - 'rate': 0, - 'sourced_by_supplier': 1 - }) + bom.append( + "items", + { + "item_code": "Test Extra Item 3", + "qty": 1, + "uom": item_doc.stock_uom, + "stock_uom": item_doc.stock_uom, + "rate": 0, + "sourced_by_supplier": 1, + }, + ) bom.insert(ignore_permissions=True) bom.update_cost() bom.submit() # test that sourced_by_supplier rate is zero even after updating cost self.assertEqual(bom.items[2].rate, 0) # test in Purchase Order sourced_by_supplier is not added to Supplied Item - po = create_purchase_order(item_code=item_code, qty=1, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1]) supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) - def test_bom_recursion_1st_level(self): """BOM should not allow BOM item again in child""" item_code = "_Test BOM Recursion" - make_item(item_code, {'is_stock_item': 1}) + make_item(item_code, {"is_stock_item": 1}) bom = frappe.new_doc("BOM") bom.item = item_code @@ -268,8 +274,8 @@ class TestBOM(ERPNextTestCase): def test_bom_recursion_transitive(self): item1 = "_Test BOM Recursion" item2 = "_Test BOM Recursion 2" - make_item(item1, {'is_stock_item': 1}) - make_item(item2, {'is_stock_item': 1}) + make_item(item1, {"is_stock_item": 1}) + make_item(item2, {"is_stock_item": 1}) bom1 = frappe.new_doc("BOM") bom1.item = item1 @@ -327,7 +333,10 @@ class TestBOM(ERPNextTestCase): def test_bom_tree_representation(self): bom_tree = { "Assembly": { - "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly1": { + "ChildPart1": {}, + "ChildPart2": {}, + }, "SubAssembly2": {"ChildPart3": {}}, "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, "ChildPart5": {}, @@ -338,7 +347,7 @@ class TestBOM(ERPNextTestCase): parent_bom = create_nested_bom(bom_tree, prefix="") created_tree = parent_bom.get_tree_representation() - reqd_order = level_order_traversal(bom_tree)[1:] # skip first item + reqd_order = level_order_traversal(bom_tree)[1:] # skip first item created_order = created_tree.level_order_traversal() self.assertEqual(len(reqd_order), len(created_order)) @@ -347,17 +356,28 @@ class TestBOM(ERPNextTestCase): self.assertEqual(reqd_item, created_item.item_code) def test_bom_item_query(self): - query = partial(item_query, doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters={"is_stock_item": 1}) + query = partial( + item_query, + doctype="Item", + txt="", + searchfield="name", + start=0, + page_len=20, + filters={"is_stock_item": 1}, + ) test_items = query(txt="_Test") filtered = query(txt="_Test Item 2") - self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results") + self.assertNotEqual( + len(test_items), len(filtered), msg="Item filtering showing excessive results" + ) self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results") - def test_valid_transfer_defaults(self): - bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}) + bom_with_op = frappe.db.get_value( + "BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1} + ) bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False) # test defaults @@ -385,10 +405,107 @@ class TestBOM(ERPNextTestCase): self.assertEqual(bom.transfer_material_against, "Work Order") bom.delete() + def test_bom_name_length(self): + """test >140 char names""" + bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}} + create_nested_bom(bom_tree, prefix="") + + def test_version_index(self): + + bom = frappe.new_doc("BOM") + + version_index_test_cases = [ + (1, []), + (1, ["BOM#XYZ"]), + (2, ["BOM/ITEM/001"]), + (2, ["BOM-ITEM-001"]), + (3, ["BOM-ITEM-001", "BOM-ITEM-002"]), + (4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]), + ] + + for expected_index, existing_boms in version_index_test_cases: + with self.subTest(): + self.assertEqual( + expected_index, + bom.get_next_version_index(existing_boms), + msg=f"Incorrect index for {existing_boms}", + ) + + def test_bom_versioning(self): + bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}} + bom = create_nested_bom(bom_tree, prefix="") + self.assertEqual(int(bom.name.split("-")[-1]), 1) + original_bom_name = bom.name + + bom.cancel() + bom.reload() + self.assertEqual(bom.name, original_bom_name) + + # create a new amendment + amendment = frappe.copy_doc(bom) + amendment.docstatus = 0 + amendment.amended_from = bom.name + + amendment.save() + amendment.submit() + amendment.reload() + + self.assertNotEqual(amendment.name, bom.name) + # `origname-001-1` version + self.assertEqual(int(amendment.name.split("-")[-1]), 1) + self.assertEqual(int(amendment.name.split("-")[-2]), 1) + + # create a new version + version = frappe.copy_doc(amendment) + version.docstatus = 0 + version.amended_from = None + version.save() + self.assertNotEqual(amendment.name, version.name) + self.assertEqual(int(version.name.split("-")[-1]), 2) + + def test_clear_inpection_quality(self): + + bom = frappe.copy_doc(test_records[2], ignore_no_copy=True) + bom.docstatus = 0 + bom.is_default = 0 + bom.quality_inspection_template = "_Test Quality Inspection Template" + bom.inspection_required = 1 + bom.save() + bom.reload() + + self.assertEqual(bom.quality_inspection_template, "_Test Quality Inspection Template") + + bom.inspection_required = 0 + bom.save() + bom.reload() + + self.assertEqual(bom.quality_inspection_template, None) + + def test_bom_pricing_based_on_lpp(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + parent = frappe.generate_hash(length=10) + child = frappe.generate_hash(length=10) + bom_tree = {parent: {child: {}}} + bom = create_nested_bom(bom_tree, prefix="") + + # add last purchase price + make_purchase_receipt(item_code=child, rate=42) + + bom = frappe.copy_doc(bom) + bom.docstatus = 0 + bom.amended_from = None + bom.rm_cost_as_per = "Last Purchase Rate" + bom.conversion_rate = 1 + bom.save() + bom.submit() + self.assertEqual(bom.items[0].rate, 42) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) + def level_order_traversal(node): traversal = [] q = deque() @@ -403,9 +520,9 @@ def level_order_traversal(node): return traversal + def create_nested_bom(tree, prefix="_Test bom "): - """ Helper function to create a simple nested bom from tree describing item names. (along with required items) - """ + """Helper function to create a simple nested bom from tree describing item names. (along with required items)""" def create_items(bom_tree): for item_code, subtree in bom_tree.items(): @@ -413,6 +530,7 @@ def create_nested_bom(tree, prefix="_Test bom "): if not frappe.db.exists("Item", bom_item_code): frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() create_items(subtree) + create_items(tree) def dfs(tree, node): @@ -434,6 +552,7 @@ def create_nested_bom(tree, prefix="_Test bom "): bom = frappe.get_doc(doctype="BOM", item=bom_item_code) for child_item in child_items.keys(): bom.append("items", {"item_code": prefix + child_item}) + bom.company = "_Test Company" bom.currency = "INR" bom.insert() bom.submit() @@ -446,10 +565,13 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non warehouse_list = [warehouse_list] if not warehouse_list: - warehouse_list = frappe.db.sql_list(""" + warehouse_list = frappe.db.sql_list( + """ select warehouse from `tabBin` where item_code=%s and actual_qty > 0 - """, item_code) + """, + item_code, + ) if not warehouse_list: warehouse_list.append("_Test Warehouse - _TC") @@ -457,44 +579,51 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non for warehouse in warehouse_list: create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) + def create_bom_with_process_loss_item( - fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1): + fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1 +): bom_doc = frappe.new_doc("BOM") bom_doc.item = fg_item.item_code bom_doc.quantity = fg_qty - bom_doc.append("items", { - "item_code": bom_item.item_code, - "qty": 1, - "uom": bom_item.stock_uom, - "stock_uom": bom_item.stock_uom, - "rate": 100.0 - }) - bom_doc.append("scrap_items", { - "item_code": fg_item.item_code, - "qty": scrap_qty, - "stock_qty": scrap_qty, - "uom": fg_item.stock_uom, - "stock_uom": fg_item.stock_uom, - "rate": scrap_rate, - "is_process_loss": is_process_loss - }) + bom_doc.append( + "items", + { + "item_code": bom_item.item_code, + "qty": 1, + "uom": bom_item.stock_uom, + "stock_uom": bom_item.stock_uom, + "rate": 100.0, + }, + ) + bom_doc.append( + "scrap_items", + { + "item_code": fg_item.item_code, + "qty": scrap_qty, + "stock_qty": scrap_qty, + "uom": fg_item.stock_uom, + "stock_uom": fg_item.stock_uom, + "rate": scrap_rate, + "is_process_loss": is_process_loss, + }, + ) bom_doc.currency = "INR" return bom_doc + def create_process_loss_bom_items(): item_list = [ ("_Test Item - Non Whole UOM", "Kg"), ("_Test Item - Whole UOM", "Unit"), - ("_Test PL BOM Item", "Unit") + ("_Test PL BOM Item", "Unit"), ] return [create_process_loss_bom_item(it) for it in item_list] + def create_process_loss_bom_item(item_tuple): item_code, stock_uom = item_tuple if frappe.db.exists("Item", item_code) is None: - return make_item( - item_code, - {'stock_uom':stock_uom, 'valuation_rate':100} - ) + return make_item(item_code, {"stock_uom": stock_uom, "valuation_rate": 100}) else: return frappe.get_doc("Item", item_code) diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index ec617f3aaa9..210c0ea6a72 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -65,7 +65,8 @@ "label": "Hour Rate", "oldfieldname": "hour_rate", "oldfieldtype": "Currency", - "options": "currency" + "options": "currency", + "precision": "2" }, { "description": "In minutes", @@ -99,7 +100,6 @@ "read_only": 1 }, { - "default": "5", "depends_on": "eval:parent.doctype == 'BOM'", "fieldname": "base_operating_cost", "fieldtype": "Currency", @@ -177,7 +177,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-13 16:45:01.092868", + "modified": "2022-04-08 01:18:33.547481", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/bom_update_log/__init__.py b/erpnext/manufacturing/doctype/bom_update_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js new file mode 100644 index 00000000000..6da808e26d1 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('BOM Update Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json new file mode 100644 index 00000000000..98c1acb71ce --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -0,0 +1,109 @@ +{ + "actions": [], + "autoname": "BOM-UPDT-LOG-.#####", + "creation": "2022-03-16 14:23:35.210155", + "description": "BOM Update Tool Log with job status maintained", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "current_bom", + "new_bom", + "column_break_3", + "update_type", + "status", + "error_log", + "amended_from" + ], + "fields": [ + { + "fieldname": "current_bom", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Current BOM", + "options": "BOM" + }, + { + "fieldname": "new_bom", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "New BOM", + "options": "BOM" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "update_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Update Type", + "options": "Replace BOM\nUpdate Cost" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Queued\nIn Progress\nCompleted\nFailed" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "BOM Update Log", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "error_log", + "fieldtype": "Link", + "label": "Error Log", + "options": "Error Log" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2022-03-31 12:51:44.885102", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Update Log", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py new file mode 100644 index 00000000000..c3df96c99b1 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -0,0 +1,165 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt +from typing import Dict, List, Optional + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import cstr, flt +from typing_extensions import Literal + +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost + + +class BOMMissingError(frappe.ValidationError): + pass + + +class BOMUpdateLog(Document): + def validate(self): + if self.update_type == "Replace BOM": + self.validate_boms_are_specified() + self.validate_same_bom() + self.validate_bom_items() + + self.status = "Queued" + + def validate_boms_are_specified(self): + if self.update_type == "Replace BOM" and not (self.current_bom and self.new_bom): + frappe.throw( + msg=_("Please mention the Current and New BOM for replacement."), + title=_("Mandatory"), + exc=BOMMissingError, + ) + + def validate_same_bom(self): + if cstr(self.current_bom) == cstr(self.new_bom): + frappe.throw(_("Current BOM and New BOM can not be same")) + + def validate_bom_items(self): + current_bom_item = frappe.db.get_value("BOM", self.current_bom, "item") + new_bom_item = frappe.db.get_value("BOM", self.new_bom, "item") + + if current_bom_item != new_bom_item: + frappe.throw(_("The selected BOMs are not for the same item")) + + def on_submit(self): + if frappe.flags.in_test: + return + + if self.update_type == "Replace BOM": + boms = {"current_bom": self.current_bom, "new_bom": self.new_bom} + frappe.enqueue( + method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job", + doc=self, + boms=boms, + timeout=40000, + ) + else: + frappe.enqueue( + method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job", + doc=self, + update_type="Update Cost", + timeout=40000, + ) + + +def replace_bom(boms: Dict) -> None: + """Replace current BOM with new BOM in parent BOMs.""" + current_bom = boms.get("current_bom") + new_bom = boms.get("new_bom") + + unit_cost = get_new_bom_unit_cost(new_bom) + update_new_bom_in_bom_items(unit_cost, current_bom, new_bom) + + frappe.cache().delete_key("bom_children") + parent_boms = get_parent_boms(new_bom) + + for bom in parent_boms: + bom_obj = frappe.get_doc("BOM", bom) + # this is only used for versioning and we do not want + # to make separate db calls by using load_doc_before_save + # which proves to be expensive while doing bulk replace + bom_obj._doc_before_save = bom_obj + bom_obj.update_exploded_items() + bom_obj.calculate_cost() + bom_obj.update_parent_cost() + bom_obj.db_update() + if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version: + bom_obj.save_version() + + +def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: + bom_item = frappe.qb.DocType("BOM Item") + ( + frappe.qb.update(bom_item) + .set(bom_item.bom_no, new_bom) + .set(bom_item.rate, unit_cost) + .set(bom_item.amount, (bom_item.stock_qty * unit_cost)) + .where( + (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM") + ) + ).run() + + +def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List: + bom_list = bom_list or [] + bom_item = frappe.qb.DocType("BOM Item") + + parents = ( + frappe.qb.from_(bom_item) + .select(bom_item.parent) + .where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) + .run(as_dict=True) + ) + + for d in parents: + if new_bom == d.parent: + frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) + + bom_list.append(d.parent) + get_parent_boms(d.parent, bom_list) + + return list(set(bom_list)) + + +def get_new_bom_unit_cost(new_bom: str) -> float: + bom = frappe.qb.DocType("BOM") + new_bom_unitcost = ( + frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run() + ) + + return flt(new_bom_unitcost[0][0]) + + +def run_bom_job( + doc: "BOMUpdateLog", + boms: Optional[Dict[str, str]] = None, + update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", +) -> None: + try: + doc.db_set("status", "In Progress") + if not frappe.flags.in_test: + frappe.db.commit() + + frappe.db.auto_commit_on_many_writes = 1 + + boms = frappe._dict(boms or {}) + + if update_type == "Replace BOM": + replace_bom(boms) + else: + update_cost() + + doc.db_set("status", "Completed") + + except Exception: + frappe.db.rollback() + error_log = frappe.log_error(message=frappe.get_traceback(), title=_("BOM Update Tool Error")) + + doc.db_set("status", "Failed") + doc.db_set("error_log", error_log.name) + + finally: + frappe.db.auto_commit_on_many_writes = 0 + frappe.db.commit() # nosemgrep diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js new file mode 100644 index 00000000000..e39b5637c78 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js @@ -0,0 +1,13 @@ +frappe.listview_settings['BOM Update Log'] = { + add_fields: ["status"], + get_indicator: function(doc) { + let status_map = { + "Queued": "orange", + "In Progress": "blue", + "Completed": "green", + "Failed": "red" + }; + + return [__(doc.status), status_map[doc.status], "status,=," + doc.status]; + } +}; \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py new file mode 100644 index 00000000000..47efea961b4 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -0,0 +1,96 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import ( + BOMMissingError, + run_bom_job, +) +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom + +test_records = frappe.get_test_records("BOM") + + +class TestBOMUpdateLog(FrappeTestCase): + "Test BOM Update Tool Operations via BOM Update Log." + + def setUp(self): + bom_doc = frappe.copy_doc(test_records[0]) + bom_doc.items[1].item_code = "_Test Item" + bom_doc.insert() + + self.boms = frappe._dict( + current_bom="BOM-_Test Item Home Desktop Manufactured-001", + new_bom=bom_doc.name, + ) + + self.new_bom_doc = bom_doc + + def tearDown(self): + frappe.db.rollback() + + if self._testMethodName == "test_bom_update_log_completion": + # clear logs and delete BOM created via setUp + frappe.db.delete("BOM Update Log") + self.new_bom_doc.cancel() + self.new_bom_doc.delete() + + # explicitly commit and restore to original state + frappe.db.commit() # nosemgrep + + def test_bom_update_log_validate(self): + "Test if BOM presence is validated." + + with self.assertRaises(BOMMissingError): + enqueue_replace_bom(boms={}) + + with self.assertRaises(frappe.ValidationError): + enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom=self.boms.new_bom)) + + with self.assertRaises(frappe.ValidationError): + enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM")) + + def test_bom_update_log_queueing(self): + "Test if BOM Update Log is created and queued." + + log = enqueue_replace_bom( + boms=self.boms, + ) + + self.assertEqual(log.docstatus, 1) + self.assertEqual(log.status, "Queued") + + def test_bom_update_log_completion(self): + "Test if BOM Update Log handles job completion correctly." + + log = enqueue_replace_bom( + boms=self.boms, + ) + + # Explicitly commits log, new bom (setUp) and replacement impact. + # Is run via background jobs IRL + run_bom_job( + doc=log, + boms=self.boms, + update_type="Replace BOM", + ) + log.reload() + + self.assertEqual(log.status, "Completed") + + # teardown (undo replace impact) due to commit + boms = frappe._dict( + current_bom=self.boms.new_bom, + new_bom=self.boms.current_bom, + ) + log2 = enqueue_replace_bom( + boms=self.boms, + ) + run_bom_job( # Explicitly commits + doc=log2, + boms=boms, + update_type="Replace BOM", + ) + self.assertEqual(log2.status, "Completed") diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index bf5fe2e18de..7ba6517a4fb 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -20,30 +20,67 @@ frappe.ui.form.on('BOM Update Tool', { refresh: function(frm) { frm.disable_save(); + frm.events.disable_button(frm, "replace"); + + frm.add_custom_button(__("View BOM Update Log"), () => { + frappe.set_route("List", "BOM Update Log"); + }); }, - replace: function(frm) { + disable_button: (frm, field, disable=true) => { + frm.get_field(field).input.disabled = disable; + }, + + current_bom: (frm) => { + if (frm.doc.current_bom && frm.doc.new_bom) { + frm.events.disable_button(frm, "replace", false); + } + }, + + new_bom: (frm) => { + if (frm.doc.current_bom && frm.doc.new_bom) { + frm.events.disable_button(frm, "replace", false); + } + }, + + replace: (frm) => { if (frm.doc.current_bom && frm.doc.new_bom) { frappe.call({ method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_replace_bom", freeze: true, args: { - args: { + boms: { "current_bom": frm.doc.current_bom, "new_bom": frm.doc.new_bom } + }, + callback: result => { + if (result && result.message && !result.exc) { + frm.events.confirm_job_start(frm, result.message); + } } }); } }, - update_latest_price_in_all_boms: function() { + update_latest_price_in_all_boms: (frm) => { frappe.call({ method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_update_cost", freeze: true, - callback: function() { - frappe.msgprint(__("Latest price updated in all BOMs")); + callback: result => { + if (result && result.message && !result.exc) { + frm.events.confirm_job_start(frm, result.message); + } } }); + }, + + confirm_job_start: (frm, log_data) => { + let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true); + frappe.msgprint({ + "message": __("BOM Updation is queued and may take a few minutes. Check {0} for progress.", [log_link]), + "title": __("BOM Update Initiated"), + "indicator": "blue" + }); } }); diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index f9c3b062179..4061c5af7c2 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -1,114 +1,71 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import json +from typing import TYPE_CHECKING, Dict, Optional, Union + +from typing_extensions import Literal + +if TYPE_CHECKING: + from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog -import click import frappe -from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, flt -from six import string_types from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order class BOMUpdateTool(Document): - def replace_bom(self): - self.validate_bom() + pass - unit_cost = get_new_bom_unit_cost(self.new_bom) - self.update_new_bom(unit_cost) - - frappe.cache().delete_key('bom_children') - bom_list = self.get_parent_boms(self.new_bom) - - with click.progressbar(bom_list) as bom_list: - pass - for bom in bom_list: - try: - bom_obj = frappe.get_cached_doc('BOM', bom) - # this is only used for versioning and we do not want - # to make separate db calls by using load_doc_before_save - # which proves to be expensive while doing bulk replace - bom_obj._doc_before_save = bom_obj - bom_obj.update_new_bom(self.current_bom, self.new_bom, unit_cost) - bom_obj.update_exploded_items() - bom_obj.calculate_cost() - bom_obj.update_parent_cost() - bom_obj.db_update() - if bom_obj.meta.get('track_changes') and not bom_obj.flags.ignore_version: - bom_obj.save_version() - except Exception: - frappe.log_error(frappe.get_traceback()) - - def validate_bom(self): - if cstr(self.current_bom) == cstr(self.new_bom): - frappe.throw(_("Current BOM and New BOM can not be same")) - - if frappe.db.get_value("BOM", self.current_bom, "item") \ - != frappe.db.get_value("BOM", self.new_bom, "item"): - frappe.throw(_("The selected BOMs are not for the same item")) - - def update_new_bom(self, unit_cost): - frappe.db.sql("""update `tabBOM Item` set bom_no=%s, - rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""", - (self.new_bom, unit_cost, unit_cost, self.current_bom)) - - def get_parent_boms(self, bom, bom_list=None): - if bom_list is None: - bom_list = [] - data = frappe.db.sql("""SELECT DISTINCT parent FROM `tabBOM Item` - WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", bom) - - for d in data: - if self.new_bom == d[0]: - frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(bom, self.new_bom)) - - bom_list.append(d[0]) - self.get_parent_boms(d[0], bom_list) - - return list(set(bom_list)) - -def get_new_bom_unit_cost(bom): - new_bom_unitcost = frappe.db.sql("""SELECT `total_cost`/`quantity` - FROM `tabBOM` WHERE name = %s""", bom) - - return flt(new_bom_unitcost[0][0]) if new_bom_unitcost else 0 @frappe.whitelist() -def enqueue_replace_bom(args): - if isinstance(args, string_types): - args = json.loads(args) +def enqueue_replace_bom( + boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None +) -> "BOMUpdateLog": + """Returns a BOM Update Log (that queues a job) for BOM Replacement.""" + boms = boms or args + if isinstance(boms, str): + boms = json.loads(boms) + + update_log = create_bom_update_log(boms=boms) + return update_log - frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", args=args, timeout=40000) - frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) @frappe.whitelist() -def enqueue_update_cost(): - frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000) - frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) +def enqueue_update_cost() -> "BOMUpdateLog": + """Returns a BOM Update Log (that queues a job) for BOM Cost Updation.""" + update_log = create_bom_update_log(update_type="Update Cost") + return update_log -def update_latest_price_in_all_boms(): + +def auto_update_latest_price_in_all_boms() -> None: + """Called via hooks.py.""" if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): update_cost() -def replace_bom(args): - frappe.db.auto_commit_on_many_writes = 1 - args = frappe._dict(args) - doc = frappe.get_doc("BOM Update Tool") - doc.current_bom = args.current_bom - doc.new_bom = args.new_bom - doc.replace_bom() - - frappe.db.auto_commit_on_many_writes = 0 - -def update_cost(): - frappe.db.auto_commit_on_many_writes = 1 +def update_cost() -> None: + """Updates Cost for all BOMs from bottom to top.""" bom_list = get_boms_in_bottom_up_order() for bom in bom_list: frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) - frappe.db.auto_commit_on_many_writes = 0 + +def create_bom_update_log( + boms: Optional[Dict[str, str]] = None, + update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", +) -> "BOMUpdateLog": + """Creates a BOM Update Log that handles the background job.""" + + boms = boms or {} + current_bom = boms.get("current_bom") + new_bom = boms.get("new_bom") + return frappe.get_doc( + { + "doctype": "BOM Update Log", + "current_bom": current_bom, + "new_bom": new_bom, + "update_type": update_type, + } + ).submit() diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 12576cbf322..fae72a0f6f7 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -2,15 +2,19 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.tests.utils import FrappeTestCase +from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item -from erpnext.tests.utils import ERPNextTestCase -test_records = frappe.get_test_records('BOM') +test_records = frappe.get_test_records("BOM") + + +class TestBOMUpdateTool(FrappeTestCase): + "Test major functions run via BOM Update Tool." -class TestBOMUpdateTool(ERPNextTestCase): def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" @@ -18,18 +22,16 @@ class TestBOMUpdateTool(ERPNextTestCase): bom_doc.items[1].item_code = "_Test Item" bom_doc.insert() - update_tool = frappe.get_doc("BOM Update Tool") - update_tool.current_bom = current_bom - update_tool.new_bom = bom_doc.name - update_tool.replace_bom() + boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name) + replace_bom(boms) self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom)) self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name)) # reverse, as it affects other testcases - update_tool.current_bom = bom_doc.name - update_tool.new_bom = current_bom - update_tool.replace_bom() + boms.current_bom = bom_doc.name + boms.new_bom = current_bom + replace_bom(boms) def test_bom_cost(self): for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: @@ -37,10 +39,13 @@ class TestBOMUpdateTool(ERPNextTestCase): if item_doc.valuation_rate != 100.00: frappe.db.set_value("Item", item_doc.name, "valuation_rate", 100) - bom_no = frappe.db.get_value('BOM', {'item': 'BOM Cost Test Item 1'}, "name") + bom_no = frappe.db.get_value("BOM", {"item": "BOM Cost Test Item 1"}, "name") if not bom_no: - doc = make_bom(item = 'BOM Cost Test Item 1', - raw_materials =['BOM Cost Test Item 2', 'BOM Cost Test Item 3'], currency="INR") + doc = make_bom( + item="BOM Cost Test Item 1", + raw_materials=["BOM Cost Test Item 2", "BOM Cost Test Item 3"], + currency="INR", + ) else: doc = frappe.get_doc("BOM", bom_no) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 768cea33dae..20bbbeaa8ea 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -28,12 +28,12 @@ frappe.ui.form.on('Job Card', { frappe.flags.resume_job = 0; let has_items = frm.doc.items && frm.doc.items.length; - if (frm.doc.__onload.work_order_stopped) { + if (!frm.is_new() && frm.doc.__onload.work_order_stopped) { frm.disable_save(); return; } - if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) { + if (!frm.is_new() && has_items && frm.doc.docstatus < 2) { let to_request = frm.doc.for_quantity > frm.doc.transferred_qty; let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer; @@ -73,7 +73,18 @@ frappe.ui.form.on('Job Card', { if (frm.doc.docstatus == 0 && !frm.is_new() && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { - frm.trigger("prepare_timer_buttons"); + + // if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started" + // and if stock mvt for WIP is required + if (frm.doc.work_order) { + frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => { + if (result.skip_transfer === 1 || result.status == 'In Process') { + frm.trigger("prepare_timer_buttons"); + } + }); + } else { + frm.trigger("prepare_timer_buttons"); + } } frm.trigger("setup_quality_inspection"); diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 137561aa2a9..dd5bb89bee3 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -420,7 +420,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-11-12 10:15:03.572401", + "modified": "2021-11-12 10:15:06.572401", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3c406156ebd..c8b37a9c80d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -26,15 +26,27 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings ) -class OverlapError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + + +class OperationMismatchError(frappe.ValidationError): + pass + + +class OperationSequenceError(frappe.ValidationError): + pass + + +class JobCardCancelError(frappe.ValidationError): + pass -class OperationMismatchError(frappe.ValidationError): pass -class OperationSequenceError(frappe.ValidationError): pass -class JobCardCancelError(frappe.ValidationError): pass class JobCard(Document): def onload(self): - excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") + excess_transfer = frappe.db.get_single_value( + "Manufacturing Settings", "job_card_excess_transfer" + ) self.set_onload("job_card_excess_transfer", excess_transfer) self.set_onload("work_order_stopped", self.is_work_order_stopped()) @@ -48,27 +60,35 @@ class JobCard(Document): self.validate_work_order() def set_sub_operations(self): - if self.operation: + if not self.sub_operations and self.operation: self.sub_operations = [] - for row in frappe.get_all('Sub Operation', - filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'): - row.status = 'Pending' + for row in frappe.get_all( + "Sub Operation", + filters={"parent": self.operation}, + fields=["operation", "idx"], + order_by="idx", + ): + row.status = "Pending" row.sub_operation = row.operation - self.append('sub_operations', row) + self.append("sub_operations", row) def validate_time_logs(self): self.total_time_in_mins = 0.0 self.total_completed_qty = 0.0 - if self.get('time_logs'): - for d in self.get('time_logs'): + if self.get("time_logs"): + for d in self.get("time_logs"): if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time): frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx)) data = self.get_overlap_for(d) if data: - frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}") - .format(d.idx, self.name, data.name), OverlapError) + frappe.throw( + _("Row {0}: From Time and To Time of {1} is overlapping with {2}").format( + d.idx, self.name, data.name + ), + OverlapError, + ) if d.from_time and d.to_time: d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 @@ -86,8 +106,9 @@ class JobCard(Document): production_capacity = 1 if self.workstation: - production_capacity = frappe.get_cached_value("Workstation", - self.workstation, 'production_capacity') or 1 + production_capacity = ( + frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1 + ) validate_overlap_for = " and jc.workstation = %(workstation)s " if args.get("employee"): @@ -95,11 +116,12 @@ class JobCard(Document): production_capacity = 1 validate_overlap_for = " and jctl.employee = %(employee)s " - extra_cond = '' + extra_cond = "" if check_next_available_slot: extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)" - existing = frappe.db.sql("""select jc.name as name, jctl.to_time from + existing = frappe.db.sql( + """select jc.name as name, jctl.to_time from `tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and ( (%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or @@ -107,15 +129,19 @@ class JobCard(Document): (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0} ) and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1} - order by jctl.to_time desc limit 1""".format(extra_cond, validate_overlap_for), + order by jctl.to_time desc limit 1""".format( + extra_cond, validate_overlap_for + ), { "from_time": args.from_time, "to_time": args.to_time, "name": args.name or "No Name", "parent": args.parent or "No Name", "employee": args.get("employee"), - "workstation": self.workstation - }, as_dict=True) + "workstation": self.workstation, + }, + as_dict=True, + ) if existing and production_capacity > len(existing): return @@ -125,10 +151,7 @@ class JobCard(Document): def schedule_time_logs(self, row): row.remaining_time_in_mins = row.time_in_mins while row.remaining_time_in_mins > 0: - args = frappe._dict({ - "from_time": row.planned_start_time, - "to_time": row.planned_end_time - }) + args = frappe._dict({"from_time": row.planned_start_time, "to_time": row.planned_end_time}) self.validate_overlap_for_workstation(args, row) self.check_workstation_time(row) @@ -141,13 +164,16 @@ class JobCard(Document): def check_workstation_time(self, row): workstation_doc = frappe.get_cached_doc("Workstation", self.workstation) - if (not workstation_doc.working_hours or - cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime"))): + if not workstation_doc.working_hours or cint( + frappe.db.get_single_value("Manufacturing Settings", "allow_overtime") + ): if get_datetime(row.planned_end_time) < get_datetime(row.planned_start_time): row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.time_in_mins) row.remaining_time_in_mins = 0.0 else: - row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time, row.planned_start_time) + row.remaining_time_in_mins -= time_diff_in_minutes( + row.planned_end_time, row.planned_start_time + ) self.update_time_logs(row) return @@ -167,14 +193,15 @@ class JobCard(Document): workstation_start_time = datetime.datetime.combine(start_date, get_time(time_slot.start_time)) workstation_end_time = datetime.datetime.combine(start_date, get_time(time_slot.end_time)) - if (get_datetime(row.planned_start_time) >= workstation_start_time and - get_datetime(row.planned_start_time) <= workstation_end_time): + if ( + get_datetime(row.planned_start_time) >= workstation_start_time + and get_datetime(row.planned_start_time) <= workstation_end_time + ): time_in_mins = time_diff_in_minutes(workstation_end_time, row.planned_start_time) # If remaining time fit in workstation time logs else split hours as per workstation time if time_in_mins > row.remaining_time_in_mins: - row.planned_end_time = add_to_date(row.planned_start_time, - minutes=row.remaining_time_in_mins) + row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.remaining_time_in_mins) row.remaining_time_in_mins = 0 else: row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins) @@ -182,14 +209,16 @@ class JobCard(Document): self.update_time_logs(row) - if total_idx != (i+1) and row.remaining_time_in_mins > 0: - row.planned_start_time = datetime.datetime.combine(start_date, - get_time(workstation_doc.working_hours[i+1].start_time)) + if total_idx != (i + 1) and row.remaining_time_in_mins > 0: + row.planned_start_time = datetime.datetime.combine( + start_date, get_time(workstation_doc.working_hours[i + 1].start_time) + ) if row.remaining_time_in_mins > 0: start_date = add_days(start_date, 1) - row.planned_start_time = datetime.datetime.combine(start_date, - get_time(workstation_doc.working_hours[0].start_time)) + row.planned_start_time = datetime.datetime.combine( + start_date, get_time(workstation_doc.working_hours[0].start_time) + ) def add_time_log(self, args): last_row = [] @@ -204,21 +233,25 @@ class JobCard(Document): if last_row and args.get("complete_time"): for row in self.time_logs: if not row.to_time: - row.update({ - "to_time": get_datetime(args.get("complete_time")), - "operation": args.get("sub_operation"), - "completed_qty": args.get("completed_qty") or 0.0 - }) + row.update( + { + "to_time": get_datetime(args.get("complete_time")), + "operation": args.get("sub_operation"), + "completed_qty": args.get("completed_qty") or 0.0, + } + ) elif args.get("start_time"): - new_args = frappe._dict({ - "from_time": get_datetime(args.get("start_time")), - "operation": args.get("sub_operation"), - "completed_qty": 0.0 - }) + new_args = frappe._dict( + { + "from_time": get_datetime(args.get("start_time")), + "operation": args.get("sub_operation"), + "completed_qty": 0.0, + } + ) if employees: for name in employees: - new_args.employee = name.get('employee') + new_args.employee = name.get("employee") self.add_start_time_log(new_args) else: self.add_start_time_log(new_args) @@ -236,10 +269,7 @@ class JobCard(Document): def set_employees(self, employees): for name in employees: - self.append('employee', { - 'employee': name.get('employee'), - 'completed_qty': 0.0 - }) + self.append("employee", {"employee": name.get("employee"), "completed_qty": 0.0}) def reset_timer_value(self, args): self.started_time = None @@ -263,13 +293,17 @@ class JobCard(Document): operation_wise_completed_time = {} for time_log in self.time_logs: if time_log.operation not in operation_wise_completed_time: - operation_wise_completed_time.setdefault(time_log.operation, - frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []})) + operation_wise_completed_time.setdefault( + time_log.operation, + frappe._dict( + {"status": "Pending", "completed_qty": 0.0, "completed_time": 0.0, "employee": []} + ), + ) op_row = operation_wise_completed_time[time_log.operation] op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" - if self.status == 'On Hold': - op_row.status = 'Pause' + if self.status == "On Hold": + op_row.status = "Pause" op_row.employee.append(time_log.employee) if time_log.time_in_mins: @@ -279,7 +313,7 @@ class JobCard(Document): for row in self.sub_operations: operation_deatils = operation_wise_completed_time.get(row.sub_operation) if operation_deatils: - if row.status != 'Complete': + if row.status != "Complete": row.status = operation_deatils.status row.completed_time = operation_deatils.completed_time @@ -289,43 +323,52 @@ class JobCard(Document): if operation_deatils.completed_qty: row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee)) else: - row.status = 'Pending' + row.status = "Pending" row.completed_time = 0.0 row.completed_qty = 0.0 def update_time_logs(self, row): - self.append("time_logs", { - "from_time": row.planned_start_time, - "to_time": row.planned_end_time, - "completed_qty": 0, - "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time), - }) + self.append( + "time_logs", + { + "from_time": row.planned_start_time, + "to_time": row.planned_end_time, + "completed_qty": 0, + "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time), + }, + ) @frappe.whitelist() def get_required_items(self): - if not self.get('work_order'): + if not self.get("work_order"): return - doc = frappe.get_doc('Work Order', self.get('work_order')) - if doc.transfer_material_against == 'Work Order' or doc.skip_transfer: + doc = frappe.get_doc("Work Order", self.get("work_order")) + if doc.transfer_material_against == "Work Order" or doc.skip_transfer: return for d in doc.required_items: if not d.operation: - frappe.throw(_("Row {0} : Operation is required against the raw material item {1}") - .format(d.idx, d.item_code)) + frappe.throw( + _("Row {0} : Operation is required against the raw material item {1}").format( + d.idx, d.item_code + ) + ) - if self.get('operation') == d.operation: - self.append('items', { - "item_code": d.item_code, - "source_warehouse": d.source_warehouse, - "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'), - "item_name": d.item_name, - "description": d.description, - "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty, - "rate": d.rate, - "amount": d.amount - }) + if self.get("operation") == d.operation: + self.append( + "items", + { + "item_code": d.item_code, + "source_warehouse": d.source_warehouse, + "uom": frappe.db.get_value("Item", d.item_code, "stock_uom"), + "item_name": d.item_name, + "description": d.description, + "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty, + "rate": d.rate, + "amount": d.amount, + }, + ) def on_submit(self): self.validate_transfer_qty() @@ -339,31 +382,52 @@ class JobCard(Document): def validate_transfer_qty(self): if self.items and self.transferred_qty < self.for_quantity: - frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}') - .format(self.name)) + frappe.throw( + _( + "Materials needs to be transferred to the work in progress warehouse for the job card {0}" + ).format(self.name) + ) def validate_job_card(self): - if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped': - frappe.throw(_("Transaction not allowed against stopped Work Order {0}") - .format(get_link_to_form('Work Order', self.work_order))) + if ( + self.work_order + and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped" + ): + frappe.throw( + _("Transaction not allowed against stopped Work Order {0}").format( + get_link_to_form("Work Order", self.work_order) + ) + ) if not self.time_logs: - frappe.throw(_("Time logs are required for {0} {1}") - .format(bold("Job Card"), get_link_to_form("Job Card", self.name))) + frappe.throw( + _("Time logs are required for {0} {1}").format( + bold("Job Card"), get_link_to_form("Job Card", self.name) + ) + ) if self.for_quantity and self.total_completed_qty != self.for_quantity: total_completed_qty = bold(_("Total Completed Qty")) qty_to_manufacture = bold(_("Qty to Manufacture")) - frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})") - .format(total_completed_qty, bold(self.total_completed_qty), qty_to_manufacture,bold(self.for_quantity))) + frappe.throw( + _("The {0} ({1}) must be equal to {2} ({3})").format( + total_completed_qty, + bold(self.total_completed_qty), + qty_to_manufacture, + bold(self.for_quantity), + ) + ) def update_work_order(self): if not self.work_order: return - if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings', - 'add_corrective_operation_cost_in_finished_good_valuation')): + if self.is_corrective_job_card and not cint( + frappe.db.get_single_value( + "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" + ) + ): return for_quantity, time_in_mins = 0, 0 @@ -375,7 +439,7 @@ class JobCard(Document): for_quantity = flt(data[0].completed_qty) time_in_mins = flt(data[0].time_in_mins) - wo = frappe.get_doc('Work Order', self.work_order) + wo = frappe.get_doc("Work Order", self.work_order) if self.is_corrective_job_card: self.update_corrective_in_work_order(wo) @@ -386,8 +450,11 @@ class JobCard(Document): def update_corrective_in_work_order(self, wo): wo.corrective_operation_cost = 0.0 - for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'], - filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}): + for row in frappe.get_all( + "Job Card", + fields=["total_time_in_mins", "hour_rate"], + filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order}, + ): wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) wo.calculate_operating_cost() @@ -395,27 +462,37 @@ class JobCard(Document): wo.save() def validate_produced_quantity(self, for_quantity, wo): - if self.docstatus < 2: return + if self.docstatus < 2: + return if wo.produced_qty > for_quantity: - first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.") - .format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item))) + first_part_msg = _( + "The {0} {1} is used to calculate the valuation cost for the finished good {2}." + ).format( + frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item) + ) - second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.") - .format(frappe.bold(get_link_to_form("Work Order", self.work_order)))) + second_part_msg = _( + "Kindly cancel the Manufacturing Entries first against the work order {0}." + ).format(frappe.bold(get_link_to_form("Work Order", self.work_order))) - frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg), - JobCardCancelError, title = _("Error")) + frappe.throw( + _("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error") + ) def update_work_order_data(self, for_quantity, time_in_mins, wo): - time_data = frappe.db.sql(""" + time_data = frappe.db.sql( + """ SELECT min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0 - """, (self.work_order, self.operation_id), as_dict=1) + """, + (self.work_order, self.operation_id), + as_dict=1, + ) for data in wo.operations: if data.get("name") == self.operation_id: @@ -434,92 +511,118 @@ class JobCard(Document): wo.save() def get_current_operation_data(self): - return frappe.get_all('Job Card', - fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id, - "is_corrective_job_card": 0}) + return frappe.get_all( + "Job Card", + fields=["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], + filters={ + "docstatus": 1, + "work_order": self.work_order, + "operation_id": self.operation_id, + "is_corrective_job_card": 0, + }, + ) def set_transferred_qty_in_job_card(self, ste_doc): for row in ste_doc.items: - if not row.job_card_item: continue + if not row.job_card_item: + continue - qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se + qty = frappe.db.sql( + """ SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and se.purpose = 'Material Transfer for Manufacture' - """, (row.job_card_item))[0][0] + """, + (row.job_card_item), + )[0][0] - frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty)) + frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty)) def set_transferred_qty(self, update_status=False): "Set total FG Qty for which RM was transferred." if not self.items: self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 - doc = frappe.get_doc('Work Order', self.get('work_order')) - if doc.transfer_material_against == 'Work Order' or doc.skip_transfer: + doc = frappe.get_doc("Work Order", self.get("work_order")) + if doc.transfer_material_against == "Work Order" or doc.skip_transfer: return if self.items: # sum of 'For Quantity' of Stock Entries against JC - self.transferred_qty = frappe.db.get_value('Stock Entry', { - 'job_card': self.name, - 'work_order': self.work_order, - 'docstatus': 1, - 'purpose': 'Material Transfer for Manufacture' - }, 'sum(fg_completed_qty)') or 0 + self.transferred_qty = ( + frappe.db.get_value( + "Stock Entry", + { + "job_card": self.name, + "work_order": self.work_order, + "docstatus": 1, + "purpose": "Material Transfer for Manufacture", + }, + "sum(fg_completed_qty)", + ) + or 0 + ) self.db_set("transferred_qty", self.transferred_qty) qty = 0 if self.work_order: - doc = frappe.get_doc('Work Order', self.work_order) - if doc.transfer_material_against == 'Job Card' and not doc.skip_transfer: + doc = frappe.get_doc("Work Order", self.work_order) + if doc.transfer_material_against == "Job Card" and not doc.skip_transfer: completed = True for d in doc.operations: - if d.status != 'Completed': + if d.status != "Completed": completed = False break if completed: - job_cards = frappe.get_all('Job Card', filters = {'work_order': self.work_order, - 'docstatus': ('!=', 2)}, fields = 'sum(transferred_qty) as qty', group_by='operation_id') + job_cards = frappe.get_all( + "Job Card", + filters={"work_order": self.work_order, "docstatus": ("!=", 2)}, + fields="sum(transferred_qty) as qty", + group_by="operation_id", + ) if job_cards: qty = min(d.qty for d in job_cards) - doc.db_set('material_transferred_for_manufacturing', qty) + doc.db_set("material_transferred_for_manufacturing", qty) self.set_status(update_status) def set_status(self, update_status=False): - if self.status == "On Hold": return + if self.status == "On Hold": + return - self.status = { - 0: "Open", - 1: "Submitted", - 2: "Cancelled" - }[self.docstatus or 0] + self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] + + if self.for_quantity <= self.transferred_qty: + self.status = "Material Transferred" if self.time_logs: - self.status = 'Work In Progress' + self.status = "Work In Progress" - if (self.docstatus == 1 and - (self.for_quantity <= self.total_completed_qty or not self.items)): - self.status = 'Completed' - - if self.status != 'Completed': - if self.for_quantity <= self.transferred_qty: - self.status = 'Material Transferred' + if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items): + self.status = "Completed" if update_status: - self.db_set('status', self.status) + self.db_set("status", self.status) def validate_operation_id(self): - if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and - frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id): + if ( + self.get("operation_id") + and self.get("operation_row_number") + and self.operation + and self.work_order + and frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") + != self.operation_id + ): work_order = bold(get_link_to_form("Work Order", self.work_order)) - frappe.throw(_("Operation {0} does not belong to the work order {1}") - .format(bold(self.operation), work_order), OperationMismatchError) + frappe.throw( + _("Operation {0} does not belong to the work order {1}").format( + bold(self.operation), work_order + ), + OperationMismatchError, + ) def validate_sequence_id(self): if self.is_corrective_job_card: @@ -535,18 +638,25 @@ class JobCard(Document): current_operation_qty += flt(self.total_completed_qty) - data = frappe.get_all("Work Order Operation", - fields = ["operation", "status", "completed_qty"], - filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)}, - order_by = "sequence_id, idx") + data = frappe.get_all( + "Work Order Operation", + fields=["operation", "status", "completed_qty"], + filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, + order_by="sequence_id, idx", + ) - message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name), - bold(get_link_to_form("Work Order", self.work_order))) + message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format( + bold(self.name), bold(get_link_to_form("Work Order", self.work_order)) + ) for row in data: if row.status != "Completed" and row.completed_qty < current_operation_qty: - frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") - .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) + frappe.throw( + _("{0}, complete the operation {1} before the operation {2}.").format( + message, bold(row.operation), bold(self.operation) + ), + OperationSequenceError, + ) def validate_work_order(self): if self.is_work_order_stopped(): @@ -554,13 +664,14 @@ class JobCard(Document): def is_work_order_stopped(self): if self.work_order: - status = frappe.get_value('Work Order', self.work_order) + status = frappe.get_value("Work Order", self.work_order) if status == "Closed": return True return False + @frappe.whitelist() def make_time_log(args): if isinstance(args, str): @@ -571,16 +682,17 @@ def make_time_log(args): doc.validate_sequence_id() doc.add_time_log(args) + @frappe.whitelist() def get_operation_details(work_order, operation): if work_order and operation: - return frappe.get_all("Work Order Operation", fields = ["name", "idx"], - filters = { - "parent": work_order, - "operation": operation - } + return frappe.get_all( + "Work Order Operation", + fields=["name", "idx"], + filters={"parent": work_order, "operation": operation}, ) + @frappe.whitelist() def get_operations(doctype, txt, searchfield, start, page_len, filters): if not filters.get("work_order"): @@ -590,12 +702,16 @@ def get_operations(doctype, txt, searchfield, start, page_len, filters): if txt: args["operation"] = ("like", "%{0}%".format(txt)) - return frappe.get_all("Work Order Operation", - filters = args, - fields = ["distinct operation as operation"], - limit_start = start, - limit_page_length = page_len, - order_by="idx asc", as_list=1) + return frappe.get_all( + "Work Order Operation", + filters=args, + fields=["distinct operation as operation"], + limit_start=start, + limit_page_length=page_len, + order_by="idx asc", + as_list=1, + ) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): @@ -605,26 +721,29 @@ def make_material_request(source_name, target_doc=None): def set_missing_values(source, target): target.material_request_type = "Material Transfer" - doclist = get_mapped_doc("Job Card", source_name, { - "Job Card": { - "doctype": "Material Request", - "field_map": { - "name": "job_card", + doclist = get_mapped_doc( + "Job Card", + source_name, + { + "Job Card": { + "doctype": "Material Request", + "field_map": { + "name": "job_card", + }, + }, + "Job Card Item": { + "doctype": "Material Request Item", + "field_map": {"required_qty": "qty", "uom": "stock_uom", "name": "job_card_item"}, + "postprocess": update_item, }, }, - "Job Card Item": { - "doctype": "Material Request Item", - "field_map": { - "required_qty": "qty", - "uom": "stock_uom", - "name": "job_card_item" - }, - "postprocess": update_item, - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def make_stock_entry(source_name, target_doc=None): def update_item(source, target, source_parent): @@ -642,7 +761,7 @@ def make_stock_entry(source_name, target_doc=None): target.from_bom = 1 # avoid negative 'For Quantity' - pending_fg_qty = flt(source.get('for_quantity', 0)) - flt(source.get('transferred_qty', 0)) + pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0)) target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0 target.set_transfer_qty() @@ -650,36 +769,45 @@ def make_stock_entry(source_name, target_doc=None): target.set_missing_values() target.set_stock_entry_type() - wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item") + wo_allows_alternate_item = frappe.db.get_value( + "Work Order", target.work_order, "allow_alternative_item" + ) for item in target.items: - item.allow_alternative_item = int(wo_allows_alternate_item and - frappe.get_cached_value("Item", item.item_code, "allow_alternative_item")) + item.allow_alternative_item = int( + wo_allows_alternate_item + and frappe.get_cached_value("Item", item.item_code, "allow_alternative_item") + ) - doclist = get_mapped_doc("Job Card", source_name, { - "Job Card": { - "doctype": "Stock Entry", - "field_map": { - "name": "job_card", - "for_quantity": "fg_completed_qty" + doclist = get_mapped_doc( + "Job Card", + source_name, + { + "Job Card": { + "doctype": "Stock Entry", + "field_map": {"name": "job_card", "for_quantity": "fg_completed_qty"}, + }, + "Job Card Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "source_warehouse": "s_warehouse", + "required_qty": "qty", + "name": "job_card_item", + }, + "postprocess": update_item, + "condition": lambda doc: doc.required_qty > 0, }, }, - "Job Card Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "source_warehouse": "s_warehouse", - "required_qty": "qty", - "name": "job_card_item" - }, - "postprocess": update_item, - "condition": lambda doc: doc.required_qty > 0 - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + def time_diff_in_minutes(string_ed_date, string_st_date): return time_diff(string_ed_date, string_st_date).total_seconds() / 60 + @frappe.whitelist() def get_job_details(start, end, filters=None): events = [] @@ -687,41 +815,49 @@ def get_job_details(start, end, filters=None): event_color = { "Completed": "#cdf5a6", "Material Transferred": "#ffdd9e", - "Work In Progress": "#D3D3D3" + "Work In Progress": "#D3D3D3", } from frappe.desk.reportview import get_filters_cond + conditions = get_filters_cond("Job Card", filters, []) - job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order, + job_cards = frappe.db.sql( + """ SELECT `tabJob Card`.name, `tabJob Card`.work_order, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''), min(`tabJob Card Time Log`.from_time) as from_time, max(`tabJob Card Time Log`.to_time) as to_time FROM `tabJob Card` , `tabJob Card Time Log` WHERE `tabJob Card`.name = `tabJob Card Time Log`.parent {0} - group by `tabJob Card`.name""".format(conditions), as_dict=1) + group by `tabJob Card`.name""".format( + conditions + ), + as_dict=1, + ) for d in job_cards: - subject_data = [] - for field in ["name", "work_order", "remarks"]: - if not d.get(field): continue + subject_data = [] + for field in ["name", "work_order", "remarks"]: + if not d.get(field): + continue - subject_data.append(d.get(field)) + subject_data.append(d.get(field)) - color = event_color.get(d.status) - job_card_data = { - 'from_time': d.from_time, - 'to_time': d.to_time, - 'name': d.name, - 'subject': '\n'.join(subject_data), - 'color': color if color else "#89bcde" - } + color = event_color.get(d.status) + job_card_data = { + "from_time": d.from_time, + "to_time": d.to_time, + "name": d.name, + "subject": "\n".join(subject_data), + "color": color if color else "#89bcde", + } - events.append(job_card_data) + events.append(job_card_data) return events + @frappe.whitelist() def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None): def set_missing_values(source, target): @@ -729,20 +865,26 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.operation = operation target.for_operation = for_operation - target.set('time_logs', []) - target.set('employee', []) - target.set('items', []) + target.set("time_logs", []) + target.set("employee", []) + target.set("items", []) target.set_sub_operations() target.get_required_items() target.validate_time_logs() - doclist = get_mapped_doc("Job Card", source_name, { - "Job Card": { - "doctype": "Job Card", - "field_map": { - "name": "for_job_card", - }, - } - }, target_doc, set_missing_values) + doclist = get_mapped_doc( + "Job Card", + source_name, + { + "Job Card": { + "doctype": "Job Card", + "field_map": { + "name": "for_job_card", + }, + } + }, + target_doc, + set_missing_values, + ) return doclist diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py index 24362f8246d..14c1f36d0dc 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py +++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'job_card', - 'non_standard_fieldnames': { - 'Quality Inspection': 'reference_name' - }, - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Material Request', 'Stock Entry'] - }, - { - 'label': _('Reference'), - 'items': ['Quality Inspection'] - } - ] + "fieldname": "job_card", + "non_standard_fieldnames": {"Quality Inspection": "reference_name"}, + "transactions": [ + {"label": _("Transactions"), "items": ["Material Request", "Stock Entry"]}, + {"label": _("Reference"), "items": ["Quality Inspection"]}, + ], } diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index 8017209e7de..7f60bdc6d92 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -1,4 +1,5 @@ frappe.listview_settings['Job Card'] = { + has_indicator_for_draft: true, get_indicator: function(doc) { if (doc.status === "Work In Progress") { return [__("Work In Progress"), "orange", "status,=,Work In Progress"]; diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index bb5004ba86f..4647ddf05f7 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import random_string from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError @@ -11,22 +12,19 @@ from erpnext.manufacturing.doctype.job_card.job_card import ( from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase -class TestJobCard(ERPNextTestCase): +class TestJobCard(FrappeTestCase): def setUp(self): make_bom_for_jc_tests() transfer_material_against, source_warehouse = None, None - tests_that_skip_setup = ( - "test_job_card_material_transfer_correctness", - ) + tests_that_skip_setup = ("test_job_card_material_transfer_correctness",) tests_that_transfer_against_jc = ( "test_job_card_multiple_materials_transfer", "test_job_card_excess_material_transfer", - "test_job_card_partial_material_transfer" + "test_job_card_partial_material_transfer", ) if self._testMethodName in tests_that_skip_setup: @@ -40,7 +38,7 @@ class TestJobCard(ERPNextTestCase): item="_Test FG Item 2", qty=2, transfer_material_against=transfer_material_against, - source_warehouse=source_warehouse + source_warehouse=source_warehouse, ) def tearDown(self): @@ -48,8 +46,9 @@ class TestJobCard(ERPNextTestCase): def test_job_card(self): - job_cards = frappe.get_all('Job Card', - filters = {'work_order': self.work_order.name}, fields = ["operation_id", "name"]) + job_cards = frappe.get_all( + "Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"] + ) if job_cards: job_card = job_cards[0] @@ -63,30 +62,38 @@ class TestJobCard(ERPNextTestCase): frappe.delete_doc("Job Card", d.name) def test_job_card_with_different_work_station(self): - job_cards = frappe.get_all('Job Card', - filters = {'work_order': self.work_order.name}, - fields = ["operation_id", "workstation", "name", "for_quantity"]) + job_cards = frappe.get_all( + "Job Card", + filters={"work_order": self.work_order.name}, + fields=["operation_id", "workstation", "name", "for_quantity"], + ) job_card = job_cards[0] if job_card: - workstation = frappe.db.get_value("Workstation", - {"name": ("not in", [job_card.workstation])}, "name") + workstation = frappe.db.get_value( + "Workstation", {"name": ("not in", [job_card.workstation])}, "name" + ) if not workstation or job_card.workstation == workstation: workstation = make_workstation(workstation_name=random_string(5)).name doc = frappe.get_doc("Job Card", job_card.name) doc.workstation = workstation - doc.append("time_logs", { - "from_time": "2009-01-01 12:06:25", - "to_time": "2009-01-01 12:37:25", - "time_in_mins": "31.00002", - "completed_qty": job_card.for_quantity - }) + doc.append( + "time_logs", + { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "time_in_mins": "31.00002", + "completed_qty": job_card.for_quantity, + }, + ) doc.submit() - completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty") + completed_qty = frappe.db.get_value( + "Work Order Operation", job_card.operation_id, "completed_qty" + ) self.assertEqual(completed_qty, job_card.for_quantity) doc.cancel() @@ -97,51 +104,49 @@ class TestJobCard(ERPNextTestCase): def test_job_card_overlap(self): wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2) - jc1_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) - jc2_name = frappe.db.get_value("Job Card", {'work_order': wo2.name}) + jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) + jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name}) jc1 = frappe.get_doc("Job Card", jc1_name) jc2 = frappe.get_doc("Job Card", jc2_name) - employee = "_T-Employee-00001" # from test records + employee = "_T-Employee-00001" # from test records - jc1.append("time_logs", { - "from_time": "2021-01-01 00:00:00", - "to_time": "2021-01-01 08:00:00", - "completed_qty": 1, - "employee": employee, - }) + jc1.append( + "time_logs", + { + "from_time": "2021-01-01 00:00:00", + "to_time": "2021-01-01 08:00:00", + "completed_qty": 1, + "employee": employee, + }, + ) jc1.save() # add a new entry in same time slice - jc2.append("time_logs", { - "from_time": "2021-01-01 00:01:00", - "to_time": "2021-01-01 06:00:00", - "completed_qty": 1, - "employee": employee, - }) + jc2.append( + "time_logs", + { + "from_time": "2021-01-01 00:01:00", + "to_time": "2021-01-01 06:00:00", + "completed_qty": 1, + "employee": employee, + }, + ) self.assertRaises(OverlapError, jc2.save) def test_job_card_multiple_materials_transfer(self): "Test transferring RMs separately against Job Card with multiple RMs." + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100) make_stock_entry( - item_code="_Test Item", - target="Stores - _TC", - qty=10, - basic_rate=100 - ) - make_stock_entry( - item_code="_Test Item Home Desktop Manufactured", - target="Stores - _TC", - qty=6, - basic_rate=100 + item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100 ) - job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) job_card = frappe.get_doc("Job Card", job_card_name) transfer_entry_1 = make_stock_entry_from_jc(job_card_name) - del transfer_entry_1.items[1] # transfer only 1 of 2 RMs + del transfer_entry_1.items[1] # transfer only 1 of 2 RMs transfer_entry_1.insert() transfer_entry_1.submit() @@ -162,13 +167,14 @@ class TestJobCard(ERPNextTestCase): def test_job_card_excess_material_transfer(self): "Test transferring more than required RM against Job Card." - make_stock_entry(item_code="_Test Item", target="Stores - _TC", - qty=25, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop Manufactured", - target="Stores - _TC", qty=15, basic_rate=100) + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100 + ) - job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) job_card = frappe.get_doc("Job Card", job_card_name) + self.assertEqual(job_card.status, "Open") # fully transfer both RMs transfer_entry_1 = make_stock_entry_from_jc(job_card_name) @@ -192,11 +198,10 @@ class TestJobCard(ERPNextTestCase): transfer_entry_3 = make_stock_entry_from_jc(job_card_name) self.assertEqual(transfer_entry_3.fg_completed_qty, 0) - job_card.append("time_logs", { - "from_time": "2021-01-01 00:01:00", - "to_time": "2021-01-01 06:00:00", - "completed_qty": 2 - }) + job_card.append( + "time_logs", + {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2}, + ) job_card.save() job_card.submit() @@ -206,12 +211,12 @@ class TestJobCard(ERPNextTestCase): def test_job_card_partial_material_transfer(self): "Test partial material transfer against Job Card" - make_stock_entry(item_code="_Test Item", target="Stores - _TC", - qty=25, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop Manufactured", - target="Stores - _TC", qty=15, basic_rate=100) + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100 + ) - job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) job_card = frappe.get_doc("Job Card", job_card_name) # partially transfer @@ -241,15 +246,14 @@ class TestJobCard(ERPNextTestCase): def test_job_card_material_transfer_correctness(self): """ - 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card - 2. Test impact of changing 'For Qty' in such a Stock Entry + 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card + 2. Test impact of changing 'For Qty' in such a Stock Entry """ create_bom_with_multiple_operations() work_order = make_wo_with_transfer_against_jc() job_card_name = frappe.db.get_value( - "Job Card", - {"work_order": work_order.name,"operation": "Test Operation A"} + "Job Card", {"work_order": work_order.name, "operation": "Test Operation A"} ) job_card = frappe.get_doc("Job Card", job_card_name) @@ -275,6 +279,7 @@ class TestJobCard(ERPNextTestCase): # rollback via tearDown method + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" from erpnext.manufacturing.doctype.operation.test_operation import make_operation @@ -286,19 +291,22 @@ def create_bom_with_multiple_operations(): "operation": "Test Operation A", "workstation": "_Test Workstation A", "hour_rate_rent": 300, - "time_in_mins": 60 + "time_in_mins": 60, } make_workstation(row) make_operation(row) - bom_doc.append("operations", { - "operation": "Test Operation A", - "description": "Test Operation A", - "workstation": "_Test Workstation A", - "hour_rate": 300, - "time_in_mins": 60, - "operating_cost": 100 - }) + bom_doc.append( + "operations", + { + "operation": "Test Operation A", + "description": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate": 300, + "time_in_mins": 60, + "operating_cost": 100, + }, + ) bom_doc.transfer_material_against = "Job Card" bom_doc.save() @@ -306,6 +314,7 @@ def create_bom_with_multiple_operations(): return bom_doc + def make_wo_with_transfer_against_jc(): "Create a WO with multiple operations and Material Transfer against Job Card" @@ -314,7 +323,7 @@ def make_wo_with_transfer_against_jc(): qty=4, transfer_material_against="Job Card", source_warehouse="Stores - _TC", - do_not_submit=True + do_not_submit=True, ) work_order.required_items[0].operation = "Test Operation A" work_order.required_items[1].operation = "_Test Operation 1" @@ -322,8 +331,9 @@ def make_wo_with_transfer_against_jc(): return work_order + def make_bom_for_jc_tests(): - test_records = frappe.get_test_records('BOM') + test_records = frappe.get_test_records("BOM") bom = frappe.copy_doc(test_records[2]) bom.set_rate_of_sub_assembly_item_based_on_bom = 0 bom.rm_cost_as_per = "Valuation Rate" diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index c919e8bef1d..730a8575247 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -11,14 +11,19 @@ from frappe.utils import cint class ManufacturingSettings(Document): pass + def get_mins_between_operations(): - return relativedelta(minutes=cint(frappe.db.get_single_value("Manufacturing Settings", - "mins_between_operations")) or 10) + return relativedelta( + minutes=cint(frappe.db.get_single_value("Manufacturing Settings", "mins_between_operations")) + or 10 + ) + @frappe.whitelist() def is_material_consumption_enabled(): - if not hasattr(frappe.local, 'material_consumption'): - frappe.local.material_consumption = cint(frappe.db.get_single_value('Manufacturing Settings', - 'material_consumption')) + if not hasattr(frappe.local, "material_consumption"): + frappe.local.material_consumption = cint( + frappe.db.get_single_value("Manufacturing Settings", "material_consumption") + ) return frappe.local.material_consumption diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py index 41726f30cf5..9c8f9ac8d05 100644 --- a/erpnext/manufacturing/doctype/operation/operation.py +++ b/erpnext/manufacturing/doctype/operation/operation.py @@ -19,12 +19,14 @@ class Operation(Document): operation_list = [] for row in self.sub_operations: if row.operation in operation_list: - frappe.throw(_("The operation {0} can not add multiple times") - .format(frappe.bold(row.operation))) + frappe.throw( + _("The operation {0} can not add multiple times").format(frappe.bold(row.operation)) + ) if self.name == row.operation: - frappe.throw(_("The operation {0} can not be the sub operation") - .format(frappe.bold(row.operation))) + frappe.throw( + _("The operation {0} can not be the sub operation").format(frappe.bold(row.operation)) + ) operation_list.append(row.operation) diff --git a/erpnext/manufacturing/doctype/operation/operation_dashboard.py b/erpnext/manufacturing/doctype/operation/operation_dashboard.py index 4a548a64709..8dc901a2968 100644 --- a/erpnext/manufacturing/doctype/operation/operation_dashboard.py +++ b/erpnext/manufacturing/doctype/operation/operation_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'operation', - 'transactions': [ - { - 'label': _('Manufacture'), - 'items': ['BOM', 'Work Order', 'Job Card'] - } - ] + "fieldname": "operation", + "transactions": [{"label": _("Manufacture"), "items": ["BOM", "Work Order", "Job Card"]}], } diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py index e511084e7d0..ce9f8e06495 100644 --- a/erpnext/manufacturing/doctype/operation/test_operation.py +++ b/erpnext/manufacturing/doctype/operation/test_operation.py @@ -5,11 +5,13 @@ import unittest import frappe -test_records = frappe.get_test_records('Operation') +test_records = frappe.get_test_records("Operation") + class TestOperation(unittest.TestCase): pass + def make_operation(*args, **kwargs): args = args if args else kwargs if isinstance(args, tuple): @@ -18,11 +20,9 @@ def make_operation(*args, **kwargs): args = frappe._dict(args) if not frappe.db.exists("Operation", args.operation): - doc = frappe.get_doc({ - "doctype": "Operation", - "name": args.operation, - "workstation": args.workstation - }) + doc = frappe.get_doc( + {"doctype": "Operation", "name": args.operation, "workstation": args.workstation} + ) doc.insert() return doc diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index f3ded994814..5653e1be75d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -2,6 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Production Plan', { + + before_save: function(frm) { + // preserve temporary names on production plan item to re-link sub-assembly items + frm.doc.po_items.forEach(item => { + item.temporary_name = item.name; + }); + }, setup: function(frm) { frm.custom_make_buttons = { 'Work Order': 'Work Order / Subcontract PO', diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 56cf2b4f08a..23b32379413 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -189,7 +189,7 @@ "label": "Select Items to Manufacture" }, { - "depends_on": "get_items_from", + "depends_on": "eval:doc.get_items_from && doc.docstatus == 0", "fieldname": "get_items", "fieldtype": "Button", "label": "Get Finished Goods for Manufacture" @@ -197,6 +197,7 @@ { "fieldname": "po_items", "fieldtype": "Table", + "label": "Assembly Items", "no_copy": 1, "options": "Production Plan Item", "reqd": 1 @@ -357,6 +358,7 @@ "options": "Production Plan Sub Assembly Item" }, { + "depends_on": "eval:doc.po_items && doc.po_items.length && doc.docstatus == 0", "fieldname": "get_sub_assembly_items", "fieldtype": "Button", "label": "Get Sub Assembly Items" @@ -376,7 +378,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-09-06 18:35:59.642232", + "modified": "2022-03-25 09:15:25.017664", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", @@ -397,5 +399,6 @@ } ], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 60771592da5..ff739e9d72b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -32,10 +32,11 @@ class ProductionPlan(Document): self.set_pending_qty_in_row_without_reference() self.calculate_total_planned_qty() self.set_status() + self._rename_temporary_references() def set_pending_qty_in_row_without_reference(self): "Set Pending Qty in independent rows (not from SO or MR)." - if self.docstatus > 0: # set only to initialise value before submit + if self.docstatus > 0: # set only to initialise value before submit return for item in self.po_items: @@ -48,7 +49,7 @@ class ProductionPlan(Document): self.total_planned_qty += flt(d.planned_qty) def validate_data(self): - for d in self.get('po_items'): + for d in self.get("po_items"): if not d.bom_no: frappe.throw(_("Please select BOM for Item in Row {0}").format(d.idx)) else: @@ -57,9 +58,21 @@ class ProductionPlan(Document): if not flt(d.planned_qty): frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx)) + def _rename_temporary_references(self): + """po_items and sub_assembly_items items are both constructed client side without saving. + + Attempt to fix linkages by using temporary names to map final row names. + """ + new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name} + actual_names = {d.name for d in self.po_items} + + for sub_assy in self.sub_assembly_items: + if sub_assy.production_plan_item not in actual_names: + sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item) + @frappe.whitelist() def get_open_sales_orders(self): - """ Pull sales orders which are pending to deliver based on criteria selected""" + """Pull sales orders which are pending to deliver based on criteria selected""" open_so = get_sales_orders(self) if open_so: @@ -68,20 +81,23 @@ class ProductionPlan(Document): frappe.msgprint(_("Sales orders are not available for production")) def add_so_in_table(self, open_so): - """ Add sales orders in the table""" - self.set('sales_orders', []) + """Add sales orders in the table""" + self.set("sales_orders", []) for data in open_so: - self.append('sales_orders', { - 'sales_order': data.name, - 'sales_order_date': data.transaction_date, - 'customer': data.customer, - 'grand_total': data.base_grand_total - }) + self.append( + "sales_orders", + { + "sales_order": data.name, + "sales_order_date": data.transaction_date, + "customer": data.customer, + "grand_total": data.base_grand_total, + }, + ) @frappe.whitelist() def get_pending_material_requests(self): - """ Pull Material Requests that are pending based on criteria selected""" + """Pull Material Requests that are pending based on criteria selected""" mr_filter = item_filter = "" if self.from_date: mr_filter += " and mr.transaction_date >= %(from_date)s" @@ -93,7 +109,8 @@ class ProductionPlan(Document): if self.item_code: item_filter += " and mr_item.item_code = %(item)s" - pending_mr = frappe.db.sql(""" + pending_mr = frappe.db.sql( + """ select distinct mr.name, mr.transaction_date from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr_item.parent = mr.name @@ -102,29 +119,34 @@ class ProductionPlan(Document): and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1} and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code and bom.is_active = 1)) - """.format(mr_filter, item_filter), { + """.format( + mr_filter, item_filter + ), + { "from_date": self.from_date, "to_date": self.to_date, "warehouse": self.warehouse, "item": self.item_code, - "company": self.company - }, as_dict=1) + "company": self.company, + }, + as_dict=1, + ) self.add_mr_in_table(pending_mr) def add_mr_in_table(self, pending_mr): - """ Add Material Requests in the table""" - self.set('material_requests', []) + """Add Material Requests in the table""" + self.set("material_requests", []) for data in pending_mr: - self.append('material_requests', { - 'material_request': data.name, - 'material_request_date': data.transaction_date - }) + self.append( + "material_requests", + {"material_request": data.name, "material_request_date": data.transaction_date}, + ) @frappe.whitelist() def get_items(self): - self.set('po_items', []) + self.set("po_items", []) if self.get_items_from == "Sales Order": self.get_so_items() @@ -139,10 +161,12 @@ class ProductionPlan(Document): def get_bom_item(self): """Check if Item or if its Template has a BOM.""" bom_item = None - has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1}) + has_bom = frappe.db.exists({"doctype": "BOM", "item": self.item_code, "docstatus": 1}) if not has_bom: - template_item = frappe.db.get_value('Item', self.item_code, ['variant_of']) - bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item + template_item = frappe.db.get_value("Item", self.item_code, ["variant_of"]) + bom_item = ( + "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item + ) return bom_item def get_so_items(self): @@ -154,11 +178,12 @@ class ProductionPlan(Document): item_condition = "" bom_item = "bom.item = so_item.item_code" - if self.item_code and frappe.db.exists('Item', self.item_code): + if self.item_code and frappe.db.exists("Item", self.item_code): bom_item = self.get_bom_item() or bom_item - item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) + item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select distinct parent, item_code, warehouse, (qty - work_order_qty) * conversion_factor as pending_qty, @@ -168,16 +193,17 @@ class ProductionPlan(Document): where parent in (%s) and docstatus = 1 and qty > work_order_qty and exists (select name from `tabBOM` bom where %s - and bom.is_active = 1) %s""" % - (", ".join(["%s"] * len(so_list)), - bom_item, - item_condition), - tuple(so_list), as_dict=1) + and bom.is_active = 1) %s""" + % (", ".join(["%s"] * len(so_list)), bom_item, item_condition), + tuple(so_list), + as_dict=1, + ) if self.item_code: - item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) + item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) - packed_items = frappe.db.sql("""select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, + packed_items = frappe.db.sql( + """select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty) as pending_qty, pi.parent_item, pi.description, so_item.name from `tabSales Order Item` so_item, `tabPacked Item` pi @@ -185,16 +211,23 @@ class ProductionPlan(Document): and pi.parent_item = so_item.item_code and so_item.parent in (%s) and so_item.qty > so_item.work_order_qty and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1) %s""" % \ - (", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1) + and bom.is_active = 1) %s""" + % (", ".join(["%s"] * len(so_list)), item_condition), + tuple(so_list), + as_dict=1, + ) self.add_items(items + packed_items) self.calculate_total_planned_qty() def get_mr_items(self): # Check for empty table or empty rows - if not self.get("material_requests") or not self.get_so_mr_list("material_request", "material_requests"): - frappe.throw(_("Please fill the Material Requests table"), title=_("Material Requests Required")) + if not self.get("material_requests") or not self.get_so_mr_list( + "material_request", "material_requests" + ): + frappe.throw( + _("Please fill the Material Requests table"), title=_("Material Requests Required") + ) mr_list = self.get_so_mr_list("material_request", "material_requests") @@ -202,13 +235,17 @@ class ProductionPlan(Document): if self.item_code: item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code)) - items = frappe.db.sql("""select distinct parent, name, item_code, warehouse, description, + items = frappe.db.sql( + """select distinct parent, name, item_code, warehouse, description, (qty - ordered_qty) * conversion_factor as pending_qty from `tabMaterial Request Item` mr_item where parent in (%s) and docstatus = 1 and qty > ordered_qty and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1) %s""" % \ - (", ".join(["%s"] * len(mr_list)), item_condition), tuple(mr_list), as_dict=1) + and bom.is_active = 1) %s""" + % (", ".join(["%s"] * len(mr_list)), item_condition), + tuple(mr_list), + as_dict=1, + ) self.add_items(items) self.calculate_total_planned_qty() @@ -219,37 +256,36 @@ class ProductionPlan(Document): 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 + 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': [] + "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 - }) + 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', { - 'warehouse': data.warehouse, - 'item_code': data.item_code, - 'description': data.description or item_details.description, - 'stock_uom': item_details and item_details.stock_uom or '', - 'bom_no': item_details and item_details.bom_no or '', - 'planned_qty': data.pending_qty, - 'pending_qty': data.pending_qty, - 'planned_start_date': now_datetime(), - 'product_bundle_item': data.parent_item - }) + pi = self.append( + "po_items", + { + "warehouse": data.warehouse, + "item_code": data.item_code, + "description": data.description or item_details.description, + "stock_uom": item_details and item_details.stock_uom or "", + "bom_no": item_details and item_details.bom_no or "", + "planned_qty": data.pending_qty, + "pending_qty": data.pending_qty, + "planned_start_date": now_datetime(), + "product_bundle_item": data.parent_item, + }, + ) pi._set_defaults() if self.get_items_from == "Sales Order": @@ -264,20 +300,23 @@ class ProductionPlan(Document): 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 = '' + 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'] - }) + 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_produced_qty(self): self.total_produced_qty = 0 @@ -295,27 +334,24 @@ class ProductionPlan(Document): self.calculate_total_produced_qty() self.set_status() - self.db_set('status', self.status) + self.db_set("status", self.status) def on_cancel(self): - self.db_set('status', 'Cancelled') + self.db_set("status", "Cancelled") self.delete_draft_work_order() def delete_draft_work_order(self): - for d in frappe.get_all('Work Order', fields = ["name"], - filters = {'docstatus': 0, 'production_plan': ("=", self.name)}): - frappe.delete_doc('Work Order', d.name) + for d in frappe.get_all( + "Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)} + ): + frappe.delete_doc("Work Order", d.name) @frappe.whitelist() def set_status(self, close=None): - self.status = { - 0: 'Draft', - 1: 'Submitted', - 2: 'Cancelled' - }.get(self.docstatus) + self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus) if close: - self.db_set('status', 'Closed') + self.db_set("status", "Closed") return if self.total_produced_qty > 0: @@ -323,12 +359,12 @@ class ProductionPlan(Document): if self.all_items_completed(): self.status = "Completed" - if self.status != 'Completed': + if self.status != "Completed": self.update_ordered_status() self.update_requested_status() if close is not None: - self.db_set('status', self.status) + self.db_set("status", self.status) def update_ordered_status(self): update_status = False @@ -336,8 +372,8 @@ class ProductionPlan(Document): if d.planned_qty == d.ordered_qty: update_status = True - if update_status and self.status != 'Completed': - self.status = 'In Process' + if update_status and self.status != "Completed": + self.status = "In Process" def update_requested_status(self): if not self.mr_items: @@ -349,44 +385,44 @@ class ProductionPlan(Document): update_status = False if update_status: - self.status = 'Material Requested' + self.status = "Material Requested" def get_production_items(self): item_dict = {} for d in self.po_items: item_details = { - "production_item" : d.item_code, - "use_multi_level_bom" : d.include_exploded_items, - "sales_order" : d.sales_order, - "sales_order_item" : d.sales_order_item, - "material_request" : d.material_request, - "material_request_item" : d.material_request_item, - "bom_no" : d.bom_no, - "description" : d.description, - "stock_uom" : d.stock_uom, - "company" : self.company, - "fg_warehouse" : d.warehouse, - "production_plan" : self.name, - "production_plan_item" : d.name, - "product_bundle_item" : d.product_bundle_item, - "planned_start_date" : d.planned_start_date, - "project" : self.project + "production_item": d.item_code, + "use_multi_level_bom": d.include_exploded_items, + "sales_order": d.sales_order, + "sales_order_item": d.sales_order_item, + "material_request": d.material_request, + "material_request_item": d.material_request_item, + "bom_no": d.bom_no, + "description": d.description, + "stock_uom": d.stock_uom, + "company": self.company, + "fg_warehouse": d.warehouse, + "production_plan": self.name, + "production_plan_item": d.name, + "product_bundle_item": d.product_bundle_item, + "planned_start_date": d.planned_start_date, + "project": self.project, } - if not item_details['project'] and d.sales_order: - item_details['project'] = frappe.get_cached_value("Sales Order", d.sales_order, "project") + if not item_details["project"] and d.sales_order: + item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project") if self.get_items_from == "Material Request": - item_details.update({ - "qty": d.planned_qty - }) + item_details.update({"qty": d.planned_qty}) item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details else: - item_details.update({ - "qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse),{}) - .get("qty")) + (flt(d.planned_qty) - flt(d.ordered_qty)) - }) + item_details.update( + { + "qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse), {}).get("qty")) + + (flt(d.planned_qty) - flt(d.ordered_qty)) + } + ) item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details return item_dict @@ -402,15 +438,15 @@ class ProductionPlan(Document): self.make_work_order_for_finished_goods(wo_list, default_warehouses) self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses) self.make_subcontracted_purchase_order(subcontracted_po, po_list) - self.show_list_created_message('Work Order', wo_list) - self.show_list_created_message('Purchase Order', po_list) + self.show_list_created_message("Work Order", wo_list) + self.show_list_created_message("Purchase Order", po_list) def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() for key, item in items_data.items(): if self.sub_assembly_items: - item['use_multi_level_bom'] = 0 + item["use_multi_level_bom"] = 0 set_default_warehouses(item, default_warehouses) work_order = self.create_work_order(item) @@ -419,13 +455,14 @@ class ProductionPlan(Document): def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses): for row in self.sub_assembly_items: - if row.type_of_manufacturing == 'Subcontract': + if row.type_of_manufacturing == "Subcontract": subcontracted_po.setdefault(row.supplier, []).append(row) continue work_order_data = { - 'wip_warehouse': default_warehouses.get('wip_warehouse'), - 'fg_warehouse': default_warehouses.get('fg_warehouse') + "wip_warehouse": default_warehouses.get("wip_warehouse"), + "fg_warehouse": default_warehouses.get("fg_warehouse"), + "company": self.get("company"), } self.prepare_data_for_sub_assembly_items(row, work_order_data) @@ -434,41 +471,60 @@ class ProductionPlan(Document): wo_list.append(work_order) def prepare_data_for_sub_assembly_items(self, row, wo_data): - for field in ["production_item", "item_name", "qty", "fg_warehouse", - "description", "bom_no", "stock_uom", "bom_level", - "production_plan_item", "schedule_date"]: + for field in [ + "production_item", + "item_name", + "qty", + "fg_warehouse", + "description", + "bom_no", + "stock_uom", + "bom_level", + "production_plan_item", + "schedule_date", + ]: if row.get(field): wo_data[field] = row.get(field) - wo_data.update({ - "use_multi_level_bom": 0, - "production_plan": self.name, - "production_plan_sub_assembly_item": row.name - }) + wo_data.update( + { + "use_multi_level_bom": 0, + "production_plan": self.name, + "production_plan_sub_assembly_item": row.name, + } + ) def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders): if not subcontracted_po: return for supplier, po_list in subcontracted_po.items(): - po = frappe.new_doc('Purchase Order') + po = frappe.new_doc("Purchase Order") + po.company = self.company po.supplier = supplier po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() - po.is_subcontracted = 'Yes' + po.is_subcontracted = "Yes" for row in po_list: po_data = { - 'item_code': row.production_item, - 'warehouse': row.fg_warehouse, - 'production_plan_sub_assembly_item': row.name, - 'bom': row.bom_no, - 'production_plan': self.name + "item_code": row.production_item, + "warehouse": row.fg_warehouse, + "production_plan_sub_assembly_item": row.name, + "bom": row.bom_no, + "production_plan": self.name, } - for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name', - 'description', 'production_plan_item']: + for field in [ + "schedule_date", + "qty", + "uom", + "stock_uom", + "item_name", + "description", + "production_plan_item", + ]: po_data[field] = row.get(field) - po.append('items', po_data) + po.append("items", po_data) po.set_missing_values() po.flags.ignore_mandatory = True @@ -490,7 +546,7 @@ class ProductionPlan(Document): wo = frappe.new_doc("Work Order") wo.update(item) - wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date') + wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date") if item.get("warehouse"): wo.fg_warehouse = item.get("warehouse") @@ -508,54 +564,60 @@ class ProductionPlan(Document): @frappe.whitelist() def make_material_request(self): - '''Create Material Requests grouped by Sales Order and Material Request Type''' + """Create Material Requests grouped by Sales Order and Material Request Type""" material_request_list = [] material_request_map = {} for item in self.mr_items: - item_doc = frappe.get_cached_doc('Item', item.item_code) + item_doc = frappe.get_cached_doc("Item", item.item_code) material_request_type = item.material_request_type or item_doc.default_material_request_type # key for Sales Order:Material Request Type:Customer - key = '{}:{}:{}'.format(item.sales_order, material_request_type, item_doc.customer or '') + key = "{}:{}:{}".format(item.sales_order, material_request_type, item_doc.customer or "") schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) if not key in material_request_map: # make a new MR for the combination material_request_map[key] = frappe.new_doc("Material Request") material_request = material_request_map[key] - material_request.update({ - "transaction_date": nowdate(), - "status": "Draft", - "company": self.company, - 'material_request_type': material_request_type, - 'customer': item_doc.customer or '' - }) + material_request.update( + { + "transaction_date": nowdate(), + "status": "Draft", + "company": self.company, + "material_request_type": material_request_type, + "customer": item_doc.customer or "", + } + ) material_request_list.append(material_request) else: material_request = material_request_map[key] # add item - material_request.append("items", { - "item_code": item.item_code, - "from_warehouse": item.from_warehouse, - "qty": item.quantity, - "schedule_date": schedule_date, - "warehouse": item.warehouse, - "sales_order": item.sales_order, - 'production_plan': self.name, - 'material_request_plan_item': item.name, - "project": frappe.db.get_value("Sales Order", item.sales_order, "project") \ - if item.sales_order else None - }) + material_request.append( + "items", + { + "item_code": item.item_code, + "from_warehouse": item.from_warehouse, + "qty": item.quantity, + "schedule_date": schedule_date, + "warehouse": item.warehouse, + "sales_order": item.sales_order, + "production_plan": self.name, + "material_request_plan_item": item.name, + "project": frappe.db.get_value("Sales Order", item.sales_order, "project") + if item.sales_order + else None, + }, + ) for material_request in material_request_list: # submit material_request.flags.ignore_permissions = 1 material_request.run_method("set_missing_values") - if self.get('submit_material_request'): + if self.get("submit_material_request"): material_request.submit() else: material_request.save() @@ -563,10 +625,12 @@ class ProductionPlan(Document): frappe.flags.mute_messages = False if material_request_list: - material_request_list = ["""{1}""".format(m.name, m.name) \ - for m in material_request_list] + material_request_list = [ + """{1}""".format(m.name, m.name) + for m in material_request_list + ] msgprint(_("{0} created").format(comma_and(material_request_list))) - else : + else: msgprint(_("No material request created")) @frappe.whitelist() @@ -577,7 +641,7 @@ class ProductionPlan(Document): get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) - self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True) + self.sub_assembly_items.sort(key=lambda d: d.bom_level, reverse=True) for idx, row in enumerate(self.sub_assembly_items, start=1): row.idx = idx @@ -587,14 +651,16 @@ class ProductionPlan(Document): data.production_plan_item = row.name data.fg_warehouse = row.warehouse data.schedule_date = row.planned_start_date - data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item - else "In House") + data.type_of_manufacturing = manufacturing_type or ( + "Subcontract" if data.is_sub_contracted_item else "In House" + ) self.append("sub_assembly_items", data) def all_items_completed(self): - all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 - for d in self.po_items) + all_items_produced = all( + flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 for d in self.po_items + ) if not all_items_produced: return False @@ -611,40 +677,81 @@ class ProductionPlan(Document): all_work_orders_completed = all(s == "Completed" for s in wo_status) return all_work_orders_completed + @frappe.whitelist() def download_raw_materials(doc, warehouses=None): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - item_list = [['Item Code', 'Item Name', 'Description', - 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', - 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', - 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] + item_list = [ + [ + "Item Code", + "Item Name", + "Description", + "Stock UOM", + "Warehouse", + "Required Qty as per BOM", + "Projected Qty", + "Available Qty In Hand", + "Ordered Qty", + "Planned Qty", + "Reserved Qty for Production", + "Safety Stock", + "Required Qty", + ] + ] doc.warehouse = None frappe.flags.show_qty_in_stock_uom = 1 - items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) + items = get_items_for_material_requests( + doc, warehouses=warehouses, get_parent_warehouse_data=True + ) for d in items: - item_list.append([d.get('item_code'), d.get('item_name'), - d.get('description'), d.get('stock_uom'), d.get('warehouse'), - d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), - d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) + item_list.append( + [ + d.get("item_code"), + d.get("item_name"), + d.get("description"), + d.get("stock_uom"), + d.get("warehouse"), + d.get("required_bom_qty"), + d.get("projected_qty"), + d.get("actual_qty"), + d.get("ordered_qty"), + d.get("planned_qty"), + d.get("reserved_qty_for_production"), + d.get("safety_stock"), + d.get("quantity"), + ] + ) - if not doc.get('for_warehouse'): - row = {'item_code': d.get('item_code')} + if not doc.get("for_warehouse"): + row = {"item_code": d.get("item_code")} for bin_dict in get_bin_details(row, doc.company, all_warehouse=True): - if d.get("warehouse") == bin_dict.get('warehouse'): + if d.get("warehouse") == bin_dict.get("warehouse"): continue - item_list.append(['', '', '', bin_dict.get('warehouse'), '', - bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0), - bin_dict.get('ordered_qty', 0), bin_dict.get('reserved_qty_for_production', 0)]) + item_list.append( + [ + "", + "", + "", + bin_dict.get("warehouse"), + "", + bin_dict.get("projected_qty", 0), + bin_dict.get("actual_qty", 0), + bin_dict.get("ordered_qty", 0), + bin_dict.get("reserved_qty_for_production", 0), + ] + ) build_csv_response(item_list, doc.name) + def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1): - for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom, + for d in frappe.db.sql( + """select bei.item_code, item.default_bom as bom, ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, @@ -660,21 +767,38 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p where bei.docstatus < 2 and bom.name=%s and item.is_stock_item in (1, {0}) - group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1), - (planned_qty, company, bom_no), as_dict=1): + group by bei.item_code, bei.stock_uom""".format( + 0 if include_non_stock_items else 1 + ), + (planned_qty, company, bom_no), + as_dict=1, + ): if not d.conversion_factor and d.purchase_uom: d.conversion_factor = get_uom_conversion_factor(d.item_code, d.purchase_uom) - item_details.setdefault(d.get('item_code'), d) + item_details.setdefault(d.get("item_code"), d) return item_details -def get_uom_conversion_factor(item_code, uom): - return frappe.db.get_value('UOM Conversion Detail', - {'parent': item_code, 'uom': uom}, 'conversion_factor') -def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_items, - include_subcontracted_items, parent_qty, planned_qty=1): - items = frappe.db.sql(""" +def get_uom_conversion_factor(item_code, uom): + return frappe.db.get_value( + "UOM Conversion Detail", {"parent": item_code, "uom": uom}, "conversion_factor" + ) + + +def get_subitems( + doc, + data, + item_details, + bom_no, + company, + include_non_stock_items, + include_subcontracted_items, + parent_qty, + planned_qty=1, +): + items = frappe.db.sql( + """ SELECT bom_item.item_code, default_material_request_type, item.item_name, ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, @@ -694,15 +818,15 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite bom.name = %(bom)s and bom_item.docstatus < 2 and item.is_stock_item in (1, {0}) - group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{ - 'bom': bom_no, - 'parent_qty': parent_qty, - 'planned_qty': planned_qty, - 'company': company - }, as_dict=1) + group by bom_item.item_code""".format( + 0 if include_non_stock_items else 1 + ), + {"bom": bom_no, "parent_qty": parent_qty, "planned_qty": planned_qty, "company": company}, + as_dict=1, + ) for d in items: - if not data.get('include_exploded_items') or not d.default_bom: + if not data.get("include_exploded_items") or not d.default_bom: if d.item_code in item_details: item_details[d.item_code].qty = item_details[d.item_code].qty + d.qty else: @@ -711,89 +835,107 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite item_details[d.item_code] = d - if data.get('include_exploded_items') and d.default_bom: - if ((d.default_material_request_type in ["Manufacture", "Purchase"] and - not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)): + if data.get("include_exploded_items") and d.default_bom: + if ( + d.default_material_request_type in ["Manufacture", "Purchase"] and not d.is_sub_contracted + ) or (d.is_sub_contracted and include_subcontracted_items): if d.qty > 0: - get_subitems(doc, data, item_details, d.default_bom, company, - include_non_stock_items, include_subcontracted_items, d.qty) + get_subitems( + doc, + data, + item_details, + d.default_bom, + company, + include_non_stock_items, + include_subcontracted_items, + d.qty, + ) return item_details -def get_material_request_items(row, sales_order, company, - ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict): - total_qty = row['qty'] + +def get_material_request_items( + row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict +): + total_qty = row["qty"] required_qty = 0 if ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0: required_qty = total_qty elif total_qty > bin_dict.get("projected_qty", 0): required_qty = total_qty - bin_dict.get("projected_qty", 0) - if required_qty > 0 and required_qty < row['min_order_qty']: - required_qty = row['min_order_qty'] + if required_qty > 0 and required_qty < row["min_order_qty"]: + required_qty = row["min_order_qty"] item_group_defaults = get_item_group_defaults(row.item_code, company) - if not row['purchase_uom']: - row['purchase_uom'] = row['stock_uom'] + if not row["purchase_uom"]: + row["purchase_uom"] = row["stock_uom"] - if row['purchase_uom'] != row['stock_uom']: - if not (row['conversion_factor'] or frappe.flags.show_qty_in_stock_uom): - frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}") - .format(row['purchase_uom'], row['stock_uom'], row.item_code)) + if row["purchase_uom"] != row["stock_uom"]: + if not (row["conversion_factor"] or frappe.flags.show_qty_in_stock_uom): + frappe.throw( + _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( + row["purchase_uom"], row["stock_uom"], row.item_code + ) + ) - required_qty = required_qty / row['conversion_factor'] + required_qty = required_qty / row["conversion_factor"] - if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"): + if frappe.db.get_value("UOM", row["purchase_uom"], "must_be_whole_number"): required_qty = ceil(required_qty) if include_safety_stock: - required_qty += flt(row['safety_stock']) + required_qty += flt(row["safety_stock"]) if required_qty > 0: return { - 'item_code': row.item_code, - 'item_name': row.item_name, - 'quantity': required_qty, - 'required_bom_qty': total_qty, - 'stock_uom': row.get("stock_uom"), - 'warehouse': warehouse or row.get('source_warehouse') \ - or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"), - 'safety_stock': row.safety_stock, - 'actual_qty': bin_dict.get("actual_qty", 0), - 'projected_qty': bin_dict.get("projected_qty", 0), - 'ordered_qty': bin_dict.get("ordered_qty", 0), - 'reserved_qty_for_production': bin_dict.get("reserved_qty_for_production", 0), - 'min_order_qty': row['min_order_qty'], - 'material_request_type': row.get("default_material_request_type"), - 'sales_order': sales_order, - 'description': row.get("description"), - 'uom': row.get("purchase_uom") or row.get("stock_uom") + "item_code": row.item_code, + "item_name": row.item_name, + "quantity": required_qty, + "required_bom_qty": total_qty, + "stock_uom": row.get("stock_uom"), + "warehouse": warehouse + or row.get("source_warehouse") + or row.get("default_warehouse") + or item_group_defaults.get("default_warehouse"), + "safety_stock": row.safety_stock, + "actual_qty": bin_dict.get("actual_qty", 0), + "projected_qty": bin_dict.get("projected_qty", 0), + "ordered_qty": bin_dict.get("ordered_qty", 0), + "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0), + "min_order_qty": row["min_order_qty"], + "material_request_type": row.get("default_material_request_type"), + "sales_order": sales_order, + "description": row.get("description"), + "uom": row.get("purchase_uom") or row.get("stock_uom"), } + def get_sales_orders(self): so_filter = item_filter = "" bom_item = "bom.item = so_item.item_code" date_field_mapper = { - 'from_date': ('>=', 'so.transaction_date'), - 'to_date': ('<=', 'so.transaction_date'), - 'from_delivery_date': ('>=', 'so_item.delivery_date'), - 'to_delivery_date': ('<=', 'so_item.delivery_date') + "from_date": (">=", "so.transaction_date"), + "to_date": ("<=", "so.transaction_date"), + "from_delivery_date": (">=", "so_item.delivery_date"), + "to_delivery_date": ("<=", "so_item.delivery_date"), } for field, value in date_field_mapper.items(): if self.get(field): so_filter += f" and {value[1]} {value[0]} %({field})s" - for field in ['customer', 'project', 'sales_order_status']: + for field in ["customer", "project", "sales_order_status"]: if self.get(field): - so_field = 'status' if field == 'sales_order_status' else field + so_field = "status" if field == "sales_order_status" else field so_filter += f" and so.{so_field} = %({field})s" - if self.item_code and frappe.db.exists('Item', self.item_code): + if self.item_code and frappe.db.exists("Item", self.item_code): bom_item = self.get_bom_item() or bom_item item_filter += " and so_item.item_code = %(item_code)s" - open_so = frappe.db.sql(f""" + open_so = frappe.db.sql( + f""" select distinct so.name, so.transaction_date, so.customer, so.base_grand_total from `tabSales Order` so, `tabSales Order Item` so_item where so_item.parent = so.name @@ -806,10 +948,14 @@ def get_sales_orders(self): where pi.parent = so.name and pi.parent_item = so_item.item_code and exists (select name from `tabBOM` bom where bom.item=pi.item_code and bom.is_active = 1))) - """, self.as_dict(), as_dict=1) + """, + self.as_dict(), + as_dict=1, + ) return open_so + @frappe.whitelist() def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): if isinstance(row, str): @@ -818,30 +964,42 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): company = frappe.db.escape(company) conditions, warehouse = "", "" - conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format(company) + conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format( + company + ) if not all_warehouse: - warehouse = for_warehouse or row.get('source_warehouse') or row.get('default_warehouse') + warehouse = for_warehouse or row.get("source_warehouse") or row.get("default_warehouse") if warehouse: lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) conditions = """ and warehouse in (select name from `tabWarehouse` where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse and company = {2}) - """.format(lft, rgt, company) + """.format( + lft, rgt, company + ) - return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, + return frappe.db.sql( + """ select ifnull(sum(projected_qty),0) as projected_qty, ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, ifnull(sum(planned_qty),0) as planned_qty from `tabBin` where item_code = %(item_code)s {conditions} group by item_code, warehouse - """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) + """.format( + conditions=conditions + ), + {"item_code": row["item_code"]}, + as_dict=1, + ) + @frappe.whitelist() def get_so_details(sales_order): - return frappe.db.get_value("Sales Order", sales_order, - ['transaction_date', 'customer', 'grand_total'], as_dict=1 + return frappe.db.get_value( + "Sales Order", sales_order, ["transaction_date", "customer", "grand_total"], as_dict=1 ) + def get_warehouse_list(warehouses): warehouse_list = [] @@ -849,7 +1007,7 @@ def get_warehouse_list(warehouses): warehouses = json.loads(warehouses) for row in warehouses: - child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) + child_warehouses = frappe.db.get_descendants("Warehouse", row.get("warehouse")) if child_warehouses: warehouse_list.extend(child_warehouses) else: @@ -857,6 +1015,7 @@ def get_warehouse_list(warehouses): return warehouse_list + @frappe.whitelist() def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): if isinstance(doc, str): @@ -865,73 +1024,92 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d if warehouses: warehouses = list(set(get_warehouse_list(warehouses))) - if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: + if ( + doc.get("for_warehouse") + and not get_parent_warehouse_data + and doc.get("for_warehouse") in warehouses + ): warehouses.remove(doc.get("for_warehouse")) - doc['mr_items'] = [] + doc["mr_items"] = [] - po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') + po_items = doc.get("po_items") if doc.get("po_items") else doc.get("items") # Check for empty table or empty rows - if not po_items or not [row.get('item_code') for row in po_items if row.get('item_code')]: - frappe.throw(_("Items to Manufacture are required to pull the Raw Materials associated with it."), - title=_("Items Required")) + if not po_items or not [row.get("item_code") for row in po_items if row.get("item_code")]: + frappe.throw( + _("Items to Manufacture are required to pull the Raw Materials associated with it."), + title=_("Items Required"), + ) - company = doc.get('company') - ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') - include_safety_stock = doc.get('include_safety_stock') + company = doc.get("company") + ignore_existing_ordered_qty = doc.get("ignore_existing_ordered_qty") + include_safety_stock = doc.get("include_safety_stock") so_item_details = frappe._dict() for data in po_items: if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): data["include_exploded_items"] = 1 - planned_qty = data.get('required_qty') or data.get('planned_qty') - ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty - warehouse = doc.get('for_warehouse') + planned_qty = data.get("required_qty") or data.get("planned_qty") + ignore_existing_ordered_qty = ( + data.get("ignore_existing_ordered_qty") or ignore_existing_ordered_qty + ) + warehouse = doc.get("for_warehouse") item_details = {} if data.get("bom") or data.get("bom_no"): - if data.get('required_qty'): - bom_no = data.get('bom') + if data.get("required_qty"): + bom_no = data.get("bom") include_non_stock_items = 1 - include_subcontracted_items = 1 if data.get('include_exploded_items') else 0 + include_subcontracted_items = 1 if data.get("include_exploded_items") else 0 else: - bom_no = data.get('bom_no') - include_subcontracted_items = doc.get('include_subcontracted_items') - include_non_stock_items = doc.get('include_non_stock_items') + bom_no = data.get("bom_no") + include_subcontracted_items = doc.get("include_subcontracted_items") + include_non_stock_items = doc.get("include_non_stock_items") if not planned_qty: - frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx'))) + frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) if bom_no: - if data.get('include_exploded_items') and include_subcontracted_items: + if data.get("include_exploded_items") and include_subcontracted_items: # fetch exploded items from BOM - item_details = get_exploded_items(item_details, - company, bom_no, include_non_stock_items, planned_qty=planned_qty) + item_details = get_exploded_items( + item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty + ) else: - item_details = get_subitems(doc, data, item_details, bom_no, company, - include_non_stock_items, include_subcontracted_items, 1, planned_qty=planned_qty) - elif data.get('item_code'): - item_master = frappe.get_doc('Item', data['item_code']).as_dict() + item_details = get_subitems( + doc, + data, + item_details, + bom_no, + company, + include_non_stock_items, + include_subcontracted_items, + 1, + planned_qty=planned_qty, + ) + elif data.get("item_code"): + item_master = frappe.get_doc("Item", data["item_code"]).as_dict() purchase_uom = item_master.purchase_uom or item_master.stock_uom - conversion_factor = (get_uom_conversion_factor(item_master.name, purchase_uom) - if item_master.purchase_uom else 1.0) + conversion_factor = ( + get_uom_conversion_factor(item_master.name, purchase_uom) if item_master.purchase_uom else 1.0 + ) item_details[item_master.name] = frappe._dict( { - 'item_name' : item_master.item_name, - 'default_bom' : doc.bom, - 'purchase_uom' : purchase_uom, - 'default_warehouse': item_master.default_warehouse, - 'min_order_qty' : item_master.min_order_qty, - 'default_material_request_type' : item_master.default_material_request_type, - 'qty': planned_qty or 1, - 'is_sub_contracted' : item_master.is_subcontracted_item, - 'item_code' : item_master.name, - 'description' : item_master.description, - 'stock_uom' : item_master.stock_uom, - 'conversion_factor' : conversion_factor, - 'safety_stock': item_master.safety_stock + "item_name": item_master.item_name, + "default_bom": doc.bom, + "purchase_uom": purchase_uom, + "default_warehouse": item_master.default_warehouse, + "min_order_qty": item_master.min_order_qty, + "default_material_request_type": item_master.default_material_request_type, + "qty": planned_qty or 1, + "is_sub_contracted": item_master.is_subcontracted_item, + "item_code": item_master.name, + "description": item_master.description, + "stock_uom": item_master.stock_uom, + "conversion_factor": conversion_factor, + "safety_stock": item_master.safety_stock, } ) @@ -940,7 +1118,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d for item_code, details in iteritems(item_details): so_item_details.setdefault(sales_order, frappe._dict()) if item_code in so_item_details.get(sales_order, {}): - so_item_details[sales_order][item_code]['qty'] = so_item_details[sales_order][item_code].get("qty", 0) + flt(details.qty) + so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get( + "qty", 0 + ) + flt(details.qty) else: so_item_details[sales_order][item_code] = details @@ -952,8 +1132,15 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d bin_dict = bin_dict[0] if bin_dict else {} if details.qty > 0: - items = get_material_request_items(details, sales_order, company, - ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict) + items = get_material_request_items( + details, + sales_order, + company, + ignore_existing_ordered_qty, + include_safety_stock, + warehouse, + bin_dict, + ) if items: mr_items.append(items) @@ -966,51 +1153,62 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d if not mr_items: to_enable = frappe.bold(_("Ignore Existing Projected Quantity")) - warehouse = frappe.bold(doc.get('for_warehouse')) - message = _("As there are sufficient raw materials, Material Request is not required for Warehouse {0}.").format(warehouse) + "

" + warehouse = frappe.bold(doc.get("for_warehouse")) + message = ( + _( + "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." + ).format(warehouse) + + "

" + ) message += _("If you still want to proceed, please enable {0}.").format(to_enable) frappe.msgprint(message, title=_("Note")) return mr_items + def get_materials_from_other_locations(item, warehouses, new_mr_items, company): from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations - locations = get_available_item_locations(item.get("item_code"), - warehouses, item.get("quantity"), company, ignore_validation=True) + + locations = get_available_item_locations( + item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True + ) required_qty = item.get("quantity") # get available material by transferring to production warehouse for d in locations: - if required_qty <=0: return + if required_qty <= 0: + return new_dict = copy.deepcopy(item) quantity = required_qty if d.get("qty") > required_qty else d.get("qty") - if required_qty > 0: - new_dict.update({ + new_dict.update( + { "quantity": quantity, "material_request_type": "Material Transfer", "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM - "from_warehouse": d.get("warehouse") - }) + "from_warehouse": d.get("warehouse"), + } + ) - required_qty -= quantity - new_mr_items.append(new_dict) + required_qty -= quantity + new_mr_items.append(new_dict) # raise purchase request for remaining qty if required_qty: stock_uom, purchase_uom = frappe.db.get_value( - 'Item', - item['item_code'], - ['stock_uom', 'purchase_uom'] + "Item", item["item_code"], ["stock_uom", "purchase_uom"] ) - if purchase_uom != stock_uom and purchase_uom == item['uom']: - conversion_factor = get_uom_conversion_factor(item['item_code'], item['uom']) + if purchase_uom != stock_uom and purchase_uom == item["uom"]: + conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) if not (conversion_factor or frappe.flags.show_qty_in_stock_uom): - frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}") - .format(purchase_uom, stock_uom, item['item_code'])) + frappe.throw( + _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( + purchase_uom, stock_uom, item["item_code"] + ) + ) required_qty = required_qty / conversion_factor @@ -1021,6 +1219,7 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): new_mr_items.append(item) + @frappe.whitelist() def get_item_data(item_code): item_details = get_item_details(item_code) @@ -1028,33 +1227,39 @@ def get_item_data(item_code): return { "bom_no": item_details.get("bom_no"), "stock_uom": item_details.get("stock_uom") -# "description": item_details.get("description") + # "description": item_details.get("description") } + def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): - data = get_children('BOM', parent = bom_no) + data = get_children("BOM", parent=bom_no) for d in data: if d.expandable: parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) - bom_data.append(frappe._dict({ - 'parent_item_code': parent_item_code, - 'description': d.description, - 'production_item': d.item_code, - 'item_name': d.item_name, - 'stock_uom': d.stock_uom, - 'uom': d.stock_uom, - 'bom_no': d.value, - 'is_sub_contracted_item': d.is_sub_contracted_item, - 'bom_level': indent, - 'indent': indent, - 'stock_qty': stock_qty - })) + bom_data.append( + frappe._dict( + { + "parent_item_code": parent_item_code, + "description": d.description, + "production_item": d.item_code, + "item_name": d.item_name, + "stock_uom": d.stock_uom, + "uom": d.stock_uom, + "bom_no": d.value, + "is_sub_contracted_item": d.is_sub_contracted_item, + "bom_level": indent, + "indent": indent, + "stock_qty": stock_qty, + } + ) + ) if d.value: - get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1) + get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent + 1) + def set_default_warehouses(row, default_warehouses): - for field in ['wip_warehouse', 'fg_warehouse']: + for field in ["wip_warehouse", "fg_warehouse"]: if not row.get(field): row[field] = default_warehouses.get(field) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py index ef009765f92..6fc28a30971 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py @@ -1,18 +1,11 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'production_plan', - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Work Order', 'Material Request'] - }, - { - 'label': _('Subcontract'), - 'items': ['Purchase Order'] - }, - ] + "fieldname": "production_plan", + "transactions": [ + {"label": _("Transactions"), "items": ["Work Order", "Material Request"]}, + {"label": _("Subcontract"), "items": ["Purchase Order"]}, + ], } diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2359815813d..e70f997a53c 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1,6 +1,7 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, flt, now_datetime, nowdate from erpnext.controllers.item_variant import create_variant @@ -16,82 +17,89 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.tests.utils import ERPNextTestCase -class TestProductionPlan(ERPNextTestCase): +class TestProductionPlan(FrappeTestCase): def setUp(self): - for item in ['Test Production Item 1', 'Subassembly Item 1', - 'Raw Material Item 1', 'Raw Material Item 2']: + for item in [ + "Test Production Item 1", + "Subassembly Item 1", + "Raw Material Item 1", + "Raw Material Item 2", + ]: create_item(item, valuation_rate=100) - sr = frappe.db.get_value('Stock Reconciliation Item', - {'item_code': item, 'docstatus': 1}, 'parent') + sr = frappe.db.get_value( + "Stock Reconciliation Item", {"item_code": item, "docstatus": 1}, "parent" + ) if sr: - sr_doc = frappe.get_doc('Stock Reconciliation', sr) + sr_doc = frappe.get_doc("Stock Reconciliation", sr) sr_doc.cancel() - create_item('Test Non Stock Raw Material', is_stock_item=0) - for item, raw_materials in {'Subassembly Item 1': ['Raw Material Item 1', 'Raw Material Item 2'], - 'Test Production Item 1': ['Raw Material Item 1', 'Subassembly Item 1', - 'Test Non Stock Raw Material']}.items(): - if not frappe.db.get_value('BOM', {'item': item}): - make_bom(item = item, raw_materials = raw_materials) + create_item("Test Non Stock Raw Material", is_stock_item=0) + for item, raw_materials in { + "Subassembly Item 1": ["Raw Material Item 1", "Raw Material Item 2"], + "Test Production Item 1": [ + "Raw Material Item 1", + "Subassembly Item 1", + "Test Non Stock Raw Material", + ], + }.items(): + if not frappe.db.get_value("BOM", {"item": item}): + make_bom(item=item, raw_materials=raw_materials) def test_production_plan_mr_creation(self): "Test if MRs are created for unavailable raw materials." - pln = create_production_plan(item_code='Test Production Item 1') + pln = create_production_plan(item_code="Test Production Item 1") self.assertTrue(len(pln.mr_items), 2) pln.make_material_request() pln.reload() - self.assertTrue(pln.status, 'Material Requested') + self.assertTrue(pln.status, "Material Requested") material_requests = frappe.get_all( - 'Material Request Item', - fields = ['distinct parent'], - filters = {'production_plan': pln.name}, - as_list=1 + "Material Request Item", + fields=["distinct parent"], + filters={"production_plan": pln.name}, + as_list=1, ) self.assertTrue(len(material_requests), 2) pln.make_work_order() - work_orders = frappe.get_all('Work Order', fields = ['name'], - filters = {'production_plan': pln.name}, as_list=1) + work_orders = frappe.get_all( + "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 + ) self.assertTrue(len(work_orders), len(pln.po_items)) for name in material_requests: - mr = frappe.get_doc('Material Request', name[0]) + mr = frappe.get_doc("Material Request", name[0]) if mr.docstatus != 0: mr.cancel() for name in work_orders: - mr = frappe.delete_doc('Work Order', name[0]) + mr = frappe.delete_doc("Work Order", name[0]) - pln = frappe.get_doc('Production Plan', pln.name) + pln = frappe.get_doc("Production Plan", pln.name) pln.cancel() def test_production_plan_start_date(self): "Test if Work Order has same Planned Start Date as Prod Plan." planned_date = add_to_date(date=None, days=3) plan = create_production_plan( - item_code='Test Production Item 1', - planned_start_date=planned_date + item_code="Test Production Item 1", planned_start_date=planned_date ) plan.make_work_order() work_orders = frappe.get_all( - 'Work Order', - fields = ['name', 'planned_start_date'], - filters = {'production_plan': plan.name} + "Work Order", fields=["name", "planned_start_date"], filters={"production_plan": plan.name} ) self.assertEqual(work_orders[0].planned_start_date, planned_date) for wo in work_orders: - frappe.delete_doc('Work Order', wo.name) + frappe.delete_doc("Work Order", wo.name) plan.reload() plan.cancel() @@ -101,15 +109,14 @@ class TestProductionPlan(ERPNextTestCase): - Enable 'ignore_existing_ordered_qty'. - Test if MR Planning table pulls Raw Material Qty even if it is in stock. """ - sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", - target="_Test Warehouse - _TC", qty=1, rate=110) - sr2 = create_stock_reconciliation(item_code="Raw Material Item 2", - target="_Test Warehouse - _TC", qty=1, rate=120) - - pln = create_production_plan( - item_code='Test Production Item 1', - ignore_existing_ordered_qty=1 + sr1 = create_stock_reconciliation( + item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=110 ) + sr2 = create_stock_reconciliation( + item_code="Raw Material Item 2", target="_Test Warehouse - _TC", qty=1, rate=120 + ) + + pln = create_production_plan(item_code="Test Production Item 1", ignore_existing_ordered_qty=1) self.assertTrue(len(pln.mr_items), 1) self.assertTrue(flt(pln.mr_items[0].quantity), 1.0) @@ -119,19 +126,13 @@ class TestProductionPlan(ERPNextTestCase): def test_production_plan_with_non_stock_item(self): "Test if MR Planning table includes Non Stock RM." - pln = create_production_plan( - item_code='Test Production Item 1', - include_non_stock_items=1 - ) + pln = create_production_plan(item_code="Test Production Item 1", include_non_stock_items=1) self.assertTrue(len(pln.mr_items), 3) pln.cancel() def test_production_plan_without_multi_level(self): "Test MR Planning table for non exploded BOM." - pln = create_production_plan( - item_code='Test Production Item 1', - use_multi_level_bom=0 - ) + pln = create_production_plan(item_code="Test Production Item 1", use_multi_level_bom=0) self.assertTrue(len(pln.mr_items), 2) pln.cancel() @@ -141,15 +142,15 @@ class TestProductionPlan(ERPNextTestCase): - Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for non exploded BOM. """ - sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", - target="_Test Warehouse - _TC", qty=1, rate=130) - sr2 = create_stock_reconciliation(item_code="Subassembly Item 1", - target="_Test Warehouse - _TC", qty=1, rate=140) + sr1 = create_stock_reconciliation( + item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=130 + ) + sr2 = create_stock_reconciliation( + item_code="Subassembly Item 1", target="_Test Warehouse - _TC", qty=1, rate=140 + ) pln = create_production_plan( - item_code='Test Production Item 1', - use_multi_level_bom=0, - ignore_existing_ordered_qty=0 + item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=0 ) self.assertTrue(len(pln.mr_items), 0) @@ -159,73 +160,86 @@ class TestProductionPlan(ERPNextTestCase): def test_production_plan_sales_orders(self): "Test if previously fulfilled SO (with WO) is pulled into Prod Plan." - item = 'Test Production Item 1' + item = "Test Production Item 1" so = make_sales_order(item_code=item, qty=1) sales_order = so.name sales_order_item = so.items[0].name - pln = frappe.new_doc('Production Plan') + pln = frappe.new_doc("Production Plan") pln.company = so.company - pln.get_items_from = 'Sales Order' + 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 - }) + pln.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) pln.get_so_items() pln.submit() pln.make_work_order() - work_order = frappe.db.get_value('Work Order', {'sales_order': sales_order, - 'production_plan': pln.name, 'sales_order_item': sales_order_item}, 'name') + work_order = frappe.db.get_value( + "Work Order", + {"sales_order": sales_order, "production_plan": pln.name, "sales_order_item": sales_order_item}, + "name", + ) - wo_doc = frappe.get_doc('Work Order', work_order) - wo_doc.update({ - 'wip_warehouse': 'Work In Progress - _TC', - 'fg_warehouse': 'Finished Goods - _TC' - }) + wo_doc = frappe.get_doc("Work Order", work_order) + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} + ) wo_doc.submit() - so_wo_qty = frappe.db.get_value('Sales Order Item', sales_order_item, 'work_order_qty') + so_wo_qty = frappe.db.get_value("Sales Order Item", sales_order_item, "work_order_qty") self.assertTrue(so_wo_qty, 5) - pln = frappe.new_doc('Production Plan') - pln.update({ - 'from_date': so.transaction_date, - 'to_date': so.transaction_date, - 'customer': so.customer, - 'item_code': item, - 'sales_order_status': so.status - }) + pln = frappe.new_doc("Production Plan") + pln.update( + { + "from_date": so.transaction_date, + "to_date": so.transaction_date, + "customer": so.customer, + "item_code": item, + "sales_order_status": so.status, + } + ) sales_orders = get_sales_orders(pln) or {} - sales_orders = [d.get('name') for d in sales_orders if d.get('name') == sales_order] + sales_orders = [d.get("name") for d in sales_orders if d.get("name") == sales_order] self.assertEqual(sales_orders, []) def test_production_plan_combine_items(self): "Test combining FG items in Production Plan." - item = 'Test Production Item 1' + item = "Test Production Item 1" so1 = make_sales_order(item_code=item, qty=1) - pln = frappe.new_doc('Production Plan') + pln = frappe.new_doc("Production Plan") pln.company = so1.company - pln.get_items_from = 'Sales Order' - pln.append('sales_orders', { - 'sales_order': so1.name, - 'sales_order_date': so1.transaction_date, - 'customer': so1.customer, - 'grand_total': so1.grand_total - }) + pln.get_items_from = "Sales Order" + pln.append( + "sales_orders", + { + "sales_order": so1.name, + "sales_order_date": so1.transaction_date, + "customer": so1.customer, + "grand_total": so1.grand_total, + }, + ) so2 = make_sales_order(item_code=item, qty=2) - pln.append('sales_orders', { - 'sales_order': so2.name, - 'sales_order_date': so2.transaction_date, - 'customer': so2.customer, - 'grand_total': so2.grand_total - }) + pln.append( + "sales_orders", + { + "sales_order": so2.name, + "sales_order_date": so2.transaction_date, + "customer": so2.customer, + "grand_total": so2.grand_total, + }, + ) pln.combine_items = 1 pln.get_items() pln.submit() @@ -233,51 +247,60 @@ class TestProductionPlan(ERPNextTestCase): 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') + 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 = 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') + 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') + so_wo_qty = frappe.db.get_value("Sales Order Item", so_item, "work_order_qty") self.assertEqual(so_wo_qty, 0.0) pln.reload() pln.cancel() def test_pp_to_mr_customer_provided(self): - " Test Material Request from Production Plan for Customer Provided Item." - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) - create_item('Production Item CUST') + "Test Material Request from Production Plan for Customer Provided Item." + create_item( + "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) + create_item("Production Item CUST") - for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items(): - if not frappe.db.get_value('BOM', {'item': item}): - make_bom(item = item, raw_materials = raw_materials) - production_plan = create_production_plan(item_code = 'Production Item CUST') + for item, raw_materials in { + "Production Item CUST": ["Raw Material Item 1", "CUST-0987"] + }.items(): + if not frappe.db.get_value("BOM", {"item": item}): + make_bom(item=item, raw_materials=raw_materials) + production_plan = create_production_plan(item_code="Production Item CUST") production_plan.make_material_request() material_request = frappe.db.get_value( - 'Material Request Item', - {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, - 'parent' + "Material Request Item", + {"production_plan": production_plan.name, "item_code": "CUST-0987"}, + "parent", ) - mr = frappe.get_doc('Material Request', material_request) + mr = frappe.get_doc("Material Request", material_request) - self.assertTrue(mr.material_request_type, 'Customer Provided') - self.assertTrue(mr.customer, '_Test Customer') + self.assertTrue(mr.material_request_type, "Customer Provided") + self.assertTrue(mr.customer, "_Test Customer") def test_production_plan_with_multi_level_bom(self): """ @@ -291,33 +314,34 @@ class TestProductionPlan(ERPNextTestCase): create_item(item_code, is_stock_item=1) # created bom upto 3 level - if not frappe.db.get_value('BOM', {'item': "Test BOM 3"}): - make_bom(item = "Test BOM 3", raw_materials = ["Test RM BOM 1"], rm_qty=3) + if not frappe.db.get_value("BOM", {"item": "Test BOM 3"}): + make_bom(item="Test BOM 3", raw_materials=["Test RM BOM 1"], rm_qty=3) - if not frappe.db.get_value('BOM', {'item': "Test BOM 2"}): - make_bom(item = "Test BOM 2", raw_materials = ["Test BOM 3"], rm_qty=3) + if not frappe.db.get_value("BOM", {"item": "Test BOM 2"}): + make_bom(item="Test BOM 2", raw_materials=["Test BOM 3"], rm_qty=3) - if not frappe.db.get_value('BOM', {'item': "Test BOM 1"}): - make_bom(item = "Test BOM 1", raw_materials = ["Test BOM 2"], rm_qty=2) + if not frappe.db.get_value("BOM", {"item": "Test BOM 1"}): + make_bom(item="Test BOM 1", raw_materials=["Test BOM 2"], rm_qty=2) item_code = "Test BOM 1" - pln = frappe.new_doc('Production Plan') + pln = frappe.new_doc("Production Plan") pln.company = "_Test Company" - pln.append("po_items", { - "item_code": item_code, - "bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}), - "planned_qty": 3 - }) + pln.append( + "po_items", + { + "item_code": item_code, + "bom_no": frappe.db.get_value("BOM", {"item": "Test BOM 1"}), + "planned_qty": 3, + }, + ) - pln.get_sub_assembly_items('In House') + pln.get_sub_assembly_items("In House") pln.submit() pln.make_work_order() - #last level sub-assembly work order produce qty + # last level sub-assembly work order produce qty to_produce_qty = frappe.db.get_value( - "Work Order", - {"production_plan": pln.name, "production_item": "Test BOM 3"}, - "qty" + "Work Order", {"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty" ) self.assertEqual(to_produce_qty, 18.0) @@ -326,70 +350,72 @@ class TestProductionPlan(ERPNextTestCase): def test_get_warehouse_list_group(self): "Check if required child warehouses are returned." - warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]' + warehouse_json = '[{"warehouse":"_Test Warehouse Group - _TC"}]' warehouses = set(get_warehouse_list(warehouse_json)) expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"} missing_warehouse = expected_warehouses - warehouses - self.assertTrue(len(missing_warehouse) == 0, - msg=f"Following warehouses were expected {', '.join(missing_warehouse)}") + self.assertTrue( + len(missing_warehouse) == 0, + msg=f"Following warehouses were expected {', '.join(missing_warehouse)}", + ) def test_get_warehouse_list_single(self): "Check if same warehouse is returned in absence of child warehouses." - warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]' + warehouse_json = '[{"warehouse":"_Test Scrap Warehouse - _TC"}]' warehouses = set(get_warehouse_list(warehouse_json)) - expected_warehouses = {"_Test Scrap Warehouse - _TC", } + expected_warehouses = { + "_Test Scrap Warehouse - _TC", + } self.assertEqual(warehouses, expected_warehouses) def test_get_sales_order_with_variant(self): "Check if Template BOM is fetched in absence of Variant BOM." - rm_item = create_item('PIV_RM', valuation_rate = 100) - if not frappe.db.exists('Item', {"item_code": 'PIV'}): - item = create_item('PIV', valuation_rate = 100) + rm_item = create_item("PIV_RM", valuation_rate=100) + if not frappe.db.exists("Item", {"item_code": "PIV"}): + item = create_item("PIV", valuation_rate=100) variant_settings = { "attributes": [ - { - "attribute": "Colour" - }, + {"attribute": "Colour"}, ], - "has_variants": 1 + "has_variants": 1, } item.update(variant_settings) item.save() - parent_bom = make_bom(item = 'PIV', raw_materials = [rm_item.item_code]) - if not frappe.db.exists('BOM', {"item": 'PIV'}): - parent_bom = make_bom(item = 'PIV', raw_materials = [rm_item.item_code]) + parent_bom = make_bom(item="PIV", raw_materials=[rm_item.item_code]) + if not frappe.db.exists("BOM", {"item": "PIV"}): + parent_bom = make_bom(item="PIV", raw_materials=[rm_item.item_code]) else: - parent_bom = frappe.get_doc('BOM', {"item": 'PIV'}) + parent_bom = frappe.get_doc("BOM", {"item": "PIV"}) - if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}): + if not frappe.db.exists("Item", {"item_code": "PIV-RED"}): variant = create_variant("PIV", {"Colour": "Red"}) variant.save() - variant_bom = make_bom(item = variant.item_code, raw_materials = [rm_item.item_code]) + variant_bom = make_bom(item=variant.item_code, raw_materials=[rm_item.item_code]) else: - variant = frappe.get_doc('Item', 'PIV-RED') - if not frappe.db.exists('BOM', {"item": 'PIV-RED'}): - variant_bom = make_bom(item = variant.item_code, raw_materials = [rm_item.item_code]) + variant = frappe.get_doc("Item", "PIV-RED") + if not frappe.db.exists("BOM", {"item": "PIV-RED"}): + variant_bom = make_bom(item=variant.item_code, raw_materials=[rm_item.item_code]) """Testing when item variant has a BOM""" so = make_sales_order(item_code="PIV-RED", qty=5) - pln = frappe.new_doc('Production Plan') + pln = frappe.new_doc("Production Plan") pln.company = so.company - pln.get_items_from = 'Sales Order' - pln.item_code = 'PIV-RED' + pln.get_items_from = "Sales Order" + pln.item_code = "PIV-RED" pln.get_open_sales_orders() self.assertEqual(pln.sales_orders[0].sales_order, so.name) pln.get_so_items() - self.assertEqual(pln.po_items[0].item_code, 'PIV-RED') + self.assertEqual(pln.po_items[0].item_code, "PIV-RED") self.assertEqual(pln.po_items[0].bom_no, variant_bom.name) so.cancel() - frappe.delete_doc('Sales Order', so.name) + frappe.delete_doc("Sales Order", so.name) variant_bom.cancel() - frappe.delete_doc('BOM', variant_bom.name) + frappe.delete_doc("BOM", variant_bom.name) """Testing when item variant doesn't have a BOM""" so = make_sales_order(item_code="PIV-RED", qty=5) @@ -397,7 +423,7 @@ class TestProductionPlan(ERPNextTestCase): self.assertEqual(pln.sales_orders[0].sales_order, so.name) pln.po_items = [] pln.get_so_items() - self.assertEqual(pln.po_items[0].item_code, 'PIV-RED') + self.assertEqual(pln.po_items[0].item_code, "PIV-RED") self.assertEqual(pln.po_items[0].bom_no, parent_bom.name) frappe.db.rollback() @@ -409,27 +435,35 @@ class TestProductionPlan(ERPNextTestCase): prefix = "_TestLevel_" boms = { "Assembly": { - "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly1": { + "ChildPart1": {}, + "ChildPart2": {}, + }, "ChildPart6": {}, "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, }, "MegaDeepAssy": { - "SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},}, - # ^ assert that this is - # first item in subassy table - } + "SecretSubassy": { + "SecretPart": {"VerySecret": {"SuperSecret": {"Classified": {}}}}, + }, + # ^ assert that this is + # first item in subassy table + }, } create_nested_bom(boms, prefix=prefix) items = [prefix + item_code for item_code in boms.keys()] plan = create_production_plan(item_code=items[0], do_not_save=True) - plan.append("po_items", { - 'use_multi_level_bom': 1, - 'item_code': items[1], - 'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'), - 'planned_qty': 1, - 'planned_start_date': now_datetime() - }) + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": items[1], + "bom_no": frappe.db.get_value("Item", items[1], "default_bom"), + "planned_qty": 1, + "planned_start_date": now_datetime(), + }, + ) plan.get_sub_assembly_items() bom_level_order = [d.bom_level for d in plan.sub_assembly_items] @@ -439,6 +473,7 @@ class TestProductionPlan(ERPNextTestCase): def test_multiple_work_order_for_production_plan_item(self): "Test producing Prod Plan (making WO) in parts." + def create_work_order(item, pln, qty): # Get Production Items items_data = pln.get_production_items() @@ -449,14 +484,13 @@ class TestProductionPlan(ERPNextTestCase): # Create and Submit Work Order for each item in items_data for key, item in items_data.items(): if pln.sub_assembly_items: - item['use_multi_level_bom'] = 0 + item["use_multi_level_bom"] = 0 wo_name = pln.create_work_order(item) wo_doc = frappe.get_doc("Work Order", wo_name) - wo_doc.update({ - 'wip_warehouse': 'Work In Progress - _TC', - 'fg_warehouse': 'Finished Goods - _TC' - }) + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} + ) wo_doc.submit() wo_list.append(wo_name) @@ -506,33 +540,29 @@ class TestProductionPlan(ERPNextTestCase): make_stock_entry as make_se_from_wo, ) - make_stock_entry(item_code="Raw Material Item 1", - target="Work In Progress - _TC", - qty=2, basic_rate=100 + make_stock_entry( + item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 ) - make_stock_entry(item_code="Raw Material Item 2", - target="Work In Progress - _TC", - qty=2, basic_rate=100 + make_stock_entry( + item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100 ) - item = 'Test Production Item 1' + item = "Test Production Item 1" so = make_sales_order(item_code=item, qty=1) pln = create_production_plan( - company=so.company, - get_items_from="Sales Order", - sales_order=so, - skip_getting_mr_items=True + company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True ) self.assertEqual(pln.po_items[0].pending_qty, 1) wo = make_wo_order_test_record( - item_code=item, qty=1, + item_code=item, + qty=1, company=so.company, - wip_warehouse='Work In Progress - _TC', - fg_warehouse='Finished Goods - _TC', + wip_warehouse="Work In Progress - _TC", + fg_warehouse="Finished Goods - _TC", skip_transfer=1, - do_not_submit=True + do_not_submit=True, ) wo.production_plan = pln.name wo.production_plan_item = pln.po_items[0].name @@ -555,28 +585,24 @@ class TestProductionPlan(ERPNextTestCase): make_stock_entry as make_se_from_wo, ) - make_stock_entry(item_code="Raw Material Item 1", - target="Work In Progress - _TC", - qty=2, basic_rate=100 + make_stock_entry( + item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 ) - make_stock_entry(item_code="Raw Material Item 2", - target="Work In Progress - _TC", - qty=2, basic_rate=100 + make_stock_entry( + item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100 ) - pln = create_production_plan( - item_code='Test Production Item 1', - skip_getting_mr_items=True - ) + pln = create_production_plan(item_code="Test Production Item 1", skip_getting_mr_items=True) self.assertEqual(pln.po_items[0].pending_qty, 1) wo = make_wo_order_test_record( - item_code='Test Production Item 1', qty=1, + item_code="Test Production Item 1", + qty=1, company=pln.company, - wip_warehouse='Work In Progress - _TC', - fg_warehouse='Finished Goods - _TC', + wip_warehouse="Work In Progress - _TC", + fg_warehouse="Finished Goods - _TC", skip_transfer=1, - do_not_submit=True + do_not_submit=True, ) wo.production_plan = pln.name wo.production_plan_item = pln.po_items[0].name @@ -594,17 +620,57 @@ class TestProductionPlan(ERPNextTestCase): def test_qty_based_status(self): pp = frappe.new_doc("Production Plan") - pp.po_items = [ - frappe._dict(planned_qty=5, produce_qty=4) - ] + pp.po_items = [frappe._dict(planned_qty=5, produce_qty=4)] self.assertFalse(pp.all_items_completed()) pp.po_items = [ frappe._dict(planned_qty=5, produce_qty=10), - frappe._dict(planned_qty=5, produce_qty=4) + frappe._dict(planned_qty=5, produce_qty=4), ] self.assertFalse(pp.all_items_completed()) + def test_production_plan_planned_qty(self): + pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55) + pln.make_work_order() + work_order = frappe.db.get_value("Work Order", {"production_plan": pln.name}, "name") + wo_doc = frappe.get_doc("Work Order", work_order) + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} + ) + wo_doc.submit() + self.assertEqual(wo_doc.qty, 0.55) + + def test_temporary_name_relinking(self): + + pp = frappe.new_doc("Production Plan") + + # this can not be unittested so mocking data that would be expected + # from client side. + for _ in range(10): + po_item = pp.append( + "po_items", + { + "name": frappe.generate_hash(length=10), + "temporary_name": frappe.generate_hash(length=10), + }, + ) + pp.append("sub_assembly_items", {"production_plan_item": po_item.temporary_name}) + pp._rename_temporary_references() + + for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): + self.assertEqual(po_item.name, subassy_item.production_plan_item) + + # bad links should be erased + pp.append("sub_assembly_items", {"production_plan_item": frappe.generate_hash(length=16)}) + pp._rename_temporary_references() + self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item) + pp.sub_assembly_items.pop() + + # reattempting on same doc shouldn't change anything + pp._rename_temporary_references() + for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): + self.assertEqual(po_item.name, subassy_item.production_plan_item) + def create_production_plan(**args): """ @@ -614,40 +680,48 @@ def create_production_plan(**args): """ args = frappe._dict(args) - pln = frappe.get_doc({ - 'doctype': 'Production Plan', - 'company': args.company or '_Test Company', - 'customer': args.customer or '_Test Customer', - 'posting_date': nowdate(), - 'include_non_stock_items': args.include_non_stock_items or 0, - 'include_subcontracted_items': args.include_subcontracted_items or 0, - 'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 0, - 'get_items_from': 'Sales Order' - }) + pln = frappe.get_doc( + { + "doctype": "Production Plan", + "company": args.company or "_Test Company", + "customer": args.customer or "_Test Customer", + "posting_date": nowdate(), + "include_non_stock_items": args.include_non_stock_items or 0, + "include_subcontracted_items": args.include_subcontracted_items or 0, + "ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0, + "get_items_from": "Sales Order", + } + ) if not args.get("sales_order"): - pln.append('po_items', { - 'use_multi_level_bom': args.use_multi_level_bom or 1, - 'item_code': args.item_code, - 'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'), - 'planned_qty': args.planned_qty or 1, - 'planned_start_date': args.planned_start_date or now_datetime() - }) + pln.append( + "po_items", + { + "use_multi_level_bom": args.use_multi_level_bom or 1, + "item_code": args.item_code, + "bom_no": frappe.db.get_value("Item", args.item_code, "default_bom"), + "planned_qty": args.planned_qty or 1, + "planned_start_date": args.planned_start_date or now_datetime(), + }, + ) if args.get("get_items_from") == "Sales Order" and args.get("sales_order"): so = args.get("sales_order") - pln.append('sales_orders', { - 'sales_order': so.name, - 'sales_order_date': so.transaction_date, - 'customer': so.customer, - 'grand_total': so.grand_total - }) + pln.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) pln.get_items() if not args.get("skip_getting_mr_items"): mr_items = get_items_for_material_requests(pln.as_dict()) for d in mr_items: - pln.append('mr_items', d) + pln.append("mr_items", d) if not args.do_not_save: pln.insert() @@ -656,31 +730,37 @@ def create_production_plan(**args): return pln + def make_bom(**args): args = frappe._dict(args) - bom = frappe.get_doc({ - 'doctype': 'BOM', - 'is_default': 1, - 'item': args.item, - 'currency': args.currency or 'USD', - 'quantity': args.quantity or 1, - 'company': args.company or '_Test Company', - 'routing': args.routing, - 'with_operations': args.with_operations or 0 - }) + bom = frappe.get_doc( + { + "doctype": "BOM", + "is_default": 1, + "item": args.item, + "currency": args.currency or "USD", + "quantity": args.quantity or 1, + "company": args.company or "_Test Company", + "routing": args.routing, + "with_operations": args.with_operations or 0, + } + ) for item in args.raw_materials: - item_doc = frappe.get_doc('Item', item) + item_doc = frappe.get_doc("Item", item) - bom.append('items', { - 'item_code': item, - 'qty': args.rm_qty or 1.0, - 'uom': item_doc.stock_uom, - 'stock_uom': item_doc.stock_uom, - 'rate': item_doc.valuation_rate or args.rate, - 'source_warehouse': args.source_warehouse - }) + bom.append( + "items", + { + "item_code": item, + "qty": args.rm_qty or 1.0, + "uom": item_doc.stock_uom, + "stock_uom": item_doc.stock_uom, + "rate": item_doc.valuation_rate or args.rate, + "source_warehouse": args.source_warehouse, + }, + ) if not args.do_not_save: bom.insert(ignore_permissions=True) 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 f829d57475a..df5862fcac8 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -27,7 +27,8 @@ "material_request", "material_request_item", "product_bundle_item", - "item_reference" + "item_reference", + "temporary_name" ], "fields": [ { @@ -204,17 +205,25 @@ "fieldtype": "Data", "hidden": 1, "label": "Item Reference" + }, + { + "fieldname": "temporary_name", + "fieldtype": "Data", + "hidden": 1, + "label": "temporary name" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-28 18:31:06.822168", + "modified": "2022-03-24 04:54:09.940224", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index b207906c5e3..d4c37cf79e7 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -19,9 +19,11 @@ class Routing(Document): def calculate_operating_cost(self): for operation in self.operations: if not operation.hour_rate: - operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate') - operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, - operation.precision("operating_cost")) + operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, "hour_rate") + operation.operating_cost = flt( + flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, + operation.precision("operating_cost"), + ) def set_routing_id(self): sequence_id = 0 @@ -29,7 +31,10 @@ class Routing(Document): if not row.sequence_id: row.sequence_id = sequence_id + 1 elif sequence_id and row.sequence_id and cint(sequence_id) > cint(row.sequence_id): - frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}") - .format(row.idx, row.sequence_id, sequence_id)) + frappe.throw( + _("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}").format( + row.idx, row.sequence_id, sequence_id + ) + ) sequence_id = row.sequence_id diff --git a/erpnext/manufacturing/doctype/routing/routing_dashboard.py b/erpnext/manufacturing/doctype/routing/routing_dashboard.py index 4bd4192de5d..65d7a452778 100644 --- a/erpnext/manufacturing/doctype/routing/routing_dashboard.py +++ b/erpnext/manufacturing/doctype/routing/routing_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'routing', - 'transactions': [ - { - 'items': ['BOM'] - } - ] - } + return {"fieldname": "routing", "transactions": [{"items": ["BOM"]}]} diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 8bd60ea4aca..48f1851cb10 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -2,38 +2,41 @@ # See license.txt import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.stock.doctype.item.test_item import make_item -from erpnext.tests.utils import ERPNextTestCase -class TestRouting(ERPNextTestCase): +class TestRouting(FrappeTestCase): @classmethod def setUpClass(cls): cls.item_code = "Test Routing Item - A" @classmethod def tearDownClass(cls): - frappe.db.sql('delete from tabBOM where item=%s', cls.item_code) + frappe.db.sql("delete from tabBOM where item=%s", cls.item_code) def test_sequence_id(self): - operations = [{"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30}, - {"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20}] + operations = [ + {"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30}, + {"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20}, + ] make_test_records("UOM") setup_operations(operations) routing_doc = create_routing(routing_name="Testing Route", operations=operations) bom_doc = setup_bom(item_code=self.item_code, routing=routing_doc.name) - wo_doc = make_wo_order_test_record(production_item = self.item_code, bom_no=bom_doc.name) + wo_doc = make_wo_order_test_record(production_item=self.item_code, bom_no=bom_doc.name) for row in routing_doc.operations: self.assertEqual(row.sequence_id, row.idx) - for data in frappe.get_all("Job Card", - filters={"work_order": wo_doc.name}, order_by="sequence_id desc"): + for data in frappe.get_all( + "Job Card", filters={"work_order": wo_doc.name}, order_by="sequence_id desc" + ): job_card_doc = frappe.get_doc("Job Card", data.name) job_card_doc.time_logs[0].completed_qty = 10 if job_card_doc.sequence_id != 1: @@ -52,33 +55,25 @@ class TestRouting(ERPNextTestCase): "operation": "Test Operation A", "workstation": "_Test Workstation A", "hour_rate_rent": 300, - "hour_rate_labour": 750 , - "time_in_mins": 30 + "hour_rate_labour": 750, + "time_in_mins": 30, }, { "operation": "Test Operation B", "workstation": "_Test Workstation B", "hour_rate_labour": 200, "hour_rate_rent": 1000, - "time_in_mins": 20 - } + "time_in_mins": 20, + }, ] test_routing_operations = [ - { - "operation": "Test Operation A", - "workstation": "_Test Workstation A", - "time_in_mins": 30 - }, - { - "operation": "Test Operation B", - "workstation": "_Test Workstation A", - "time_in_mins": 20 - } + {"operation": "Test Operation A", "workstation": "_Test Workstation A", "time_in_mins": 30}, + {"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 20}, ] setup_operations(operations) routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations) - bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR') + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR") self.assertEqual(routing_doc.operations[0].time_in_mins, 30) self.assertEqual(routing_doc.operations[1].time_in_mins, 20) routing_doc.operations[0].time_in_mins = 90 @@ -93,10 +88,12 @@ class TestRouting(ERPNextTestCase): def setup_operations(rows): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + for row in rows: make_workstation(row) make_operation(row) + def create_routing(**args): args = frappe._dict(args) @@ -108,7 +105,7 @@ def create_routing(**args): doc.insert() except frappe.DuplicateEntryError: doc = frappe.get_doc("Routing", args.routing_name) - doc.delete_key('operations') + doc.delete_key("operations") for operation in args.operations: doc.append("operations", operation) @@ -116,28 +113,35 @@ def create_routing(**args): return doc + def setup_bom(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom args = frappe._dict(args) - if not frappe.db.exists('Item', args.item_code): - make_item(args.item_code, { - 'is_stock_item': 1 - }) + if not frappe.db.exists("Item", args.item_code): + make_item(args.item_code, {"is_stock_item": 1}) if not args.raw_materials: - if not frappe.db.exists('Item', "Test Extra Item N-1"): - make_item("Test Extra Item N-1", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item N-1"): + make_item( + "Test Extra Item N-1", + { + "is_stock_item": 1, + }, + ) - args.raw_materials = ['Test Extra Item N-1'] + args.raw_materials = ["Test Extra Item N-1"] - name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name') + name = frappe.db.get_value("BOM", {"item": args.item_code}, "name") if not name: - bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"), - routing = args.routing, with_operations=1, currency = args.currency) + bom_doc = make_bom( + item=args.item_code, + raw_materials=args.get("raw_materials"), + routing=args.routing, + with_operations=1, + currency=args.currency, + ) else: bom_doc = frappe.get_doc("BOM", name) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 975216d1bd9..7131c335c8a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.tests.utils import FrappeTestCase, change_settings, timeout from frappe.utils import add_days, add_months, cint, flt, now, today from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError @@ -21,31 +22,37 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_bin -from erpnext.tests.utils import ERPNextTestCase, timeout -class TestWorkOrder(ERPNextTestCase): +class TestWorkOrder(FrappeTestCase): def setUp(self): - self.warehouse = '_Test Warehouse 2 - _TC' - self.item = '_Test Item' + self.warehouse = "_Test Warehouse 2 - _TC" + self.item = "_Test Item" def check_planned_qty(self): - planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item", - "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty") or 0 + planned0 = ( + frappe.db.get_value( + "Bin", {"item_code": "_Test FG Item", "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty" + ) + or 0 + ) wo_order = make_wo_order_test_record() - planned1 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item", - "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty") + planned1 = frappe.db.get_value( + "Bin", {"item_code": "_Test FG Item", "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty" + ) self.assertEqual(planned1, planned0 + 10) # add raw materials to stores - test_stock_entry.make_stock_entry(item_code="_Test Item", - target="Stores - _TC", qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - _TC", qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=100, basic_rate=100 + ) # from stores to wip s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4)) @@ -61,8 +68,9 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(frappe.db.get_value("Work Order", wo_order.name, "produced_qty"), 4) - planned2 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item", - "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty") + planned2 = frappe.db.get_value( + "Bin", {"item_code": "_Test FG Item", "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty" + ) self.assertEqual(planned2, planned0 + 6) @@ -71,10 +79,12 @@ class TestWorkOrder(ERPNextTestCase): def test_over_production(self): wo_doc = self.check_planned_qty() - test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100 + ) s = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 7)) s.insert() @@ -82,13 +92,14 @@ class TestWorkOrder(ERPNextTestCase): self.assertRaises(StockOverProductionError, s.submit) def test_planned_operating_cost(self): - wo_order = make_wo_order_test_record(item="_Test FG Item 2", - planned_start_date=now(), qty=1, do_not_save=True) + wo_order = make_wo_order_test_record( + item="_Test FG Item 2", planned_start_date=now(), qty=1, do_not_save=True + ) wo_order.set_work_order_operations() cost = wo_order.planned_operating_cost wo_order.qty = 2 wo_order.set_work_order_operations() - self.assertEqual(wo_order.planned_operating_cost, cost*2) + self.assertEqual(wo_order.planned_operating_cost, cost * 2) def test_reserved_qty_for_partial_completion(self): item = "_Test Item" @@ -99,27 +110,30 @@ class TestWorkOrder(ERPNextTestCase): # reset to correct value bin1_at_start.update_reserved_qty_for_production() - wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, - source_warehouse=warehouse, skip_transfer=1) + wo_order = make_wo_order_test_record( + item="_Test FG Item", qty=2, source_warehouse=warehouse, skip_transfer=1 + ) reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production) # reserved qty for production is updated self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission) - - test_stock_entry.make_stock_entry(item_code="_Test Item", - target=warehouse, qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target=warehouse, qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target=warehouse, qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target=warehouse, qty=100, basic_rate=100 + ) s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 1)) s.submit() bin1_at_completion = get_bin(item, warehouse) - self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), - reserved_qty_on_submission - 1) + self.assertEqual( + cint(bin1_at_completion.reserved_qty_for_production), reserved_qty_on_submission - 1 + ) def test_production_item(self): wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) @@ -143,16 +157,20 @@ class TestWorkOrder(ERPNextTestCase): # reset to correct value self.bin1_at_start.update_reserved_qty_for_production() - self.wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, - source_warehouse=self.warehouse) + self.wo_order = make_wo_order_test_record( + item="_Test FG Item", qty=2, source_warehouse=self.warehouse + ) self.bin1_on_submit = get_bin(self.item, self.warehouse) # reserved qty for production is updated - self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production) + 2, - cint(self.bin1_on_submit.reserved_qty_for_production)) - self.assertEqual(cint(self.bin1_at_start.projected_qty), - cint(self.bin1_on_submit.projected_qty) + 2) + self.assertEqual( + cint(self.bin1_at_start.reserved_qty_for_production) + 2, + cint(self.bin1_on_submit.reserved_qty_for_production), + ) + self.assertEqual( + cint(self.bin1_at_start.projected_qty), cint(self.bin1_on_submit.projected_qty) + 2 + ) def test_reserved_qty_for_production_cancel(self): self.test_reserved_qty_for_production_submit() @@ -162,52 +180,57 @@ class TestWorkOrder(ERPNextTestCase): bin1_on_cancel = get_bin(self.item, self.warehouse) # reserved_qty_for_producion updated - self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production), - cint(bin1_on_cancel.reserved_qty_for_production)) - self.assertEqual(self.bin1_at_start.projected_qty, - cint(bin1_on_cancel.projected_qty)) + self.assertEqual( + cint(self.bin1_at_start.reserved_qty_for_production), + cint(bin1_on_cancel.reserved_qty_for_production), + ) + self.assertEqual(self.bin1_at_start.projected_qty, cint(bin1_on_cancel.projected_qty)) def test_reserved_qty_for_production_on_stock_entry(self): - test_stock_entry.make_stock_entry(item_code="_Test Item", - target= self.warehouse, qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target= self.warehouse, qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target=self.warehouse, qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target=self.warehouse, qty=100, basic_rate=100 + ) self.test_reserved_qty_for_production_submit() - s = frappe.get_doc(make_stock_entry(self.wo_order.name, - "Material Transfer for Manufacture", 2)) + s = frappe.get_doc(make_stock_entry(self.wo_order.name, "Material Transfer for Manufacture", 2)) s.submit() bin1_on_start_production = get_bin(self.item, self.warehouse) # reserved_qty_for_producion updated - self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production), - cint(bin1_on_start_production.reserved_qty_for_production)) + self.assertEqual( + cint(self.bin1_at_start.reserved_qty_for_production), + cint(bin1_on_start_production.reserved_qty_for_production), + ) # projected qty will now be 2 less (becuase of item movement) - self.assertEqual(cint(self.bin1_at_start.projected_qty), - cint(bin1_on_start_production.projected_qty) + 2) + self.assertEqual( + cint(self.bin1_at_start.projected_qty), cint(bin1_on_start_production.projected_qty) + 2 + ) s = frappe.get_doc(make_stock_entry(self.wo_order.name, "Manufacture", 2)) bin1_on_end_production = get_bin(self.item, self.warehouse) # no change in reserved / projected - self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), - cint(bin1_on_start_production.reserved_qty_for_production)) + self.assertEqual( + cint(bin1_on_end_production.reserved_qty_for_production), + cint(bin1_on_start_production.reserved_qty_for_production), + ) def test_reserved_qty_for_production_closed(self): - wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2, - source_warehouse=self.warehouse) + wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=self.warehouse) item = wo1.required_items[0].item_code bin_before = get_bin(item, self.warehouse) bin_before.update_reserved_qty_for_production() - make_wo_order_test_record(item="_Test FG Item", qty=2, - source_warehouse=self.warehouse) + make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=self.warehouse) close_work_order(wo1.name, "Closed") bin_after = get_bin(item, self.warehouse) @@ -217,10 +240,15 @@ class TestWorkOrder(ERPNextTestCase): cancel_stock_entry = [] allow_overproduction("overproduction_percentage_for_work_order", 30) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100) - ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0) - ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0) + ste1 = test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0 + ) + ste2 = test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=240, + basic_rate=1000.0, + ) cancel_stock_entry.extend([ste1.name, ste2.name]) @@ -250,33 +278,37 @@ class TestWorkOrder(ERPNextTestCase): allow_overproduction("overproduction_percentage_for_work_order", 0) def test_reserved_qty_for_stopped_production(self): - test_stock_entry.make_stock_entry(item_code="_Test Item", - target= self.warehouse, qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target= self.warehouse, qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target=self.warehouse, qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target=self.warehouse, qty=100, basic_rate=100 + ) # 0 0 0 self.test_reserved_qty_for_production_submit() - #2 0 -2 + # 2 0 -2 - s = frappe.get_doc(make_stock_entry(self.wo_order.name, - "Material Transfer for Manufacture", 1)) + s = frappe.get_doc(make_stock_entry(self.wo_order.name, "Material Transfer for Manufacture", 1)) s.submit() - #1 -1 0 + # 1 -1 0 bin1_on_start_production = get_bin(self.item, self.warehouse) # reserved_qty_for_producion updated - self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production) + 1, - cint(bin1_on_start_production.reserved_qty_for_production)) + self.assertEqual( + cint(self.bin1_at_start.reserved_qty_for_production) + 1, + cint(bin1_on_start_production.reserved_qty_for_production), + ) # projected qty will now be 2 less (becuase of item movement) - self.assertEqual(cint(self.bin1_at_start.projected_qty), - cint(bin1_on_start_production.projected_qty) + 2) + self.assertEqual( + cint(self.bin1_at_start.projected_qty), cint(bin1_on_start_production.projected_qty) + 2 + ) # STOP stop_unstop(self.wo_order.name, "Stopped") @@ -284,19 +316,24 @@ class TestWorkOrder(ERPNextTestCase): bin1_on_stop_production = get_bin(self.item, self.warehouse) # no change in reserved / projected - self.assertEqual(cint(bin1_on_stop_production.reserved_qty_for_production), - cint(self.bin1_at_start.reserved_qty_for_production)) - self.assertEqual(cint(bin1_on_stop_production.projected_qty) + 1, - cint(self.bin1_at_start.projected_qty)) + self.assertEqual( + cint(bin1_on_stop_production.reserved_qty_for_production), + cint(self.bin1_at_start.reserved_qty_for_production), + ) + self.assertEqual( + cint(bin1_on_stop_production.projected_qty) + 1, cint(self.bin1_at_start.projected_qty) + ) def test_scrap_material_qty(self): wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) # add raw materials to stores - test_stock_entry.make_stock_entry(item_code="_Test Item", - target="Stores - _TC", qty=10, basic_rate=5000.0) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - _TC", qty=10, basic_rate=1000.0) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0 + ) s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2)) for d in s.get("items"): @@ -308,8 +345,9 @@ class TestWorkOrder(ERPNextTestCase): s.insert() s.submit() - wo_order_details = frappe.db.get_value("Work Order", wo_order.name, - ["scrap_warehouse", "qty", "produced_qty", "bom_no"], as_dict=1) + wo_order_details = frappe.db.get_value( + "Work Order", wo_order.name, ["scrap_warehouse", "qty", "produced_qty", "bom_no"], as_dict=1 + ) scrap_item_details = get_scrap_item_details(wo_order_details.bom_no) @@ -318,15 +356,20 @@ class TestWorkOrder(ERPNextTestCase): for item in s.items: if item.bom_no and item.item_code in scrap_item_details: self.assertEqual(wo_order_details.scrap_warehouse, item.t_warehouse) - self.assertEqual(flt(wo_order_details.qty)*flt(scrap_item_details[item.item_code]), item.qty) + self.assertEqual(flt(wo_order_details.qty) * flt(scrap_item_details[item.item_code]), item.qty) def test_allow_overproduction(self): allow_overproduction("overproduction_percentage_for_work_order", 0) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) - test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=10, basic_rate=1000.0) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=10, + basic_rate=1000.0, + ) s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 3)) s.insert() @@ -343,43 +386,47 @@ class TestWorkOrder(ERPNextTestCase): so = make_sales_order(item_code="_Test FG Item", qty=2) allow_overproduction("overproduction_percentage_for_sales_order", 0) - wo_order = make_wo_order_test_record(planned_start_date=now(), - sales_order=so.name, qty=3, do_not_save=True) + wo_order = make_wo_order_test_record( + planned_start_date=now(), sales_order=so.name, qty=3, do_not_save=True + ) self.assertRaises(OverProductionError, wo_order.save) allow_overproduction("overproduction_percentage_for_sales_order", 50) - wo_order = make_wo_order_test_record(planned_start_date=now(), - sales_order=so.name, qty=3) + wo_order = make_wo_order_test_record(planned_start_date=now(), sales_order=so.name, qty=3) - wo_order.submit() self.assertEqual(wo_order.docstatus, 1) allow_overproduction("overproduction_percentage_for_sales_order", 0) def test_work_order_with_non_stock_item(self): - items = {'Finished Good Test Item For non stock': 1, '_Test FG Item': 1, '_Test FG Non Stock Item': 0} + items = { + "Finished Good Test Item For non stock": 1, + "_Test FG Item": 1, + "_Test FG Non Stock Item": 0, + } for item, is_stock_item in items.items(): - make_item(item, { - 'is_stock_item': is_stock_item - }) + make_item(item, {"is_stock_item": is_stock_item}) - if not frappe.db.get_value('Item Price', {'item_code': '_Test FG Non Stock Item'}): - frappe.get_doc({ - 'doctype': 'Item Price', - 'item_code': '_Test FG Non Stock Item', - 'price_list_rate': 1000, - 'price_list': 'Standard Buying' - }).insert(ignore_permissions=True) + if not frappe.db.get_value("Item Price", {"item_code": "_Test FG Non Stock Item"}): + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": "_Test FG Non Stock Item", + "price_list_rate": 1000, + "price_list": "Standard Buying", + } + ).insert(ignore_permissions=True) - fg_item = 'Finished Good Test Item For non stock' - test_stock_entry.make_stock_entry(item_code="_Test FG Item", - target="_Test Warehouse - _TC", qty=1, basic_rate=100) + fg_item = "Finished Good Test Item For non stock" + test_stock_entry.make_stock_entry( + item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) - if not frappe.db.get_value('BOM', {'item': fg_item}): - make_bom(item=fg_item, rate=1000, raw_materials = ['_Test FG Item', '_Test FG Non Stock Item']) + if not frappe.db.get_value("BOM", {"item": fg_item}): + make_bom(item=fg_item, rate=1000, raw_materials=["_Test FG Item", "_Test FG Non Stock Item"]) - wo = make_wo_order_test_record(production_item = fg_item) + wo = make_wo_order_test_record(production_item=fg_item) se = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 1)) se.insert() @@ -393,25 +440,25 @@ class TestWorkOrder(ERPNextTestCase): @timeout(seconds=60) def test_job_card(self): stock_entries = [] - bom = frappe.get_doc('BOM', { - 'docstatus': 1, - 'with_operations': 1, - 'company': '_Test Company' - }) + bom = frappe.get_doc("BOM", {"docstatus": 1, "with_operations": 1, "company": "_Test Company"}) - work_order = make_wo_order_test_record(item=bom.item, qty=1, - bom_no=bom.name, source_warehouse="_Test Warehouse - _TC") + work_order = make_wo_order_test_record( + item=bom.item, qty=1, bom_no=bom.name, source_warehouse="_Test Warehouse - _TC" + ) for row in work_order.required_items: - stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code, - target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100) + stock_entry_doc = test_stock_entry.make_stock_entry( + item_code=row.item_code, target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100 + ) stock_entries.append(stock_entry_doc) ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) ste.submit() stock_entries.append(ste) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc') + job_cards = frappe.get_all( + "Job Card", filters={"work_order": work_order.name}, order_by="creation asc" + ) self.assertEqual(len(job_cards), len(bom.operations)) for i, job_card in enumerate(job_cards): @@ -432,29 +479,33 @@ class TestWorkOrder(ERPNextTestCase): stock_entry.cancel() def test_capcity_planning(self): - frappe.db.set_value("Manufacturing Settings", None, { - "disable_capacity_planning": 0, - "capacity_planning_for_days": 1 - }) + frappe.db.set_value( + "Manufacturing Settings", + None, + {"disable_capacity_planning": 0, "capacity_planning_for_days": 1}, + ) - data = frappe.get_cached_value('BOM', {'docstatus': 1, 'item': '_Test FG Item 2', - 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + data = frappe.get_cached_value( + "BOM", + {"docstatus": 1, "item": "_Test FG Item 2", "with_operations": 1, "company": "_Test Company"}, + ["name", "item"], + ) if data: bom, bom_item = data planned_start_date = add_months(today(), months=-1) - work_order = make_wo_order_test_record(item=bom_item, - qty=10, bom_no=bom, planned_start_date=planned_start_date) + work_order = make_wo_order_test_record( + item=bom_item, qty=10, bom_no=bom, planned_start_date=planned_start_date + ) - work_order1 = make_wo_order_test_record(item=bom_item, - qty=30, bom_no=bom, planned_start_date=planned_start_date, do_not_submit=1) + work_order1 = make_wo_order_test_record( + item=bom_item, qty=30, bom_no=bom, planned_start_date=planned_start_date, do_not_submit=1 + ) self.assertRaises(CapacityError, work_order1.submit) - frappe.db.set_value("Manufacturing Settings", None, { - "capacity_planning_for_days": 30 - }) + frappe.db.set_value("Manufacturing Settings", None, {"capacity_planning_for_days": 30}) work_order1.reload() work_order1.submit() @@ -464,22 +515,22 @@ class TestWorkOrder(ERPNextTestCase): work_order.cancel() def test_work_order_with_non_transfer_item(self): - items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0} + items = {"Finished Good Transfer Item": 1, "_Test FG Item": 1, "_Test FG Item 1": 0} for item, allow_transfer in items.items(): - make_item(item, { - 'include_item_in_manufacturing': allow_transfer - }) + make_item(item, {"include_item_in_manufacturing": allow_transfer}) - fg_item = 'Finished Good Transfer Item' - test_stock_entry.make_stock_entry(item_code="_Test FG Item", - target="_Test Warehouse - _TC", qty=1, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test FG Item 1", - target="_Test Warehouse - _TC", qty=1, basic_rate=100) + fg_item = "Finished Good Transfer Item" + test_stock_entry.make_stock_entry( + item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test FG Item 1", target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) - if not frappe.db.get_value('BOM', {'item': fg_item}): - make_bom(item=fg_item, raw_materials = ['_Test FG Item', '_Test FG Item 1']) + if not frappe.db.get_value("BOM", {"item": fg_item}): + make_bom(item=fg_item, raw_materials=["_Test FG Item", "_Test FG Item 1"]) - wo = make_wo_order_test_record(production_item = fg_item) + wo = make_wo_order_test_record(production_item=fg_item) ste = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 1)) ste.insert() ste.submit() @@ -497,39 +548,42 @@ class TestWorkOrder(ERPNextTestCase): rm1 = "Test Batch Size Item RM 1 For BOM" for item in ["Test Batch Size Item For BOM", "Test Batch Size Item RM 1 For BOM"]: - make_item(item, { - "include_item_in_manufacturing": 1, - "is_stock_item": 1 - }) + make_item(item, {"include_item_in_manufacturing": 1, "is_stock_item": 1}) - bom_name = frappe.db.get_value("BOM", - {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + bom_name = frappe.db.get_value( + "BOM", {"item": fg_item, "is_active": 1, "with_operations": 1}, "name" + ) if not bom_name: - bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True) bom.with_operations = 1 - bom.append("operations", { - "operation": "_Test Operation 1", - "workstation": "_Test Workstation 1", - "description": "Test Data", - "operating_cost": 100, - "time_in_mins": 40, - "batch_size": 5 - }) + bom.append( + "operations", + { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "description": "Test Data", + "operating_cost": 100, + "time_in_mins": 40, + "batch_size": 5, + }, + ) bom.save() bom.submit() bom_name = bom.name - work_order = make_wo_order_test_record(item=fg_item, - planned_start_date=now(), qty=1, do_not_save=True) + work_order = make_wo_order_test_record( + item=fg_item, planned_start_date=now(), qty=1, do_not_save=True + ) work_order.set_work_order_operations() work_order.save() self.assertEqual(work_order.operations[0].time_in_mins, 8.0) - work_order1 = make_wo_order_test_record(item=fg_item, - planned_start_date=now(), qty=5, do_not_save=True) + work_order1 = make_wo_order_test_record( + item=fg_item, planned_start_date=now(), qty=5, do_not_save=True + ) work_order1.set_work_order_operations() work_order1.save() @@ -539,65 +593,73 @@ class TestWorkOrder(ERPNextTestCase): fg_item = "Test Batch Size Item For BOM 3" rm1 = "Test Batch Size Item RM 1 For BOM 3" - frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + frappe.db.set_value("Manufacturing Settings", None, "make_serial_no_batch_from_work_order", 0) for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]: - item_args = { - "include_item_in_manufacturing": 1, - "is_stock_item": 1 - } + item_args = {"include_item_in_manufacturing": 1, "is_stock_item": 1} if item == fg_item: - item_args['has_batch_no'] = 1 - item_args['create_new_batch'] = 1 - item_args['batch_number_series'] = 'TBSI3.#####' + item_args["has_batch_no"] = 1 + item_args["create_new_batch"] = 1 + item_args["batch_number_series"] = "TBSI3.#####" make_item(item, item_args) - bom_name = frappe.db.get_value("BOM", - {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + bom_name = frappe.db.get_value( + "BOM", {"item": fg_item, "is_active": 1, "with_operations": 1}, "name" + ) if not bom_name: - bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True) bom.save() bom.submit() bom_name = bom.name - work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + work_order = make_wo_order_test_record( + item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1 + ) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) - for row in ste1.get('items'): + for row in ste1.get("items"): if row.is_finished_item: self.assertEqual(row.item_code, fg_item) - work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) - frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1) + work_order = make_wo_order_test_record( + item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1 + ) + frappe.db.set_value("Manufacturing Settings", None, "make_serial_no_batch_from_work_order", 1) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) - for row in ste1.get('items'): + for row in ste1.get("items"): if row.is_finished_item: self.assertEqual(row.item_code, fg_item) - work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), - qty=30, do_not_save = True) + work_order = make_wo_order_test_record( + item=fg_item, skip_transfer=True, planned_start_date=now(), qty=30, do_not_save=True + ) work_order.batch_size = 10 work_order.insert() work_order.submit() self.assertEqual(work_order.has_batch_no, 1) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30)) - for row in ste1.get('items'): + for row in ste1.get("items"): if row.is_finished_item: self.assertEqual(row.item_code, fg_item) self.assertEqual(row.qty, 10) - frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + frappe.db.set_value("Manufacturing Settings", None, "make_serial_no_batch_from_work_order", 0) def test_partial_material_consumption(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) ste_cancel_list = [] - ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0) - ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0) + ste1 = test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0 + ) + ste2 = test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=20, + basic_rate=1000.0, + ) ste_cancel_list.extend([ste1, ste2]) @@ -623,16 +685,25 @@ class TestWorkOrder(ERPNextTestCase): def test_extra_material_transfer(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0) - frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", - "Material Transferred for Manufacture") + frappe.db.set_value( + "Manufacturing Settings", + None, + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) ste_cancel_list = [] - ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0) - ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0) + ste1 = test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0 + ) + ste2 = test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=20, + basic_rate=1000.0, + ) ste_cancel_list.extend([ste1, ste2]) @@ -664,30 +735,31 @@ class TestWorkOrder(ERPNextTestCase): frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") def test_make_stock_entry_for_customer_provided_item(self): - finished_item = 'Test Item for Make Stock Entry 1' - make_item(finished_item, { + finished_item = "Test Item for Make Stock Entry 1" + make_item(finished_item, {"include_item_in_manufacturing": 1, "is_stock_item": 1}) + + customer_provided_item = "CUST-0987" + make_item( + customer_provided_item, + { + "is_purchase_item": 0, + "is_customer_provided_item": 1, + "is_stock_item": 1, "include_item_in_manufacturing": 1, - "is_stock_item": 1 - }) + "customer": "_Test Customer", + }, + ) - customer_provided_item = 'CUST-0987' - make_item(customer_provided_item, { - 'is_purchase_item': 0, - 'is_customer_provided_item': 1, - "is_stock_item": 1, - "include_item_in_manufacturing": 1, - 'customer': '_Test Customer' - }) - - if not frappe.db.exists('BOM', {'item': finished_item}): + if not frappe.db.exists("BOM", {"item": finished_item}): make_bom(item=finished_item, raw_materials=[customer_provided_item], rm_qty=1) company = "_Test Company with perpetual inventory" customer_warehouse = create_warehouse("Test Customer Provided Warehouse", company=company) - wo = make_wo_order_test_record(item=finished_item, qty=1, source_warehouse=customer_warehouse, - company=company) + wo = make_wo_order_test_record( + item=finished_item, qty=1, source_warehouse=customer_warehouse, company=company + ) - ste = frappe.get_doc(make_stock_entry(wo.name, purpose='Material Transfer for Manufacture')) + ste = frappe.get_doc(make_stock_entry(wo.name, purpose="Material Transfer for Manufacture")) ste.insert() self.assertEqual(len(ste.items), 1) @@ -696,26 +768,33 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(item.valuation_rate, 0) def test_valuation_rate_missing_on_make_stock_entry(self): - item_name = 'Test Valuation Rate Missing' - rm_item = '_Test raw material item' - make_item(item_name, { - "is_stock_item": 1, - "include_item_in_manufacturing": 1, - }) - make_item('_Test raw material item', { - "is_stock_item": 1, - "include_item_in_manufacturing": 1, - }) + item_name = "Test Valuation Rate Missing" + rm_item = "_Test raw material item" + make_item( + item_name, + { + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + }, + ) + make_item( + "_Test raw material item", + { + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + }, + ) - if not frappe.db.get_value('BOM', {'item': item_name}): + if not frappe.db.get_value("BOM", {"item": item_name}): make_bom(item=item_name, raw_materials=[rm_item], rm_qty=1) company = "_Test Company with perpetual inventory" source_warehouse = create_warehouse("Test Valuation Rate Missing Warehouse", company=company) - wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse, - company=company) + wo = make_wo_order_test_record( + item=item_name, qty=1, source_warehouse=source_warehouse, company=company + ) - stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture')) + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture")) self.assertRaises(frappe.ValidationError, stock_entry.save) def test_wo_completion_with_pl_bom(self): @@ -725,19 +804,19 @@ class TestWorkOrder(ERPNextTestCase): ) qty = 4 - scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG + scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG source_warehouse = "Stores - _TC" wip_warehouse = "_Test Warehouse - _TC" fg_item_non_whole, _, bom_item = create_process_loss_bom_items() - test_stock_entry.make_stock_entry(item_code=bom_item.item_code, - target=source_warehouse, qty=4, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code=bom_item.item_code, target=source_warehouse, qty=4, basic_rate=100 + ) bom_no = f"BOM-{fg_item_non_whole.item_code}-001" if not frappe.db.exists("BOM", bom_no): bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=scrap_qty, - scrap_rate=0, fg_qty=1, is_process_loss=1 + fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1, is_process_loss=1 ) bom_doc.submit() @@ -750,16 +829,12 @@ class TestWorkOrder(ERPNextTestCase): stock_uom=fg_item_non_whole.stock_uom, ) - se = frappe.get_doc( - make_stock_entry(wo.name, "Material Transfer for Manufacture", qty) - ) + se = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", qty)) se.get("items")[0].s_warehouse = "Stores - _TC" se.insert() se.submit() - se = frappe.get_doc( - make_stock_entry(wo.name, "Manufacture", qty) - ) + se = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", qty)) se.insert() se.submit() @@ -776,41 +851,52 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(fg_item.qty, actual_fg_qty) # Testing Work Order values - self.assertEqual( - frappe.db.get_value("Work Order", wo.name, "produced_qty"), - qty - ) - self.assertEqual( - frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), - total_pl_qty - ) + self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty) + self.assertEqual(frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), total_pl_qty) @timeout(seconds=60) def test_job_card_scrap_item(self): - items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test', - 'Test RM Item 2 for Scrap Item Test'] + items = [ + "Test FG Item for Scrap Item Test", + "Test RM Item 1 for Scrap Item Test", + "Test RM Item 2 for Scrap Item Test", + ] - company = '_Test Company with perpetual inventory' + company = "_Test Company with perpetual inventory" for item_code in items: - create_item(item_code = item_code, is_stock_item = 1, - is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1') + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + opening_stock=100, + valuation_rate=10, + company=company, + warehouse="Stores - TCP1", + ) - item = 'Test FG Item for Scrap Item Test' - raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test'] - if not frappe.db.get_value('BOM', {'item': item}): - bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True) + item = "Test FG Item for Scrap Item Test" + raw_materials = ["Test RM Item 1 for Scrap Item Test", "Test RM Item 2 for Scrap Item Test"] + if not frappe.db.get_value("BOM", {"item": item}): + bom = make_bom( + item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True + ) bom.with_operations = 1 - bom.append('operations', { - 'operation': '_Test Operation 1', - 'workstation': '_Test Workstation 1', - 'hour_rate': 20, - 'time_in_mins': 60 - }) + bom.append( + "operations", + { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "hour_rate": 20, + "time_in_mins": 60, + }, + ) bom.submit() - wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1) - job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + wo_order = make_wo_order_test_record( + item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1 + ) + job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name") update_job_card(job_card) stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) @@ -819,8 +905,10 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(row.qty, 1) # Partial Job Card 1 with qty 10 - wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1) - job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + wo_order = make_wo_order_test_record( + item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1 + ) + job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name") update_job_card(job_card, 10) stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) @@ -833,12 +921,12 @@ class TestWorkOrder(ERPNextTestCase): wo_order.load_from_db() for row in wo_order.operations: n_dict = row.as_dict() - n_dict['qty'] = 10 - n_dict['pending_qty'] = 10 + n_dict["qty"] = 10 + n_dict["pending_qty"] = 10 operations.append(n_dict) make_job_card(wo_order.name, operations) - job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name') + job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name, "docstatus": 0}, "name") update_job_card(job_card, 10) stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) @@ -847,145 +935,273 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(row.qty, 2) def test_close_work_order(self): - items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO', - 'Test RM Item 2 for Closed WO'] + items = [ + "Test FG Item for Closed WO", + "Test RM Item 1 for Closed WO", + "Test RM Item 2 for Closed WO", + ] - company = '_Test Company with perpetual inventory' + company = "_Test Company with perpetual inventory" for item_code in items: - create_item(item_code = item_code, is_stock_item = 1, - is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1') + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + opening_stock=100, + valuation_rate=10, + company=company, + warehouse="Stores - TCP1", + ) - item = 'Test FG Item for Closed WO' - raw_materials = ['Test RM Item 1 for Closed WO', 'Test RM Item 2 for Closed WO'] - if not frappe.db.get_value('BOM', {'item': item}): - bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True) + item = "Test FG Item for Closed WO" + raw_materials = ["Test RM Item 1 for Closed WO", "Test RM Item 2 for Closed WO"] + if not frappe.db.get_value("BOM", {"item": item}): + bom = make_bom( + item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True + ) bom.with_operations = 1 - bom.append('operations', { - 'operation': '_Test Operation 1', - 'workstation': '_Test Workstation 1', - 'hour_rate': 20, - 'time_in_mins': 60 - }) + bom.append( + "operations", + { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "hour_rate": 20, + "time_in_mins": 60, + }, + ) bom.submit() - wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1) - job_cards = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + wo_order = make_wo_order_test_record( + item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1 + ) + job_cards = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name") if len(job_cards) == len(bom.operations): for jc in job_cards: - job_card_doc = frappe.get_doc('Job Card', jc) - job_card_doc.append('time_logs', { - 'from_time': now(), - 'time_in_mins': 60, - 'completed_qty': job_card_doc.for_quantity - }) + job_card_doc = frappe.get_doc("Job Card", jc) + job_card_doc.append( + "time_logs", + {"from_time": now(), "time_in_mins": 60, "completed_qty": job_card_doc.for_quantity}, + ) job_card_doc.submit() close_work_order(wo_order, "Closed") - self.assertEqual(wo_order.get('status'), "Closed") + self.assertEqual(wo_order.get("status"), "Closed") def test_partial_manufacture_entries(self): cancel_stock_entry = [] - frappe.db.set_value("Manufacturing Settings", None, - "backflush_raw_materials_based_on", "Material Transferred for Manufacture") + frappe.db.set_value( + "Manufacturing Settings", + None, + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100) - ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0) + ste1 = test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0 + ) - ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0) + ste2 = test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=240, + basic_rate=1000.0, + ) cancel_stock_entry.extend([ste1.name, ste2.name]) sm = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 100)) - for row in sm.get('items'): - if row.get('item_code') == '_Test Item': + for row in sm.get("items"): + if row.get("item_code") == "_Test Item": row.qty = 110 sm.submit() cancel_stock_entry.append(sm.name) s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 90)) - for row in s.get('items'): - if row.get('item_code') == '_Test Item': - self.assertEqual(row.get('qty'), 100) + for row in s.get("items"): + if row.get("item_code") == "_Test Item": + self.assertEqual(row.get("qty"), 100) s.submit() cancel_stock_entry.append(s.name) s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5)) - for row in s1.get('items'): - if row.get('item_code') == '_Test Item': - self.assertEqual(row.get('qty'), 5) + for row in s1.get("items"): + if row.get("item_code") == "_Test Item": + self.assertEqual(row.get("qty"), 5) s1.submit() cancel_stock_entry.append(s1.name) s2 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5)) - for row in s2.get('items'): - if row.get('item_code') == '_Test Item': - self.assertEqual(row.get('qty'), 5) + for row in s2.get("items"): + if row.get("item_code") == "_Test Item": + self.assertEqual(row.get("qty"), 5) cancel_stock_entry.reverse() for ste in cancel_stock_entry: doc = frappe.get_doc("Stock Entry", ste) doc.cancel() - frappe.db.set_value("Manufacturing Settings", None, - "backflush_raw_materials_based_on", "BOM") + frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") + + @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1}) + def test_auto_batch_creation(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + fg_item = frappe.generate_hash(length=20) + child_item = frappe.generate_hash(length=20) + + bom_tree = {fg_item: {child_item: {}}} + + create_nested_bom(bom_tree, prefix="") + + item = frappe.get_doc("Item", fg_item) + item.has_batch_no = 1 + item.create_new_batch = 0 + item.save() + + try: + make_wo_order_test_record(item=fg_item) + 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) + + @change_settings( + "Manufacturing Settings", + {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, + ) + def test_work_order_multiple_material_transfer(self): + """ + Test transferring multiple RMs in separate Stock Entries. + """ + work_order = make_wo_order_test_record(planned_start_date=now(), qty=1) + test_stock_entry.make_stock_entry( # stock up RM + item_code="_Test Item", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=5000.0, + ) + test_stock_entry.make_stock_entry( # stock up RM + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=2, + basic_rate=1000.0, + ) + + transfer_entry = frappe.get_doc( + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1) + ) + del transfer_entry.get("items")[0] # transfer only one RM + transfer_entry.submit() + + # WO's "Material Transferred for Mfg" shows all is transferred, one RM is pending + work_order.reload() + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) + self.assertEqual(work_order.required_items[0].transferred_qty, 0) + self.assertEqual(work_order.required_items[1].transferred_qty, 2) + + final_transfer_entry = frappe.get_doc( # transfer last RM with For Quantity = 0 + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0) + ) + final_transfer_entry.save() + + self.assertEqual(final_transfer_entry.fg_completed_qty, 0.0) + self.assertEqual(final_transfer_entry.items[0].qty, 1) + + final_transfer_entry.submit() + work_order.reload() + + # WO's "Material Transferred for Mfg" shows all is transferred, no RM is pending + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) + self.assertEqual(work_order.required_items[0].transferred_qty, 1) + self.assertEqual(work_order.required_items[1].transferred_qty, 2) + def update_job_card(job_card, jc_qty=None): - employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') + employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") - job_card_doc = frappe.get_doc('Job Card', job_card) - job_card_doc.set('scrap_items', [ - { - 'item_code': 'Test RM Item 1 for Scrap Item Test', - 'stock_qty': 2 - }, - { - 'item_code': 'Test RM Item 2 for Scrap Item Test', - 'stock_qty': 2 - }, - ]) + job_card_doc = frappe.get_doc("Job Card", job_card) + job_card_doc.set( + "scrap_items", + [ + {"item_code": "Test RM Item 1 for Scrap Item Test", "stock_qty": 2}, + {"item_code": "Test RM Item 2 for Scrap Item Test", "stock_qty": 2}, + ], + ) if jc_qty: job_card_doc.for_quantity = jc_qty - job_card_doc.append('time_logs', { - 'from_time': now(), - 'employee': employee, - 'time_in_mins': 60, - 'completed_qty': job_card_doc.for_quantity - }) + job_card_doc.append( + "time_logs", + { + "from_time": now(), + "employee": employee, + "time_in_mins": 60, + "completed_qty": job_card_doc.for_quantity, + }, + ) job_card_doc.submit() + def get_scrap_item_details(bom_no): scrap_items = {} - for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` - where parent = %s""", bom_no, as_dict=1): + for item in frappe.db.sql( + """select item_code, stock_qty from `tabBOM Scrap Item` + where parent = %s""", + bom_no, + as_dict=1, + ): scrap_items[item.item_code] = item.stock_qty return scrap_items + def allow_overproduction(fieldname, percentage): doc = frappe.get_doc("Manufacturing Settings") - doc.update({ - fieldname: percentage - }) + doc.update({fieldname: percentage}) doc.save() + def make_wo_order_test_record(**args): args = frappe._dict(args) if args.company and args.company != "_Test Company": - warehouse_map = { - "fg_warehouse": "_Test FG Warehouse", - "wip_warehouse": "_Test WIP Warehouse" - } + warehouse_map = {"fg_warehouse": "_Test FG Warehouse", "wip_warehouse": "_Test WIP Warehouse"} for attr, wh_name in warehouse_map.items(): if not args.get(attr): @@ -993,16 +1209,17 @@ def make_wo_order_test_record(**args): wo_order = frappe.new_doc("Work Order") wo_order.production_item = args.production_item or args.item or args.item_code or "_Test FG Item" - wo_order.bom_no = args.bom_no or frappe.db.get_value("BOM", {"item": wo_order.production_item, - "is_active": 1, "is_default": 1}) + wo_order.bom_no = args.bom_no or frappe.db.get_value( + "BOM", {"item": wo_order.production_item, "is_active": 1, "is_default": 1} + ) wo_order.qty = args.qty or 10 wo_order.wip_warehouse = args.wip_warehouse or "_Test Warehouse - _TC" wo_order.fg_warehouse = args.fg_warehouse or "_Test Warehouse 1 - _TC" wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC" wo_order.company = args.company or "_Test Company" wo_order.stock_uom = args.stock_uom or "_Test UOM" - wo_order.use_multi_level_bom=0 - wo_order.skip_transfer=args.skip_transfer or 0 + wo_order.use_multi_level_bom = 0 + wo_order.skip_transfer = args.skip_transfer or 0 wo_order.get_items_and_operations_from_bom() wo_order.sales_order = args.sales_order or None wo_order.planned_start_date = args.planned_start_date or now() @@ -1019,4 +1236,5 @@ def make_wo_order_test_record(**args): wo_order.submit() return wo_order -test_records = frappe.get_test_records('Work Order') + +test_records = frappe.get_test_records("Work Order") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 3f2f39e73af..9b0c8382c53 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -540,8 +540,10 @@ erpnext.work_order = { || frm.doc.transfer_material_against == 'Job Card') ? 0 : 1; if (show_start_btn) { - if ((flt(doc.material_transferred_for_manufacturing) < flt(doc.qty)) - && frm.doc.status != 'Stopped') { + let pending_to_transfer = frm.doc.required_items.some( + item => flt(item.transferred_qty) < flt(item.required_qty) + ); + if (pending_to_transfer && frm.doc.status != 'Stopped') { frm.has_start_btn = true; frm.add_custom_button(__('Create Pick List'), function() { erpnext.work_order.create_pick_list(frm); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 47fe3296cf1..dc553c15ad5 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -42,11 +42,26 @@ from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehous from erpnext.utilities.transaction_base import validate_uom_is_integer -class OverProductionError(frappe.ValidationError): pass -class CapacityError(frappe.ValidationError): pass -class StockOverProductionError(frappe.ValidationError): pass -class OperationTooLongError(frappe.ValidationError): pass -class ItemHasVariantError(frappe.ValidationError): pass +class OverProductionError(frappe.ValidationError): + pass + + +class CapacityError(frappe.ValidationError): + pass + + +class StockOverProductionError(frappe.ValidationError): + pass + + +class OperationTooLongError(frappe.ValidationError): + pass + + +class ItemHasVariantError(frappe.ValidationError): + pass + + class SerialNoQtyError(frappe.ValidationError): pass @@ -74,12 +89,13 @@ class WorkOrder(Document): validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) - self.set_required_items(reset_only_qty = len(self.get("required_items"))) + self.set_required_items(reset_only_qty=len(self.get("required_items"))) def validate_sales_order(self): if self.sales_order: self.check_sales_order_on_hold_or_close() - so = frappe.db.sql(""" + so = frappe.db.sql( + """ select so.name, so_item.delivery_date, so.project from `tabSales Order` so inner join `tabSales Order Item` so_item on so_item.parent = so.name @@ -88,10 +104,14 @@ class WorkOrder(Document): and so.skip_delivery_note = 0 and ( so_item.item_code=%s or pk_item.item_code=%s ) - """, (self.sales_order, self.production_item, self.production_item), as_dict=1) + """, + (self.sales_order, self.production_item, self.production_item), + as_dict=1, + ) if not so: - so = frappe.db.sql(""" + so = frappe.db.sql( + """ select so.name, so_item.delivery_date, so.project from @@ -102,7 +122,10 @@ class WorkOrder(Document): and so.skip_delivery_note = 0 and so_item.item_code = packed_item.parent_item and so.docstatus = 1 and packed_item.item_code=%s - """, (self.sales_order, self.production_item), as_dict=1) + """, + (self.sales_order, self.production_item), + as_dict=1, + ) if len(so): if not self.expected_delivery_date: @@ -123,7 +146,9 @@ class WorkOrder(Document): def set_default_warehouse(self): if not self.wip_warehouse: - self.wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") + self.wip_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_wip_warehouse" + ) if not self.fg_warehouse: self.fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_fg_warehouse") @@ -145,40 +170,55 @@ class WorkOrder(Document): self.planned_operating_cost += flt(d.planned_operating_cost) self.actual_operating_cost += flt(d.actual_operating_cost) - variable_cost = self.actual_operating_cost if self.actual_operating_cost \ - else self.planned_operating_cost + variable_cost = ( + self.actual_operating_cost if self.actual_operating_cost else self.planned_operating_cost + ) - self.total_operating_cost = (flt(self.additional_operating_cost) - + flt(variable_cost) + flt(self.corrective_operation_cost)) + self.total_operating_cost = ( + flt(self.additional_operating_cost) + flt(variable_cost) + flt(self.corrective_operation_cost) + ) def validate_work_order_against_so(self): # already ordered qty - ordered_qty_against_so = frappe.db.sql("""select sum(qty) from `tabWork Order` + ordered_qty_against_so = frappe.db.sql( + """select sum(qty) from `tabWork Order` where production_item = %s and sales_order = %s and docstatus < 2 and name != %s""", - (self.production_item, self.sales_order, self.name))[0][0] + (self.production_item, self.sales_order, self.name), + )[0][0] total_qty = flt(ordered_qty_against_so) + flt(self.qty) # get qty from Sales Order Item table - so_item_qty = frappe.db.sql("""select sum(stock_qty) from `tabSales Order Item` + so_item_qty = frappe.db.sql( + """select sum(stock_qty) from `tabSales Order Item` where parent = %s and item_code = %s""", - (self.sales_order, self.production_item))[0][0] + (self.sales_order, self.production_item), + )[0][0] # get qty from Packing Item table - dnpi_qty = frappe.db.sql("""select sum(qty) from `tabPacked Item` + dnpi_qty = frappe.db.sql( + """select sum(qty) from `tabPacked Item` where parent = %s and parenttype = 'Sales Order' and item_code = %s""", - (self.sales_order, self.production_item))[0][0] + (self.sales_order, self.production_item), + )[0][0] # total qty in SO so_qty = flt(so_item_qty) + flt(dnpi_qty) - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_sales_order")) + allowance_percentage = flt( + frappe.db.get_single_value( + "Manufacturing Settings", "overproduction_percentage_for_sales_order" + ) + ) - if total_qty > so_qty + (allowance_percentage/100 * so_qty): - frappe.throw(_("Cannot produce more Item {0} than Sales Order quantity {1}") - .format(self.production_item, so_qty), OverProductionError) + if total_qty > so_qty + (allowance_percentage / 100 * so_qty): + frappe.throw( + _("Cannot produce more Item {0} than Sales Order quantity {1}").format( + self.production_item, so_qty + ), + OverProductionError, + ) def update_status(self, status=None): - '''Update status of work order if unknown''' + """Update status of work order if unknown""" if status != "Stopped" and status != "Closed": status = self.get_status(status) @@ -190,17 +230,22 @@ class WorkOrder(Document): return status def get_status(self, status=None): - '''Return the status based on stock entries against this work order''' + """Return the status based on stock entries against this work order""" if not status: status = self.status - if self.docstatus==0: - status = 'Draft' - elif self.docstatus==1: - if status != 'Stopped': - stock_entries = frappe._dict(frappe.db.sql("""select purpose, sum(fg_completed_qty) + if self.docstatus == 0: + status = "Draft" + elif self.docstatus == 1: + if status != "Stopped": + stock_entries = frappe._dict( + frappe.db.sql( + """select purpose, sum(fg_completed_qty) from `tabStock Entry` where work_order=%s and docstatus=1 - group by purpose""", self.name)) + group by purpose""", + self.name, + ) + ) status = "Not Started" if stock_entries: @@ -209,31 +254,46 @@ class WorkOrder(Document): if flt(produced_qty) >= flt(self.qty): status = "Completed" else: - status = 'Cancelled' + status = "Cancelled" return status def update_work_order_qty(self): """Update **Manufactured Qty** and **Material Transferred for Qty** in Work Order - based on Stock Entry""" + based on Stock Entry""" - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) - for purpose, fieldname in (("Manufacture", "produced_qty"), - ("Material Transfer for Manufacture", "material_transferred_for_manufacturing")): - if (purpose == 'Material Transfer for Manufacture' and - self.operations and self.transfer_material_against == 'Job Card'): + for purpose, fieldname in ( + ("Manufacture", "produced_qty"), + ("Material Transfer for Manufacture", "material_transferred_for_manufacturing"), + ): + if ( + purpose == "Material Transfer for Manufacture" + and self.operations + and self.transfer_material_against == "Job Card" + ): continue - qty = flt(frappe.db.sql("""select sum(fg_completed_qty) + qty = flt( + frappe.db.sql( + """select sum(fg_completed_qty) from `tabStock Entry` where work_order=%s and docstatus=1 - and purpose=%s""", (self.name, purpose))[0][0]) + and purpose=%s""", + (self.name, purpose), + )[0][0] + ) - completed_qty = self.qty + (allowance_percentage/100 * self.qty) + completed_qty = self.qty + (allowance_percentage / 100 * self.qty) if qty > completed_qty: - frappe.throw(_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format(\ - self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError) + frappe.throw( + _("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format( + self.meta.get_label(fieldname), qty, completed_qty, self.name + ), + StockOverProductionError, + ) self.db_set(fieldname, qty) self.set_process_loss_qty() @@ -247,7 +307,9 @@ class WorkOrder(Document): self.update_production_plan_status() def set_process_loss_qty(self): - process_loss_qty = flt(frappe.db.sql(""" + process_loss_qty = flt( + frappe.db.sql( + """ SELECT sum(qty) FROM `tabStock Entry Detail` WHERE is_process_loss=1 @@ -258,21 +320,33 @@ class WorkOrder(Document): AND purpose='Manufacture' AND docstatus=1 ) - """, (self.name, ))[0][0]) + """, + (self.name,), + )[0][0] + ) if process_loss_qty is not None: - self.db_set('process_loss_qty', process_loss_qty) + self.db_set("process_loss_qty", process_loss_qty) def update_production_plan_status(self): - production_plan = frappe.get_doc('Production Plan', self.production_plan) + production_plan = frappe.get_doc("Production Plan", self.production_plan) produced_qty = 0 if self.production_plan_item: - total_qty = frappe.get_all("Work Order", fields = "sum(produced_qty) as produced_qty", - filters = {'docstatus': 1, 'production_plan': self.production_plan, - 'production_plan_item': self.production_plan_item}, as_list=1) + total_qty = frappe.get_all( + "Work Order", + fields="sum(produced_qty) as produced_qty", + filters={ + "docstatus": 1, + "production_plan": self.production_plan, + "production_plan_item": self.production_plan_item, + }, + as_list=1, + ) produced_qty = total_qty[0][0] if total_qty else 0 - production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item) + production_plan.run_method( + "update_produced_pending_qty", produced_qty, self.production_plan_item + ) def before_submit(self): self.create_serial_no_batch_no() @@ -283,7 +357,9 @@ class WorkOrder(Document): 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}): + 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() @@ -296,9 +372,11 @@ class WorkOrder(Document): def on_cancel(self): self.validate_cancel() - frappe.db.set(self,'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") - if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): + 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() @@ -314,16 +392,15 @@ class WorkOrder(Document): if not (self.has_serial_no or self.has_batch_no): return - if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + if not cint( + frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") + ): return if self.has_batch_no: self.create_batch_for_finished_good() - args = { - "item_code": self.production_item, - "work_order": self.name - } + args = {"item_code": self.production_item, "work_order": self.name} if self.has_serial_no: self.make_serial_nos(args) @@ -333,6 +410,17 @@ class WorkOrder(Document): if not self.batch_size: self.batch_size = total_qty + batch_auto_creation = frappe.get_cached_value("Item", self.production_item, "create_new_batch") + if not batch_auto_creation: + frappe.msgprint( + _("Batch not created for item {} since it does not have a batch series.").format( + frappe.bold(self.production_item) + ), + alert=True, + indicator="orange", + ) + return + while total_qty > 0: qty = self.batch_size if self.batch_size >= total_qty: @@ -344,19 +432,23 @@ class WorkOrder(Document): qty = total_qty total_qty = 0 - make_batch(frappe._dict({ - "item": self.production_item, - "qty_to_produce": qty, - "reference_doctype": self.doctype, - "reference_name": self.name - })) + make_batch( + frappe._dict( + { + "item": self.production_item, + "qty_to_produce": qty, + "reference_doctype": self.doctype, + "reference_name": self.name, + } + ) + ) def delete_auto_created_batch_and_serial_no(self): - for row in frappe.get_all("Serial No", filters = {"work_order": self.name}): + for row in frappe.get_all("Serial No", filters={"work_order": self.name}): frappe.delete_doc("Serial No", row.name) self.db_set("serial_no", "") - for row in frappe.get_all("Batch", filters = {"reference_name": self.name}): + for row in frappe.get_all("Batch", filters={"reference_name": self.name}): frappe.delete_doc("Batch", row.name) def make_serial_nos(self, args): @@ -371,8 +463,12 @@ class WorkOrder(Document): serial_nos_length = len(get_serial_nos(self.serial_no)) if serial_nos_length != self.qty: - frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.") - .format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError) + frappe.throw( + _("{0} Serial Numbers required for Item {1}. You have provided {2}.").format( + self.qty, self.production_item, serial_nos_length + ), + SerialNoQtyError, + ) def create_job_card(self): manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") @@ -385,8 +481,7 @@ class WorkOrder(Document): while qty > 0: qty = split_qty_based_on_batch_size(self, row, qty) if row.job_card_qty > 0: - self.prepare_data_for_job_card(row, index, - plan_days, enable_capacity_planning) + self.prepare_data_for_job_card(row, index, plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: @@ -396,12 +491,14 @@ class WorkOrder(Document): self.set_operation_start_end_time(index, row) if not row.workstation: - frappe.throw(_("Row {0}: select the workstation against the operation {1}") - .format(row.idx, row.operation)) + frappe.throw( + _("Row {0}: select the workstation against the operation {1}").format(row.idx, row.operation) + ) original_start_time = row.planned_start_time - job_card_doc = create_job_card(self, row, auto_create=True, - enable_capacity_planning=enable_capacity_planning) + job_card_doc = create_job_card( + self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning + ) if enable_capacity_planning and job_card_doc: row.planned_start_time = job_card_doc.time_logs[-1].from_time @@ -409,22 +506,29 @@ class WorkOrder(Document): if date_diff(row.planned_start_time, original_start_time) > plan_days: frappe.message_log.pop() - frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") - .format(plan_days, row.operation), CapacityError) + frappe.throw( + _("Unable to find the time slot in the next {0} days for the operation {1}.").format( + plan_days, row.operation + ), + CapacityError, + ) row.db_update() def set_operation_start_end_time(self, idx, row): """Set start and end time for given operation. If first operation, set start as `planned_start_date`, else add time diff to end time of earlier operation.""" - if idx==0: + if idx == 0: # first operation at planned_start date row.planned_start_time = self.planned_start_date else: - row.planned_start_time = get_datetime(self.operations[idx-1].planned_end_time)\ - + get_mins_between_operations() + row.planned_start_time = ( + get_datetime(self.operations[idx - 1].planned_end_time) + get_mins_between_operations() + ) - row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta(minutes = row.time_in_mins) + row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta( + minutes=row.time_in_mins + ) if row.planned_start_time == row.planned_end_time: frappe.throw(_("Capacity Planning Error, planned start time can not be same as end time")) @@ -434,22 +538,35 @@ class WorkOrder(Document): frappe.throw(_("Stopped Work Order cannot be cancelled, Unstop it first to cancel")) # Check whether any stock entry exists against this Work Order - stock_entry = frappe.db.sql("""select name from `tabStock Entry` - where work_order = %s and docstatus = 1""", self.name) + stock_entry = frappe.db.sql( + """select name from `tabStock Entry` + where work_order = %s and docstatus = 1""", + self.name, + ) if stock_entry: - frappe.throw(_("Cannot cancel because submitted Stock Entry {0} exists").format(frappe.utils.get_link_to_form('Stock Entry', stock_entry[0][0]))) + frappe.throw( + _("Cannot cancel because submitted Stock Entry {0} exists").format( + frappe.utils.get_link_to_form("Stock Entry", stock_entry[0][0]) + ) + ) def update_planned_qty(self): - update_bin_qty(self.production_item, self.fg_warehouse, { - "planned_qty": get_planned_qty(self.production_item, self.fg_warehouse) - }) + update_bin_qty( + self.production_item, + self.fg_warehouse, + {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}, + ) if self.material_request: mr_obj = frappe.get_doc("Material Request", self.material_request) mr_obj.update_requested_qty([self.material_request_item]) def update_ordered_qty(self): - if self.production_plan and self.production_plan_item: + if ( + self.production_plan + and self.production_plan_item + and not self.production_plan_sub_assembly_item + ): qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0 if self.docstatus == 1: @@ -457,12 +574,11 @@ class WorkOrder(Document): elif self.docstatus == 2: qty -= self.qty - frappe.db.set_value('Production Plan Item', - self.production_plan_item, 'ordered_qty', qty) + frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) - doc = frappe.get_doc('Production Plan', self.production_plan) + doc = frappe.get_doc("Production Plan", self.production_plan) doc.set_status() - doc.db_set('status', doc.status) + doc.db_set("status", doc.status) def update_work_order_qty_in_so(self): if not self.sales_order and not self.sales_order_item: @@ -470,8 +586,11 @@ class WorkOrder(Document): 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] + 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) @@ -479,45 +598,63 @@ class WorkOrder(Document): cond = "product_bundle_item = %s" if self.product_bundle_item else "production_item = %s" - qty = frappe.db.sql(""" select sum(qty) from + qty = frappe.db.sql( + """ select sum(qty) from `tabWork Order` where sales_order = %s and docstatus = 1 and {0} - """.format(cond), (self.sales_order, (self.product_bundle_item or self.production_item)), as_list=1) + """.format( + cond + ), + (self.sales_order, (self.product_bundle_item or self.production_item)), + as_list=1, + ) 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)) + 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] + 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') + 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) + 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]) + frappe.get_doc("Material Request", self.material_request).update_completed_qty( + [self.material_request_item] + ) def set_work_order_operations(self): """Fetch operations from BOM and set in 'Work Order'""" def _get_operations(bom_no, qty=1): return frappe.db.sql( - f"""select + f"""select operation, description, workstation, idx, base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins, "Pending" as status, parent as bom, batch_size, sequence_id @@ -525,11 +662,13 @@ class WorkOrder(Document): `tabBOM Operation` where parent = %s order by idx - """, bom_no, as_dict=1) + """, + bom_no, + as_dict=1, + ) - - self.set('operations', []) - if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'): + self.set("operations", []) + if not self.bom_no or not frappe.get_cached_value("BOM", self.bom_no, "with_operations"): return operations = [] @@ -543,12 +682,12 @@ class WorkOrder(Document): operations.extend(_get_operations(node.name, qty=node.exploded_qty)) bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity") - operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) + operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty)) for correct_index, operation in enumerate(operations, start=1): operation.idx = correct_index - self.set('operations', operations) + self.set("operations", operations) self.calculate_time() def calculate_time(self): @@ -563,16 +702,27 @@ class WorkOrder(Document): holidays = {} if holiday_list not in holidays: - holiday_list_days = [getdate(d[0]) for d in frappe.get_all("Holiday", fields=["holiday_date"], - filters={"parent": holiday_list}, order_by="holiday_date", limit_page_length=0, as_list=1)] + holiday_list_days = [ + getdate(d[0]) + for d in frappe.get_all( + "Holiday", + fields=["holiday_date"], + filters={"parent": holiday_list}, + order_by="holiday_date", + limit_page_length=0, + as_list=1, + ) + ] holidays[holiday_list] = holiday_list_days return holidays[holiday_list] def update_operation_status(self): - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) - max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage/100 * flt(self.qty)) + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) + max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty)) for d in self.get("operations"): if not d.completed_qty: @@ -588,7 +738,9 @@ class WorkOrder(Document): def set_actual_dates(self): if self.get("operations"): - actual_start_dates = [d.actual_start_time for d in self.get("operations") if d.actual_start_time] + actual_start_dates = [ + d.actual_start_time for d in self.get("operations") if d.actual_start_time + ] if actual_start_dates: self.actual_start_date = min(actual_start_dates) @@ -596,20 +748,21 @@ class WorkOrder(Document): if actual_end_dates: self.actual_end_date = max(actual_end_dates) else: - data = frappe.get_all("Stock Entry", - fields = ["timestamp(posting_date, posting_time) as posting_datetime"], - filters = { + data = frappe.get_all( + "Stock Entry", + fields=["timestamp(posting_date, posting_time) as posting_datetime"], + filters={ "work_order": self.name, - "purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]) - } + "purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]), + }, ) if data and len(data): dates = [d.posting_datetime for d in data] - self.db_set('actual_start_date', min(dates)) + self.db_set("actual_start_date", min(dates)) if self.status == "Completed": - self.db_set('actual_end_date', max(dates)) + self.db_set("actual_end_date", max(dates)) self.set_lead_time() @@ -632,20 +785,39 @@ class WorkOrder(Document): if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) - if self.production_plan and self.production_plan_item: - qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1) + if ( + self.production_plan + and self.production_plan_item + and not self.production_plan_sub_assembly_item + ): + qty_dict = frappe.db.get_value( + "Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1 + ) - allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0) + if not qty_dict: + return + + allowance_qty = ( + flt( + frappe.db.get_single_value( + "Manufacturing Settings", "overproduction_percentage_for_work_order" + ) + ) + / 100 + * qty_dict.get("planned_qty", 0) + ) max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0) - if max_qty < 1: - frappe.throw(_("Cannot produce more item for {0}") - .format(self.production_item), OverProductionError) + if not max_qty > 0: + frappe.throw( + _("Cannot produce more item for {0}").format(self.production_item), OverProductionError + ) elif self.qty > max_qty: - frappe.throw(_("Cannot produce more than {0} items for {1}") - .format(max_qty, self.production_item), OverProductionError) + frappe.throw( + _("Cannot produce more than {0} items for {1}").format(max_qty, self.production_item), + OverProductionError, + ) def validate_transfer_against(self): if not self.docstatus == 1: @@ -654,8 +826,10 @@ class WorkOrder(Document): if not self.operations: self.transfer_material_against = "Work Order" if not self.transfer_material_against: - frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value")) - + frappe.throw( + _("Setting {} is required").format(self.meta.get_label("transfer_material_against")), + title=_("Missing value"), + ) def validate_operation_time(self): for d in self.operations: @@ -663,14 +837,14 @@ class WorkOrder(Document): frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation)) def update_required_items(self): - ''' + """ update bin reserved_qty_for_production called from Stock Entry for production, after submit, cancel - ''' + """ # calculate consumed qty based on submitted stock entries self.update_consumed_qty_for_required_items() - if self.docstatus==1: + if self.docstatus == 1: # calculate transferred qty based on submitted stock entries self.update_transferred_qty_for_required_items() @@ -678,7 +852,7 @@ class WorkOrder(Document): self.update_reserved_qty_for_production() def update_reserved_qty_for_production(self, items=None): - '''update reserved_qty_for_production in bins''' + """update reserved_qty_for_production in bins""" for d in self.required_items: if d.source_warehouse: stock_bin = get_bin(d.item_code, d.source_warehouse) @@ -700,17 +874,18 @@ class WorkOrder(Document): d.available_qty_at_wip_warehouse = get_latest_stock_qty(d.item_code, self.wip_warehouse) def set_required_items(self, reset_only_qty=False): - '''set required_items for production to keep track of reserved qty''' + """set required_items for production to keep track of reserved qty""" if not reset_only_qty: self.required_items = [] operation = None - if self.get('operations') and len(self.operations) == 1: + if self.get("operations") and len(self.operations) == 1: operation = self.operations[0].operation if self.bom_no and self.qty: - item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=self.qty, - fetch_exploded = self.use_multi_level_bom) + item_dict = get_bom_items_as_dict( + self.bom_no, self.company, qty=self.qty, fetch_exploded=self.use_multi_level_bom + ) if reset_only_qty: for d in self.get("required_items"): @@ -720,19 +895,22 @@ class WorkOrder(Document): if not d.operation: d.operation = operation else: - for item in sorted(item_dict.values(), key=lambda d: d['idx'] or float('inf')): - self.append('required_items', { - 'rate': item.rate, - 'amount': item.rate * item.qty, - 'operation': item.operation or operation, - 'item_code': item.item_code, - 'item_name': item.item_name, - 'description': item.description, - 'allow_alternative_item': item.allow_alternative_item, - 'required_qty': item.qty, - 'source_warehouse': item.source_warehouse or item.default_warehouse, - 'include_item_in_manufacturing': item.include_item_in_manufacturing - }) + for item in sorted(item_dict.values(), key=lambda d: d["idx"] or float("inf")): + self.append( + "required_items", + { + "rate": item.rate, + "amount": item.rate * item.qty, + "operation": item.operation or operation, + "item_code": item.item_code, + "item_name": item.item_name, + "description": item.description, + "allow_alternative_item": item.allow_alternative_item, + "required_qty": item.qty, + "source_warehouse": item.source_warehouse or item.default_warehouse, + "include_item_in_manufacturing": item.include_item_in_manufacturing, + }, + ) if not self.project: self.project = item.get("project") @@ -740,32 +918,33 @@ class WorkOrder(Document): self.set_available_qty() def update_transferred_qty_for_required_items(self): - '''update transferred qty from submitted stock entries for that item against - the work order''' + """update transferred qty from submitted stock entries for that item against + the work order""" for d in self.required_items: - transferred_qty = frappe.db.sql('''select sum(qty) + transferred_qty = frappe.db.sql( + """select sum(qty) from `tabStock Entry` entry, `tabStock Entry Detail` detail where entry.work_order = %(name)s and entry.purpose = "Material Transfer for Manufacture" and entry.docstatus = 1 and detail.parent = entry.name - and (detail.item_code = %(item)s or detail.original_item = %(item)s)''', { - 'name': self.name, - 'item': d.item_code - })[0][0] + and (detail.item_code = %(item)s or detail.original_item = %(item)s)""", + {"name": self.name, "item": d.item_code}, + )[0][0] - d.db_set('transferred_qty', flt(transferred_qty), update_modified = False) + d.db_set("transferred_qty", flt(transferred_qty), update_modified=False) def update_consumed_qty_for_required_items(self): - ''' - Update consumed qty from submitted stock entries - against a work order for each stock item - ''' + """ + Update consumed qty from submitted stock entries + against a work order for each stock item + """ for item in self.required_items: - consumed_qty = frappe.db.sql(''' + consumed_qty = frappe.db.sql( + """ SELECT SUM(qty) FROM @@ -780,85 +959,97 @@ class WorkOrder(Document): AND detail.s_warehouse IS NOT null AND (detail.item_code = %(item)s OR detail.original_item = %(item)s) - ''', { - 'name': self.name, - 'item': item.item_code - })[0][0] + """, + {"name": self.name, "item": item.item_code}, + )[0][0] - item.db_set('consumed_qty', flt(consumed_qty), update_modified=False) + item.db_set("consumed_qty", flt(consumed_qty), update_modified=False) @frappe.whitelist() def make_bom(self): - data = frappe.db.sql(""" select sed.item_code, sed.qty, sed.s_warehouse + data = frappe.db.sql( + """ select sed.item_code, sed.qty, sed.s_warehouse from `tabStock Entry Detail` sed, `tabStock Entry` se where se.name = sed.parent and se.purpose = 'Manufacture' and (sed.t_warehouse is null or sed.t_warehouse = '') and se.docstatus = 1 - and se.work_order = %s""", (self.name), as_dict=1) + and se.work_order = %s""", + (self.name), + as_dict=1, + ) bom = frappe.new_doc("BOM") bom.item = self.production_item bom.conversion_rate = 1 for d in data: - bom.append('items', { - 'item_code': d.item_code, - 'qty': d.qty, - 'source_warehouse': d.s_warehouse - }) + bom.append("items", {"item_code": d.item_code, "qty": d.qty, "source_warehouse": d.s_warehouse}) if self.operations: - bom.set('operations', self.operations) + bom.set("operations", self.operations) bom.with_operations = 1 bom.set_bom_material_details() return bom def update_batch_produced_qty(self, stock_entry_doc): - if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + if not cint( + frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") + ): return for row in stock_entry_doc.items: if row.batch_no and (row.is_finished_item or row.is_scrap_item): - qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1}, - or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0] + qty = frappe.get_all( + "Stock Entry Detail", + filters={"batch_no": row.batch_no, "docstatus": 1}, + or_filters={"is_finished_item": 1, "is_scrap_item": 1}, + fields=["sum(qty)"], + as_list=1, + )[0][0] frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty)) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): if txt: - filters['operation'] = ('like', '%%%s%%' % txt) + filters["operation"] = ("like", "%%%s%%" % txt) + + return frappe.get_all("BOM Operation", filters=filters, fields=["operation"], as_list=1) - return frappe.get_all('BOM Operation', - filters = filters, fields = ['operation'], as_list=1) @frappe.whitelist() -def get_item_details(item, project = None, skip_bom_info=False): - res = frappe.db.sql(""" +def get_item_details(item, project=None, skip_bom_info=False): + res = frappe.db.sql( + """ select stock_uom, description, item_name, allow_alternative_item, include_item_in_manufacturing from `tabItem` where disabled=0 and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %s) and name=%s - """, (nowdate(), item), as_dict=1) + """, + (nowdate(), item), + as_dict=1, + ) if not res: return {} res = res[0] - if skip_bom_info: return res + if skip_bom_info: + return res filters = {"item": item, "is_default": 1, "docstatus": 1} if project: filters = {"item": item, "project": project} - res["bom_no"] = frappe.db.get_value("BOM", filters = filters) + res["bom_no"] = frappe.db.get_value("BOM", filters=filters) if not res["bom_no"]: - variant_of= frappe.db.get_value("Item", item, "variant_of") + variant_of = frappe.db.get_value("Item", item, "variant_of") if variant_of: res["bom_no"] = frappe.db.get_value("BOM", filters={"item": variant_of, "is_default": 1}) @@ -866,19 +1057,26 @@ def get_item_details(item, project = None, skip_bom_info=False): if not res["bom_no"]: if project: res = get_item_details(item) - frappe.msgprint(_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1) + frappe.msgprint( + _("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1 + ) else: frappe.throw(_("Default BOM for {0} not found").format(item)) - bom_data = frappe.db.get_value('BOM', res['bom_no'], - ['project', 'allow_alternative_item', 'transfer_material_against', 'item_name'], as_dict=1) + bom_data = frappe.db.get_value( + "BOM", + res["bom_no"], + ["project", "allow_alternative_item", "transfer_material_against", "item_name"], + as_dict=1, + ) - res['project'] = project or bom_data.pop("project") + res["project"] = project or bom_data.pop("project") res.update(bom_data) res.update(check_if_scrap_warehouse_mandatory(res["bom_no"])) return res + @frappe.whitelist() def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): if not frappe.has_permission("Work Order", "write"): @@ -900,43 +1098,51 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): return wo_doc + def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): if isinstance(variant_items, str): variant_items = json.loads(variant_items) for item in variant_items: - args = frappe._dict({ - "item_code": item.get("variant_item_code"), - "required_qty": item.get("qty"), - "qty": item.get("qty"), # for bom - "source_warehouse": item.get("source_warehouse"), - "operation": item.get("operation") - }) + args = frappe._dict( + { + "item_code": item.get("variant_item_code"), + "required_qty": item.get("qty"), + "qty": item.get("qty"), # for bom + "source_warehouse": item.get("source_warehouse"), + "operation": item.get("operation"), + } + ) bom_doc = frappe.get_cached_doc("BOM", bom_no) item_data = get_item_details(args.item_code, skip_bom_info=True) args.update(item_data) - args["rate"] = get_bom_item_rate({ - "company": wo_doc.company, - "item_code": args.get("item_code"), - "qty": args.get("required_qty"), - "uom": args.get("stock_uom"), - "stock_uom": args.get("stock_uom"), - "conversion_factor": 1 - }, bom_doc) + args["rate"] = get_bom_item_rate( + { + "company": wo_doc.company, + "item_code": args.get("item_code"), + "qty": args.get("required_qty"), + "uom": args.get("stock_uom"), + "stock_uom": args.get("stock_uom"), + "conversion_factor": 1, + }, + bom_doc, + ) if not args.source_warehouse: - args["source_warehouse"] = get_item_defaults(item.get("variant_item_code"), - wo_doc.company).default_warehouse + args["source_warehouse"] = get_item_defaults( + item.get("variant_item_code"), wo_doc.company + ).default_warehouse args["amount"] = flt(args.get("required_qty")) * flt(args.get("rate")) args["uom"] = item_data.stock_uom wo_doc.append(table_name, args) + @frappe.whitelist() def check_if_scrap_warehouse_mandatory(bom_no): - res = {"set_scrap_wh_mandatory": False } + res = {"set_scrap_wh_mandatory": False} if bom_no: bom = frappe.get_doc("BOM", bom_no) @@ -945,12 +1151,14 @@ def check_if_scrap_warehouse_mandatory(bom_no): return res + @frappe.whitelist() def set_work_order_ops(name): - po = frappe.get_doc('Work Order', name) + po = frappe.get_doc("Work Order", name) po.set_work_order_operations() po.save() + @frappe.whitelist() def make_stock_entry(work_order_id, purpose, qty=None): work_order = frappe.get_doc("Work Order", work_order_id) @@ -966,12 +1174,17 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.from_bom = 1 stock_entry.bom_no = work_order.bom_no stock_entry.use_multi_level_bom = work_order.use_multi_level_bom - stock_entry.fg_completed_qty = qty or (flt(work_order.qty) - flt(work_order.produced_qty)) - if work_order.bom_no: - stock_entry.inspection_required = frappe.db.get_value('BOM', - work_order.bom_no, 'inspection_required') + # accept 0 qty as well + stock_entry.fg_completed_qty = ( + qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty)) + ) - if purpose=="Material Transfer for Manufacture": + if work_order.bom_no: + stock_entry.inspection_required = frappe.db.get_value( + "BOM", work_order.bom_no, "inspection_required" + ) + + if purpose == "Material Transfer for Manufacture": stock_entry.to_warehouse = wip_warehouse stock_entry.project = work_order.project else: @@ -984,6 +1197,7 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.set_serial_no_batch_for_finished_good() return stock_entry.as_dict() + @frappe.whitelist() def get_default_warehouse(): doc = frappe.get_cached_doc("Manufacturing Settings") @@ -991,12 +1205,13 @@ def get_default_warehouse(): return { "wip_warehouse": doc.default_wip_warehouse, "fg_warehouse": doc.default_fg_warehouse, - "scrap_warehouse": doc.default_scrap_warehouse + "scrap_warehouse": doc.default_scrap_warehouse, } + @frappe.whitelist() def stop_unstop(work_order, status): - """ Called from client side on Stop/Unstop event""" + """Called from client side on Stop/Unstop event""" if not frappe.has_permission("Work Order", "write"): frappe.throw(_("Not permitted"), frappe.PermissionError) @@ -1013,24 +1228,29 @@ def stop_unstop(work_order, status): return pro_order.status + @frappe.whitelist() def query_sales_order(production_item): - out = frappe.db.sql_list(""" + out = frappe.db.sql_list( + """ select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1 union select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1 - """, (production_item, production_item)) + """, + (production_item, production_item), + ) return out + @frappe.whitelist() def make_job_card(work_order, operations): if isinstance(operations, str): operations = json.loads(operations) - work_order = frappe.get_doc('Work Order', work_order) + work_order = frappe.get_doc("Work Order", work_order) for row in operations: row = frappe._dict(row) validate_operation_data(row) @@ -1040,6 +1260,7 @@ def make_job_card(work_order, operations): if row.job_card_qty > 0: create_job_card(work_order, row, auto_create=True) + @frappe.whitelist() def close_work_order(work_order, status): if not frappe.has_permission("Work Order", "write"): @@ -1047,15 +1268,17 @@ def close_work_order(work_order, status): work_order = frappe.get_doc("Work Order", work_order) if work_order.get("operations"): - job_cards = frappe.get_list("Job Card", - filters={ - "work_order": work_order.name, - "status": "Work In Progress" - }, pluck='name') + job_cards = frappe.get_list( + "Job Card", filters={"work_order": work_order.name, "status": "Work In Progress"}, pluck="name" + ) if job_cards: job_cards = ", ".join(job_cards) - frappe.throw(_("Can not close Work Order. Since {0} Job Cards are in Work In Progress state.").format(job_cards)) + frappe.throw( + _("Can not close Work Order. Since {0} Job Cards are in Work In Progress state.").format( + job_cards + ) + ) work_order.update_status(status) work_order.update_planned_qty() @@ -1063,9 +1286,11 @@ def close_work_order(work_order, status): work_order.notify_update() return work_order.status + def split_qty_based_on_batch_size(wo_doc, row, qty): - if not cint(frappe.db.get_value("Operation", - row.operation, "create_job_card_based_on_batch_size")): + if not cint( + frappe.db.get_value("Operation", row.operation, "create_job_card_based_on_batch_size") + ): row.batch_size = row.get("qty") or wo_doc.qty row.job_card_qty = row.batch_size @@ -1079,55 +1304,63 @@ def split_qty_based_on_batch_size(wo_doc, row, qty): return qty + def get_serial_nos_for_job_card(row, wo_doc): if not wo_doc.serial_no: return serial_nos = get_serial_nos(wo_doc.serial_no) used_serial_nos = [] - for d in frappe.get_all('Job Card', fields=['serial_no'], - filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}): + for d in frappe.get_all( + "Job Card", + fields=["serial_no"], + filters={"docstatus": ("<", 2), "work_order": wo_doc.name, "operation_id": row.name}, + ): used_serial_nos.extend(get_serial_nos(d.serial_no)) serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos))) - row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty]) + row.serial_no = "\n".join(serial_nos[0 : cint(row.job_card_qty)]) + def validate_operation_data(row): if row.get("qty") <= 0: - frappe.throw(_("Quantity to Manufacture can not be zero for the operation {0}") - .format( + frappe.throw( + _("Quantity to Manufacture can not be zero for the operation {0}").format( frappe.bold(row.get("operation")) ) ) if row.get("qty") > row.get("pending_qty"): - frappe.throw(_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})") - .format( + frappe.throw( + _("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format( frappe.bold(row.get("operation")), frappe.bold(row.get("qty")), - frappe.bold(row.get("pending_qty")) + frappe.bold(row.get("pending_qty")), ) ) + def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False): doc = frappe.new_doc("Job Card") - doc.update({ - 'work_order': work_order.name, - 'operation': row.get("operation"), - 'workstation': row.get("workstation"), - 'posting_date': nowdate(), - 'for_quantity': row.job_card_qty or work_order.get('qty', 0), - 'operation_id': row.get("name"), - 'bom_no': work_order.bom_no, - 'project': work_order.project, - 'company': work_order.company, - 'sequence_id': row.get("sequence_id"), - 'wip_warehouse': work_order.wip_warehouse, - 'hour_rate': row.get("hour_rate"), - 'serial_no': row.get("serial_no") - }) + doc.update( + { + "work_order": work_order.name, + "operation": row.get("operation"), + "workstation": row.get("workstation"), + "posting_date": nowdate(), + "for_quantity": row.job_card_qty or work_order.get("qty", 0), + "operation_id": row.get("name"), + "bom_no": work_order.bom_no, + "project": work_order.project, + "company": work_order.company, + "sequence_id": row.get("sequence_id"), + "wip_warehouse": work_order.wip_warehouse, + "hour_rate": row.get("hour_rate"), + "serial_no": row.get("serial_no"), + } + ) - if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: + if work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer: doc.get_required_items() if auto_create: @@ -1136,19 +1369,28 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create doc.schedule_time_logs(row) doc.insert() - frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True) + frappe.msgprint( + _("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True + ) + + if enable_capacity_planning: + # automatically added scheduling rows shouldn't change status to WIP + doc.db_set("status", "Open") return doc + def get_work_order_operation_data(work_order, operation, workstation): for d in work_order.operations: if d.operation == operation and d.workstation == workstation: return d + @frappe.whitelist() def create_pick_list(source_name, target_doc=None, for_qty=None): - for_qty = for_qty or json.loads(target_doc).get('for_qty') - max_finished_goods_qty = frappe.db.get_value('Work Order', source_name, 'qty') + for_qty = for_qty or json.loads(target_doc).get("for_qty") + max_finished_goods_qty = frappe.db.get_value("Work Order", source_name, "qty") + def update_item_quantity(source, target, source_parent): pending_to_issue = flt(source.required_qty) - flt(source.transferred_qty) desire_to_transfer = flt(source.required_qty) / max_finished_goods_qty * flt(for_qty) @@ -1162,25 +1404,25 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): if qty: target.qty = qty target.stock_qty = qty - target.uom = frappe.get_value('Item', source.item_code, 'stock_uom') + target.uom = frappe.get_value("Item", source.item_code, "stock_uom") target.stock_uom = target.uom target.conversion_factor = 1 else: target.delete() - doc = get_mapped_doc('Work Order', source_name, { - 'Work Order': { - 'doctype': 'Pick List', - 'validation': { - 'docstatus': ['=', 1] - } + doc = get_mapped_doc( + "Work Order", + source_name, + { + "Work Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Work Order Item": { + "doctype": "Pick List Item", + "postprocess": update_item_quantity, + "condition": lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty), + }, }, - 'Work Order Item': { - 'doctype': 'Pick List Item', - 'postprocess': update_item_quantity, - 'condition': lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty) - }, - }, target_doc) + target_doc, + ) doc.for_qty = for_qty @@ -1188,26 +1430,31 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): return doc + def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: """Get total reserved quantity for any item in specified warehouse""" wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") return ( - frappe.qb - .from_(wo) + frappe.qb.from_(wo) .from_(wo_item) - .select(Sum(Case() + .select( + Sum( + Case() .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) - .else_(wo_item.required_qty - wo_item.consumed_qty)) + .else_(wo_item.required_qty - wo_item.consumed_qty) ) + ) .where( (wo_item.item_code == item_code) & (wo_item.parent == wo.name) & (wo.docstatus == 1) & (wo_item.source_warehouse == warehouse) & (wo.status.notin(["Stopped", "Completed", "Closed"])) - & ((wo_item.required_qty > wo_item.transferred_qty) - | (wo_item.required_qty > wo_item.consumed_qty)) + & ( + (wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty) + ) ) ).run()[0][0] or 0.0 diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py index 91279d8e616..465460f95d8 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py +++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'work_order', - 'non_standard_fieldnames': { - 'Batch': 'reference_name' - }, - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Stock Entry', 'Job Card', 'Pick List'] - }, - { - 'label': _('Reference'), - 'items': ['Serial No', 'Batch'] - } - ] + "fieldname": "work_order", + "non_standard_fieldnames": {"Batch": "reference_name"}, + "transactions": [ + {"label": _("Transactions"), "items": ["Stock Entry", "Job Card", "Pick List"]}, + {"label": _("Reference"), "items": ["Serial No", "Batch"]}, + ], } diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py index 4311d3bf17f..179274707e3 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py @@ -9,5 +9,6 @@ from frappe.model.document import Document class WorkOrderItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Work Order Item", ["item_code", "source_warehouse"]) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index c298c0a8dbb..6db985c8c2e 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -2,6 +2,7 @@ # See license.txt import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_bom @@ -10,22 +11,44 @@ from erpnext.manufacturing.doctype.workstation.workstation import ( WorkstationHolidayError, check_if_within_operating_hours, ) -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Warehouse"] -test_records = frappe.get_test_records('Workstation') -make_test_records('Workstation') +test_records = frappe.get_test_records("Workstation") +make_test_records("Workstation") -class TestWorkstation(ERPNextTestCase): + +class TestWorkstation(FrappeTestCase): def test_validate_timings(self): - check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") - check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") - self.assertRaises(NotInWorkingHoursError, check_if_within_operating_hours, - "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") - self.assertRaises(NotInWorkingHoursError, check_if_within_operating_hours, - "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") - self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, - "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + check_if_within_operating_hours( + "_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00" + ) + check_if_within_operating_hours( + "_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00" + ) + self.assertRaises( + NotInWorkingHoursError, + check_if_within_operating_hours, + "_Test Workstation 1", + "Operation 1", + "2013-02-02 05:00:00", + "2013-02-02 20:00:00", + ) + self.assertRaises( + NotInWorkingHoursError, + check_if_within_operating_hours, + "_Test Workstation 1", + "Operation 1", + "2013-02-02 05:00:00", + "2013-02-02 20:00:00", + ) + self.assertRaises( + WorkstationHolidayError, + check_if_within_operating_hours, + "_Test Workstation 1", + "Operation 1", + "2013-02-01 10:00:00", + "2013-02-02 20:00:00", + ) def test_update_bom_operation_rate(self): operations = [ @@ -33,14 +56,14 @@ class TestWorkstation(ERPNextTestCase): "operation": "Test Operation A", "workstation": "_Test Workstation A", "hour_rate_rent": 300, - "time_in_mins": 60 + "time_in_mins": 60, }, { "operation": "Test Operation B", "workstation": "_Test Workstation B", "hour_rate_rent": 1000, - "time_in_mins": 60 - } + "time_in_mins": 60, + }, ] for row in operations: @@ -48,21 +71,13 @@ class TestWorkstation(ERPNextTestCase): make_operation(row) test_routing_operations = [ - { - "operation": "Test Operation A", - "workstation": "_Test Workstation A", - "time_in_mins": 60 - }, - { - "operation": "Test Operation B", - "workstation": "_Test Workstation A", - "time_in_mins": 60 - } + {"operation": "Test Operation A", "workstation": "_Test Workstation A", "time_in_mins": 60}, + {"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 60}, ] - routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations) + routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations) bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR") w1 = frappe.get_doc("Workstation", "_Test Workstation A") - #resets values + # resets values w1.hour_rate_rent = 300 w1.hour_rate_labour = 0 w1.save() @@ -72,13 +87,14 @@ class TestWorkstation(ERPNextTestCase): self.assertEqual(bom_doc.operations[0].hour_rate, 300) w1.hour_rate_rent = 250 w1.save() - #updating after setting new rates in workstations + # updating after setting new rates in workstations bom_doc.update_cost() bom_doc.reload() self.assertEqual(w1.hour_rate, 250) self.assertEqual(bom_doc.operations[0].hour_rate, 250) self.assertEqual(bom_doc.operations[1].hour_rate, 250) + def make_workstation(*args, **kwargs): args = args if args else kwargs if isinstance(args, tuple): @@ -88,10 +104,7 @@ def make_workstation(*args, **kwargs): workstation_name = args.workstation_name or args.workstation if not frappe.db.exists("Workstation", workstation_name): - doc = frappe.get_doc({ - "doctype": "Workstation", - "workstation_name": workstation_name - }) + doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name}) doc.hour_rate_rent = args.get("hour_rate_rent") doc.hour_rate_labour = args.get("hour_rate_labour") doc.insert() diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 4cfd410ac72..59e5318ab8d 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -19,14 +19,26 @@ from frappe.utils import ( from erpnext.support.doctype.issue.issue import get_holidays -class WorkstationHolidayError(frappe.ValidationError): pass -class NotInWorkingHoursError(frappe.ValidationError): pass -class OverlapError(frappe.ValidationError): pass +class WorkstationHolidayError(frappe.ValidationError): + pass + + +class NotInWorkingHoursError(frappe.ValidationError): + pass + + +class OverlapError(frappe.ValidationError): + pass + class Workstation(Document): def validate(self): - self.hour_rate = (flt(self.hour_rate_labour) + flt(self.hour_rate_electricity) + - flt(self.hour_rate_consumable) + flt(self.hour_rate_rent)) + self.hour_rate = ( + flt(self.hour_rate_labour) + + flt(self.hour_rate_electricity) + + flt(self.hour_rate_consumable) + + flt(self.hour_rate_rent) + ) def on_update(self): self.validate_overlap_for_operation_timings() @@ -35,29 +47,41 @@ class Workstation(Document): def validate_overlap_for_operation_timings(self): """Check if there is no overlap in setting Workstation Operating Hours""" for d in self.get("working_hours"): - existing = frappe.db.sql_list("""select idx from `tabWorkstation Working Hour` + existing = frappe.db.sql_list( + """select idx from `tabWorkstation Working Hour` where parent = %s and name != %s and ( (start_time between %s and %s) or (end_time between %s and %s) or (%s between start_time and end_time)) - """, (self.name, d.name, d.start_time, d.end_time, d.start_time, d.end_time, d.start_time)) + """, + (self.name, d.name, d.start_time, d.end_time, d.start_time, d.end_time, d.start_time), + ) if existing: - frappe.throw(_("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError) + frappe.throw( + _("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError + ) def update_bom_operation(self): - bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` - where workstation = %s and parenttype = 'routing' """, self.name) + bom_list = frappe.db.sql( + """select DISTINCT parent from `tabBOM Operation` + where workstation = %s and parenttype = 'routing' """, + self.name, + ) for bom_no in bom_list: - frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s + frappe.db.sql( + """update `tabBOM Operation` set hour_rate = %s where parent = %s and workstation = %s""", - (self.hour_rate, bom_no[0], self.name)) + (self.hour_rate, bom_no[0], self.name), + ) def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False): - if not skip_holiday_list_check and (not self.holiday_list or - cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"))): + if not skip_holiday_list_check and ( + not self.holiday_list + or cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays")) + ): return schedule_date if schedule_date in tuple(get_holidays(self.holiday_list)): @@ -66,18 +90,25 @@ class Workstation(Document): return schedule_date + @frappe.whitelist() def get_default_holiday_list(): - return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list") + return frappe.get_cached_value( + "Company", frappe.defaults.get_user_default("Company"), "default_holiday_list" + ) + def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime): if from_datetime and to_datetime: - if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")): + if not cint( + frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") + ): check_workstation_for_holiday(workstation, from_datetime, to_datetime) if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")): is_within_operating_hours(workstation, operation, from_datetime, to_datetime) + def is_within_operating_hours(workstation, operation, from_datetime, to_datetime): operation_length = time_diff_in_seconds(to_datetime, from_datetime) workstation = frappe.get_doc("Workstation", workstation) @@ -87,21 +118,35 @@ def is_within_operating_hours(workstation, operation, from_datetime, to_datetime for working_hour in workstation.working_hours: if working_hour.start_time and working_hour.end_time: - slot_length = (to_timedelta(working_hour.end_time or "") - to_timedelta(working_hour.start_time or "")).total_seconds() + slot_length = ( + to_timedelta(working_hour.end_time or "") - to_timedelta(working_hour.start_time or "") + ).total_seconds() if slot_length >= operation_length: return - frappe.throw(_("Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations").format(operation, workstation.name), NotInWorkingHoursError) + frappe.throw( + _( + "Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations" + ).format(operation, workstation.name), + NotInWorkingHoursError, + ) + def check_workstation_for_holiday(workstation, from_datetime, to_datetime): holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") if holiday_list and from_datetime and to_datetime: applicable_holidays = [] - for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s + for d in frappe.db.sql( + """select holiday_date from `tabHoliday` where parent = %s and holiday_date between %s and %s """, - (holiday_list, getdate(from_datetime), getdate(to_datetime))): - applicable_holidays.append(formatdate(d[0])) + (holiday_list, getdate(from_datetime), getdate(to_datetime)), + ): + applicable_holidays.append(formatdate(d[0])) if applicable_holidays: - frappe.throw(_("Workstation is closed on the following dates as per Holiday List: {0}") - .format(holiday_list) + "\n" + "\n".join(applicable_holidays), WorkstationHolidayError) + frappe.throw( + _("Workstation is closed on the following dates as per Holiday List: {0}").format(holiday_list) + + "\n" + + "\n".join(applicable_holidays), + WorkstationHolidayError, + ) diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py index 9c0f6b8b789..6d022216bd0 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py +++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py @@ -1,20 +1,24 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'workstation', - 'transactions': [ + "fieldname": "workstation", + "transactions": [ + {"label": _("Master"), "items": ["BOM", "Routing", "Operation"]}, { - 'label': _('Master'), - 'items': ['BOM', 'Routing', 'Operation'] + "label": _("Transaction"), + "items": [ + "Work Order", + "Job Card", + ], }, - { - 'label': _('Transaction'), - 'items': ['Work Order', 'Job Card',] - } ], - 'disable_create_buttons': ['BOM', 'Routing', 'Operation', - 'Work Order', 'Job Card',] + "disable_create_buttons": [ + "BOM", + "Routing", + "Operation", + "Work Order", + "Job Card", + ], } diff --git a/erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.py b/erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.py +++ b/erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 19a80ab4076..c0affd9cada 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -11,30 +11,37 @@ def execute(filters=None): get_data(filters, data) return columns, data + def get_data(filters, data): get_exploded_items(filters.bom, data) + def get_exploded_items(bom, data, indent=0, qty=1): - exploded_items = frappe.get_all("BOM Item", + exploded_items = frappe.get_all( + "BOM Item", filters={"parent": bom}, - fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom']) + fields=["qty", "bom_no", "qty", "scrap", "item_code", "item_name", "description", "uom"], + ) for item in exploded_items: print(item.bom_no, indent) item["indent"] = indent - data.append({ - 'item_code': item.item_code, - 'item_name': item.item_name, - 'indent': indent, - 'bom_level': indent, - 'bom': item.bom_no, - 'qty': item.qty * qty, - 'uom': item.uom, - 'description': item.description, - 'scrap': item.scrap - }) + data.append( + { + "item_code": item.item_code, + "item_name": item.item_name, + "indent": indent, + "bom_level": indent, + "bom": item.bom_no, + "qty": item.qty * qty, + "uom": item.uom, + "description": item.description, + "scrap": item.scrap, + } + ) if item.bom_no: - get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty) + get_exploded_items(item.bom_no, data, indent=indent + 1, qty=item.qty) + def get_columns(): return [ @@ -43,49 +50,13 @@ def get_columns(): "fieldtype": "Link", "fieldname": "item_code", "width": 300, - "options": "Item" - }, - { - "label": "Item Name", - "fieldtype": "data", - "fieldname": "item_name", - "width": 100 - }, - { - "label": "BOM", - "fieldtype": "Link", - "fieldname": "bom", - "width": 150, - "options": "BOM" - }, - { - "label": "Qty", - "fieldtype": "data", - "fieldname": "qty", - "width": 100 - }, - { - "label": "UOM", - "fieldtype": "data", - "fieldname": "uom", - "width": 100 - }, - { - "label": "BOM Level", - "fieldtype": "Int", - "fieldname": "bom_level", - "width": 100 - }, - { - "label": "Standard Description", - "fieldtype": "data", - "fieldname": "description", - "width": 150 - }, - { - "label": "Scrap", - "fieldtype": "data", - "fieldname": "scrap", - "width": 100 + "options": "Item", }, + {"label": "Item Name", "fieldtype": "data", "fieldname": "item_name", "width": 100}, + {"label": "BOM", "fieldtype": "Link", "fieldname": "bom", "width": 150, "options": "BOM"}, + {"label": "Qty", "fieldtype": "data", "fieldname": "qty", "width": 100}, + {"label": "UOM", "fieldtype": "data", "fieldname": "uom", "width": 100}, + {"label": "BOM Level", "fieldtype": "Int", "fieldname": "bom_level", "width": 100}, + {"label": "Standard Description", "fieldtype": "data", "fieldname": "description", "width": 150}, + {"label": "Scrap", "fieldtype": "data", "fieldname": "scrap", "width": 100}, ] diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py index eda9eb9d701..92c69cf3e0a 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py @@ -11,6 +11,7 @@ def execute(filters=None): columns = get_columns(filters) return columns, data + def get_data(filters): bom_wise_data = {} bom_data, report_data = [], [] @@ -24,11 +25,9 @@ def get_data(filters): bom_data.append(d.name) row.update(d) else: - row.update({ - "operation": d.operation, - "workstation": d.workstation, - "time_in_mins": d.time_in_mins - }) + row.update( + {"operation": d.operation, "workstation": d.workstation, "time_in_mins": d.time_in_mins} + ) # maintain BOM wise data for grouping such as: # {"BOM A": [{Row1}, {Row2}], "BOM B": ...} @@ -43,20 +42,25 @@ def get_data(filters): return report_data + def get_filtered_data(filters): bom = frappe.qb.DocType("BOM") bom_ops = frappe.qb.DocType("BOM Operation") bom_ops_query = ( frappe.qb.from_(bom) - .join(bom_ops).on(bom.name == bom_ops.parent) + .join(bom_ops) + .on(bom.name == bom_ops.parent) .select( - bom.name, bom.item, bom.item_name, bom.uom, - bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins - ).where( - (bom.docstatus == 1) - & (bom.is_active == 1) + bom.name, + bom.item, + bom.item_name, + bom.uom, + bom_ops.operation, + bom_ops.workstation, + bom_ops.time_in_mins, ) + .where((bom.docstatus == 1) & (bom.is_active == 1)) ) if filters.get("item_code"): @@ -66,18 +70,20 @@ def get_filtered_data(filters): bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id"))) if filters.get("workstation"): - bom_ops_query = bom_ops_query.where( - bom_ops.workstation == filters.get("workstation") - ) + bom_ops_query = bom_ops_query.where(bom_ops.workstation == filters.get("workstation")) bom_operation_data = bom_ops_query.run(as_dict=True) return bom_operation_data + def get_bom_count(bom_data): - data = frappe.get_all("BOM Item", + data = frappe.get_all( + "BOM Item", fields=["count(name) as count", "bom_no"], - filters= {"bom_no": ("in", bom_data)}, group_by = "bom_no") + filters={"bom_no": ("in", bom_data)}, + group_by="bom_no", + ) bom_count = {} for d in data: @@ -85,58 +91,42 @@ def get_bom_count(bom_data): return bom_count + def get_args(): - return frappe._dict({ - "name": "", - "item": "", - "item_name": "", - "uom": "" - }) + return frappe._dict({"name": "", "item": "", "item_name": "", "uom": ""}) + def get_columns(filters): - return [{ - "label": _("BOM ID"), - "options": "BOM", - "fieldname": "name", - "fieldtype": "Link", - "width": 220 - }, { - "label": _("Item Code"), - "options": "Item", - "fieldname": "item", - "fieldtype": "Link", - "width": 150 - }, { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 110 - }, { - "label": _("UOM"), - "options": "UOM", - "fieldname": "uom", - "fieldtype": "Link", - "width": 100 - }, { - "label": _("Operation"), - "options": "Operation", - "fieldname": "operation", - "fieldtype": "Link", - "width": 140 - }, { - "label": _("Workstation"), - "options": "Workstation", - "fieldname": "workstation", - "fieldtype": "Link", - "width": 110 - }, { - "label": _("Time (In Mins)"), - "fieldname": "time_in_mins", - "fieldtype": "Float", - "width": 120 - }, { - "label": _("Sub-assembly BOM Count"), - "fieldname": "used_as_subassembly_items", - "fieldtype": "Int", - "width": 200 - }] + return [ + {"label": _("BOM ID"), "options": "BOM", "fieldname": "name", "fieldtype": "Link", "width": 220}, + { + "label": _("Item Code"), + "options": "Item", + "fieldname": "item", + "fieldtype": "Link", + "width": 150, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 110}, + {"label": _("UOM"), "options": "UOM", "fieldname": "uom", "fieldtype": "Link", "width": 100}, + { + "label": _("Operation"), + "options": "Operation", + "fieldname": "operation", + "fieldtype": "Link", + "width": 140, + }, + { + "label": _("Workstation"), + "options": "Workstation", + "fieldname": "workstation", + "fieldtype": "Link", + "width": 110, + }, + {"label": _("Time (In Mins)"), "fieldname": "time_in_mins", "fieldtype": "Float", "width": 120}, + { + "label": _("Sub-assembly BOM Count"), + "fieldname": "used_as_subassembly_items", + "fieldtype": "Int", + "width": 200, + }, + ] diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index 26933523246..933be3e0140 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -23,14 +23,24 @@ def execute(filters=None): summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details)) return columns, summ_data + def get_report_data(last_pur_price, reqd_qty, row, manufacture_details): to_build = row.to_build if row.to_build > 0 else 0 diff_qty = to_build - reqd_qty - return [row.item_code, row.description, - comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer', []), add_quotes=False), - comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer_part', []), add_quotes=False), - row.actual_qty, str(to_build), - reqd_qty, diff_qty, last_pur_price] + return [ + row.item_code, + row.description, + comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False), + comma_and( + manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False + ), + row.actual_qty, + str(to_build), + reqd_qty, + diff_qty, + last_pur_price, + ] + def get_columns(): """return columns""" @@ -41,12 +51,13 @@ def get_columns(): _("Manufacturer Part Number") + "::250", _("Qty") + ":Float:50", _("Stock Qty") + ":Float:100", - _("Reqd Qty")+ ":Float:100", - _("Diff Qty")+ ":Float:100", - _("Last Purchase Price")+ ":Float:100", + _("Reqd Qty") + ":Float:100", + _("Diff Qty") + ":Float:100", + _("Last Purchase Price") + ":Float:100", ] return columns + def get_bom_stock(filters): conditions = "" bom = filters.get("bom") @@ -59,18 +70,23 @@ def get_bom_stock(filters): qty_field = "stock_qty" if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" % (warehouse_details.lft, - warehouse_details.rgt) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) else: conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse")) else: conditions += "" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT bom_item.item_code, bom_item.description, @@ -86,14 +102,21 @@ def get_bom_stock(filters): WHERE bom_item.parent = '{bom}' and bom_item.parenttype='BOM' - GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) + GROUP BY bom_item.item_code""".format( + qty_field=qty_field, table=table, conditions=conditions, bom=bom + ), + as_dict=1, + ) + def get_manufacturer_records(): - details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"]) + details = frappe.get_all( + "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] + ) manufacture_details = frappe._dict() for detail in details: - dic = manufacture_details.setdefault(detail.get('item_code'), {}) - dic.setdefault('manufacturer', []).append(detail.get('manufacturer')) - dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no')) + dic = manufacture_details.setdefault(detail.get("item_code"), {}) + dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) + dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) return manufacture_details diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index fa943912617..34e9826305e 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -7,7 +7,8 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() @@ -15,6 +16,7 @@ def execute(filters=None): return columns, data + def get_columns(): """return columns""" columns = [ @@ -29,6 +31,7 @@ def get_columns(): return columns + def get_bom_stock(filters): conditions = "" bom = filters.get("bom") @@ -37,25 +40,30 @@ def get_bom_stock(filters): qty_field = "stock_qty" qty_to_produce = filters.get("qty_to_produce", 1) - if int(qty_to_produce) <= 0: + if int(qty_to_produce) <= 0: frappe.throw(_("Quantity to Produce can not be less than Zero")) if filters.get("show_exploded_view"): table = "`tabBOM Explosion Item`" if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" % (warehouse_details.lft, - warehouse_details.rgt) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) else: conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse")) else: conditions += "" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT bom_item.item_code, bom_item.description , @@ -74,9 +82,10 @@ def get_bom_stock(filters): bom_item.parent = {bom} and bom_item.parenttype='BOM' GROUP BY bom_item.item_code""".format( - qty_field=qty_field, - table=table, - conditions=conditions, - bom=frappe.db.escape(bom), - qty_to_produce=qty_to_produce or 1) - ) + qty_field=qty_field, + table=table, + conditions=conditions, + bom=frappe.db.escape(bom), + qty_to_produce=qty_to_produce or 1, + ) + ) diff --git a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py index a5ae43e9add..3fe2198966c 100644 --- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py +++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py @@ -12,98 +12,99 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): - columns = [{ + columns = [ + { "label": _("Work Order"), "fieldname": "work_order", "fieldtype": "Link", "options": "Work Order", - "width": 120 - }] - - if not filters.get('bom_no'): - columns.extend([ - { - "label": _("BOM No"), - "fieldname": "bom_no", - "fieldtype": "Link", - "options": "BOM", - "width": 180 - } - ]) - - columns.extend([ - { - "label": _("Finished Good"), - "fieldname": "production_item", - "fieldtype": "Link", - "options": "Item", - "width": 120 - }, - { - "label": _("Ordered Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Produced Qty"), - "fieldname": "produced_qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Raw Material"), - "fieldname": "raw_material_code", - "fieldtype": "Link", - "options": "Item", - "width": 120 - }, - { - "label": _("Required Qty"), - "fieldname": "required_qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Consumed Qty"), - "fieldname": "consumed_qty", - "fieldtype": "Float", - "width": 120 + "width": 120, } - ]) + ] + + if not filters.get("bom_no"): + columns.extend( + [ + { + "label": _("BOM No"), + "fieldname": "bom_no", + "fieldtype": "Link", + "options": "BOM", + "width": 180, + } + ] + ) + + columns.extend( + [ + { + "label": _("Finished Good"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + {"label": _("Ordered Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 120}, + {"label": _("Produced Qty"), "fieldname": "produced_qty", "fieldtype": "Float", "width": 120}, + { + "label": _("Raw Material"), + "fieldname": "raw_material_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + {"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 120}, + {"label": _("Consumed Qty"), "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120}, + ] + ) return columns + def get_data(filters): cond = "1=1" - if filters.get('bom_no') and not filters.get('work_order'): - cond += " and bom_no = '%s'" % filters.get('bom_no') + if filters.get("bom_no") and not filters.get("work_order"): + cond += " and bom_no = '%s'" % filters.get("bom_no") - if filters.get('work_order'): - cond += " and name = '%s'" % filters.get('work_order') + if filters.get("work_order"): + cond += " and name = '%s'" % filters.get("work_order") results = [] - for d in frappe.db.sql(""" select name as work_order, qty, produced_qty, production_item, bom_no - from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(cond), as_dict=1): + for d in frappe.db.sql( + """ select name as work_order, qty, produced_qty, production_item, bom_no + from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format( + cond + ), + as_dict=1, + ): results.append(d) - for data in frappe.get_all('Work Order Item', fields=["item_code as raw_material_code", - "required_qty", "consumed_qty"], filters={'parent': d.work_order, 'parenttype': 'Work Order'}): + for data in frappe.get_all( + "Work Order Item", + fields=["item_code as raw_material_code", "required_qty", "consumed_qty"], + filters={"parent": d.work_order, "parenttype": "Work Order"}, + ): results.append(data) return results + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_work_orders(doctype, txt, searchfield, start, page_len, filters): cond = "1=1" - if filters.get('bom_no'): - cond += " and bom_no = '%s'" % filters.get('bom_no') + if filters.get("bom_no"): + cond += " and bom_no = '%s'" % filters.get("bom_no") - return frappe.db.sql("""select name from `tabWork Order` + return frappe.db.sql( + """select name from `tabWork Order` where name like %(name)s and {0} and produced_qty > qty and docstatus = 1 - order by name limit {1}, {2}""".format(cond, start, page_len),{ - 'name': "%%%s%%" % txt - }, as_list=1) + order by name limit {1}, {2}""".format( + cond, start, page_len + ), + {"name": "%%%s%%" % txt}, + as_list=1, + ) diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 88b21170e8b..481fe51d739 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -11,58 +11,77 @@ def execute(filters=None): def get_data(report_filters): data = [] - operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1}) + operations = frappe.get_all("Operation", filters={"is_corrective_operation": 1}) if operations: - if report_filters.get('operation'): - operations = [report_filters.get('operation')] + if report_filters.get("operation"): + operations = [report_filters.get("operation")] else: operations = [d.name for d in operations] job_card = frappe.qb.DocType("Job Card") - operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost') - item_code = (job_card.production_item).as_('item_code') + operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_( + "operating_cost" + ) + item_code = (job_card.production_item).as_("item_code") - query = (frappe.qb - .from_(job_card) - .select(job_card.name, job_card.work_order, item_code, job_card.item_name, - job_card.operation, job_card.serial_no, job_card.batch_no, - job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate, - operating_cost) - .where( - (job_card.docstatus == 1) - & (job_card.is_corrective_job_card == 1)) - .groupby(job_card.name) - ) + query = ( + frappe.qb.from_(job_card) + .select( + job_card.name, + job_card.work_order, + item_code, + job_card.item_name, + job_card.operation, + job_card.serial_no, + job_card.batch_no, + job_card.workstation, + job_card.total_time_in_mins, + job_card.hour_rate, + operating_cost, + ) + .where((job_card.docstatus == 1) & (job_card.is_corrective_job_card == 1)) + .groupby(job_card.name) + ) query = append_filters(query, report_filters, operations, job_card) data = query.run(as_dict=True) return data -def append_filters(query, report_filters, operations, job_card): - """Append optional filters to query builder. """ - for field in ("name", "work_order", "operation", "workstation", - "company", "serial_no", "batch_no", "production_item"): +def append_filters(query, report_filters, operations, job_card): + """Append optional filters to query builder.""" + + for field in ( + "name", + "work_order", + "operation", + "workstation", + "company", + "serial_no", + "batch_no", + "production_item", + ): if report_filters.get(field): - if field == 'serial_no': - query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field)))) - elif field == 'operation': + if field == "serial_no": + query = query.where(job_card[field].like("%{}%".format(report_filters.get(field)))) + elif field == "operation": query = query.where(job_card[field].isin(operations)) else: query = query.where(job_card[field] == report_filters.get(field)) - if report_filters.get('from_date') or report_filters.get('to_date'): + if report_filters.get("from_date") or report_filters.get("to_date"): job_card_time_log = frappe.qb.DocType("Job Card Time Log") query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent) - if report_filters.get('from_date'): - query = query.where(job_card_time_log.from_time >= report_filters.get('from_date')) - if report_filters.get('to_date'): - query = query.where(job_card_time_log.to_time <= report_filters.get('to_date')) + if report_filters.get("from_date"): + query = query.where(job_card_time_log.from_time >= report_filters.get("from_date")) + if report_filters.get("to_date"): + query = query.where(job_card_time_log.to_time <= report_filters.get("to_date")) return query + def get_columns(filters): return [ { @@ -70,64 +89,49 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "name", "options": "Job Card", - "width": "120" + "width": "120", }, { "label": _("Work Order"), "fieldtype": "Link", "fieldname": "work_order", "options": "Work Order", - "width": "100" + "width": "100", }, { "label": _("Item Code"), "fieldtype": "Link", "fieldname": "item_code", "options": "Item", - "width": "100" - }, - { - "label": _("Item Name"), - "fieldtype": "Data", - "fieldname": "item_name", - "width": "100" + "width": "100", }, + {"label": _("Item Name"), "fieldtype": "Data", "fieldname": "item_name", "width": "100"}, { "label": _("Operation"), "fieldtype": "Link", "fieldname": "operation", "options": "Operation", - "width": "100" - }, - { - "label": _("Serial No"), - "fieldtype": "Data", - "fieldname": "serial_no", - "width": "100" - }, - { - "label": _("Batch No"), - "fieldtype": "Data", - "fieldname": "batch_no", - "width": "100" + "width": "100", }, + {"label": _("Serial No"), "fieldtype": "Data", "fieldname": "serial_no", "width": "100"}, + {"label": _("Batch No"), "fieldtype": "Data", "fieldname": "batch_no", "width": "100"}, { "label": _("Workstation"), "fieldtype": "Link", "fieldname": "workstation", "options": "Workstation", - "width": "100" + "width": "100", }, { "label": _("Operating Cost"), "fieldtype": "Currency", "fieldname": "operating_cost", - "width": "150" + "width": "150", }, { "label": _("Total Time (in Mins)"), "fieldtype": "Float", "fieldname": "total_time_in_mins", - "width": "150" - } + "width": "150", + }, ] diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py index 2c515d1b36f..80a15648670 100644 --- a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py @@ -14,10 +14,20 @@ def execute(filters=None): chart_data = get_chart_data(data, filters) return columns, data, None, chart_data + def get_data(filters): query_filters = {} - fields = ["name", "workstation", "operator", "from_time", "to_time", "downtime", "stop_reason", "remarks"] + fields = [ + "name", + "workstation", + "operator", + "from_time", + "to_time", + "downtime", + "stop_reason", + "remarks", + ] query_filters["from_time"] = (">=", filters.get("from_date")) query_filters["to_time"] = ("<=", filters.get("to_date")) @@ -25,13 +35,14 @@ def get_data(filters): if filters.get("workstation"): query_filters["workstation"] = filters.get("workstation") - data = frappe.get_all("Downtime Entry", fields= fields, filters=query_filters) or [] + data = frappe.get_all("Downtime Entry", fields=fields, filters=query_filters) or [] for d in data: if d.downtime: d.downtime = d.downtime / 60 return data + def get_chart_data(data, columns): labels = sorted(list(set([d.workstation for d in data]))) @@ -47,17 +58,13 @@ def get_chart_data(data, columns): datasets.append(workstation_wise_data.get(label, 0)) chart = { - "data": { - "labels": labels, - "datasets": [ - {"name": "Machine Downtime", "values": datasets} - ] - }, - "type": "bar" + "data": {"labels": labels, "datasets": [{"name": "Machine Downtime", "values": datasets}]}, + "type": "bar", } return chart + def get_columns(filters): return [ { @@ -65,50 +72,25 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Downtime Entry", - "width": 100 + "width": 100, }, { "label": _("Machine"), "fieldname": "workstation", "fieldtype": "Link", "options": "Workstation", - "width": 100 + "width": 100, }, { "label": _("Operator"), "fieldname": "operator", "fieldtype": "Link", "options": "Employee", - "width": 130 + "width": 130, }, - { - "label": _("From Time"), - "fieldname": "from_time", - "fieldtype": "Datetime", - "width": 160 - }, - { - "label": _("To Time"), - "fieldname": "to_time", - "fieldtype": "Datetime", - "width": 160 - }, - { - "label": _("Downtime (In Hours)"), - "fieldname": "downtime", - "fieldtype": "Float", - "width": 150 - }, - { - "label": _("Stop Reason"), - "fieldname": "stop_reason", - "fieldtype": "Data", - "width": 220 - }, - { - "label": _("Remarks"), - "fieldname": "remarks", - "fieldtype": "Text", - "width": 100 - } + {"label": _("From Time"), "fieldname": "from_time", "fieldtype": "Datetime", "width": 160}, + {"label": _("To Time"), "fieldname": "to_time", "fieldtype": "Datetime", "width": 160}, + {"label": _("Downtime (In Hours)"), "fieldname": "downtime", "fieldtype": "Float", "width": 150}, + {"label": _("Stop Reason"), "fieldname": "stop_reason", "fieldtype": "Data", "width": 220}, + {"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Text", "width": 100}, ] diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index 26b3359dee1..7500744c228 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -14,6 +14,7 @@ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses def execute(filters=None): return ForecastingReport(filters).execute_report() + class ExponentialSmoothingForecast(object): def forecast_future_data(self): for key, value in self.period_wise_data.items(): @@ -26,24 +27,22 @@ class ExponentialSmoothingForecast(object): elif forecast_data: previous_period_data = forecast_data[-1] - value[forecast_key] = (previous_period_data[1] + - flt(self.filters.smoothing_constant) * ( - flt(previous_period_data[0]) - flt(previous_period_data[1]) - ) + value[forecast_key] = previous_period_data[1] + flt(self.filters.smoothing_constant) * ( + flt(previous_period_data[0]) - flt(previous_period_data[1]) ) if value.get(forecast_key): # will be use to forecaset next period forecast_data.append([value.get(period.key), value.get(forecast_key)]) + class ForecastingReport(ExponentialSmoothingForecast): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.data = [] self.doctype = self.filters.based_on_document self.child_doctype = self.doctype + " Item" - self.based_on_field = ("qty" - if self.filters.based_on_field == "Qty" else "amount") + self.based_on_field = "qty" if self.filters.based_on_field == "Qty" else "amount" self.fieldtype = "Float" if self.based_on_field == "qty" else "Currency" self.company_currency = erpnext.get_company_currency(self.filters.company) @@ -63,8 +62,15 @@ class ForecastingReport(ExponentialSmoothingForecast): self.period_wise_data = {} from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1) - self.period_list = get_period_list(from_date, self.filters.to_date, - from_date, self.filters.to_date, "Date Range", self.filters.periodicity, ignore_fiscal_year=True) + self.period_list = get_period_list( + from_date, + self.filters.to_date, + from_date, + self.filters.to_date, + "Date Range", + self.filters.periodicity, + ignore_fiscal_year=True, + ) order_data = self.get_data_for_forecast() or [] @@ -76,8 +82,10 @@ class ForecastingReport(ExponentialSmoothingForecast): period_data = self.period_wise_data[key] for period in self.period_list: # check if posting date is within the period - if (entry.posting_date >= period.from_date and entry.posting_date <= period.to_date): - period_data[period.key] = period_data.get(period.key, 0.0) + flt(entry.get(self.based_on_field)) + if entry.posting_date >= period.from_date and entry.posting_date <= period.to_date: + period_data[period.key] = period_data.get(period.key, 0.0) + flt( + entry.get(self.based_on_field) + ) for key, value in self.period_wise_data.items(): list_of_period_value = [value.get(p.key, 0) for p in self.period_list] @@ -90,12 +98,12 @@ class ForecastingReport(ExponentialSmoothingForecast): def get_data_for_forecast(self): cond = "" if self.filters.item_code: - cond = " AND soi.item_code = %s" %(frappe.db.escape(self.filters.item_code)) + cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code)) warehouses = [] if self.filters.warehouse: warehouses = get_child_warehouses(self.filters.warehouse) - cond += " AND soi.warehouse in ({})".format(','.join(['%s'] * len(warehouses))) + cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses))) input_data = [self.filters.from_date, self.filters.company] if warehouses: @@ -103,7 +111,8 @@ class ForecastingReport(ExponentialSmoothingForecast): date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT so.{date_field} as posting_date, soi.item_code, soi.warehouse, soi.item_name, soi.stock_qty as qty, soi.base_amount as amount @@ -112,23 +121,27 @@ class ForecastingReport(ExponentialSmoothingForecast): WHERE so.docstatus = 1 AND so.name = soi.parent AND so.{date_field} < %s AND so.company = %s {cond} - """.format(doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond), - tuple(input_data), as_dict=1) + """.format( + doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond + ), + tuple(input_data), + as_dict=1, + ) def prepare_final_data(self): self.data = [] - if not self.period_wise_data: return + if not self.period_wise_data: + return for key in self.period_wise_data: self.data.append(self.period_wise_data.get(key)) def add_total(self): - if not self.data: return + if not self.data: + return - total_row = { - "item_code": _(frappe.bold("Total Quantity")) - } + total_row = {"item_code": _(frappe.bold("Total Quantity"))} for value in self.data: for period in self.period_list: @@ -145,43 +158,52 @@ class ForecastingReport(ExponentialSmoothingForecast): self.data.append(total_row) def get_columns(self): - columns = [{ - "label": _("Item Code"), - "options": "Item", - "fieldname": "item_code", - "fieldtype": "Link", - "width": 130 - }, { - "label": _("Warehouse"), - "options": "Warehouse", - "fieldname": "warehouse", - "fieldtype": "Link", - "width": 130 - }] + columns = [ + { + "label": _("Item Code"), + "options": "Item", + "fieldname": "item_code", + "fieldtype": "Link", + "width": 130, + }, + { + "label": _("Warehouse"), + "options": "Warehouse", + "fieldname": "warehouse", + "fieldtype": "Link", + "width": 130, + }, + ] - width = 180 if self.filters.periodicity in ['Yearly', "Half-Yearly", "Quarterly"] else 100 + width = 180 if self.filters.periodicity in ["Yearly", "Half-Yearly", "Quarterly"] else 100 for period in self.period_list: - if (self.filters.periodicity in ['Yearly', "Half-Yearly", "Quarterly"] - or period.from_date >= getdate(self.filters.from_date)): + if self.filters.periodicity in [ + "Yearly", + "Half-Yearly", + "Quarterly", + ] or period.from_date >= getdate(self.filters.from_date): forecast_key = period.key label = _(period.label) if period.from_date >= getdate(self.filters.from_date): - forecast_key = 'forecast_' + period.key + forecast_key = "forecast_" + period.key label = _(period.label) + " " + _("(Forecast)") - columns.append({ - "label": label, - "fieldname": forecast_key, - "fieldtype": self.fieldtype, - "width": width, - "default": 0.0 - }) + columns.append( + { + "label": label, + "fieldname": forecast_key, + "fieldtype": self.fieldtype, + "width": width, + "default": 0.0, + } + ) return columns def get_chart_data(self): - if not self.data: return + if not self.data: + return labels = [] self.total_demand = [] @@ -206,40 +228,35 @@ class ForecastingReport(ExponentialSmoothingForecast): "data": { "labels": labels, "datasets": [ - { - "name": "Demand", - "values": self.total_demand - }, - { - "name": "Forecast", - "values": self.total_forecast - } - ] + {"name": "Demand", "values": self.total_demand}, + {"name": "Forecast", "values": self.total_forecast}, + ], }, - "type": "line" + "type": "line", } def get_summary_data(self): - if not self.data: return + if not self.data: + return return [ { "value": sum(self.total_demand), "label": _("Total Demand (Past Data)"), "currency": self.company_currency, - "datatype": self.fieldtype + "datatype": self.fieldtype, }, { "value": sum(self.total_history_forecast), "label": _("Total Forecast (Past Data)"), "currency": self.company_currency, - "datatype": self.fieldtype + "datatype": self.fieldtype, }, { "value": sum(self.total_future_forecast), "indicator": "Green", "label": _("Total Forecast (Future Data)"), "currency": self.company_currency, - "datatype": self.fieldtype - } + "datatype": self.fieldtype, + }, ] diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py index 4046bb12b86..a86c7a47c36 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -16,23 +16,34 @@ def execute(filters=None): chart_data = get_chart_data(data, filters) return columns, data, None, chart_data + def get_data(filters): query_filters = { "docstatus": ("<", 2), - "posting_date": ("between", [filters.from_date, filters.to_date]) + "posting_date": ("between", [filters.from_date, filters.to_date]), } - fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date", - "total_completed_qty", "workstation", "operation", "total_time_in_mins"] + fields = [ + "name", + "status", + "work_order", + "production_item", + "item_name", + "posting_date", + "total_completed_qty", + "workstation", + "operation", + "total_time_in_mins", + ] for field in ["work_order", "workstation", "operation", "company"]: if filters.get(field): query_filters[field] = ("in", filters.get(field)) - data = frappe.get_all("Job Card", - fields= fields, filters=query_filters) + data = frappe.get_all("Job Card", fields=fields, filters=query_filters) - if not data: return [] + if not data: + return [] job_cards = [d.name for d in data] @@ -42,9 +53,12 @@ def get_data(filters): } job_card_time_details = {} - for job_card_data in frappe.get_all("Job Card Time Log", + for job_card_data in frappe.get_all( + "Job Card Time Log", fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"], - filters=job_card_time_filter, group_by="parent"): + filters=job_card_time_filter, + group_by="parent", + ): job_card_time_details[job_card_data.parent] = job_card_data res = [] @@ -60,6 +74,7 @@ def get_data(filters): return res + def get_chart_data(job_card_details, filters): labels, periodic_data = prepare_chart_data(job_card_details, filters) @@ -73,23 +88,15 @@ def get_chart_data(job_card_details, filters): datasets.append({"name": "Open", "values": open_job_cards}) datasets.append({"name": "Completed", "values": completed}) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - }, - "type": "bar" - } + chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} return chart + def prepare_chart_data(job_card_details, filters): labels = [] - periodic_data = { - "Open": {}, - "Completed": {} - } + periodic_data = {"Open": {}, "Completed": {}} filters.range = "Monthly" @@ -110,6 +117,7 @@ def prepare_chart_data(job_card_details, filters): return labels, periodic_data + def get_columns(filters): columns = [ { @@ -117,84 +125,62 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Job Card", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, ] if not filters.get("status"): columns.append( - { - "label": _("Status"), - "fieldname": "status", - "width": 100 - }, + {"label": _("Status"), "fieldname": "status", "width": 100}, ) - columns.extend([ - { - "label": _("Work Order"), - "fieldname": "work_order", - "fieldtype": "Link", - "options": "Work Order", - "width": 100 - }, - { - "label": _("Production Item"), - "fieldname": "production_item", - "fieldtype": "Link", - "options": "Item", - "width": 110 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Workstation"), - "fieldname": "workstation", - "fieldtype": "Link", - "options": "Workstation", - "width": 110 - }, - { - "label": _("Operation"), - "fieldname": "operation", - "fieldtype": "Link", - "options": "Operation", - "width": 110 - }, - { - "label": _("Total Completed Qty"), - "fieldname": "total_completed_qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("From Time"), - "fieldname": "from_time", - "fieldtype": "Datetime", - "width": 120 - }, - { - "label": _("To Time"), - "fieldname": "to_time", - "fieldtype": "Datetime", - "width": 120 - }, - { - "label": _("Time Required (In Mins)"), - "fieldname": "total_time_in_mins", - "fieldtype": "Float", - "width": 100 - } - ]) + columns.extend( + [ + { + "label": _("Work Order"), + "fieldname": "work_order", + "fieldtype": "Link", + "options": "Work Order", + "width": 100, + }, + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 110, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + { + "label": _("Workstation"), + "fieldname": "workstation", + "fieldtype": "Link", + "options": "Workstation", + "width": 110, + }, + { + "label": _("Operation"), + "fieldname": "operation", + "fieldtype": "Link", + "options": "Operation", + "width": 110, + }, + { + "label": _("Total Completed Qty"), + "fieldname": "total_completed_qty", + "fieldtype": "Float", + "width": 120, + }, + {"label": _("From Time"), "fieldname": "from_time", "fieldtype": "Datetime", "width": 120}, + {"label": _("To Time"), "fieldname": "to_time", "fieldtype": "Datetime", "width": 120}, + { + "label": _("Time Required (In Mins)"), + "fieldname": "total_time_in_mins", + "fieldtype": "Float", + "width": 100, + }, + ] + ) return columns diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py index d3dfd52b773..b10e6434223 100644 --- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py +++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py @@ -12,87 +12,71 @@ Data = List[Row] Columns = List[Dict[str, str]] QueryArgs = Dict[str, str] + def execute(filters: Filters) -> Tuple[Columns, Data]: columns = get_columns() data = get_data(filters) return columns, data + def get_data(filters: Filters) -> Data: query_args = get_query_args(filters) data = run_query(query_args) update_data_with_total_pl_value(data) return data + def get_columns() -> Columns: return [ { - 'label': _('Work Order'), - 'fieldname': 'name', - 'fieldtype': 'Link', - 'options': 'Work Order', - 'width': '200' + "label": _("Work Order"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": "200", }, { - 'label': _('Item'), - 'fieldname': 'production_item', - 'fieldtype': 'Link', - 'options': 'Item', - 'width': '100' + "label": _("Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": "100", }, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": "100"}, { - 'label': _('Status'), - 'fieldname': 'status', - 'fieldtype': 'Data', - 'width': '100' + "label": _("Manufactured Qty"), + "fieldname": "produced_qty", + "fieldtype": "Float", + "width": "150", }, + {"label": _("Loss Qty"), "fieldname": "process_loss_qty", "fieldtype": "Float", "width": "150"}, { - 'label': _('Manufactured Qty'), - 'fieldname': 'produced_qty', - 'fieldtype': 'Float', - 'width': '150' + "label": _("Actual Manufactured Qty"), + "fieldname": "actual_produced_qty", + "fieldtype": "Float", + "width": "150", }, + {"label": _("Loss Value"), "fieldname": "total_pl_value", "fieldtype": "Float", "width": "150"}, + {"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"}, { - 'label': _('Loss Qty'), - 'fieldname': 'process_loss_qty', - 'fieldtype': 'Float', - 'width': '150' + "label": _("Raw Material Value"), + "fieldname": "total_rm_value", + "fieldtype": "Float", + "width": "150", }, - { - 'label': _('Actual Manufactured Qty'), - 'fieldname': 'actual_produced_qty', - 'fieldtype': 'Float', - 'width': '150' - }, - { - 'label': _('Loss Value'), - 'fieldname': 'total_pl_value', - 'fieldtype': 'Float', - 'width': '150' - }, - { - 'label': _('FG Value'), - 'fieldname': 'total_fg_value', - 'fieldtype': 'Float', - 'width': '150' - }, - { - 'label': _('Raw Material Value'), - 'fieldname': 'total_rm_value', - 'fieldtype': 'Float', - 'width': '150' - } ] + def get_query_args(filters: Filters) -> QueryArgs: query_args = {} query_args.update(filters) - query_args.update( - get_filter_conditions(filters) - ) + query_args.update(get_filter_conditions(filters)) return query_args + def run_query(query_args: QueryArgs) -> Data: - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT wo.name, wo.status, wo.production_item, wo.qty, wo.produced_qty, wo.process_loss_qty, @@ -111,24 +95,26 @@ def run_query(query_args: QueryArgs) -> Data: {work_order_filter} GROUP BY se.work_order - """.format(**query_args), query_args, as_dict=1) + """.format( + **query_args + ), + query_args, + as_dict=1, + ) + def update_data_with_total_pl_value(data: Data) -> None: for row in data: - value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty'] - row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg + value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"] + row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg + def get_filter_conditions(filters: Filters) -> QueryArgs: filter_conditions = dict(item_filter="", work_order_filter="") if "item" in filters: production_item = filters.get("item") - filter_conditions.update( - {"item_filter": f"AND wo.production_item='{production_item}'"} - ) + filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"}) if "work_order" in filters: work_order_name = filters.get("work_order") - filter_conditions.update( - {"work_order_filter": f"AND wo.name='{work_order_name}'"} - ) + filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"}) return filter_conditions - diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index d4743d3a8ef..12b5d19ba87 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -12,16 +12,11 @@ from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get def execute(filters=None): columns = get_columns(filters) data, chart = get_data(filters, columns) - return columns, data, None , chart + return columns, data, None, chart + def get_columns(filters): - columns =[ - { - "label": _("Status"), - "fieldname": "Status", - "fieldtype": "Data", - "width": 140 - }] + columns = [{"label": _("Status"), "fieldname": "Status", "fieldtype": "Data", "width": 140}] ranges = get_period_date_ranges(filters) @@ -29,22 +24,20 @@ def get_columns(filters): period = get_period(end_date, filters) - columns.append({ - "label": _(period), - "fieldname": scrub(period), - "fieldtype": "Float", - "width": 120 - }) + columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} + ) return columns + def get_periodic_data(filters, entry): periodic_data = { "All Work Orders": {}, "Not Started": {}, "Overdue": {}, "Pending": {}, - "Completed": {} + "Completed": {}, } ranges = get_period_date_ranges(filters) @@ -52,34 +45,37 @@ def get_periodic_data(filters, entry): for from_date, end_date in ranges: period = get_period(end_date, filters) for d in entry: - if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date) : + if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date): periodic_data = update_periodic_data(periodic_data, "All Work Orders", period) - if d.status == 'Completed': - if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate(from_date): + if d.status == "Completed": + if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate( + from_date + ): periodic_data = update_periodic_data(periodic_data, "Completed", period) - elif getdate(d.actual_start_date) < getdate(from_date) : + elif getdate(d.actual_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Pending", period) - elif getdate(d.planned_start_date) < getdate(from_date) : + elif getdate(d.planned_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: periodic_data = update_periodic_data(periodic_data, "Not Started", period) - elif d.status == 'In Process': - if getdate(d.actual_start_date) < getdate(from_date) : + elif d.status == "In Process": + if getdate(d.actual_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Pending", period) - elif getdate(d.planned_start_date) < getdate(from_date) : + elif getdate(d.planned_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: periodic_data = update_periodic_data(periodic_data, "Not Started", period) - elif d.status == 'Not Started': - if getdate(d.planned_start_date) < getdate(from_date) : + elif d.status == "Not Started": + if getdate(d.planned_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: periodic_data = update_periodic_data(periodic_data, "Not Started", period) return periodic_data + def update_periodic_data(periodic_data, status, period): if periodic_data.get(status).get(period): periodic_data[status][period] += 1 @@ -88,22 +84,33 @@ def update_periodic_data(periodic_data, status, period): return periodic_data + def get_data(filters, columns): data = [] - entry = frappe.get_all("Work Order", - fields=["creation", "modified", "actual_start_date", "actual_end_date", "planned_start_date", "planned_end_date", "status"], - filters={"docstatus": 1, "company": filters["company"] }) + entry = frappe.get_all( + "Work Order", + fields=[ + "creation", + "modified", + "actual_start_date", + "actual_end_date", + "planned_start_date", + "planned_end_date", + "status", + ], + filters={"docstatus": 1, "company": filters["company"]}, + ) - periodic_data = get_periodic_data(filters,entry) + periodic_data = get_periodic_data(filters, entry) labels = ["All Work Orders", "Not Started", "Overdue", "Pending", "Completed"] - chart_data = get_chart_data(periodic_data,columns) + chart_data = get_chart_data(periodic_data, columns) ranges = get_period_date_ranges(filters) for label in labels: work = {} work["Status"] = label - for dummy,end_date in ranges: + for dummy, end_date in ranges: period = get_period(end_date, filters) if periodic_data.get(label).get(period): work[scrub(period)] = periodic_data.get(label).get(period) @@ -113,10 +120,11 @@ def get_data(filters, columns): return data, chart_data + def get_chart_data(periodic_data, columns): labels = [d.get("label") for d in columns[1:]] - all_data, not_start, overdue, pending, completed = [], [], [] , [], [] + all_data, not_start, overdue, pending, completed = [], [], [], [], [] datasets = [] for d in labels: @@ -126,18 +134,13 @@ def get_chart_data(periodic_data, columns): pending.append(periodic_data.get("Pending").get(d)) completed.append(periodic_data.get("Completed").get(d)) - datasets.append({'name':'All Work Orders', 'values': all_data}) - datasets.append({'name':'Not Started', 'values': not_start}) - datasets.append({'name':'Overdue', 'values': overdue}) - datasets.append({'name':'Pending', 'values': pending}) - datasets.append({'name':'Completed', 'values': completed}) + datasets.append({"name": "All Work Orders", "values": all_data}) + datasets.append({"name": "Not Started", "values": not_start}) + datasets.append({"name": "Overdue", "values": overdue}) + datasets.append({"name": "Pending", "values": pending}) + datasets.append({"name": "Completed", "values": completed}) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - } - } + chart = {"data": {"labels": labels, "datasets": datasets}} chart["type"] = "line" return chart diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index aaa231466fd..17f7f5e51fa 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -13,6 +13,7 @@ def execute(filters=None): return columns, data + def get_data(filters): data = [] @@ -23,6 +24,7 @@ def get_data(filters): return data + def get_production_plan_item_details(filters, data, order_details): itemwise_indent = {} @@ -30,77 +32,85 @@ def get_production_plan_item_details(filters, data, order_details): for row in production_plan_doc.po_items: work_order = frappe.get_value( "Work Order", - { - "production_plan_item": row.name, - "bom_no": row.bom_no, - "production_item": row.item_code - }, - "name" + {"production_plan_item": row.name, "bom_no": row.bom_no, "production_item": row.item_code}, + "name", ) if row.item_code not in itemwise_indent: itemwise_indent.setdefault(row.item_code, {}) - data.append({ - "indent": 0, - "item_code": row.item_code, - "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), - "qty": row.planned_qty, - "document_type": "Work Order", - "document_name": work_order or "", - "bom_level": 0, - "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), - "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) - }) + data.append( + { + "indent": 0, + "item_code": row.item_code, + "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), + "qty": row.planned_qty, + "document_type": "Work Order", + "document_name": work_order or "", + "bom_level": 0, + "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), + "pending_qty": flt(row.planned_qty) + - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)), + } + ) - get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details) + get_production_plan_sub_assembly_item_details( + filters, row, production_plan_doc, data, order_details + ) -def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details): + +def get_production_plan_sub_assembly_item_details( + filters, row, production_plan_doc, data, order_details +): for item in production_plan_doc.sub_assembly_items: if row.name == item.production_plan_item: - subcontracted_item = (item.type_of_manufacturing == 'Subcontract') + subcontracted_item = item.type_of_manufacturing == "Subcontract" if subcontracted_item: docname = frappe.get_value( "Purchase Order Item", - { - "production_plan_sub_assembly_item": item.name, - "docstatus": ("<", 2) - }, - "parent" + {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, + "parent", ) else: docname = frappe.get_value( - "Work Order", - { - "production_plan_sub_assembly_item": item.name, - "docstatus": ("<", 2) - }, - "name" + "Work Order", {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name" ) - data.append({ - "indent": 1, - "item_code": item.production_item, - "item_name": item.item_name, - "qty": item.qty, - "document_type": "Work Order" if not subcontracted_item else "Purchase Order", - "document_name": docname or "", - "bom_level": item.bom_level, - "produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0), - "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)) - }) + data.append( + { + "indent": 1, + "item_code": item.production_item, + "item_name": item.item_name, + "qty": item.qty, + "document_type": "Work Order" if not subcontracted_item else "Purchase Order", + "document_name": docname or "", + "bom_level": item.bom_level, + "produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0), + "pending_qty": flt(item.qty) + - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)), + } + ) + def get_work_order_details(filters, order_details): - for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")}, - fields=["name", "produced_qty", "production_plan", "production_item"]): + for row in frappe.get_all( + "Work Order", + filters={"production_plan": filters.get("production_plan")}, + fields=["name", "produced_qty", "production_plan", "production_item"], + ): order_details.setdefault((row.name, row.production_item), row) + def get_purchase_order_details(filters, order_details): - for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")}, - fields=["parent", "received_qty as produced_qty", "item_code"]): + for row in frappe.get_all( + "Purchase Order Item", + filters={"production_plan": filters.get("production_plan")}, + fields=["parent", "received_qty as produced_qty", "item_code"], + ): order_details.setdefault((row.parent, row.item_code), row) + def get_column(filters): return [ { @@ -108,49 +118,24 @@ def get_column(filters): "fieldtype": "Link", "fieldname": "item_code", "width": 300, - "options": "Item" - }, - { - "label": "Item Name", - "fieldtype": "data", - "fieldname": "item_name", - "width": 100 + "options": "Item", }, + {"label": "Item Name", "fieldtype": "data", "fieldname": "item_name", "width": 100}, { "label": "Document Type", "fieldtype": "Link", "fieldname": "document_type", "width": 150, - "options": "DocType" + "options": "DocType", }, { "label": "Document Name", "fieldtype": "Dynamic Link", "fieldname": "document_name", - "width": 150 + "width": 150, }, - { - "label": "BOM Level", - "fieldtype": "Int", - "fieldname": "bom_level", - "width": 100 - }, - { - "label": "Order Qty", - "fieldtype": "Float", - "fieldname": "qty", - "width": 120 - }, - { - "label": "Received Qty", - "fieldtype": "Float", - "fieldname": "produced_qty", - "width": 160 - }, - { - "label": "Pending Qty", - "fieldtype": "Float", - "fieldname": "pending_qty", - "width": 110 - } + {"label": "BOM Level", "fieldtype": "Int", "fieldname": "bom_level", "width": 100}, + {"label": "Order Qty", "fieldtype": "Float", "fieldname": "qty", "width": 120}, + {"label": "Received Qty", "fieldtype": "Float", "fieldname": "produced_qty", "width": 160}, + {"label": "Pending Qty", "fieldtype": "Float", "fieldname": "pending_qty", "width": 110}, ] diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index e1e7225e057..140488820a5 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -15,38 +15,36 @@ mapper = { stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse, `tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """, "filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty - and `tabSales Order`.per_delivered < 100.0""" + and `tabSales Order`.per_delivered < 100.0""", }, "Material Request": { "fields": """ item_code as production_item, item_name as production_item_name, stock_uom, stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse, `tabMaterial Request Item`.schedule_date """, "filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100 - and `tabMaterial Request`.material_request_type = 'Manufacture' """ + and `tabMaterial Request`.material_request_type = 'Manufacture' """, }, "Work Order": { "fields": """ production_item, item_name as production_item_name, planned_start_date, stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """, - "filters": "docstatus = 1 and status not in ('Completed', 'Stopped')" + "filters": "docstatus = 1 and status not in ('Completed', 'Stopped')", }, } order_mapper = { "Sales Order": { "Delivery Date": "`tabSales Order Item`.delivery_date asc", - "Total Amount": "`tabSales Order`.base_grand_total desc" + "Total Amount": "`tabSales Order`.base_grand_total desc", }, - "Material Request": { - "Required Date": "`tabMaterial Request Item`.schedule_date asc" - }, - "Work Order": { - "Planned Start Date": "planned_start_date asc" - } + "Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"}, + "Work Order": {"Planned Start Date": "planned_start_date asc"}, } + def execute(filters=None): return ProductionPlanReport(filters).execute_report() + class ProductionPlanReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -65,46 +63,64 @@ class ProductionPlanReport(object): return self.columns, self.data def get_open_orders(self): - doctype = ("`tabWork Order`" if self.filters.based_on == "Work Order" - else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on)) + doctype = ( + "`tabWork Order`" + if self.filters.based_on == "Work Order" + else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on) + ) filters = mapper.get(self.filters.based_on)["filters"] filters = self.prepare_other_conditions(filters, self.filters.based_on) order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by]) - self.orders = frappe.db.sql(""" SELECT {fields} from {doctype} + self.orders = frappe.db.sql( + """ SELECT {fields} from {doctype} WHERE {filters} {order_by}""".format( - doctype = doctype, - filters = filters, - order_by = order_by, - fields = mapper.get(self.filters.based_on)["fields"] - ), tuple(self.filters.docnames), as_dict=1) + doctype=doctype, + filters=filters, + order_by=order_by, + fields=mapper.get(self.filters.based_on)["fields"], + ), + tuple(self.filters.docnames), + as_dict=1, + ) def prepare_other_conditions(self, filters, doctype): if self.filters.docnames: field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype) - filters += " and %s in (%s)" % (field, ','.join(['%s'] * len(self.filters.docnames))) + filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames))) if doctype != "Work Order": filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype) if self.filters.company: - filters += " and `tab%s`.company = %s" %(doctype, frappe.db.escape(self.filters.company)) + filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company)) return filters def get_raw_materials(self): - if not self.orders: return + if not self.orders: + return self.warehouses = [d.warehouse for d in self.orders] self.item_codes = [d.production_item for d in self.orders] if self.filters.based_on == "Work Order": work_orders = [d.name for d in self.orders] - raw_materials = frappe.get_all("Work Order Item", - fields=["parent", "item_code", "item_name as raw_material_name", - "source_warehouse as warehouse", "required_qty"], - filters = {"docstatus": 1, "parent": ("in", work_orders), "source_warehouse": ("!=", "")}) or [] + raw_materials = ( + frappe.get_all( + "Work Order Item", + fields=[ + "parent", + "item_code", + "item_name as raw_material_name", + "source_warehouse as warehouse", + "required_qty", + ], + filters={"docstatus": 1, "parent": ("in", work_orders), "source_warehouse": ("!=", "")}, + ) + or [] + ) self.warehouses.extend([d.source_warehouse for d in raw_materials]) else: @@ -118,21 +134,32 @@ class ProductionPlanReport(object): bom_nos.append(bom_no) - bom_doctype = ("BOM Explosion Item" - if self.filters.include_subassembly_raw_materials else "BOM Item") + bom_doctype = ( + "BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item" + ) - qty_field = ("qty_consumed_per_unit" - if self.filters.include_subassembly_raw_materials else "(bom_item.qty / bom.quantity)") + qty_field = ( + "qty_consumed_per_unit" + if self.filters.include_subassembly_raw_materials + else "(bom_item.qty / bom.quantity)" + ) - raw_materials = frappe.db.sql(""" SELECT bom_item.parent, bom_item.item_code, + raw_materials = frappe.db.sql( + """ SELECT bom_item.parent, bom_item.item_code, bom_item.item_name as raw_material_name, {0} as required_qty_per_unit FROM `tabBOM` as bom, `tab{1}` as bom_item WHERE bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1 - """.format(qty_field, bom_doctype, ','.join(["%s"] * len(bom_nos))), tuple(bom_nos), as_dict=1) + """.format( + qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos)) + ), + tuple(bom_nos), + as_dict=1, + ) - if not raw_materials: return + if not raw_materials: + return self.item_codes.extend([d.item_code for d in raw_materials]) @@ -144,15 +171,20 @@ class ProductionPlanReport(object): rows.append(d) def get_item_details(self): - if not (self.orders and self.item_codes): return + if not (self.orders and self.item_codes): + return self.item_details = {} - for d in frappe.get_all("Item Default", fields = ["parent", "default_warehouse"], - filters = {"company": self.filters.company, "parent": ("in", self.item_codes)}): + for d in frappe.get_all( + "Item Default", + fields=["parent", "default_warehouse"], + filters={"company": self.filters.company, "parent": ("in", self.item_codes)}, + ): self.item_details[d.parent] = d def get_bin_details(self): - if not (self.orders and self.raw_materials_dict): return + if not (self.orders and self.raw_materials_dict): + return self.bin_details = {} self.mrp_warehouses = [] @@ -160,48 +192,55 @@ class ProductionPlanReport(object): self.mrp_warehouses.extend(get_child_warehouses(self.filters.raw_material_warehouse)) self.warehouses.extend(self.mrp_warehouses) - for d in frappe.get_all("Bin", + for d in frappe.get_all( + "Bin", fields=["warehouse", "item_code", "actual_qty", "ordered_qty", "projected_qty"], - filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)}): + filters={"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)}, + ): key = (d.item_code, d.warehouse) if key not in self.bin_details: self.bin_details.setdefault(key, d) def get_purchase_details(self): - if not (self.orders and self.raw_materials_dict): return + if not (self.orders and self.raw_materials_dict): + return self.purchase_details = {} - purchased_items = frappe.get_all("Purchase Order Item", + purchased_items = frappe.get_all( + "Purchase Order Item", fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"], filters={ "item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses), "docstatus": 1, }, - group_by = "item_code, warehouse") + group_by="item_code, warehouse", + ) for d in purchased_items: key = (d.item_code, d.warehouse) if key not in self.purchase_details: self.purchase_details.setdefault(key, d) def prepare_data(self): - if not self.orders: return + if not self.orders: + return for d in self.orders: key = d.name if self.filters.based_on == "Work Order" else d.bom_no - if not self.raw_materials_dict.get(key): continue + if not self.raw_materials_dict.get(key): + continue bin_data = self.bin_details.get((d.production_item, d.warehouse)) or {} - d.update({ - "for_warehouse": d.warehouse, - "available_qty": 0 - }) + d.update({"for_warehouse": d.warehouse, "available_qty": 0}) if bin_data and bin_data.get("actual_qty") > 0 and d.qty_to_manufacture: - d.available_qty = (bin_data.get("actual_qty") - if (d.qty_to_manufacture > bin_data.get("actual_qty")) else d.qty_to_manufacture) + d.available_qty = ( + bin_data.get("actual_qty") + if (d.qty_to_manufacture > bin_data.get("actual_qty")) + else d.qty_to_manufacture + ) bin_data["actual_qty"] -= d.available_qty @@ -232,8 +271,9 @@ class ProductionPlanReport(object): d.remaining_qty = d.required_qty self.pick_materials_from_warehouses(d, data, warehouses) - if (d.remaining_qty and self.filters.raw_material_warehouse - and d.remaining_qty != d.required_qty): + if ( + d.remaining_qty and self.filters.raw_material_warehouse and d.remaining_qty != d.required_qty + ): row = self.get_args() d.warehouse = self.filters.raw_material_warehouse d.required_qty = d.remaining_qty @@ -243,7 +283,8 @@ class ProductionPlanReport(object): def pick_materials_from_warehouses(self, args, order_data, warehouses): for index, warehouse in enumerate(warehouses): - if not args.remaining_qty: return + if not args.remaining_qty: + return row = self.get_args() @@ -255,14 +296,18 @@ class ProductionPlanReport(object): args.allotted_qty = 0 if bin_data and bin_data.get("actual_qty") > 0: - args.allotted_qty = (bin_data.get("actual_qty") - if (args.required_qty > bin_data.get("actual_qty")) else args.required_qty) + args.allotted_qty = ( + bin_data.get("actual_qty") + if (args.required_qty > bin_data.get("actual_qty")) + else args.required_qty + ) args.remaining_qty -= args.allotted_qty bin_data["actual_qty"] -= args.allotted_qty - if ((self.mrp_warehouses and (args.allotted_qty or index == len(warehouses) - 1)) - or not self.mrp_warehouses): + if ( + self.mrp_warehouses and (args.allotted_qty or index == len(warehouses) - 1) + ) or not self.mrp_warehouses: if not self.index: row.update(order_data) self.index += 1 @@ -275,52 +320,45 @@ class ProductionPlanReport(object): self.data.append(row) def get_args(self): - return frappe._dict({ - "work_order": "", - "sales_order": "", - "production_item": "", - "production_item_name": "", - "qty_to_manufacture": "", - "produced_qty": "" - }) + return frappe._dict( + { + "work_order": "", + "sales_order": "", + "production_item": "", + "production_item_name": "", + "qty_to_manufacture": "", + "produced_qty": "", + } + ) def get_columns(self): based_on = self.filters.based_on - self.columns = [{ - "label": _("ID"), - "options": based_on, - "fieldname": "name", - "fieldtype": "Link", - "width": 100 - }, { - "label": _("Item Code"), - "fieldname": "production_item", - "fieldtype": "Link", - "options": "Item", - "width": 120 - }, { - "label": _("Item Name"), - "fieldname": "production_item_name", - "fieldtype": "Data", - "width": 130 - }, { - "label": _("Warehouse"), - "options": "Warehouse", - "fieldname": "for_warehouse", - "fieldtype": "Link", - "width": 100 - }, { - "label": _("Order Qty"), - "fieldname": "qty_to_manufacture", - "fieldtype": "Float", - "width": 80 - }, { - "label": _("Available"), - "fieldname": "available_qty", - "fieldtype": "Float", - "width": 80 - }] + self.columns = [ + {"label": _("ID"), "options": based_on, "fieldname": "name", "fieldtype": "Link", "width": 100}, + { + "label": _("Item Code"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + { + "label": _("Item Name"), + "fieldname": "production_item_name", + "fieldtype": "Data", + "width": 130, + }, + { + "label": _("Warehouse"), + "options": "Warehouse", + "fieldname": "for_warehouse", + "fieldtype": "Link", + "width": 100, + }, + {"label": _("Order Qty"), "fieldname": "qty_to_manufacture", "fieldtype": "Float", "width": 80}, + {"label": _("Available"), "fieldname": "available_qty", "fieldtype": "Float", "width": 80}, + ] fieldname, fieldtype = "delivery_date", "Date" if self.filters.based_on == "Sales Order" and self.filters.order_by == "Total Amount": @@ -330,48 +368,50 @@ class ProductionPlanReport(object): elif self.filters.based_on == "Work Order": fieldname = "planned_start_date" - self.columns.append({ - "label": _(self.filters.order_by), - "fieldname": fieldname, - "fieldtype": fieldtype, - "width": 100 - }) + self.columns.append( + { + "label": _(self.filters.order_by), + "fieldname": fieldname, + "fieldtype": fieldtype, + "width": 100, + } + ) - self.columns.extend([{ - "label": _("Raw Material Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 120 - }, { - "label": _("Raw Material Name"), - "fieldname": "raw_material_name", - "fieldtype": "Data", - "width": 130 - }, { - "label": _("Warehouse"), - "options": "Warehouse", - "fieldname": "warehouse", - "fieldtype": "Link", - "width": 110 - }, { - "label": _("Required Qty"), - "fieldname": "required_qty", - "fieldtype": "Float", - "width": 100 - }, { - "label": _("Allotted Qty"), - "fieldname": "allotted_qty", - "fieldtype": "Float", - "width": 100 - }, { - "label": _("Expected Arrival Date"), - "fieldname": "arrival_date", - "fieldtype": "Date", - "width": 160 - }, { - "label": _("Arrival Quantity"), - "fieldname": "arrival_qty", - "fieldtype": "Float", - "width": 140 - }]) + self.columns.extend( + [ + { + "label": _("Raw Material Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + { + "label": _("Raw Material Name"), + "fieldname": "raw_material_name", + "fieldtype": "Data", + "width": 130, + }, + { + "label": _("Warehouse"), + "options": "Warehouse", + "fieldname": "warehouse", + "fieldtype": "Link", + "width": 110, + }, + {"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 100}, + {"label": _("Allotted Qty"), "fieldname": "allotted_qty", "fieldtype": "Float", "width": 100}, + { + "label": _("Expected Arrival Date"), + "fieldname": "arrival_date", + "fieldtype": "Date", + "width": 160, + }, + { + "label": _("Arrival Quantity"), + "fieldname": "arrival_qty", + "fieldtype": "Float", + "width": 140, + }, + ] + ) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index a0c4a43e90f..0a79130f1b2 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -11,13 +11,24 @@ def execute(filters=None): data = get_data(filters) columns = get_columns(filters) chart_data = get_chart_data(data, filters) - return columns, data , None, chart_data + return columns, data, None, chart_data + def get_data(filters): query_filters = {"docstatus": ("<", 2)} - fields = ["name", "status", "report_date", "item_code", "item_name", "sample_size", - "inspection_type", "reference_type", "reference_name", "inspected_by"] + fields = [ + "name", + "status", + "report_date", + "item_code", + "item_name", + "sample_size", + "inspection_type", + "reference_type", + "reference_name", + "inspected_by", + ] for field in ["status", "item_code", "status", "inspected_by"]: if filters.get(field): @@ -26,36 +37,33 @@ def get_data(filters): query_filters["report_date"] = (">=", filters.get("from_date")) query_filters["report_date"] = ("<=", filters.get("to_date")) - return frappe.get_all("Quality Inspection", - fields= fields, filters=query_filters, order_by="report_date asc") + return frappe.get_all( + "Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc" + ) + def get_chart_data(periodic_data, columns): labels = ["Rejected", "Accepted"] - status_wise_data = { - "Accepted": 0, - "Rejected": 0 - } + status_wise_data = {"Accepted": 0, "Rejected": 0} datasets = [] for d in periodic_data: status_wise_data[d.status] += 1 - datasets.append({'name':'Qty Wise Chart', - 'values': [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")]}) + datasets.append( + { + "name": "Qty Wise Chart", + "values": [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")], + } + ) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - }, - "type": "donut", - "height": 300 - } + chart = {"data": {"labels": labels, "datasets": datasets}, "type": "donut", "height": 300} return chart + def get_columns(filters): columns = [ { @@ -63,71 +71,49 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Work Order", - "width": 100 + "width": 100, }, - { - "label": _("Report Date"), - "fieldname": "report_date", - "fieldtype": "Date", - "width": 150 - } + {"label": _("Report Date"), "fieldname": "report_date", "fieldtype": "Date", "width": 150}, ] if not filters.get("status"): columns.append( - { - "label": _("Status"), - "fieldname": "status", - "width": 100 - }, + {"label": _("Status"), "fieldname": "status", "width": 100}, ) - columns.extend([ - { - "label": _("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 130 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 130 - }, - { - "label": _("Sample Size"), - "fieldname": "sample_size", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Inspection Type"), - "fieldname": "inspection_type", - "fieldtype": "Data", - "width": 110 - }, - { - "label": _("Document Type"), - "fieldname": "reference_type", - "fieldtype": "Data", - "width": 90 - }, - { - "label": _("Document Name"), - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "options": "reference_type", - "width": 150 - }, - { - "label": _("Inspected By"), - "fieldname": "inspected_by", - "fieldtype": "Link", - "options": "User", - "width": 150 - } - ]) + columns.extend( + [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 130, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 130}, + {"label": _("Sample Size"), "fieldname": "sample_size", "fieldtype": "Float", "width": 110}, + { + "label": _("Inspection Type"), + "fieldname": "inspection_type", + "fieldtype": "Data", + "width": 110, + }, + {"label": _("Document Type"), "fieldname": "reference_type", "fieldtype": "Data", "width": 90}, + { + "label": _("Document Name"), + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "options": "reference_type", + "width": 150, + }, + { + "label": _("Inspected By"), + "fieldname": "inspected_by", + "fieldtype": "Link", + "options": "User", + "width": 150, + }, + ] + ) return columns diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py index db0b239ae20..c6b7e58d656 100644 --- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py +++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py @@ -12,17 +12,20 @@ def execute(filters=None): columns = get_columns() return columns, data + def get_item_list(wo_list, filters): out = [] - #Add a row for each item/qty + # Add a row for each item/qty for wo_details in wo_list: desc = frappe.db.get_value("BOM", wo_details.bom_no, "description") - for wo_item_details in frappe.db.get_values("Work Order Item", - {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1): + for wo_item_details in frappe.db.get_values( + "Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1 + ): - item_list = frappe.db.sql("""SELECT + item_list = frappe.db.sql( + """SELECT bom_item.item_code as item_code, ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty FROM @@ -36,8 +39,14 @@ def get_item_list(wo_list, filters): and bom.name = %(bom)s GROUP BY bom_item.item_code""", - {"bom": wo_details.bom_no, "warehouse": wo_item_details.source_warehouse, - "filterhouse": filters.warehouse, "item_code": wo_item_details.item_code}, as_dict=1) + { + "bom": wo_details.bom_no, + "warehouse": wo_item_details.source_warehouse, + "filterhouse": filters.warehouse, + "item_code": wo_item_details.item_code, + }, + as_dict=1, + ) stock_qty = 0 count = 0 @@ -54,97 +63,99 @@ def get_item_list(wo_list, filters): else: build = "N" - row = frappe._dict({ - "work_order": wo_details.name, - "status": wo_details.status, - "req_items": cint(count), - "instock": stock_qty, - "description": desc, - "source_warehouse": wo_item_details.source_warehouse, - "item_code": wo_item_details.item_code, - "bom_no": wo_details.bom_no, - "qty": wo_details.qty, - "buildable_qty": buildable_qty, - "ready_to_build": build - }) + row = frappe._dict( + { + "work_order": wo_details.name, + "status": wo_details.status, + "req_items": cint(count), + "instock": stock_qty, + "description": desc, + "source_warehouse": wo_item_details.source_warehouse, + "item_code": wo_item_details.item_code, + "bom_no": wo_details.bom_no, + "qty": wo_details.qty, + "buildable_qty": buildable_qty, + "ready_to_build": build, + } + ) out.append(row) return out + def get_work_orders(): - out = frappe.get_all("Work Order", filters={"docstatus": 1, "status": ( "!=","Completed")}, - fields=["name","status", "bom_no", "qty", "produced_qty"], order_by='name') + out = frappe.get_all( + "Work Order", + filters={"docstatus": 1, "status": ("!=", "Completed")}, + fields=["name", "status", "bom_no", "qty", "produced_qty"], + order_by="name", + ) return out + def get_columns(): - columns = [{ - "fieldname": "work_order", - "label": "Work Order", - "fieldtype": "Link", - "options": "Work Order", - "width": 110 - }, { - "fieldname": "bom_no", - "label": "BOM", - "fieldtype": "Link", - "options": "BOM", - "width": 120 - }, { - "fieldname": "description", - "label": "Description", - "fieldtype": "Data", - "options": "", - "width": 230 - }, { - "fieldname": "item_code", - "label": "Item Code", - "fieldtype": "Link", - "options": "Item", - "width": 110 - },{ - "fieldname": "source_warehouse", - "label": "Source Warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 110 - },{ - "fieldname": "qty", - "label": "Qty to Build", - "fieldtype": "Data", - "options": "", - "width": 110 - }, { - "fieldname": "status", - "label": "Status", - "fieldtype": "Data", - "options": "", - "width": 100 - }, { - "fieldname": "req_items", - "label": "# Req'd Items", - "fieldtype": "Data", - "options": "", - "width": 105 - }, { - "fieldname": "instock", - "label": "# In Stock", - "fieldtype": "Data", - "options": "", - "width": 105 - }, { - "fieldname": "buildable_qty", - "label": "Buildable Qty", - "fieldtype": "Data", - "options": "", - "width": 100 - }, { - "fieldname": "ready_to_build", - "label": "Build All?", - "fieldtype": "Data", - "options": "", - "width": 90 - }] + columns = [ + { + "fieldname": "work_order", + "label": "Work Order", + "fieldtype": "Link", + "options": "Work Order", + "width": 110, + }, + {"fieldname": "bom_no", "label": "BOM", "fieldtype": "Link", "options": "BOM", "width": 120}, + { + "fieldname": "description", + "label": "Description", + "fieldtype": "Data", + "options": "", + "width": 230, + }, + { + "fieldname": "item_code", + "label": "Item Code", + "fieldtype": "Link", + "options": "Item", + "width": 110, + }, + { + "fieldname": "source_warehouse", + "label": "Source Warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 110, + }, + {"fieldname": "qty", "label": "Qty to Build", "fieldtype": "Data", "options": "", "width": 110}, + {"fieldname": "status", "label": "Status", "fieldtype": "Data", "options": "", "width": 100}, + { + "fieldname": "req_items", + "label": "# Req'd Items", + "fieldtype": "Data", + "options": "", + "width": 105, + }, + { + "fieldname": "instock", + "label": "# In Stock", + "fieldtype": "Data", + "options": "", + "width": 105, + }, + { + "fieldname": "buildable_qty", + "label": "Buildable Qty", + "fieldtype": "Data", + "options": "", + "width": 100, + }, + { + "fieldname": "ready_to_build", + "label": "Build All?", + "fieldtype": "Data", + "options": "", + "width": 90, + }, + ] return columns diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index d7469ddfdd6..2368bfdf2c6 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -21,11 +21,23 @@ def execute(filters=None): chart_data = get_chart_data(data, filters) return columns, data, None, chart_data + def get_data(filters): query_filters = {"docstatus": ("<", 2)} - fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty", - "planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"] + fields = [ + "name", + "status", + "sales_order", + "production_item", + "qty", + "produced_qty", + "planned_start_date", + "planned_end_date", + "actual_start_date", + "actual_end_date", + "lead_time", + ] for field in ["sales_order", "production_item", "status", "company"]: if filters.get(field): @@ -34,15 +46,16 @@ def get_data(filters): query_filters["planned_start_date"] = (">=", filters.get("from_date")) query_filters["planned_end_date"] = ("<=", filters.get("to_date")) - data = frappe.get_all("Work Order", - fields= fields, filters=query_filters, order_by="planned_start_date asc") + data = frappe.get_all( + "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc" + ) res = [] for d in data: start_date = d.actual_start_date or d.planned_start_date d.age = 0 - if d.status != 'Completed': + if d.status != "Completed": d.age = date_diff(today(), start_date) if filters.get("age") <= d.age: @@ -50,6 +63,7 @@ def get_data(filters): return res + def get_chart_data(data, filters): if filters.get("charts_based_on") == "Status": return get_chart_based_on_status(data) @@ -58,6 +72,7 @@ def get_chart_data(data, filters): else: return get_chart_based_on_qty(data, filters) + def get_chart_based_on_status(data): labels = frappe.get_meta("Work Order").get_options("status").split("\n") if "" in labels: @@ -71,25 +86,18 @@ def get_chart_based_on_status(data): values = [status_wise_data[label] for label in labels] chart = { - "data": { - 'labels': labels, - 'datasets': [{'name':'Qty Wise Chart', 'values': values}] - }, + "data": {"labels": labels, "datasets": [{"name": "Qty Wise Chart", "values": values}]}, "type": "donut", - "height": 300 + "height": 300, } return chart + def get_chart_based_on_age(data): labels = ["0-30 Days", "30-60 Days", "60-90 Days", "90 Above"] - age_wise_data = { - "0-30 Days": 0, - "30-60 Days": 0, - "60-90 Days": 0, - "90 Above": 0 - } + age_wise_data = {"0-30 Days": 0, "30-60 Days": 0, "60-90 Days": 0, "90 Above": 0} for d in data: if d.age > 0 and d.age <= 30: @@ -101,20 +109,22 @@ def get_chart_based_on_age(data): else: age_wise_data["90 Above"] += 1 - values = [age_wise_data["0-30 Days"], age_wise_data["30-60 Days"], - age_wise_data["60-90 Days"], age_wise_data["90 Above"]] + values = [ + age_wise_data["0-30 Days"], + age_wise_data["30-60 Days"], + age_wise_data["60-90 Days"], + age_wise_data["90 Above"], + ] chart = { - "data": { - 'labels': labels, - 'datasets': [{'name':'Qty Wise Chart', 'values': values}] - }, + "data": {"labels": labels, "datasets": [{"name": "Qty Wise Chart", "values": values}]}, "type": "donut", - "height": 300 + "height": 300, } return chart + def get_chart_based_on_qty(data, filters): labels, periodic_data = prepare_chart_data(data, filters) @@ -129,25 +139,18 @@ def get_chart_based_on_qty(data, filters): datasets.append({"name": "Completed", "values": completed}) chart = { - "data": { - 'labels': labels, - 'datasets': datasets - }, + "data": {"labels": labels, "datasets": datasets}, "type": "bar", - "barOptions": { - "stacked": 1 - } + "barOptions": {"stacked": 1}, } return chart + def prepare_chart_data(data, filters): labels = [] - periodic_data = { - "Pending": {}, - "Completed": {} - } + periodic_data = {"Pending": {}, "Completed": {}} filters.range = "Monthly" @@ -165,11 +168,12 @@ def prepare_chart_data(data, filters): for d in data: if getdate(d.planned_start_date) >= from_date and getdate(d.planned_start_date) <= end_date: - periodic_data["Pending"][period] += (flt(d.qty) - flt(d.produced_qty)) + periodic_data["Pending"][period] += flt(d.qty) - flt(d.produced_qty) periodic_data["Completed"][period] += flt(d.produced_qty) return labels, periodic_data + def get_columns(filters): columns = [ { @@ -177,90 +181,77 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Work Order", - "width": 100 + "width": 100, }, ] if not filters.get("status"): columns.append( - { - "label": _("Status"), - "fieldname": "status", - "width": 100 - }, + {"label": _("Status"), "fieldname": "status", "width": 100}, ) - columns.extend([ - { - "label": _("Production Item"), - "fieldname": "production_item", - "fieldtype": "Link", - "options": "Item", - "width": 130 - }, - { - "label": _("Produce Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Produced Qty"), - "fieldname": "produced_qty", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 90 - }, - { - "label": _("Planned Start Date"), - "fieldname": "planned_start_date", - "fieldtype": "Date", - "width": 150 - }, - { - "label": _("Planned End Date"), - "fieldname": "planned_end_date", - "fieldtype": "Date", - "width": 150 - } - ]) - - if filters.get("status") != 'Not Started': - columns.extend([ + columns.extend( + [ { - "label": _("Actual Start Date"), - "fieldname": "actual_start_date", + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 130, + }, + {"label": _("Produce Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 110}, + {"label": _("Produced Qty"), "fieldname": "produced_qty", "fieldtype": "Float", "width": 110}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 90, + }, + { + "label": _("Planned Start Date"), + "fieldname": "planned_start_date", "fieldtype": "Date", - "width": 100 + "width": 150, }, { - "label": _("Actual End Date"), - "fieldname": "actual_end_date", + "label": _("Planned End Date"), + "fieldname": "planned_end_date", "fieldtype": "Date", - "width": 100 + "width": 150, }, - { - "label": _("Age"), - "fieldname": "age", - "fieldtype": "Float", - "width": 110 - }, - ]) + ] + ) - if filters.get("status") == 'Completed': - columns.extend([ - { - "label": _("Lead Time (in mins)"), - "fieldname": "lead_time", - "fieldtype": "Float", - "width": 110 - }, - ]) + if filters.get("status") != "Not Started": + columns.extend( + [ + { + "label": _("Actual Start Date"), + "fieldname": "actual_start_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Actual End Date"), + "fieldname": "actual_end_date", + "fieldtype": "Date", + "width": 100, + }, + {"label": _("Age"), "fieldname": "age", "fieldtype": "Float", "width": 110}, + ] + ) + + if filters.get("status") == "Completed": + columns.extend( + [ + { + "label": _("Lead Time (in mins)"), + "fieldname": "lead_time", + "fieldtype": "Float", + "width": 110, + }, + ] + ) return columns diff --git a/erpnext/non_profit/doctype/chapter/chapter.py b/erpnext/non_profit/doctype/chapter/chapter.py index c01b1ef3e42..ae2f75824d4 100644 --- a/erpnext/non_profit/doctype/chapter/chapter.py +++ b/erpnext/non_profit/doctype/chapter/chapter.py @@ -8,22 +8,21 @@ from frappe.website.website_generator import WebsiteGenerator class Chapter(WebsiteGenerator): _website = frappe._dict( - condition_field = "published", + condition_field="published", ) def get_context(self, context): context.no_cache = True context.show_sidebar = True - context.parents = [dict(label='View All Chapters', - route='chapters', title='View Chapters')] + context.parents = [dict(label="View All Chapters", route="chapters", title="View Chapters")] def validate(self): - if not self.route: #pylint: disable=E0203 - self.route = 'chapters/' + self.scrub(self.name) + if not self.route: # pylint: disable=E0203 + self.route = "chapters/" + self.scrub(self.name) def enable(self): - chapter = frappe.get_doc('Chapter', frappe.form_dict.name) - chapter.append('members', dict(enable=self.value)) + chapter = frappe.get_doc("Chapter", frappe.form_dict.name) + chapter.append("members", dict(enable=self.value)) chapter.save(ignore_permissions=1) frappe.db.commit() @@ -32,9 +31,9 @@ def get_list_context(context): context.allow_guest = True context.no_cache = True context.show_sidebar = True - context.title = 'All Chapters' + context.title = "All Chapters" context.no_breadcrumbs = True - context.order_by = 'creation desc' + context.order_by = "creation desc" @frappe.whitelist() diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json index 6759569d54d..d2bcd17a20e 100644 --- a/erpnext/non_profit/doctype/donation/donation.json +++ b/erpnext/non_profit/doctype/donation/donation.json @@ -17,7 +17,8 @@ "paid", "amount", "mode_of_payment", - "razorpay_payment_id", + "column_break_12", + "payment_id", "amended_from" ], "fields": [ @@ -73,12 +74,6 @@ "label": "Mode of Payment", "options": "Mode of Payment" }, - { - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "Razorpay Payment ID", - "read_only": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -108,12 +103,21 @@ "options": "Donation", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "payment_id", + "fieldtype": "Data", + "label": "Payment ID" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-03-11 10:53:11.269005", + "modified": "2022-03-16 17:18:45.611741", "modified_by": "Administrator", "module": "Non Profit", "name": "Donation", diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py index 617979ef745..8e5ac5b61bf 100644 --- a/erpnext/non_profit/doctype/donation/donation.py +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -16,28 +16,30 @@ from erpnext.non_profit.doctype.membership.membership import verify_signature class Donation(Document): def validate(self): - if not self.donor or not frappe.db.exists('Donor', self.donor): + if not self.donor or not frappe.db.exists("Donor", self.donor): # for web forms - user_type = frappe.db.get_value('User', frappe.session.user, 'user_type') - if user_type == 'Website User': + user_type = frappe.db.get_value("User", frappe.session.user, "user_type") + if user_type == "Website User": self.create_donor_for_website_user() else: - frappe.throw(_('Please select a Member')) + frappe.throw(_("Please select a Member")) def create_donor_for_website_user(self): - donor_name = frappe.get_value('Donor', dict(email=frappe.session.user)) + donor_name = frappe.get_value("Donor", dict(email=frappe.session.user)) if not donor_name: - user = frappe.get_doc('User', frappe.session.user) - donor = frappe.get_doc(dict( - doctype='Donor', - donor_type=self.get('donor_type'), - email=frappe.session.user, - member_name=user.get_fullname() - )).insert(ignore_permissions=True) + user = frappe.get_doc("User", frappe.session.user) + donor = frappe.get_doc( + dict( + doctype="Donor", + donor_type=self.get("donor_type"), + email=frappe.session.user, + member_name=user.get_fullname(), + ) + ).insert(ignore_permissions=True) donor_name = donor.name - if self.get('__islocal'): + if self.get("__islocal"): self.donor = donor_name def on_payment_authorized(self, *args, **kwargs): @@ -45,13 +47,16 @@ class Donation(Document): self.create_payment_entry() def create_payment_entry(self, date=None): - settings = frappe.get_doc('Non Profit Settings') + settings = frappe.get_doc("Non Profit Settings") if not settings.automate_donation_payment_entries: return if not settings.donation_payment_account: - frappe.throw(_('You need to set Payment Account for Donation in {0}').format( - get_link_to_form('Non Profit Settings', 'Non Profit Settings'))) + frappe.throw( + _("You need to set Payment Account for Donation in {0}").format( + get_link_to_form("Non Profit Settings", "Non Profit Settings") + ) + ) from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -71,77 +76,77 @@ class Donation(Document): @frappe.whitelist(allow_guest=True) def capture_razorpay_donations(*args, **kwargs): """ - Creates Donation from Razorpay Webhook Request Data on payment.captured event - Creates Donor from email if not found + Creates Donation from Razorpay Webhook Request Data on payment.captured event + Creates Donor from email if not found """ data = frappe.request.get_data(as_text=True) try: - verify_signature(data, endpoint='Donation') + verify_signature(data, endpoint="Donation") except Exception as e: - log = frappe.log_error(e, 'Donation Webhook Verification Error') + log = frappe.log_error(e, "Donation Webhook Verification Error") notify_failure(log) - return { 'status': 'Failed', 'reason': e } + return {"status": "Failed", "reason": e} if isinstance(data, six.string_types): data = json.loads(data) data = frappe._dict(data) - payment = data.payload.get('payment', {}).get('entity', {}) + payment = data.payload.get("payment", {}).get("entity", {}) payment = frappe._dict(payment) try: - if not data.event == 'payment.captured': + if not data.event == "payment.captured": return # to avoid capturing subscription payments as donations - if payment.description and 'subscription' in str(payment.description).lower(): + if payment.description and "subscription" in str(payment.description).lower(): return donor = get_donor(payment.email) if not donor: donor = create_donor(payment) - donation = create_donation(donor, payment) - donation.run_method('create_payment_entry') + donation = create_razorpay_donation(donor, payment) + donation.run_method("create_payment_entry") except Exception as e: - message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id) - log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name)) + message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) + log = frappe.log_error(message, _("Error creating donation entry for {0}").format(donor.name)) notify_failure(log) - return { 'status': 'Failed', 'reason': e } + return {"status": "Failed", "reason": e} - return { 'status': 'Success' } + return {"status": "Success"} -def create_donation(donor, payment): - if not frappe.db.exists('Mode of Payment', payment.method): +def create_razorpay_donation(donor, payment): + if not frappe.db.exists("Mode of Payment", payment.method): create_mode_of_payment(payment.method) company = get_company_for_donations() - donation = frappe.get_doc({ - 'doctype': 'Donation', - 'company': company, - 'donor': donor.name, - 'donor_name': donor.donor_name, - 'email': donor.email, - 'date': getdate(), - 'amount': flt(payment.amount) / 100, # Convert to rupees from paise - 'mode_of_payment': payment.method, - 'razorpay_payment_id': payment.id - }).insert(ignore_mandatory=True) + donation = frappe.get_doc( + { + "doctype": "Donation", + "company": company, + "donor": donor.name, + "donor_name": donor.donor_name, + "email": donor.email, + "date": getdate(), + "amount": flt(payment.amount) / 100, # Convert to rupees from paise + "mode_of_payment": payment.method, + "payment_id": payment.id, + } + ).insert(ignore_mandatory=True) donation.submit() return donation def get_donor(email): - donors = frappe.get_all('Donor', - filters={'email': email}, - order_by='creation desc') + donors = frappe.get_all("Donor", filters={"email": email}, order_by="creation desc") try: - return frappe.get_doc('Donor', donors[0]['name']) + return frappe.get_doc("Donor", donors[0]["name"]) except Exception: return None @@ -149,17 +154,19 @@ def get_donor(email): @frappe.whitelist() def create_donor(payment): donor_details = frappe._dict(payment) - donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type') + donor_type = frappe.db.get_single_value("Non Profit Settings", "default_donor_type") - donor = frappe.new_doc('Donor') - donor.update({ - 'donor_name': donor_details.email, - 'donor_type': donor_type, - 'email': donor_details.email, - 'contact': donor_details.contact - }) + donor = frappe.new_doc("Donor") + donor.update( + { + "donor_name": donor_details.email, + "donor_type": donor_type, + "email": donor_details.email, + "contact": donor_details.contact, + } + ) - if donor_details.get('notes'): + if donor_details.get("notes"): donor = get_additional_notes(donor, donor_details) donor.insert(ignore_mandatory=True) @@ -167,9 +174,10 @@ def create_donor(payment): def get_company_for_donations(): - company = frappe.db.get_single_value('Non Profit Settings', 'donation_company') + company = frappe.db.get_single_value("Non Profit Settings", "donation_company") if not company: from erpnext.healthcare.setup import get_company + company = get_company() return company @@ -177,45 +185,44 @@ def get_company_for_donations(): def get_additional_notes(donor, donor_details): if type(donor_details.notes) == dict: for k, v in donor_details.notes.items(): - notes = '\n'.join('{}: {}'.format(k, v)) + notes = "\n".join("{}: {}".format(k, v)) # extract donor name from notes - if 'name' in k.lower(): - donor.update({ - 'donor_name': donor_details.notes.get(k) - }) + if "name" in k.lower(): + donor.update({"donor_name": donor_details.notes.get(k)}) # extract pan from notes - if 'pan' in k.lower(): - donor.update({ - 'pan_number': donor_details.notes.get(k) - }) + if "pan" in k.lower(): + donor.update({"pan_number": donor_details.notes.get(k)}) - donor.add_comment('Comment', notes) + donor.add_comment("Comment", notes) elif type(donor_details.notes) == str: - donor.add_comment('Comment', donor_details.notes) + donor.add_comment("Comment", donor_details.notes) return donor def create_mode_of_payment(method): - frappe.get_doc({ - 'doctype': 'Mode of Payment', - 'mode_of_payment': method - }).insert(ignore_mandatory=True) + frappe.get_doc({"doctype": "Mode of Payment", "mode_of_payment": method}).insert( + ignore_mandatory=True + ) def notify_failure(log): try: - content = ''' + content = """ Dear System Manager, Razorpay webhook for creating donation failed due to some reason. Please check the error log linked below Error Log: {0} Regards, Administrator - '''.format(get_link_to_form('Error Log', log.name)) + """.format( + get_link_to_form("Error Log", log.name) + ) - sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content) + sendmail_to_system_managers( + _("[Important] [ERPNext] Razorpay donation webhook failed, please check."), content + ) except Exception: pass diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py index 1d43ba23dcf..53700ca1ecb 100644 --- a/erpnext/non_profit/doctype/donation/donation_dashboard.py +++ b/erpnext/non_profit/doctype/donation/donation_dashboard.py @@ -1,17 +1,9 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'donation', - 'non_standard_fieldnames': { - 'Payment Entry': 'reference_name' - }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry'] - } - ] + "fieldname": "donation", + "non_standard_fieldnames": {"Payment Entry": "reference_name"}, + "transactions": [{"label": _("Payment"), "items": ["Payment Entry"]}], } diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py index 5fa731a6aa3..f24d0555b2a 100644 --- a/erpnext/non_profit/doctype/donation/test_donation.py +++ b/erpnext/non_profit/doctype/donation/test_donation.py @@ -5,32 +5,28 @@ import unittest import frappe -from erpnext.non_profit.doctype.donation.donation import create_donation +from erpnext.non_profit.doctype.donation.donation import create_razorpay_donation class TestDonation(unittest.TestCase): def setUp(self): create_donor_type() - settings = frappe.get_doc('Non Profit Settings') - settings.company = '_Test Company' - settings.donation_company = '_Test Company' - settings.default_donor_type = '_Test Donor' + settings = frappe.get_doc("Non Profit Settings") + settings.company = "_Test Company" + settings.donation_company = "_Test Company" + settings.default_donor_type = "_Test Donor" settings.automate_donation_payment_entries = 1 - settings.donation_debit_account = 'Debtors - _TC' - settings.donation_payment_account = 'Cash - _TC' - settings.creation_user = 'Administrator' + settings.donation_debit_account = "Debtors - _TC" + settings.donation_payment_account = "Cash - _TC" + settings.creation_user = "Administrator" settings.flags.ignore_permissions = True settings.save() def test_payment_entry_for_donations(self): donor = create_donor() create_mode_of_payment() - payment = frappe._dict({ - 'amount': 100, - 'method': 'Debit Card', - 'id': 'pay_MeXAmsgeKOhq7O' - }) - donation = create_donation(donor, payment) + payment = frappe._dict({"amount": 100, "method": "Debit Card", "id": "pay_MeXAmsgeKOhq7O"}) + donation = create_razorpay_donation(donor, payment) self.assertTrue(donation.name) @@ -41,37 +37,35 @@ class TestDonation(unittest.TestCase): donation.reload() self.assertEqual(donation.paid, 1) - self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name})) + self.assertTrue(frappe.db.exists("Payment Entry", {"reference_no": donation.name})) def create_donor_type(): - if not frappe.db.exists('Donor Type', '_Test Donor'): - frappe.get_doc({ - 'doctype': 'Donor Type', - 'donor_type': '_Test Donor' - }).insert() + if not frappe.db.exists("Donor Type", "_Test Donor"): + frappe.get_doc({"doctype": "Donor Type", "donor_type": "_Test Donor"}).insert() def create_donor(): - donor = frappe.db.exists('Donor', 'donor@test.com') + donor = frappe.db.exists("Donor", "donor@test.com") if donor: - return frappe.get_doc('Donor', 'donor@test.com') + return frappe.get_doc("Donor", "donor@test.com") else: - return frappe.get_doc({ - 'doctype': 'Donor', - 'donor_name': '_Test Donor', - 'donor_type': '_Test Donor', - 'email': 'donor@test.com' - }).insert() + return frappe.get_doc( + { + "doctype": "Donor", + "donor_name": "_Test Donor", + "donor_type": "_Test Donor", + "email": "donor@test.com", + } + ).insert() def create_mode_of_payment(): - if not frappe.db.exists('Mode of Payment', 'Debit Card'): - frappe.get_doc({ - 'doctype': 'Mode of Payment', - 'mode_of_payment': 'Debit Card', - 'accounts': [{ - 'company': '_Test Company', - 'default_account': 'Cash - _TC' - }] - }).insert() + if not frappe.db.exists("Mode of Payment", "Debit Card"): + frappe.get_doc( + { + "doctype": "Mode of Payment", + "mode_of_payment": "Debit Card", + "accounts": [{"company": "_Test Company", "default_account": "Cash - _TC"}], + } + ).insert() diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py index 058321b1591..77ec17e823f 100644 --- a/erpnext/non_profit/doctype/donor/donor.py +++ b/erpnext/non_profit/doctype/donor/donor.py @@ -13,5 +13,6 @@ class Donor(Document): def validate(self): from frappe.utils import validate_email_address + if self.email: validate_email_address(self.email.strip(), True) diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.py b/erpnext/non_profit/doctype/grant_application/grant_application.py index cc5e1b1442d..03db783696a 100644 --- a/erpnext/non_profit/doctype/grant_application/grant_application.py +++ b/erpnext/non_profit/doctype/grant_application/grant_application.py @@ -11,12 +11,12 @@ from frappe.website.website_generator import WebsiteGenerator class GrantApplication(WebsiteGenerator): _website = frappe._dict( - condition_field = "published", + condition_field="published", ) def validate(self): - if not self.route: #pylint: disable=E0203 - self.route = 'grant-application/' + self.scrub(self.name) + if not self.route: # pylint: disable=E0203 + self.route = "grant-application/" + self.scrub(self.name) def onload(self): """Load address and contacts in `__onload`""" @@ -25,32 +25,35 @@ class GrantApplication(WebsiteGenerator): def get_context(self, context): context.no_cache = True context.show_sidebar = True - context.parents = [dict(label='View All Grant Applications', - route='grant-application', title='View Grants')] + context.parents = [ + dict(label="View All Grant Applications", route="grant-application", title="View Grants") + ] + def get_list_context(context): context.allow_guest = True context.no_cache = True context.no_breadcrumbs = True context.show_sidebar = True - context.order_by = 'creation desc' - context.introduction =''' - Apply for new Grant Application''' + context.order_by = "creation desc" + context.introduction = """ + Apply for new Grant Application""" + @frappe.whitelist() def send_grant_review_emails(grant_application): grant = frappe.get_doc("Grant Application", grant_application) - url = get_url('grant-application/{0}'.format(grant_application)) + url = get_url("grant-application/{0}".format(grant_application)) frappe.sendmail( - recipients= grant.assessment_manager, + recipients=grant.assessment_manager, sender=frappe.session.user, - subject='Grant Application for {0}'.format(grant.applicant_name), - message='

Please Review this grant application


' + url, + subject="Grant Application for {0}".format(grant.applicant_name), + message="

Please Review this grant application


" + url, reference_doctype=grant.doctype, - reference_name=grant.name + reference_name=grant.name, ) - grant.status = 'In Progress' + grant.status = "In Progress" grant.email_notification_sent = 1 grant.save() frappe.db.commit() diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 4d80e57eccf..d2e4ae45fb0 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -17,20 +17,21 @@ class Member(Document): """Load address and contacts in `__onload`""" load_address_and_contact(self) - def validate(self): if self.email_id: self.validate_email_type(self.email_id) def validate_email_type(self, email): from frappe.utils import validate_email_address + validate_email_address(email.strip(), True) def setup_subscription(self): - non_profit_settings = frappe.get_doc('Non Profit Settings') + non_profit_settings = frappe.get_doc("Non Profit Settings") if not non_profit_settings.enable_razorpay_for_memberships: - frappe.throw(_('Please check Enable Razorpay for Memberships in {0} to setup subscription')).format( - get_link_to_form('Non Profit Settings', 'Non Profit Settings')) + frappe.throw( + _("Please check Enable Razorpay for Memberships in {0} to setup subscription") + ).format(get_link_to_form("Non Profit Settings", "Non Profit Settings")) controller = get_payment_gateway_controller("Razorpay") settings = controller.get_settings({}) @@ -43,12 +44,10 @@ class Member(Document): subscription_details = { "plan_id": plan_id, "billing_frequency": cint(non_profit_settings.billing_frequency), - "customer_notify": 1 + "customer_notify": 1, } - args = { - 'subscription_details': subscription_details - } + args = {"subscription_details": subscription_details} subscription = controller.setup_subscription(settings, **args) @@ -59,11 +58,9 @@ class Member(Document): if self.customer: frappe.msgprint(_("A customer is already linked to this Member")) - customer = create_customer(frappe._dict({ - 'fullname': self.member_name, - 'email': self.email_id, - 'phone': None - })) + customer = create_customer( + frappe._dict({"fullname": self.member_name, "email": self.email_id, "phone": None}) + ) self.customer = customer self.save() @@ -71,24 +68,29 @@ class Member(Document): def get_or_create_member(user_details): - member_list = frappe.get_all("Member", filters={'email': user_details.email, 'membership_type': user_details.plan_id}) + member_list = frappe.get_all( + "Member", filters={"email": user_details.email, "membership_type": user_details.plan_id} + ) if member_list and member_list[0]: - return member_list[0]['name'] + return member_list[0]["name"] else: return create_member(user_details) + def create_member(user_details): user_details = frappe._dict(user_details) member = frappe.new_doc("Member") - member.update({ - "member_name": user_details.fullname, - "email_id": user_details.email, - "pan_number": user_details.pan or None, - "membership_type": user_details.plan_id, - "customer_id": user_details.customer_id or None, - "subscription_id": user_details.subscription_id or None, - "subscription_status": user_details.subscription_status or "" - }) + member.update( + { + "member_name": user_details.fullname, + "email_id": user_details.email, + "pan_number": user_details.pan or None, + "membership_type": user_details.plan_id, + "customer_id": user_details.customer_id or None, + "subscription_id": user_details.subscription_id or None, + "subscription_status": user_details.subscription_status or "", + } + ) member.insert(ignore_permissions=True) member.customer = create_customer(user_details, member.name) @@ -96,14 +98,18 @@ def create_member(user_details): return member + def create_customer(user_details, member=None): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" + customer.customer_group = frappe.db.get_single_value("Selling Settings", "customer_group") + customer.territory = frappe.db.get_single_value("Selling Settings", "territory") customer.flags.ignore_mandatory = True customer.insert(ignore_permissions=True) try: + frappe.db.savepoint("contact_creation") contact = frappe.new_doc("Contact") contact.first_name = user_details.fullname if user_details.mobile: @@ -112,16 +118,10 @@ def create_customer(user_details, member=None): contact.add_email(user_details.email, is_primary=1) contact.insert(ignore_permissions=True) - contact.append("links", { - "link_doctype": "Customer", - "link_name": customer.name - }) + contact.append("links", {"link_doctype": "Customer", "link_name": customer.name}) if member: - contact.append("links", { - "link_doctype": "Member", - "link_name": member - }) + contact.append("links", {"link_doctype": "Member", "link_name": member}) contact.save(ignore_permissions=True) @@ -129,28 +129,30 @@ def create_customer(user_details, member=None): return customer.name except Exception as e: + frappe.db.rollback(save_point="contact_creation") frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed")) pass return customer.name + @frappe.whitelist(allow_guest=True) def create_member_subscription_order(user_details): """Create Member subscription and order for payment Args: - user_details (TYPE): Description + user_details (TYPE): Description Returns: - Dictionary: Dictionary with subscription details - { - 'subscription_details': { - 'plan_id': 'plan_EXwyxDYDCj3X4v', - 'billing_frequency': 24, - 'customer_notify': 1 - }, - 'subscription_id': 'sub_EZycCvXFvqnC6p' - } + Dictionary: Dictionary with subscription details + { + 'subscription_details': { + 'plan_id': 'plan_EXwyxDYDCj3X4v', + 'billing_frequency': 24, + 'customer_notify': 1 + }, + 'subscription_id': 'sub_EZycCvXFvqnC6p' + } """ user_details = frappe._dict(user_details) @@ -158,28 +160,31 @@ def create_member_subscription_order(user_details): subscription = member.setup_subscription() - member.subscription_id = subscription.get('subscription_id') + member.subscription_id = subscription.get("subscription_id") member.save(ignore_permissions=True) return subscription + @frappe.whitelist() def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): plan = get_membership_type(rzpay_plan_id) if not plan: raise frappe.DoesNotExistError - member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) + member = frappe.db.exists("Member", {"email": email, "subscription_id": subscription_id}) if member: return member else: - member = create_member(dict( - fullname=fullname, - email=email, - plan_id=plan, - subscription_id=subscription_id, - pan=pan, - mobile=mobile - )) + member = create_member( + dict( + fullname=fullname, + email=email, + plan_id=plan, + subscription_id=subscription_id, + pan=pan, + mobile=mobile, + ) + ) return member.name diff --git a/erpnext/non_profit/doctype/member/member_dashboard.py b/erpnext/non_profit/doctype/member/member_dashboard.py index 80bb9e3250d..19d9c388e19 100644 --- a/erpnext/non_profit/doctype/member/member_dashboard.py +++ b/erpnext/non_profit/doctype/member/member_dashboard.py @@ -1,23 +1,14 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('Member Activity'), - 'fieldname': 'member', - 'non_standard_fieldnames': { - 'Bank Account': 'party' - }, - 'transactions': [ - { - 'label': _('Membership Details'), - 'items': ['Membership'] - }, - { - 'label': _('Fee'), - 'items': ['Bank Account'] - } - ] + "heatmap": True, + "heatmap_message": _("Member Activity"), + "fieldname": "member", + "non_standard_fieldnames": {"Bank Account": "party"}, + "transactions": [ + {"label": _("Membership Details"), "items": ["Membership"]}, + {"label": _("Fee"), "items": ["Bank Account"]}, + ], } diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 11d32f9c2b4..df7f723c944 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -21,9 +21,11 @@ "paid", "currency", "amount", + "column_break_16", "invoice", "razorpay_details_section", "subscription_id", + "column_break_19", "payment_id" ], "fields": [ @@ -106,20 +108,17 @@ { "fieldname": "razorpay_details_section", "fieldtype": "Section Break", - "hidden": 1, "label": "Razorpay Details" }, { "fieldname": "subscription_id", "fieldtype": "Data", - "label": "Subscription ID", - "read_only": 1 + "label": "Subscription ID" }, { "fieldname": "payment_id", "fieldtype": "Data", - "label": "Payment ID", - "read_only": 1 + "label": "Payment ID" }, { "fieldname": "invoice", @@ -140,11 +139,19 @@ "label": "Company", "options": "Company", "reqd": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-19 14:33:44.925122", + "modified": "2022-03-16 17:37:28.672916", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 297a2dccb65..f29005a6d4b 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -33,12 +33,14 @@ class Membership(Document): if not member_name: user = frappe.get_doc("User", frappe.session.user) - member = frappe.get_doc(dict( - doctype="Member", - email_id=frappe.session.user, - membership_type=self.membership_type, - member_name=user.get_fullname() - )).insert(ignore_permissions=True) + member = frappe.get_doc( + dict( + doctype="Member", + email_id=frappe.session.user, + membership_type=self.membership_type, + member_name=user.get_fullname(), + ) + ).insert(ignore_permissions=True) member_name = member.name if self.get("__islocal"): @@ -49,9 +51,13 @@ class Membership(Document): last_membership = erpnext.get_last_membership(self.member) # if person applied for offline membership - if last_membership and last_membership.name != self.name and not frappe.session.user == "Administrator": + if ( + last_membership + and last_membership.name != self.name + and not frappe.session.user == "Administrator" + ): # if last membership does not expire in 30 days, then do not allow to renew - if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : + if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()): frappe.throw(_("You can only renew if your membership expires within 30 days")) self.from_date = add_days(last_membership.to_date, 1) @@ -72,13 +78,16 @@ class Membership(Document): self.db_set("paid", 1) settings = frappe.get_doc("Non Profit Settings") if settings.allow_invoicing and settings.automate_membership_invoicing: - self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) - + self.generate_invoice( + with_payment_entry=settings.automate_membership_payment_entries, save=True + ) @frappe.whitelist() def generate_invoice(self, save=True, with_payment_entry=False): if not (self.paid or self.currency or self.amount): - frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) + frappe.throw( + _("The payment for this membership is not paid. To generate invoice fill the payment details") + ) if self.invoice: frappe.throw(_("An invoice is already linked to this document")) @@ -104,27 +113,36 @@ class Membership(Document): return invoice def validate_membership_type_and_settings(self, plan, settings): - settings_link = get_link_to_form("Membership Type", self.membership_type) + settings_link = get_link_to_form("Non Profit Settings", "Non Profit Settings") if not settings.membership_debit_account: frappe.throw(_("You need to set Debit Account in {0}").format(settings_link)) if not settings.company: - frappe.throw(_("You need to set Default Company for invoicing in {0}").format(settings_link)) + frappe.throw( + _("You need to set Default Company for invoicing in {0}").format(settings_link) + ) if not plan.linked_item: - frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format( - get_link_to_form("Membership Type", self.membership_type))) + frappe.throw( + _("Please set a Linked Item for the Membership Type {0}").format( + get_link_to_form("Membership Type", self.membership_type) + ) + ) def make_payment_entry(self, settings, invoice): if not settings.membership_payment_account: - frappe.throw(_("You need to set Payment Account for Membership in {0}").format( - get_link_to_form("Non Profit Settings", "Non Profit Settings"))) + frappe.throw( + _("You need to set Payment Account for Membership in {0}").format( + get_link_to_form("Non Profit Settings", "Non Profit Settings") + ) + ) from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + frappe.flags.ignore_account_permission = True pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) - frappe.flags.ignore_account_permission=False + frappe.flags.ignore_account_permission = False pe.paid_to = settings.membership_payment_account pe.reference_no = self.name pe.reference_date = getdate() @@ -136,22 +154,33 @@ class Membership(Document): def send_acknowlement(self): settings = frappe.get_doc("Non Profit Settings") if not settings.send_email: - frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format( - get_link_to_form("Non Profit Settings", "Non Profit Settings"))) + frappe.throw( + _("You need to enable Send Acknowledge Email in {0}").format( + get_link_to_form("Non Profit Settings", "Non Profit Settings") + ) + ) member = frappe.get_doc("Member", self.member) if not member.email_id: - frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member))) + frappe.throw( + _("Email address of member {0} is missing").format( + frappe.utils.get_link_to_form("Member", self.member) + ) + ) plan = frappe.get_doc("Membership Type", self.membership_type) email = member.email_id - attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] + attachments = [ + frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format) + ] if self.invoice and settings.send_invoice: - attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + attachments.append( + frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format) + ) email_template = frappe.get_doc("Email Template", settings.email_template) - context = { "doc": self, "member": member} + context = {"doc": self, "member": member} email_args = { "recipients": [email], @@ -159,7 +188,7 @@ class Membership(Document): "subject": frappe.render_template(email_template.get("subject"), context), "attachments": attachments, "reference_doctype": self.doctype, - "reference_name": self.name + "reference_name": self.name, } if not frappe.flags.in_test: @@ -173,21 +202,17 @@ class Membership(Document): def make_invoice(membership, member, plan, settings): - invoice = frappe.get_doc({ - "doctype": "Sales Invoice", - "customer": member.customer, - "debit_to": settings.membership_debit_account, - "currency": membership.currency, - "company": settings.company, - "is_pos": 0, - "items": [ - { - "item_code": plan.linked_item, - "rate": membership.amount, - "qty": 1 - } - ] - }) + invoice = frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": member.customer, + "debit_to": settings.membership_debit_account, + "currency": membership.currency, + "company": settings.company, + "is_pos": 0, + "items": [{"item_code": plan.linked_item, "rate": membership.amount, "qty": 1}], + } + ) invoice.set_missing_values() invoice.insert() invoice.submit() @@ -241,11 +266,15 @@ def trigger_razorpay_subscription(*args, **kwargs): member = get_member_based_on_subscription(subscription.id, payment.email) if not member: - member = create_member(frappe._dict({ - "fullname": payment.email, - "email": payment.email, - "plan_id": get_plan_from_razorpay_id(subscription.plan_id) - })) + member = create_member( + frappe._dict( + { + "fullname": payment.email, + "email": payment.email, + "plan_id": get_plan_from_razorpay_id(subscription.plan_id), + } + ) + ) member.subscription_id = subscription.id member.customer_id = payment.customer_id @@ -256,18 +285,20 @@ def trigger_razorpay_subscription(*args, **kwargs): company = get_company_for_memberships() # Update Membership membership = frappe.new_doc("Membership") - membership.update({ - "company": company, - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) + membership.update( + { + "company": company, + "member": member.name, + "membership_status": "Current", + "membership_type": member.membership_type, + "currency": "INR", + "paid": 1, + "payment_id": payment.id, + "from_date": datetime.fromtimestamp(subscription.current_start), + "to_date": datetime.fromtimestamp(subscription.current_end), + "amount": payment.amount / 100, # Convert to rupees from paise + } + ) membership.flags.ignore_mandatory = True membership.insert() @@ -281,7 +312,9 @@ def trigger_razorpay_subscription(*args, **kwargs): settings = frappe.get_doc("Non Profit Settings") if settings.allow_invoicing and settings.automate_membership_invoicing: membership.reload() - membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) + membership.generate_invoice( + with_payment_entry=settings.automate_membership_payment_entries, save=True + ) except Exception as e: message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) @@ -328,7 +361,9 @@ def update_halted_razorpay_subscription(*args, **kwargs): except Exception as e: message = "{0}\n\n{1}".format(e, frappe.get_traceback()) - log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name)) + log = frappe.log_error( + message, _("Error updating halted status for member {0}").format(member.name) + ) notify_failure(log) return {"status": "Failed", "reason": e} @@ -354,6 +389,7 @@ def get_company_for_memberships(): company = frappe.db.get_single_value("Non Profit Settings", "company") if not company: from erpnext.healthcare.setup import get_company + company = get_company() return company @@ -365,15 +401,11 @@ def get_additional_notes(member, subscription): # extract member name from notes if "name" in k.lower(): - member.update({ - "member_name": subscription.notes.get(k) - }) + member.update({"member_name": subscription.notes.get(k)}) # extract pan number from notes if "pan" in k.lower(): - member.update({ - "pan_number": subscription.notes.get(k) - }) + member.update({"pan_number": subscription.notes.get(k)}) member.add_comment("Comment", notes) @@ -391,15 +423,21 @@ def notify_failure(log): Please check the following error log linked below Error Log: {0} Regards, Administrator - """.format(get_link_to_form("Error Log", log.name)) + """.format( + get_link_to_form("Error Log", log.name) + ) - sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) + sendmail_to_system_managers( + "[Important] [ERPNext] Razorpay membership webhook failed , please check.", content + ) except Exception: pass def get_plan_from_razorpay_id(plan_id): - plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc") + plan = frappe.get_all( + "Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc" + ) try: return plan[0]["name"] @@ -408,9 +446,12 @@ def get_plan_from_razorpay_id(plan_id): def set_expired_status(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabMembership` SET `membership_status` = 'Expired' WHERE `membership_status` not in ('Cancelled') AND `to_date` < %s - """, (nowdate())) + """, + (nowdate()), + ) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index fbe344c6a15..aef34a69606 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -17,14 +17,16 @@ class TestMembership(unittest.TestCase): # make test member self.member_doc = create_member( - frappe._dict({ - "fullname": "_Test_Member", - "email": "_test_member_erpnext@example.com", - "plan_id": plan.name, - "subscription_id": "sub_DEX6xcJ1HSW4CR", - "customer_id": "cust_C0WlbKhp3aLA7W", - "subscription_status": "Active" - }) + frappe._dict( + { + "fullname": "_Test_Member", + "email": "_test_member_erpnext@example.com", + "plan_id": plan.name, + "subscription_id": "sub_DEX6xcJ1HSW4CR", + "customer_id": "cust_C0WlbKhp3aLA7W", + "subscription_status": "Active", + } + ) ) self.member_doc.make_customer_and_link() self.member = self.member_doc.name @@ -40,30 +42,38 @@ class TestMembership(unittest.TestCase): def test_renew_within_30_days(self): # create a membership for two months # Should work fine - make_membership(self.member, { "from_date": nowdate() }) - make_membership(self.member, { "from_date": add_months(nowdate(), 1) }) + make_membership(self.member, {"from_date": nowdate()}) + make_membership(self.member, {"from_date": add_months(nowdate(), 1)}) from frappe.utils.user import add_role + add_role("test@example.com", "Non Profit Manager") frappe.set_user("test@example.com") # create next membership with expiry not within 30 days - self.assertRaises(frappe.ValidationError, make_membership, self.member, { - "from_date": add_months(nowdate(), 2), - }) + self.assertRaises( + frappe.ValidationError, + make_membership, + self.member, + { + "from_date": add_months(nowdate(), 2), + }, + ) frappe.set_user("Administrator") # create the same membership but as administrator - make_membership(self.member, { - "from_date": add_months(nowdate(), 2), - "to_date": add_months(nowdate(), 3), - }) + make_membership( + self.member, + { + "from_date": add_months(nowdate(), 2), + "to_date": add_months(nowdate(), 3), + }, + ) def test_halted_memberships(self): - make_membership(self.member, { - "from_date": add_months(nowdate(), 2), - "to_date": add_months(nowdate(), 3) - }) + make_membership( + self.member, {"from_date": add_months(nowdate(), 2), "to_date": add_months(nowdate(), 3)} + ) self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active") payload = get_subscription_payload() @@ -73,9 +83,11 @@ class TestMembership(unittest.TestCase): def tearDown(self): frappe.db.rollback() + def set_config(key, value): frappe.db.set_value("Non Profit Settings", None, key, value) + def make_membership(member, payload={}): data = { "doctype": "Membership", @@ -85,13 +97,14 @@ def make_membership(member, payload={}): "currency": "INR", "paid": 1, "from_date": nowdate(), - "amount": 100 + "amount": 100, } data.update(payload) membership = frappe.get_doc(data) membership.insert(ignore_permissions=True, ignore_if_duplicate=True) return membership + def create_item(item_code): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") @@ -106,6 +119,7 @@ def create_item(item_code): item = frappe.get_doc("Item", item_code) return item + def setup_membership(): # Get default company company = frappe.get_doc("Company", erpnext.get_default_company()) @@ -139,14 +153,13 @@ def setup_membership(): return plan + def get_subscription_payload(): return { "entity": "event", "account_id": "acc_BFQ7uQEaa7j2z7", "event": "subscription.halted", - "contains": [ - "subscription" - ], + "contains": ["subscription"], "payload": { "subscription": { "entity": { @@ -155,10 +168,8 @@ def get_subscription_payload(): "plan_id": "_rzpy_test_milythm", "customer_id": "cust_C0WlbKhp3aLA7W", "status": "halted", - "notes": { - "Important": "Notes for Internal Reference" - }, + "notes": {"Important": "Notes for Internal Reference"}, } } - } + }, } diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index b4464215715..33a7ffd75bd 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -14,5 +14,6 @@ class MembershipType(Document): if is_stock_item: frappe.throw(_("The Linked Item should be a service item")) + def get_membership_type(razorpay_id): return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py index ace66055427..ae36a335198 100644 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py @@ -18,8 +18,12 @@ class NonProfitSettings(Document): secret_for = "Membership" if field == "membership_webhook_secret" else "Donation" frappe.msgprint( - _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "

" + key, - _("Webhook Secret") + _("Here is your webhook secret for {0} API, this will be shown to you only once.").format( + secret_for + ) + + "

" + + key, + _("Webhook Secret"), ) @frappe.whitelist() @@ -28,9 +32,12 @@ class NonProfitSettings(Document): self.save() def get_webhook_secret(self, endpoint="Membership"): - fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret" + fieldname = ( + "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret" + ) return self.get_password(fieldname=fieldname, raise_exception=False) + @frappe.whitelist() def get_plans_for_membership(*args, **kwargs): controller = get_payment_gateway_controller("Razorpay") diff --git a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py index 3ddbfdc3b0d..58f5bfec9dc 100644 --- a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py +++ b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py @@ -11,18 +11,37 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): return [ - _("Membership Type") + ":Link/Membership Type:100", _("Membership ID") + ":Link/Membership:140", - _("Member ID") + ":Link/Member:140", _("Member Name") + ":Data:140", _("Email") + ":Data:140", - _("Expiring On") + ":Date:120" + _("Membership Type") + ":Link/Membership Type:100", + _("Membership ID") + ":Link/Membership:140", + _("Member ID") + ":Link/Member:140", + _("Member Name") + ":Data:140", + _("Email") + ":Data:140", + _("Expiring On") + ":Date:120", ] + def get_data(filters): - filters["month"] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].index(filters.month) + 1 + filters["month"] = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ].index(filters.month) + 1 - return frappe.db.sql(""" + return frappe.db.sql( + """ select ms.membership_type,ms.name,m.name,m.member_name,m.email,ms.max_membership_date from `tabMember` m inner join (select name,membership_type,max(to_date) as max_membership_date,member @@ -31,4 +50,6 @@ def get_data(filters): group by member order by max_membership_date asc) ms on m.name = ms.member - where month(max_membership_date) = %(month)s and year(max_membership_date) = %(year)s """,{'month': filters.get('month'),'year':filters.get('fiscal_year')}) + where month(max_membership_date) = %(month)s and year(max_membership_date) = %(year)s """, + {"month": filters.get("month"), "year": filters.get("fiscal_year")}, + ) diff --git a/erpnext/non_profit/web_form/certification_application/certification_application.py b/erpnext/non_profit/web_form/certification_application/certification_application.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/non_profit/web_form/certification_application/certification_application.py +++ b/erpnext/non_profit/web_form/certification_application/certification_application.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py +++ b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.py b/erpnext/non_profit/web_form/grant_application/grant_application.py index 1f828940922..e14a613cd2a 100644 --- a/erpnext/non_profit/web_form/grant_application/grant_application.py +++ b/erpnext/non_profit/web_form/grant_application/grant_application.py @@ -1,6 +1,3 @@ - - def get_context(context): context.no_cache = True - context.parents = [dict(label='View All ', - route='grant-application', title='View All')] + context.parents = [dict(label="View All ", route="grant-application", title="View All")] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6aaf9aa33aa..5d95f824dce 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,6 +1,6 @@ erpnext.patches.v12_0.update_is_cancelled_field -erpnext.patches.v13_0.add_bin_unique_constraint erpnext.patches.v11_0.rename_production_order_to_work_order +erpnext.patches.v13_0.add_bin_unique_constraint erpnext.patches.v11_0.refactor_naming_series erpnext.patches.v11_0.refactor_autoname_naming execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28 @@ -350,4 +350,17 @@ erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.amazon_mws_deprecation_warning +erpnext.patches.v13_0.datev_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr +erpnext.patches.v13_0.update_accounts_in_loan_docs +erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 +erpnext.patches.v13_0.rename_non_profit_fields +erpnext.patches.v13_0.enable_ksa_vat_docs #1 +erpnext.patches.v13_0.create_gst_custom_fields_in_quotation +erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances +erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype +erpnext.patches.v13_0.set_return_against_in_pos_invoice_references +erpnext.patches.v13_0.copy_custom_field_filters_to_website_item +erpnext.patches.v13_0.set_available_for_use_date_if_missing +erpnext.patches.v13_0.education_deprecation_warning +erpnext.patches.v13_0.create_accounting_dimensions_in_orders diff --git a/erpnext/patches/v10_0/add_default_cash_flow_mappers.py b/erpnext/patches/v10_0/add_default_cash_flow_mappers.py index 165ca0243bf..5493258e3de 100644 --- a/erpnext/patches/v10_0/add_default_cash_flow_mappers.py +++ b/erpnext/patches/v10_0/add_default_cash_flow_mappers.py @@ -8,8 +8,8 @@ from erpnext.setup.install import create_default_cash_flow_mapper_templates def execute(): - frappe.reload_doc('accounts', 'doctype', frappe.scrub('Cash Flow Mapping')) - frappe.reload_doc('accounts', 'doctype', frappe.scrub('Cash Flow Mapper')) - frappe.reload_doc('accounts', 'doctype', frappe.scrub('Cash Flow Mapping Template Details')) + frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapping")) + frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapper")) + frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapping Template Details")) - create_default_cash_flow_mapper_templates() + create_default_cash_flow_mapper_templates() diff --git a/erpnext/patches/v10_0/delete_hub_documents.py b/erpnext/patches/v10_0/delete_hub_documents.py index 706d1d2b68a..986300ceffa 100644 --- a/erpnext/patches/v10_0/delete_hub_documents.py +++ b/erpnext/patches/v10_0/delete_hub_documents.py @@ -1,4 +1,3 @@ - import frappe @@ -7,7 +6,7 @@ def execute(): frappe.delete_doc(dt, dn, ignore_missing=True) if frappe.db.exists("DocType", "Data Migration Plan"): - data_migration_plans = frappe.get_all("Data Migration Plan", filters={"module": 'Hub Node'}) + data_migration_plans = frappe.get_all("Data Migration Plan", filters={"module": "Hub Node"}) for plan in data_migration_plans: plan_doc = frappe.get_doc("Data Migration Plan", plan.name) for m in plan_doc.get("mappings"): diff --git a/erpnext/patches/v10_0/fichier_des_ecritures_comptables_for_france.py b/erpnext/patches/v10_0/fichier_des_ecritures_comptables_for_france.py index cdf5ba29141..44497299c47 100644 --- a/erpnext/patches/v10_0/fichier_des_ecritures_comptables_for_france.py +++ b/erpnext/patches/v10_0/fichier_des_ecritures_comptables_for_france.py @@ -8,6 +8,6 @@ from erpnext.setup.doctype.company.company import install_country_fixtures def execute(): - frappe.reload_doc('regional', 'report', 'fichier_des_ecritures_comptables_[fec]') - for d in frappe.get_all('Company', filters = {'country': 'France'}): + frappe.reload_doc("regional", "report", "fichier_des_ecritures_comptables_[fec]") + for d in frappe.get_all("Company", filters={"country": "France"}): install_country_fixtures(d.name) diff --git a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py index ffff95d223c..e2d0943d724 100644 --- a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py +++ b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py @@ -7,26 +7,30 @@ import frappe def execute(): frappe.reload_doc("stock", "doctype", "item_barcode") - if frappe.get_all("Item Barcode", limit=1): return - if "barcode" not in frappe.db.get_table_columns("Item"): return + if frappe.get_all("Item Barcode", limit=1): + return + if "barcode" not in frappe.db.get_table_columns("Item"): + return - items_barcode = frappe.db.sql("select name, barcode from tabItem where barcode is not null", as_dict=True) + items_barcode = frappe.db.sql( + "select name, barcode from tabItem where barcode is not null", as_dict=True + ) frappe.reload_doc("stock", "doctype", "item") - - for item in items_barcode: barcode = item.barcode.strip() - if barcode and '<' not in barcode: + if barcode and "<" not in barcode: try: - frappe.get_doc({ - 'idx': 0, - 'doctype': 'Item Barcode', - 'barcode': barcode, - 'parenttype': 'Item', - 'parent': item.name, - 'parentfield': 'barcodes' - }).insert() + frappe.get_doc( + { + "idx": 0, + "doctype": "Item Barcode", + "barcode": barcode, + "parenttype": "Item", + "parent": item.name, + "parentfield": "barcodes", + } + ).insert() except (frappe.DuplicateEntryError, frappe.UniqueValidationError): continue diff --git a/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py b/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py index fd511849b2b..2cbbe055f67 100644 --- a/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py +++ b/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py @@ -6,13 +6,13 @@ import frappe def execute(): - if not frappe.db.table_exists('Daily Work Summary Group'): + if not frappe.db.table_exists("Daily Work Summary Group"): frappe.reload_doc("hr", "doctype", "daily_work_summary_group") frappe.reload_doc("hr", "doctype", "daily_work_summary_group_user") # check if Daily Work Summary Settings Company table exists try: - frappe.db.sql('DESC `tabDaily Work Summary Settings Company`') + frappe.db.sql("DESC `tabDaily Work Summary Settings Company`") except Exception: return @@ -20,19 +20,24 @@ def execute(): previous_setting = get_previous_setting() if previous_setting["companies"]: for d in previous_setting["companies"]: - users = frappe.get_list("Employee", dict( - company=d.company, user_id=("!=", " ")), "user_id as user") - if(len(users)): + users = frappe.get_list( + "Employee", dict(company=d.company, user_id=("!=", " ")), "user_id as user" + ) + if len(users): # create new group entry for each company entry - new_group = frappe.get_doc(dict(doctype="Daily Work Summary Group", - name="Daily Work Summary for " + d.company, - users=users, - send_emails_at=d.send_emails_at, - subject=previous_setting["subject"], - message=previous_setting["message"])) + new_group = frappe.get_doc( + dict( + doctype="Daily Work Summary Group", + name="Daily Work Summary for " + d.company, + users=users, + send_emails_at=d.send_emails_at, + subject=previous_setting["subject"], + message=previous_setting["message"], + ) + ) new_group.flags.ignore_permissions = True new_group.flags.ignore_validate = True - new_group.insert(ignore_if_duplicate = True) + new_group.insert(ignore_if_duplicate=True) frappe.delete_doc("DocType", "Daily Work Summary Settings") frappe.delete_doc("DocType", "Daily Work Summary Settings Company") @@ -41,11 +46,13 @@ def execute(): def get_previous_setting(): obj = {} setting_data = frappe.db.sql( - "select field, value from tabSingles where doctype='Daily Work Summary Settings'") + "select field, value from tabSingles where doctype='Daily Work Summary Settings'" + ) for field, value in setting_data: obj[field] = value obj["companies"] = get_setting_companies() return obj + def get_setting_companies(): return frappe.db.sql("select * from `tabDaily Work Summary Settings Company`", as_dict=True) diff --git a/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py b/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py index 00d0dd75e4c..a2deab62258 100644 --- a/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py +++ b/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py b/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py index 152c5b3ec48..1d5518f0728 100644 --- a/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py +++ b/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -11,5 +10,5 @@ def execute(): rename_field("Pricing Rule", "price", "rate") except Exception as e: - if e.args[0]!=1054: + if e.args[0] != 1054: raise diff --git a/erpnext/patches/v10_0/set_currency_in_pricing_rule.py b/erpnext/patches/v10_0/set_currency_in_pricing_rule.py index 374df2a4dc7..d68148eec1c 100644 --- a/erpnext/patches/v10_0/set_currency_in_pricing_rule.py +++ b/erpnext/patches/v10_0/set_currency_in_pricing_rule.py @@ -1,4 +1,3 @@ - import frappe @@ -6,8 +5,10 @@ def execute(): frappe.reload_doctype("Pricing Rule") currency = frappe.db.get_default("currency") - for doc in frappe.get_all('Pricing Rule', fields = ["company", "name"]): + for doc in frappe.get_all("Pricing Rule", fields=["company", "name"]): if doc.company: - currency = frappe.get_cached_value('Company', doc.company, "default_currency") + currency = frappe.get_cached_value("Company", doc.company, "default_currency") - frappe.db.sql("""update `tabPricing Rule` set currency = %s where name = %s""",(currency, doc.name)) + frappe.db.sql( + """update `tabPricing Rule` set currency = %s where name = %s""", (currency, doc.name) + ) diff --git a/erpnext/patches/v10_0/update_translatable_fields.py b/erpnext/patches/v10_0/update_translatable_fields.py index f111ac7b9de..481ff93984a 100644 --- a/erpnext/patches/v10_0/update_translatable_fields.py +++ b/erpnext/patches/v10_0/update_translatable_fields.py @@ -1,41 +1,42 @@ -#-*- coding: utf-8 -*- +# -*- coding: utf-8 -*- import frappe def execute(): - ''' + """ Enable translatable in these fields - Customer Name - Supplier Name - Contact Name - Item Name/ Description - Address - ''' + """ - frappe.reload_doc('core', 'doctype', 'docfield') - frappe.reload_doc('custom', 'doctype', 'custom_field') + frappe.reload_doc("core", "doctype", "docfield") + frappe.reload_doc("custom", "doctype", "custom_field") enable_for_fields = [ - ['Customer', 'customer_name'], - ['Supplier', 'supplier_name'], - ['Contact', 'first_name'], - ['Contact', 'last_name'], - ['Item', 'item_name'], - ['Item', 'description'], - ['Address', 'address_line1'], - ['Address', 'address_line2'], + ["Customer", "customer_name"], + ["Supplier", "supplier_name"], + ["Contact", "first_name"], + ["Contact", "last_name"], + ["Item", "item_name"], + ["Item", "description"], + ["Address", "address_line1"], + ["Address", "address_line2"], ] - for f in enable_for_fields: - frappe.get_doc({ - 'doctype': 'Property Setter', - 'doc_type': f[0], - 'doctype_or_field': 'DocField', - 'field_name': f[1], - 'property': 'translatable', - 'propery_type': 'Check', - 'value': 1 - }).db_insert() + frappe.get_doc( + { + "doctype": "Property Setter", + "doc_type": f[0], + "doctype_or_field": "DocField", + "field_name": f[1], + "property": "translatable", + "propery_type": "Check", + "value": 1, + } + ).db_insert() diff --git a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py index dcb4a57ba2a..87151c102b1 100644 --- a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py +++ b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py @@ -1,44 +1,59 @@ - import frappe from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('automation', 'doctype', 'auto_repeat') + frappe.reload_doc("automation", "doctype", "auto_repeat") doctypes_to_rename = { - 'accounts': ['Journal Entry', 'Payment Entry', 'Purchase Invoice', 'Sales Invoice'], - 'buying': ['Purchase Order', 'Supplier Quotation'], - 'selling': ['Quotation', 'Sales Order'], - 'stock': ['Delivery Note', 'Purchase Receipt'] + "accounts": ["Journal Entry", "Payment Entry", "Purchase Invoice", "Sales Invoice"], + "buying": ["Purchase Order", "Supplier Quotation"], + "selling": ["Quotation", "Sales Order"], + "stock": ["Delivery Note", "Purchase Receipt"], } for module, doctypes in doctypes_to_rename.items(): for doctype in doctypes: - frappe.reload_doc(module, 'doctype', frappe.scrub(doctype)) + frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) - if frappe.db.has_column(doctype, 'subscription'): - rename_field(doctype, 'subscription', 'auto_repeat') + if frappe.db.has_column(doctype, "subscription"): + rename_field(doctype, "subscription", "auto_repeat") - subscriptions = frappe.db.sql('select * from `tabSubscription`', as_dict=1) + subscriptions = frappe.db.sql("select * from `tabSubscription`", as_dict=1) for doc in subscriptions: - doc['doctype'] = 'Auto Repeat' + doc["doctype"] = "Auto Repeat" auto_repeat = frappe.get_doc(doc) auto_repeat.db_insert() - frappe.db.sql('delete from `tabSubscription`') + frappe.db.sql("delete from `tabSubscription`") frappe.db.commit() drop_columns_from_subscription() + def drop_columns_from_subscription(): - fields_to_drop = {'Subscription': []} - for field in ['naming_series', 'reference_doctype', 'reference_document', 'start_date', - 'end_date', 'submit_on_creation', 'disabled', 'frequency', 'repeat_on_day', - 'next_schedule_date', 'notify_by_email', 'subject', 'recipients', 'print_format', - 'message', 'status', 'amended_from']: + fields_to_drop = {"Subscription": []} + for field in [ + "naming_series", + "reference_doctype", + "reference_document", + "start_date", + "end_date", + "submit_on_creation", + "disabled", + "frequency", + "repeat_on_day", + "next_schedule_date", + "notify_by_email", + "subject", + "recipients", + "print_format", + "message", + "status", + "amended_from", + ]: if field in frappe.db.get_table_columns("Subscription"): - fields_to_drop['Subscription'].append(field) + fields_to_drop["Subscription"].append(field) frappe.model.delete_fields(fields_to_drop, delete=1) diff --git a/erpnext/patches/v11_0/add_default_dispatch_notification_template.py b/erpnext/patches/v11_0/add_default_dispatch_notification_template.py index 01836ca6a45..48ca9b9f5b2 100644 --- a/erpnext/patches/v11_0/add_default_dispatch_notification_template.py +++ b/erpnext/patches/v11_0/add_default_dispatch_notification_template.py @@ -1,4 +1,3 @@ - import os import frappe @@ -11,15 +10,19 @@ def execute(): if not frappe.db.exists("Email Template", _("Dispatch Notification")): base_path = frappe.get_app_path("erpnext", "stock", "doctype") - response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) + response = frappe.read_file( + os.path.join(base_path, "delivery_trip/dispatch_notification_template.html") + ) - frappe.get_doc({ - "doctype": "Email Template", - "name": _("Dispatch Notification"), - "response": response, - "subject": _("Your order is out for delivery!"), - "owner": frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Dispatch Notification"), + "response": response, + "subject": _("Your order is out for delivery!"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) delivery_settings = frappe.get_doc("Delivery Settings") delivery_settings.dispatch_template = _("Dispatch Notification") diff --git a/erpnext/patches/v11_0/add_default_email_template_for_leave.py b/erpnext/patches/v11_0/add_default_email_template_for_leave.py index e52d12429b5..1fddc7f11ef 100644 --- a/erpnext/patches/v11_0/add_default_email_template_for_leave.py +++ b/erpnext/patches/v11_0/add_default_email_template_for_leave.py @@ -1,4 +1,3 @@ - import os import frappe @@ -8,25 +7,32 @@ from frappe import _ def execute(): frappe.reload_doc("email", "doctype", "email_template") - if not frappe.db.exists("Email Template", _('Leave Approval Notification')): + if not frappe.db.exists("Email Template", _("Leave Approval Notification")): base_path = frappe.get_app_path("erpnext", "hr", "doctype") - response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html")) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _("Leave Approval Notification"), - 'response': response, - 'subject': _("Leave Approval Notification"), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + response = frappe.read_file( + os.path.join(base_path, "leave_application/leave_application_email_template.html") + ) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Leave Approval Notification"), + "response": response, + "subject": _("Leave Approval Notification"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - - if not frappe.db.exists("Email Template", _('Leave Status Notification')): + if not frappe.db.exists("Email Template", _("Leave Status Notification")): base_path = frappe.get_app_path("erpnext", "hr", "doctype") - response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html")) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _("Leave Status Notification"), - 'response': response, - 'subject': _("Leave Status Notification"), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + response = frappe.read_file( + os.path.join(base_path, "leave_application/leave_application_email_template.html") + ) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Leave Status Notification"), + "response": response, + "subject": _("Leave Status Notification"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v11_0/add_expense_claim_default_account.py b/erpnext/patches/v11_0/add_expense_claim_default_account.py index 8629798ba82..ff393502d7d 100644 --- a/erpnext/patches/v11_0/add_expense_claim_default_account.py +++ b/erpnext/patches/v11_0/add_expense_claim_default_account.py @@ -1,4 +1,3 @@ - import frappe @@ -9,4 +8,9 @@ def execute(): for company in companies: if company.default_payable_account is not None: - frappe.db.set_value("Company", company.name, "default_expense_claim_payable_account", company.default_payable_account) + frappe.db.set_value( + "Company", + company.name, + "default_expense_claim_payable_account", + company.default_payable_account, + ) diff --git a/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py b/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py index 6091216a009..c5405d7f1e8 100644 --- a/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py +++ b/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py @@ -1,10 +1,9 @@ - import frappe from frappe import _ def execute(): - """ assign lft and rgt appropriately """ + """assign lft and rgt appropriately""" if "Healthcare" not in frappe.get_active_domains(): return @@ -13,9 +12,11 @@ def execute(): company = frappe.get_value("Company", {"domain": "Healthcare"}, "name") if company: - frappe.get_doc({ - 'doctype': 'Healthcare Service Unit', - 'healthcare_service_unit_name': _('All Healthcare Service Units'), - 'is_group': 1, - 'company': company - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Healthcare Service Unit", + "healthcare_service_unit_name": _("All Healthcare Service Units"), + "is_group": 1, + "company": company, + } + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py b/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py index 7c99f580f7c..f354616fe7f 100644 --- a/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py +++ b/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py @@ -7,6 +7,16 @@ import frappe def execute(): frappe.reload_doc("assets", "doctype", "Location") - for dt in ("Account", "Cost Center", "File", "Employee", "Location", "Task", "Customer Group", "Sales Person", "Territory"): + for dt in ( + "Account", + "Cost Center", + "File", + "Employee", + "Location", + "Task", + "Customer Group", + "Sales Person", + "Territory", + ): frappe.reload_doctype(dt) frappe.get_doc("DocType", dt).run_module_method("on_doctype_update") diff --git a/erpnext/patches/v11_0/add_item_group_defaults.py b/erpnext/patches/v11_0/add_item_group_defaults.py index 026047a9612..4e6505a3565 100644 --- a/erpnext/patches/v11_0/add_item_group_defaults.py +++ b/erpnext/patches/v11_0/add_item_group_defaults.py @@ -6,31 +6,36 @@ import frappe def execute(): - ''' + """ Fields to move from item group to item defaults child table [ default_cost_center, default_expense_account, default_income_account ] - ''' + """ - frappe.reload_doc('stock', 'doctype', 'item_default') - frappe.reload_doc('setup', 'doctype', 'item_group') + frappe.reload_doc("stock", "doctype", "item_default") + frappe.reload_doc("setup", "doctype", "item_group") companies = frappe.get_all("Company") - item_groups = frappe.db.sql("""select name, default_income_account, default_expense_account,\ - default_cost_center from `tabItem Group`""", as_dict=True) + item_groups = frappe.db.sql( + """select name, default_income_account, default_expense_account,\ + default_cost_center from `tabItem Group`""", + as_dict=True, + ) if len(companies) == 1: for item_group in item_groups: doc = frappe.get_doc("Item Group", item_group.get("name")) item_group_defaults = [] - item_group_defaults.append({ - "company": companies[0].name, - "income_account": item_group.get("default_income_account"), - "expense_account": item_group.get("default_expense_account"), - "buying_cost_center": item_group.get("default_cost_center"), - "selling_cost_center": item_group.get("default_cost_center") - }) + item_group_defaults.append( + { + "company": companies[0].name, + "income_account": item_group.get("default_income_account"), + "expense_account": item_group.get("default_expense_account"), + "buying_cost_center": item_group.get("default_cost_center"), + "selling_cost_center": item_group.get("default_cost_center"), + } + ) doc.extend("item_group_defaults", item_group_defaults) for child_doc in doc.item_group_defaults: child_doc.db_insert() @@ -38,10 +43,11 @@ def execute(): item_group_dict = { "default_expense_account": ["expense_account"], "default_income_account": ["income_account"], - "default_cost_center": ["buying_cost_center", "selling_cost_center"] + "default_cost_center": ["buying_cost_center", "selling_cost_center"], } for item_group in item_groups: item_group_defaults = [] + def insert_into_item_defaults(doc_field_name, doc_field_value, company): for d in item_group_defaults: if d.get("company") == company: @@ -50,18 +56,16 @@ def execute(): d[doc_field_name[1]] = doc_field_value return - item_group_defaults.append({ - "company": company, - doc_field_name[0]: doc_field_value - }) + item_group_defaults.append({"company": company, doc_field_name[0]: doc_field_value}) - if(len(doc_field_name) > 1): - item_group_defaults[len(item_group_defaults)-1][doc_field_name[1]] = doc_field_value + if len(doc_field_name) > 1: + item_group_defaults[len(item_group_defaults) - 1][doc_field_name[1]] = doc_field_value for d in [ - ["default_expense_account", "Account"], ["default_income_account", "Account"], - ["default_cost_center", "Cost Center"] - ]: + ["default_expense_account", "Account"], + ["default_income_account", "Account"], + ["default_cost_center", "Cost Center"], + ]: if item_group.get(d[0]): company = frappe.get_value(d[1], item_group.get(d[0]), "company", cache=True) doc_field_name = item_group_dict.get(d[0]) diff --git a/erpnext/patches/v11_0/add_market_segments.py b/erpnext/patches/v11_0/add_market_segments.py index 820199569ab..d1111c21e07 100644 --- a/erpnext/patches/v11_0/add_market_segments.py +++ b/erpnext/patches/v11_0/add_market_segments.py @@ -1,12 +1,11 @@ - import frappe from erpnext.setup.setup_wizard.operations.install_fixtures import add_market_segments def execute(): - frappe.reload_doc('crm', 'doctype', 'market_segment') + frappe.reload_doc("crm", "doctype", "market_segment") - frappe.local.lang = frappe.db.get_default("lang") or 'en' + frappe.local.lang = frappe.db.get_default("lang") or "en" add_market_segments() diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py index 9df1b586e30..f3429ef1c91 100644 --- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py +++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py @@ -4,7 +4,7 @@ from erpnext.regional.india.setup import add_permissions def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return diff --git a/erpnext/patches/v11_0/add_sales_stages.py b/erpnext/patches/v11_0/add_sales_stages.py index 1699572551b..0dac1e10ed2 100644 --- a/erpnext/patches/v11_0/add_sales_stages.py +++ b/erpnext/patches/v11_0/add_sales_stages.py @@ -1,12 +1,11 @@ - import frappe from erpnext.setup.setup_wizard.operations.install_fixtures import add_sale_stages def execute(): - frappe.reload_doc('crm', 'doctype', 'sales_stage') + frappe.reload_doc("crm", "doctype", "sales_stage") - frappe.local.lang = frappe.db.get_default("lang") or 'en' + frappe.local.lang = frappe.db.get_default("lang") or "en" add_sale_stages() diff --git a/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py b/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py index 039238f9e1b..d9d7981965b 100644 --- a/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py +++ b/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py @@ -1,7 +1,6 @@ - import frappe def execute(): - frappe.reload_doc('setup', 'doctype', 'currency_exchange') + frappe.reload_doc("setup", "doctype", "currency_exchange") frappe.db.sql("""update `tabCurrency Exchange` set for_buying = 1, for_selling = 1""") diff --git a/erpnext/patches/v11_0/create_default_success_action.py b/erpnext/patches/v11_0/create_default_success_action.py index b45065cb0d2..e7b412cc5f2 100644 --- a/erpnext/patches/v11_0/create_default_success_action.py +++ b/erpnext/patches/v11_0/create_default_success_action.py @@ -1,4 +1,3 @@ - import frappe from erpnext.setup.install import create_default_success_action diff --git a/erpnext/patches/v11_0/create_department_records_for_each_company.py b/erpnext/patches/v11_0/create_department_records_for_each_company.py index a4cba0cfcca..84be2bee9dc 100644 --- a/erpnext/patches/v11_0/create_department_records_for_each_company.py +++ b/erpnext/patches/v11_0/create_department_records_for_each_company.py @@ -1,15 +1,14 @@ - import frappe from frappe import _ from frappe.utils.nestedset import rebuild_tree def execute(): - frappe.local.lang = frappe.db.get_default("lang") or 'en' + frappe.local.lang = frappe.db.get_default("lang") or "en" - for doctype in ['department', 'leave_period', 'staffing_plan', 'job_opening']: + for doctype in ["department", "leave_period", "staffing_plan", "job_opening"]: frappe.reload_doc("hr", "doctype", doctype) - frappe.reload_doc("Payroll", "doctype", 'payroll_entry') + frappe.reload_doc("Payroll", "doctype", "payroll_entry") companies = frappe.db.get_all("Company", fields=["name", "abbr"]) departments = frappe.db.get_all("Department") @@ -36,7 +35,7 @@ def execute(): # append list of new department for each company comp_dict[company.name][department.name] = copy_doc.name - rebuild_tree('Department', 'parent_department') + rebuild_tree("Department", "parent_department") doctypes = ["Asset", "Employee", "Payroll Entry", "Staffing Plan", "Job Opening"] for d in doctypes: @@ -44,7 +43,8 @@ def execute(): update_instructors(comp_dict) - frappe.local.lang = 'en' + frappe.local.lang = "en" + def update_records(doctype, comp_dict): when_then = [] @@ -52,20 +52,27 @@ def update_records(doctype, comp_dict): records = comp_dict[company] for department in records: - when_then.append(''' + when_then.append( + """ WHEN company = "%s" and department = "%s" THEN "%s" - '''%(company, department, records[department])) + """ + % (company, department, records[department]) + ) if not when_then: return - frappe.db.sql(""" + frappe.db.sql( + """ update `tab%s` set department = CASE %s END - """%(doctype, " ".join(when_then))) + """ + % (doctype, " ".join(when_then)) + ) + def update_instructors(comp_dict): when_then = [] @@ -75,17 +82,23 @@ def update_instructors(comp_dict): records = comp_dict[employee.company] if employee.company else [] for department in records: - when_then.append(''' + when_then.append( + """ WHEN employee = "%s" and department = "%s" THEN "%s" - '''%(employee.name, department, records[department])) + """ + % (employee.name, department, records[department]) + ) if not when_then: return - frappe.db.sql(""" + frappe.db.sql( + """ update `tabInstructor` set department = CASE %s END - """%(" ".join(when_then))) + """ + % (" ".join(when_then)) + ) diff --git a/erpnext/patches/v11_0/create_salary_structure_assignments.py b/erpnext/patches/v11_0/create_salary_structure_assignments.py index 823eca19b07..b81e867b9dd 100644 --- a/erpnext/patches/v11_0/create_salary_structure_assignments.py +++ b/erpnext/patches/v11_0/create_salary_structure_assignments.py @@ -13,48 +13,62 @@ from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assign def execute(): - frappe.reload_doc('Payroll', 'doctype', 'Salary Structure') + frappe.reload_doc("Payroll", "doctype", "Salary Structure") frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment") - frappe.db.sql(""" + frappe.db.sql( + """ delete from `tabSalary Structure Assignment` where salary_structure in (select name from `tabSalary Structure` where is_active='No' or docstatus!=1) - """) - if frappe.db.table_exists('Salary Structure Employee'): - ss_details = frappe.db.sql(""" + """ + ) + if frappe.db.table_exists("Salary Structure Employee"): + ss_details = frappe.db.sql( + """ select sse.employee, sse.employee_name, sse.from_date, sse.to_date, sse.base, sse.variable, sse.parent as salary_structure, ss.company from `tabSalary Structure Employee` sse, `tabSalary Structure` ss where ss.name = sse.parent AND ss.is_active='Yes' - AND sse.employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left')""", as_dict=1) + AND sse.employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left')""", + as_dict=1, + ) else: cols = "" if "base" in frappe.db.get_table_columns("Salary Structure"): cols = ", base, variable" - ss_details = frappe.db.sql(""" + ss_details = frappe.db.sql( + """ select name as salary_structure, employee, employee_name, from_date, to_date, company {0} from `tabSalary Structure` where is_active='Yes' AND employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left') - """.format(cols), as_dict=1) + """.format( + cols + ), + as_dict=1, + ) all_companies = frappe.db.get_all("Company", fields=["name", "default_currency"]) for d in all_companies: company = d.name company_currency = d.default_currency - frappe.db.sql("""update `tabSalary Structure` set currency = %s where company=%s""", (company_currency, company)) + frappe.db.sql( + """update `tabSalary Structure` set currency = %s where company=%s""", + (company_currency, company), + ) for d in ss_details: try: - joining_date, relieving_date = frappe.db.get_value("Employee", d.employee, - ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.db.get_value( + "Employee", d.employee, ["date_of_joining", "relieving_date"] + ) from_date = d.from_date if joining_date and getdate(from_date) < joining_date: from_date = joining_date elif relieving_date and getdate(from_date) > relieving_date: continue - company_currency = frappe.db.get_value('Company', d.company, 'default_currency') + company_currency = frappe.db.get_value("Company", d.company, "default_currency") s = frappe.new_doc("Salary Structure Assignment") s.employee = d.employee diff --git a/erpnext/patches/v11_0/drop_column_max_days_allowed.py b/erpnext/patches/v11_0/drop_column_max_days_allowed.py index 5c549258584..4b4770d809c 100644 --- a/erpnext/patches/v11_0/drop_column_max_days_allowed.py +++ b/erpnext/patches/v11_0/drop_column_max_days_allowed.py @@ -1,8 +1,7 @@ - import frappe def execute(): if frappe.db.exists("DocType", "Leave Type"): - if 'max_days_allowed' in frappe.db.get_table_columns("Leave Type"): + if "max_days_allowed" in frappe.db.get_table_columns("Leave Type"): frappe.db.sql("alter table `tabLeave Type` drop column max_days_allowed") diff --git a/erpnext/patches/v11_0/ewaybill_fields_gst_india.py b/erpnext/patches/v11_0/ewaybill_fields_gst_india.py index a7e662c78c5..7a06d522426 100644 --- a/erpnext/patches/v11_0/ewaybill_fields_gst_india.py +++ b/erpnext/patches/v11_0/ewaybill_fields_gst_india.py @@ -1,12 +1,11 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company: - return + company = frappe.get_all("Company", filters={"country": "India"}) + if not company: + return - make_custom_fields() + make_custom_fields() diff --git a/erpnext/patches/v11_0/hr_ux_cleanups.py b/erpnext/patches/v11_0/hr_ux_cleanups.py index 00678c781e7..0749bfc0b9d 100644 --- a/erpnext/patches/v11_0/hr_ux_cleanups.py +++ b/erpnext/patches/v11_0/hr_ux_cleanups.py @@ -1,13 +1,12 @@ - import frappe def execute(): - frappe.reload_doctype('Employee') - frappe.db.sql('update tabEmployee set first_name = employee_name') + frappe.reload_doctype("Employee") + frappe.db.sql("update tabEmployee set first_name = employee_name") # update holiday list - frappe.reload_doctype('Holiday List') - for holiday_list in frappe.get_all('Holiday List'): - holiday_list = frappe.get_doc('Holiday List', holiday_list.name) - holiday_list.db_set('total_holidays', len(holiday_list.holidays), update_modified = False) + frappe.reload_doctype("Holiday List") + for holiday_list in frappe.get_all("Holiday List"): + holiday_list = frappe.get_doc("Holiday List", holiday_list.name) + holiday_list.db_set("total_holidays", len(holiday_list.holidays), update_modified=False) diff --git a/erpnext/patches/v11_0/inter_state_field_for_gst.py b/erpnext/patches/v11_0/inter_state_field_for_gst.py index d897941eab2..b8510297c28 100644 --- a/erpnext/patches/v11_0/inter_state_field_for_gst.py +++ b/erpnext/patches/v11_0/inter_state_field_for_gst.py @@ -1,11 +1,10 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return frappe.reload_doc("Payroll", "doctype", "Employee Tax Exemption Declaration") @@ -29,38 +28,64 @@ def execute(): frappe.reload_doc("accounts", "doctype", "purchase_taxes_and_charges_template") # set is_inter_state in Taxes And Charges Templates - if frappe.db.has_column("Sales Taxes and Charges Template", "is_inter_state") and\ - frappe.db.has_column("Purchase Taxes and Charges Template", "is_inter_state"): + if frappe.db.has_column( + "Sales Taxes and Charges Template", "is_inter_state" + ) and frappe.db.has_column("Purchase Taxes and Charges Template", "is_inter_state"): - igst_accounts = set(frappe.db.sql_list('''SELECT igst_account from `tabGST Account` WHERE parent = "GST Settings"''')) - cgst_accounts = set(frappe.db.sql_list('''SELECT cgst_account FROM `tabGST Account` WHERE parenttype = "GST Settings"''')) + igst_accounts = set( + frappe.db.sql_list( + '''SELECT igst_account from `tabGST Account` WHERE parent = "GST Settings"''' + ) + ) + cgst_accounts = set( + frappe.db.sql_list( + '''SELECT cgst_account FROM `tabGST Account` WHERE parenttype = "GST Settings"''' + ) + ) when_then_sales = get_formatted_data("Sales Taxes and Charges", igst_accounts, cgst_accounts) - when_then_purchase = get_formatted_data("Purchase Taxes and Charges", igst_accounts, cgst_accounts) + when_then_purchase = get_formatted_data( + "Purchase Taxes and Charges", igst_accounts, cgst_accounts + ) if when_then_sales: - frappe.db.sql('''update `tabSales Taxes and Charges Template` + frappe.db.sql( + """update `tabSales Taxes and Charges Template` set is_inter_state = Case {when_then} Else 0 End - '''.format(when_then=" ".join(when_then_sales))) + """.format( + when_then=" ".join(when_then_sales) + ) + ) if when_then_purchase: - frappe.db.sql('''update `tabPurchase Taxes and Charges Template` + frappe.db.sql( + """update `tabPurchase Taxes and Charges Template` set is_inter_state = Case {when_then} Else 0 End - '''.format(when_then=" ".join(when_then_purchase))) + """.format( + when_then=" ".join(when_then_purchase) + ) + ) + def get_formatted_data(doctype, igst_accounts, cgst_accounts): # fetch all the rows data from child table - all_details = frappe.db.sql(''' + all_details = frappe.db.sql( + ''' select parent, account_head from `tab{doctype}` - where parenttype="{doctype} Template"'''.format(doctype=doctype), as_dict=True) + where parenttype="{doctype} Template"'''.format( + doctype=doctype + ), + as_dict=True, + ) # group the data in the form "parent: [list of accounts]"" group_detail = {} for i in all_details: - if not i['parent'] in group_detail: group_detail[i['parent']] = [] + if not i["parent"] in group_detail: + group_detail[i["parent"]] = [] for j in all_details: - if i['parent']==j['parent']: - group_detail[i['parent']].append(j['account_head']) + if i["parent"] == j["parent"]: + group_detail[i["parent"]].append(j["account_head"]) # form when_then condition based on - if list of accounts for a document # matches any account in igst_accounts list and not matches any in cgst_accounts list @@ -68,6 +93,6 @@ def get_formatted_data(doctype, igst_accounts, cgst_accounts): for i in group_detail: temp = set(group_detail[i]) if not temp.isdisjoint(igst_accounts) and temp.isdisjoint(cgst_accounts): - when_then.append('''When name='{name}' Then 1'''.format(name=i)) + when_then.append("""When name='{name}' Then 1""".format(name=i)) return when_then diff --git a/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py b/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py index cd3869b3600..213145653d3 100644 --- a/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py +++ b/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py @@ -6,40 +6,50 @@ import frappe def execute(): - frappe.reload_doc('assets', 'doctype', 'asset_finance_book') - frappe.reload_doc('assets', 'doctype', 'depreciation_schedule') - frappe.reload_doc('assets', 'doctype', 'asset_category') - frappe.reload_doc('assets', 'doctype', 'asset') - frappe.reload_doc('assets', 'doctype', 'asset_movement') - frappe.reload_doc('assets', 'doctype', 'asset_category_account') + frappe.reload_doc("assets", "doctype", "asset_finance_book") + frappe.reload_doc("assets", "doctype", "depreciation_schedule") + frappe.reload_doc("assets", "doctype", "asset_category") + frappe.reload_doc("assets", "doctype", "asset") + frappe.reload_doc("assets", "doctype", "asset_movement") + frappe.reload_doc("assets", "doctype", "asset_category_account") if frappe.db.has_column("Asset", "warehouse"): - frappe.db.sql(""" update `tabAsset` ast, `tabWarehouse` wh - set ast.location = wh.warehouse_name where ast.warehouse = wh.name""") + frappe.db.sql( + """ update `tabAsset` ast, `tabWarehouse` wh + set ast.location = wh.warehouse_name where ast.warehouse = wh.name""" + ) - for d in frappe.get_all('Asset'): - doc = frappe.get_doc('Asset', d.name) + for d in frappe.get_all("Asset"): + doc = frappe.get_doc("Asset", d.name) if doc.calculate_depreciation: - fb = doc.append('finance_books', { - 'depreciation_method': doc.depreciation_method, - 'total_number_of_depreciations': doc.total_number_of_depreciations, - 'frequency_of_depreciation': doc.frequency_of_depreciation, - 'depreciation_start_date': doc.next_depreciation_date, - 'expected_value_after_useful_life': doc.expected_value_after_useful_life, - 'value_after_depreciation': doc.value_after_depreciation - }) + fb = doc.append( + "finance_books", + { + "depreciation_method": doc.depreciation_method, + "total_number_of_depreciations": doc.total_number_of_depreciations, + "frequency_of_depreciation": doc.frequency_of_depreciation, + "depreciation_start_date": doc.next_depreciation_date, + "expected_value_after_useful_life": doc.expected_value_after_useful_life, + "value_after_depreciation": doc.value_after_depreciation, + }, + ) fb.db_update() - frappe.db.sql(""" update `tabDepreciation Schedule` ds, `tabAsset` ast - set ds.depreciation_method = ast.depreciation_method, ds.finance_book_id = 1 where ds.parent = ast.name """) + frappe.db.sql( + """ update `tabDepreciation Schedule` ds, `tabAsset` ast + set ds.depreciation_method = ast.depreciation_method, ds.finance_book_id = 1 where ds.parent = ast.name """ + ) - for category in frappe.get_all('Asset Category'): + for category in frappe.get_all("Asset Category"): asset_category_doc = frappe.get_doc("Asset Category", category) - row = asset_category_doc.append('finance_books', { - 'depreciation_method': asset_category_doc.depreciation_method, - 'total_number_of_depreciations': asset_category_doc.total_number_of_depreciations, - 'frequency_of_depreciation': asset_category_doc.frequency_of_depreciation - }) + row = asset_category_doc.append( + "finance_books", + { + "depreciation_method": asset_category_doc.depreciation_method, + "total_number_of_depreciations": asset_category_doc.total_number_of_depreciations, + "frequency_of_depreciation": asset_category_doc.frequency_of_depreciation, + }, + ) row.db_update() diff --git a/erpnext/patches/v11_0/make_italian_localization_fields.py b/erpnext/patches/v11_0/make_italian_localization_fields.py index 8ff23a50d4b..1b9793df80b 100644 --- a/erpnext/patches/v11_0/make_italian_localization_fields.py +++ b/erpnext/patches/v11_0/make_italian_localization_fields.py @@ -9,11 +9,11 @@ from erpnext.regional.italy.setup import make_custom_fields, setup_report def execute(): - company = frappe.get_all('Company', filters = {'country': 'Italy'}) + company = frappe.get_all("Company", filters={"country": "Italy"}) if not company: return - frappe.reload_doc('regional', 'report', 'electronic_invoice_register') + frappe.reload_doc("regional", "report", "electronic_invoice_register") make_custom_fields() setup_report() @@ -25,15 +25,21 @@ def execute(): if condition: condition = "state_code = (case state {0} end),".format(condition) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE tabAddress set {condition} country_code = UPPER(ifnull((select code from `tabCountry` where name = `tabAddress`.country), '')) where country_code is null and state_code is null - """.format(condition=condition)) + """.format( + condition=condition + ) + ) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSales Invoice Item` si, `tabSales Order` so set si.customer_po_no = so.po_no, si.customer_po_date = so.po_date WHERE si.sales_order = so.name and so.po_no is not null - """) + """ + ) diff --git a/erpnext/patches/v11_0/make_job_card.py b/erpnext/patches/v11_0/make_job_card.py index 120e018805a..d4b208956b8 100644 --- a/erpnext/patches/v11_0/make_job_card.py +++ b/erpnext/patches/v11_0/make_job_card.py @@ -8,21 +8,26 @@ from erpnext.manufacturing.doctype.work_order.work_order import create_job_card def execute(): - frappe.reload_doc('manufacturing', 'doctype', 'work_order') - frappe.reload_doc('manufacturing', 'doctype', 'work_order_item') - frappe.reload_doc('manufacturing', 'doctype', 'job_card') - frappe.reload_doc('manufacturing', 'doctype', 'job_card_item') + frappe.reload_doc("manufacturing", "doctype", "work_order") + frappe.reload_doc("manufacturing", "doctype", "work_order_item") + frappe.reload_doc("manufacturing", "doctype", "job_card") + frappe.reload_doc("manufacturing", "doctype", "job_card_item") - fieldname = frappe.db.get_value('DocField', {'fieldname': 'work_order', 'parent': 'Timesheet'}, 'fieldname') + fieldname = frappe.db.get_value( + "DocField", {"fieldname": "work_order", "parent": "Timesheet"}, "fieldname" + ) if not fieldname: - fieldname = frappe.db.get_value('DocField', {'fieldname': 'production_order', 'parent': 'Timesheet'}, 'fieldname') - if not fieldname: return + fieldname = frappe.db.get_value( + "DocField", {"fieldname": "production_order", "parent": "Timesheet"}, "fieldname" + ) + if not fieldname: + return - for d in frappe.get_all('Timesheet', - filters={fieldname: ['!=', ""], 'docstatus': 0}, - fields=[fieldname, 'name']): + for d in frappe.get_all( + "Timesheet", filters={fieldname: ["!=", ""], "docstatus": 0}, fields=[fieldname, "name"] + ): if d[fieldname]: - doc = frappe.get_doc('Work Order', d[fieldname]) + doc = frappe.get_doc("Work Order", d[fieldname]) for row in doc.operations: create_job_card(doc, row, auto_create=True) - frappe.delete_doc('Timesheet', d.name) + frappe.delete_doc("Timesheet", d.name) diff --git a/erpnext/patches/v11_0/make_location_from_warehouse.py b/erpnext/patches/v11_0/make_location_from_warehouse.py index ef6262be15e..c863bb7ecf7 100644 --- a/erpnext/patches/v11_0/make_location_from_warehouse.py +++ b/erpnext/patches/v11_0/make_location_from_warehouse.py @@ -7,14 +7,16 @@ from frappe.utils.nestedset import rebuild_tree def execute(): - if not frappe.db.get_value('Asset', {'docstatus': ('<', 2) }, 'name'): return - frappe.reload_doc('assets', 'doctype', 'location') - frappe.reload_doc('stock', 'doctype', 'warehouse') + if not frappe.db.get_value("Asset", {"docstatus": ("<", 2)}, "name"): + return + frappe.reload_doc("assets", "doctype", "location") + frappe.reload_doc("stock", "doctype", "warehouse") - for d in frappe.get_all('Warehouse', - fields = ['warehouse_name', 'is_group', 'parent_warehouse'], order_by="lft asc"): + for d in frappe.get_all( + "Warehouse", fields=["warehouse_name", "is_group", "parent_warehouse"], order_by="lft asc" + ): try: - loc = frappe.new_doc('Location') + loc = frappe.new_doc("Location") loc.location_name = d.warehouse_name loc.is_group = d.is_group loc.flags.ignore_mandatory = True @@ -27,5 +29,6 @@ def execute(): rebuild_tree("Location", "parent_location") + def get_parent_warehouse_name(warehouse): - return frappe.db.get_value('Warehouse', warehouse, 'warehouse_name') + return frappe.db.get_value("Warehouse", warehouse, "warehouse_name") diff --git a/erpnext/patches/v11_0/make_quality_inspection_template.py b/erpnext/patches/v11_0/make_quality_inspection_template.py index 58c9fb9239f..deebfa88e6e 100644 --- a/erpnext/patches/v11_0/make_quality_inspection_template.py +++ b/erpnext/patches/v11_0/make_quality_inspection_template.py @@ -6,21 +6,29 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'quality_inspection_template') - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "quality_inspection_template") + frappe.reload_doc("stock", "doctype", "item") - for data in frappe.get_all('Item Quality Inspection Parameter', - fields = ["distinct parent"], filters = {'parenttype': 'Item'}): + for data in frappe.get_all( + "Item Quality Inspection Parameter", fields=["distinct parent"], filters={"parenttype": "Item"} + ): qc_doc = frappe.new_doc("Quality Inspection Template") - qc_doc.quality_inspection_template_name = 'QIT/%s' % data.parent + qc_doc.quality_inspection_template_name = "QIT/%s" % data.parent qc_doc.flags.ignore_mandatory = True qc_doc.save(ignore_permissions=True) - frappe.db.set_value('Item', data.parent, "quality_inspection_template", qc_doc.name, update_modified=False) - frappe.db.sql(""" update `tabItem Quality Inspection Parameter` + frappe.db.set_value( + "Item", data.parent, "quality_inspection_template", qc_doc.name, update_modified=False + ) + frappe.db.sql( + """ update `tabItem Quality Inspection Parameter` set parentfield = 'item_quality_inspection_parameter', parenttype = 'Quality Inspection Template', - parent = %s where parenttype = 'Item' and parent = %s""", (qc_doc.name, data.parent)) + parent = %s where parenttype = 'Item' and parent = %s""", + (qc_doc.name, data.parent), + ) # update field in item variant settings - frappe.db.sql(""" update `tabVariant Field` set field_name = 'quality_inspection_template' - where field_name = 'quality_parameters'""") + frappe.db.sql( + """ update `tabVariant Field` set field_name = 'quality_inspection_template' + where field_name = 'quality_parameters'""" + ) diff --git a/erpnext/patches/v11_0/merge_land_unit_with_location.py b/erpnext/patches/v11_0/merge_land_unit_with_location.py index e1d0b127b9d..c1afef67785 100644 --- a/erpnext/patches/v11_0/merge_land_unit_with_location.py +++ b/erpnext/patches/v11_0/merge_land_unit_with_location.py @@ -8,51 +8,55 @@ from frappe.model.utils.rename_field import rename_field def execute(): # Rename and reload the Land Unit and Linked Land Unit doctypes - if frappe.db.table_exists('Land Unit') and not frappe.db.table_exists('Location'): - frappe.rename_doc('DocType', 'Land Unit', 'Location', force=True) + if frappe.db.table_exists("Land Unit") and not frappe.db.table_exists("Location"): + frappe.rename_doc("DocType", "Land Unit", "Location", force=True) - frappe.reload_doc('assets', 'doctype', 'location') + frappe.reload_doc("assets", "doctype", "location") - if frappe.db.table_exists('Linked Land Unit') and not frappe.db.table_exists('Linked Location'): - frappe.rename_doc('DocType', 'Linked Land Unit', 'Linked Location', force=True) + if frappe.db.table_exists("Linked Land Unit") and not frappe.db.table_exists("Linked Location"): + frappe.rename_doc("DocType", "Linked Land Unit", "Linked Location", force=True) - frappe.reload_doc('assets', 'doctype', 'linked_location') + frappe.reload_doc("assets", "doctype", "linked_location") - if not frappe.db.table_exists('Crop Cycle'): - frappe.reload_doc('agriculture', 'doctype', 'crop_cycle') + if not frappe.db.table_exists("Crop Cycle"): + frappe.reload_doc("agriculture", "doctype", "crop_cycle") # Rename the fields in related doctypes - if 'linked_land_unit' in frappe.db.get_table_columns('Crop Cycle'): - rename_field('Crop Cycle', 'linked_land_unit', 'linked_location') + if "linked_land_unit" in frappe.db.get_table_columns("Crop Cycle"): + rename_field("Crop Cycle", "linked_land_unit", "linked_location") - if 'land_unit' in frappe.db.get_table_columns('Linked Location'): - rename_field('Linked Location', 'land_unit', 'location') + if "land_unit" in frappe.db.get_table_columns("Linked Location"): + rename_field("Linked Location", "land_unit", "location") if not frappe.db.exists("Location", "All Land Units"): - frappe.get_doc({"doctype": "Location", "is_group": True, "location_name": "All Land Units"}).insert(ignore_permissions=True) + frappe.get_doc( + {"doctype": "Location", "is_group": True, "location_name": "All Land Units"} + ).insert(ignore_permissions=True) - if frappe.db.table_exists('Land Unit'): - land_units = frappe.get_all('Land Unit', fields=['*'], order_by='lft') + if frappe.db.table_exists("Land Unit"): + land_units = frappe.get_all("Land Unit", fields=["*"], order_by="lft") for land_unit in land_units: - if not frappe.db.exists('Location', land_unit.get('land_unit_name')): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': land_unit.get('land_unit_name'), - 'parent_location': land_unit.get('parent_land_unit') or "All Land Units", - 'is_container': land_unit.get('is_container'), - 'is_group': land_unit.get('is_group'), - 'latitude': land_unit.get('latitude'), - 'longitude': land_unit.get('longitude'), - 'area': land_unit.get('area'), - 'location': land_unit.get('location'), - 'lft': land_unit.get('lft'), - 'rgt': land_unit.get('rgt') - }).insert(ignore_permissions=True) + if not frappe.db.exists("Location", land_unit.get("land_unit_name")): + frappe.get_doc( + { + "doctype": "Location", + "location_name": land_unit.get("land_unit_name"), + "parent_location": land_unit.get("parent_land_unit") or "All Land Units", + "is_container": land_unit.get("is_container"), + "is_group": land_unit.get("is_group"), + "latitude": land_unit.get("latitude"), + "longitude": land_unit.get("longitude"), + "area": land_unit.get("area"), + "location": land_unit.get("location"), + "lft": land_unit.get("lft"), + "rgt": land_unit.get("rgt"), + } + ).insert(ignore_permissions=True) # Delete the Land Unit and Linked Land Unit doctypes - if frappe.db.table_exists('Land Unit'): - frappe.delete_doc('DocType', 'Land Unit', force=1) + if frappe.db.table_exists("Land Unit"): + frappe.delete_doc("DocType", "Land Unit", force=1) - if frappe.db.table_exists('Linked Land Unit'): - frappe.delete_doc('DocType', 'Linked Land Unit', force=1) + if frappe.db.table_exists("Linked Land Unit"): + frappe.delete_doc("DocType", "Linked Land Unit", force=1) diff --git a/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py b/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py index bfc3fbc6084..37c07799ddc 100644 --- a/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py +++ b/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py @@ -6,22 +6,23 @@ import frappe def execute(): - ''' + """ Fields to move from the item to item defaults child table [ default_warehouse, buying_cost_center, expense_account, selling_cost_center, income_account ] - ''' - if not frappe.db.has_column('Item', 'default_warehouse'): + """ + if not frappe.db.has_column("Item", "default_warehouse"): return - frappe.reload_doc('stock', 'doctype', 'item_default') - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "item_default") + frappe.reload_doc("stock", "doctype", "item") companies = frappe.get_all("Company") if len(companies) == 1 and not frappe.get_all("Item Default", limit=1): try: - frappe.db.sql(''' + frappe.db.sql( + """ INSERT INTO `tabItem Default` (name, parent, parenttype, parentfield, idx, company, default_warehouse, buying_cost_center, selling_cost_center, expense_account, income_account, default_supplier) @@ -30,22 +31,30 @@ def execute(): 'item_defaults' as parentfield, 1 as idx, %s as company, default_warehouse, buying_cost_center, selling_cost_center, expense_account, income_account, default_supplier FROM `tabItem`; - ''', companies[0].name) + """, + companies[0].name, + ) except Exception: pass else: - item_details = frappe.db.sql(""" SELECT name, default_warehouse, + item_details = frappe.db.sql( + """ SELECT name, default_warehouse, buying_cost_center, expense_account, selling_cost_center, income_account FROM tabItem WHERE - name not in (select distinct parent from `tabItem Default`) and ifnull(disabled, 0) = 0""" - , as_dict=1) + name not in (select distinct parent from `tabItem Default`) and ifnull(disabled, 0) = 0""", + as_dict=1, + ) items_default_data = {} for item_data in item_details: - for d in [["default_warehouse", "Warehouse"], ["expense_account", "Account"], - ["income_account", "Account"], ["buying_cost_center", "Cost Center"], - ["selling_cost_center", "Cost Center"]]: + for d in [ + ["default_warehouse", "Warehouse"], + ["expense_account", "Account"], + ["income_account", "Account"], + ["buying_cost_center", "Cost Center"], + ["selling_cost_center", "Cost Center"], + ]: if item_data.get(d[0]): company = frappe.get_value(d[1], item_data.get(d[0]), "company", cache=True) @@ -73,25 +82,32 @@ def execute(): for item_code, companywise_item_data in items_default_data.items(): for company, item_default_data in companywise_item_data.items(): - to_insert_data.append(( - frappe.generate_hash("", 10), - item_code, - 'Item', - 'item_defaults', - company, - item_default_data.get('default_warehouse'), - item_default_data.get('expense_account'), - item_default_data.get('income_account'), - item_default_data.get('buying_cost_center'), - item_default_data.get('selling_cost_center'), - )) + to_insert_data.append( + ( + frappe.generate_hash("", 10), + item_code, + "Item", + "item_defaults", + company, + item_default_data.get("default_warehouse"), + item_default_data.get("expense_account"), + item_default_data.get("income_account"), + item_default_data.get("buying_cost_center"), + item_default_data.get("selling_cost_center"), + ) + ) if to_insert_data: - frappe.db.sql(''' + frappe.db.sql( + """ INSERT INTO `tabItem Default` ( `name`, `parent`, `parenttype`, `parentfield`, `company`, `default_warehouse`, `expense_account`, `income_account`, `buying_cost_center`, `selling_cost_center` ) VALUES {} - '''.format(', '.join(['%s'] * len(to_insert_data))), tuple(to_insert_data)) + """.format( + ", ".join(["%s"] * len(to_insert_data)) + ), + tuple(to_insert_data), + ) diff --git a/erpnext/patches/v11_0/move_leave_approvers_from_employee.py b/erpnext/patches/v11_0/move_leave_approvers_from_employee.py index fc3dbfbab92..f91a7db2a38 100644 --- a/erpnext/patches/v11_0/move_leave_approvers_from_employee.py +++ b/erpnext/patches/v11_0/move_leave_approvers_from_employee.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -8,20 +7,23 @@ def execute(): frappe.reload_doc("hr", "doctype", "employee") frappe.reload_doc("hr", "doctype", "department") - if frappe.db.has_column('Department', 'leave_approver'): - rename_field('Department', "leave_approver", "leave_approvers") + if frappe.db.has_column("Department", "leave_approver"): + rename_field("Department", "leave_approver", "leave_approvers") - if frappe.db.has_column('Department', 'expense_approver'): - rename_field('Department', "expense_approver", "expense_approvers") + if frappe.db.has_column("Department", "expense_approver"): + rename_field("Department", "expense_approver", "expense_approvers") if not frappe.db.table_exists("Employee Leave Approver"): return - approvers = frappe.db.sql("""select distinct app.leave_approver, emp.department from + approvers = frappe.db.sql( + """select distinct app.leave_approver, emp.department from `tabEmployee Leave Approver` app, `tabEmployee` emp where app.parenttype = 'Employee' and emp.name = app.parent - """, as_dict=True) + """, + as_dict=True, + ) for record in approvers: if record.department: @@ -29,6 +31,4 @@ def execute(): if not department: return if not len(department.leave_approvers): - department.append("leave_approvers",{ - "approver": record.leave_approver - }).db_insert() + department.append("leave_approvers", {"approver": record.leave_approver}).db_insert() diff --git a/erpnext/patches/v11_0/rebuild_tree_for_company.py b/erpnext/patches/v11_0/rebuild_tree_for_company.py index 7866cfab4f7..fc06c5d30d9 100644 --- a/erpnext/patches/v11_0/rebuild_tree_for_company.py +++ b/erpnext/patches/v11_0/rebuild_tree_for_company.py @@ -1,8 +1,7 @@ - import frappe from frappe.utils.nestedset import rebuild_tree def execute(): frappe.reload_doc("setup", "doctype", "company") - rebuild_tree('Company', 'parent_company') + rebuild_tree("Company", "parent_company") diff --git a/erpnext/patches/v11_0/redesign_healthcare_billing_work_flow.py b/erpnext/patches/v11_0/redesign_healthcare_billing_work_flow.py index 9b723cb4840..674df7933eb 100644 --- a/erpnext/patches/v11_0/redesign_healthcare_billing_work_flow.py +++ b/erpnext/patches/v11_0/redesign_healthcare_billing_work_flow.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.modules import get_doctype_module, scrub @@ -10,59 +9,77 @@ sales_invoice_referenced_doc = { "Patient Encounter": "invoice", "Lab Test": "invoice", "Lab Prescription": "invoice", - "Sample Collection": "invoice" + "Sample Collection": "invoice", } + def execute(): - frappe.reload_doc('accounts', 'doctype', 'loyalty_program') - frappe.reload_doc('accounts', 'doctype', 'sales_invoice_item') + frappe.reload_doc("accounts", "doctype", "loyalty_program") + frappe.reload_doc("accounts", "doctype", "sales_invoice_item") if "Healthcare" not in frappe.get_active_domains(): return healthcare_custom_field_in_sales_invoice() for si_ref_doc in sales_invoice_referenced_doc: - if frappe.db.exists('DocType', si_ref_doc): - frappe.reload_doc(get_doctype_module(si_ref_doc), 'doctype', scrub(si_ref_doc)) + if frappe.db.exists("DocType", si_ref_doc): + frappe.reload_doc(get_doctype_module(si_ref_doc), "doctype", scrub(si_ref_doc)) - if frappe.db.has_column(si_ref_doc, sales_invoice_referenced_doc[si_ref_doc]) \ - and frappe.db.has_column(si_ref_doc, 'invoiced'): + if frappe.db.has_column( + si_ref_doc, sales_invoice_referenced_doc[si_ref_doc] + ) and frappe.db.has_column(si_ref_doc, "invoiced"): # Set Reference DocType and Reference Docname - doc_list = frappe.db.sql(""" + doc_list = frappe.db.sql( + """ select name from `tab{0}` where {1} is not null - """.format(si_ref_doc, sales_invoice_referenced_doc[si_ref_doc])) + """.format( + si_ref_doc, sales_invoice_referenced_doc[si_ref_doc] + ) + ) if doc_list: - frappe.reload_doc(get_doctype_module("Sales Invoice"), 'doctype', 'sales_invoice') + frappe.reload_doc(get_doctype_module("Sales Invoice"), "doctype", "sales_invoice") for doc_id in doc_list: - invoice_id = frappe.db.get_value(si_ref_doc, doc_id[0], sales_invoice_referenced_doc[si_ref_doc]) + invoice_id = frappe.db.get_value( + si_ref_doc, doc_id[0], sales_invoice_referenced_doc[si_ref_doc] + ) if frappe.db.exists("Sales Invoice", invoice_id): if si_ref_doc == "Lab Test": template = frappe.db.get_value("Lab Test", doc_id[0], "template") if template: item = frappe.db.get_value("Lab Test Template", template, "item") if item: - frappe.db.sql("""update `tabSales Invoice Item` set reference_dt = '{0}', - reference_dn = '{1}' where parent = '{2}' and item_code='{3}'""".format\ - (si_ref_doc, doc_id[0], invoice_id, item)) + frappe.db.sql( + """update `tabSales Invoice Item` set reference_dt = '{0}', + reference_dn = '{1}' where parent = '{2}' and item_code='{3}'""".format( + si_ref_doc, doc_id[0], invoice_id, item + ) + ) else: invoice = frappe.get_doc("Sales Invoice", invoice_id) for item_line in invoice.items: if not item_line.reference_dn: - item_line.db_set({"reference_dt":si_ref_doc, "reference_dn": doc_id[0]}) + item_line.db_set({"reference_dt": si_ref_doc, "reference_dn": doc_id[0]}) break # Documents mark invoiced for submitted sales invoice - frappe.db.sql("""update `tab{0}` doc, `tabSales Invoice` si + frappe.db.sql( + """update `tab{0}` doc, `tabSales Invoice` si set doc.invoiced = 1 where si.docstatus = 1 and doc.{1} = si.name - """.format(si_ref_doc, sales_invoice_referenced_doc[si_ref_doc])) + """.format( + si_ref_doc, sales_invoice_referenced_doc[si_ref_doc] + ) + ) + def healthcare_custom_field_in_sales_invoice(): - frappe.reload_doc('healthcare', 'doctype', 'patient') - frappe.reload_doc('healthcare', 'doctype', 'healthcare_practitioner') - if data['custom_fields']: - create_custom_fields(data['custom_fields']) + frappe.reload_doc("healthcare", "doctype", "patient") + frappe.reload_doc("healthcare", "doctype", "healthcare_practitioner") + if data["custom_fields"]: + create_custom_fields(data["custom_fields"]) - frappe.db.sql(""" + frappe.db.sql( + """ delete from `tabCustom Field` where fieldname = 'appointment' and options = 'Patient Appointment' - """) + """ + ) diff --git a/erpnext/patches/v11_0/refactor_autoname_naming.py b/erpnext/patches/v11_0/refactor_autoname_naming.py index 1c4d8f1f79f..de453ccf21d 100644 --- a/erpnext/patches/v11_0/refactor_autoname_naming.py +++ b/erpnext/patches/v11_0/refactor_autoname_naming.py @@ -6,99 +6,102 @@ import frappe from frappe.custom.doctype.property_setter.property_setter import make_property_setter doctype_series_map = { - 'Activity Cost': 'PROJ-ACC-.#####', - 'Agriculture Task': 'AG-TASK-.#####', - 'Assessment Plan': 'EDU-ASP-.YYYY.-.#####', - 'Assessment Result': 'EDU-RES-.YYYY.-.#####', - 'Asset Movement': 'ACC-ASM-.YYYY.-.#####', - 'Attendance Request': 'HR-ARQ-.YY.-.MM.-.#####', - 'Authorization Rule': 'HR-ARU-.#####', - 'Bank Guarantee': 'ACC-BG-.YYYY.-.#####', - 'Bin': 'MAT-BIN-.YYYY.-.#####', - 'Certification Application': 'NPO-CAPP-.YYYY.-.#####', - 'Certified Consultant': 'NPO-CONS-.YYYY.-.#####', - 'Chat Room': 'CHAT-ROOM-.#####', - 'Compensatory Leave Request': 'HR-CMP-.YY.-.MM.-.#####', - 'Client Script': 'SYS-SCR-.#####', - 'Employee Benefit Application': 'HR-BEN-APP-.YY.-.MM.-.#####', - 'Employee Benefit Application Detail': '', - 'Employee Benefit Claim': 'HR-BEN-CLM-.YY.-.MM.-.#####', - 'Employee Incentive': 'HR-EINV-.YY.-.MM.-.#####', - 'Employee Onboarding': 'HR-EMP-ONB-.YYYY.-.#####', - 'Employee Onboarding Template': 'HR-EMP-ONT-.#####', - 'Employee Promotion': 'HR-EMP-PRO-.YYYY.-.#####', - 'Employee Separation': 'HR-EMP-SEP-.YYYY.-.#####', - 'Employee Separation Template': 'HR-EMP-STP-.#####', - 'Employee Tax Exemption Declaration': 'HR-TAX-DEC-.YYYY.-.#####', - 'Employee Tax Exemption Proof Submission': 'HR-TAX-PRF-.YYYY.-.#####', - 'Employee Transfer': 'HR-EMP-TRN-.YYYY.-.#####', - 'Event': 'EVENT-.YYYY.-.#####', - 'Exchange Rate Revaluation': 'ACC-ERR-.YYYY.-.#####', - 'GL Entry': 'ACC-GLE-.YYYY.-.#####', - 'Guardian': 'EDU-GRD-.YYYY.-.#####', - 'Hotel Room Reservation': 'HTL-RES-.YYYY.-.#####', - 'Item Price': '', - 'Job Applicant': 'HR-APP-.YYYY.-.#####', - 'Job Offer': 'HR-OFF-.YYYY.-.#####', - 'Leave Encashment': 'HR-ENC-.YYYY.-.#####', - 'Leave Period': 'HR-LPR-.YYYY.-.#####', - 'Leave Policy': 'HR-LPOL-.YYYY.-.#####', - 'Loan': 'ACC-LOAN-.YYYY.-.#####', - 'Loan Application': 'ACC-LOAP-.YYYY.-.#####', - 'Loyalty Point Entry': '', - 'Membership': 'NPO-MSH-.YYYY.-.#####', - 'Packing Slip': 'MAT-PAC-.YYYY.-.#####', - 'Patient Appointment': 'HLC-APP-.YYYY.-.#####', - 'Payment Terms Template Detail': '', - 'Payroll Entry': 'HR-PRUN-.YYYY.-.#####', - 'Period Closing Voucher': 'ACC-PCV-.YYYY.-.#####', - 'Plant Analysis': 'AG-PLA-.YYYY.-.#####', - 'POS Closing Entry': 'POS-CLO-.YYYY.-.#####', - 'Prepared Report': 'SYS-PREP-.YYYY.-.#####', - 'Program Enrollment': 'EDU-ENR-.YYYY.-.#####', - 'Quotation Item': '', - 'Restaurant Reservation': 'RES-RES-.YYYY.-.#####', - 'Retention Bonus': 'HR-RTB-.YYYY.-.#####', - 'Room': 'HTL-ROOM-.YYYY.-.#####', - 'Salary Structure Assignment': 'HR-SSA-.YY.-.MM.-.#####', - 'Sales Taxes and Charges': '', - 'Share Transfer': 'ACC-SHT-.YYYY.-.#####', - 'Shift Assignment': 'HR-SHA-.YY.-.MM.-.#####', - 'Shift Request': 'HR-SHR-.YY.-.MM.-.#####', - 'SMS Log': 'SYS-SMS-.#####', - 'Soil Analysis': 'AG-ANA-.YY.-.MM.-.#####', - 'Soil Texture': 'AG-TEX-.YYYY.-.#####', - 'Stock Ledger Entry': 'MAT-SLE-.YYYY.-.#####', - 'Student Leave Application': 'EDU-SLA-.YYYY.-.#####', - 'Student Log': 'EDU-SLOG-.YYYY.-.#####', - 'Subscription': 'ACC-SUB-.YYYY.-.#####', - 'Task': 'TASK-.YYYY.-.#####', - 'Tax Rule': 'ACC-TAX-RULE-.YYYY.-.#####', - 'Training Feedback': 'HR-TRF-.YYYY.-.#####', - 'Training Result': 'HR-TRR-.YYYY.-.#####', - 'Travel Request': 'HR-TRQ-.YYYY.-.#####', - 'UOM Conversion Factor': 'MAT-UOM-CNV-.#####', - 'Water Analysis': 'HR-WAT-.YYYY.-.#####', - 'Workflow Action': 'SYS-WACT-.#####', + "Activity Cost": "PROJ-ACC-.#####", + "Agriculture Task": "AG-TASK-.#####", + "Assessment Plan": "EDU-ASP-.YYYY.-.#####", + "Assessment Result": "EDU-RES-.YYYY.-.#####", + "Asset Movement": "ACC-ASM-.YYYY.-.#####", + "Attendance Request": "HR-ARQ-.YY.-.MM.-.#####", + "Authorization Rule": "HR-ARU-.#####", + "Bank Guarantee": "ACC-BG-.YYYY.-.#####", + "Bin": "MAT-BIN-.YYYY.-.#####", + "Certification Application": "NPO-CAPP-.YYYY.-.#####", + "Certified Consultant": "NPO-CONS-.YYYY.-.#####", + "Chat Room": "CHAT-ROOM-.#####", + "Compensatory Leave Request": "HR-CMP-.YY.-.MM.-.#####", + "Client Script": "SYS-SCR-.#####", + "Employee Benefit Application": "HR-BEN-APP-.YY.-.MM.-.#####", + "Employee Benefit Application Detail": "", + "Employee Benefit Claim": "HR-BEN-CLM-.YY.-.MM.-.#####", + "Employee Incentive": "HR-EINV-.YY.-.MM.-.#####", + "Employee Onboarding": "HR-EMP-ONB-.YYYY.-.#####", + "Employee Onboarding Template": "HR-EMP-ONT-.#####", + "Employee Promotion": "HR-EMP-PRO-.YYYY.-.#####", + "Employee Separation": "HR-EMP-SEP-.YYYY.-.#####", + "Employee Separation Template": "HR-EMP-STP-.#####", + "Employee Tax Exemption Declaration": "HR-TAX-DEC-.YYYY.-.#####", + "Employee Tax Exemption Proof Submission": "HR-TAX-PRF-.YYYY.-.#####", + "Employee Transfer": "HR-EMP-TRN-.YYYY.-.#####", + "Event": "EVENT-.YYYY.-.#####", + "Exchange Rate Revaluation": "ACC-ERR-.YYYY.-.#####", + "GL Entry": "ACC-GLE-.YYYY.-.#####", + "Guardian": "EDU-GRD-.YYYY.-.#####", + "Hotel Room Reservation": "HTL-RES-.YYYY.-.#####", + "Item Price": "", + "Job Applicant": "HR-APP-.YYYY.-.#####", + "Job Offer": "HR-OFF-.YYYY.-.#####", + "Leave Encashment": "HR-ENC-.YYYY.-.#####", + "Leave Period": "HR-LPR-.YYYY.-.#####", + "Leave Policy": "HR-LPOL-.YYYY.-.#####", + "Loan": "ACC-LOAN-.YYYY.-.#####", + "Loan Application": "ACC-LOAP-.YYYY.-.#####", + "Loyalty Point Entry": "", + "Membership": "NPO-MSH-.YYYY.-.#####", + "Packing Slip": "MAT-PAC-.YYYY.-.#####", + "Patient Appointment": "HLC-APP-.YYYY.-.#####", + "Payment Terms Template Detail": "", + "Payroll Entry": "HR-PRUN-.YYYY.-.#####", + "Period Closing Voucher": "ACC-PCV-.YYYY.-.#####", + "Plant Analysis": "AG-PLA-.YYYY.-.#####", + "POS Closing Entry": "POS-CLO-.YYYY.-.#####", + "Prepared Report": "SYS-PREP-.YYYY.-.#####", + "Program Enrollment": "EDU-ENR-.YYYY.-.#####", + "Quotation Item": "", + "Restaurant Reservation": "RES-RES-.YYYY.-.#####", + "Retention Bonus": "HR-RTB-.YYYY.-.#####", + "Room": "HTL-ROOM-.YYYY.-.#####", + "Salary Structure Assignment": "HR-SSA-.YY.-.MM.-.#####", + "Sales Taxes and Charges": "", + "Share Transfer": "ACC-SHT-.YYYY.-.#####", + "Shift Assignment": "HR-SHA-.YY.-.MM.-.#####", + "Shift Request": "HR-SHR-.YY.-.MM.-.#####", + "SMS Log": "SYS-SMS-.#####", + "Soil Analysis": "AG-ANA-.YY.-.MM.-.#####", + "Soil Texture": "AG-TEX-.YYYY.-.#####", + "Stock Ledger Entry": "MAT-SLE-.YYYY.-.#####", + "Student Leave Application": "EDU-SLA-.YYYY.-.#####", + "Student Log": "EDU-SLOG-.YYYY.-.#####", + "Subscription": "ACC-SUB-.YYYY.-.#####", + "Task": "TASK-.YYYY.-.#####", + "Tax Rule": "ACC-TAX-RULE-.YYYY.-.#####", + "Training Feedback": "HR-TRF-.YYYY.-.#####", + "Training Result": "HR-TRR-.YYYY.-.#####", + "Travel Request": "HR-TRQ-.YYYY.-.#####", + "UOM Conversion Factor": "MAT-UOM-CNV-.#####", + "Water Analysis": "HR-WAT-.YYYY.-.#####", + "Workflow Action": "SYS-WACT-.#####", } + def execute(): series_to_set = get_series() for doctype, opts in series_to_set.items(): - set_series(doctype, opts['value']) + set_series(doctype, opts["value"]) + def set_series(doctype, value): - doc = frappe.db.exists('Property Setter', {'doc_type': doctype, 'property': 'autoname'}) + doc = frappe.db.exists("Property Setter", {"doc_type": doctype, "property": "autoname"}) if doc: - frappe.db.set_value('Property Setter', doc, 'value', value) + frappe.db.set_value("Property Setter", doc, "value", value) else: - make_property_setter(doctype, '', 'autoname', value, '', for_doctype = True) + make_property_setter(doctype, "", "autoname", value, "", for_doctype=True) + def get_series(): series_to_set = {} for doctype in doctype_series_map: - if not frappe.db.exists('DocType', doctype): + if not frappe.db.exists("DocType", doctype): continue if not frappe.db.a_row_exists(doctype): @@ -110,10 +113,11 @@ def get_series(): # set autoname property setter if series_to_preserve: - series_to_set[doctype] = {'value': series_to_preserve} + series_to_set[doctype] = {"value": series_to_preserve} return series_to_set + def get_series_to_preserve(doctype): - series_to_preserve = frappe.db.get_value('DocType', doctype, 'autoname') + series_to_preserve = frappe.db.get_value("DocType", doctype, "autoname") return series_to_preserve diff --git a/erpnext/patches/v11_0/refactor_erpnext_shopify.py b/erpnext/patches/v11_0/refactor_erpnext_shopify.py index 684b1b3fd6a..308676d057d 100644 --- a/erpnext/patches/v11_0/refactor_erpnext_shopify.py +++ b/erpnext/patches/v11_0/refactor_erpnext_shopify.py @@ -1,18 +1,17 @@ - import frappe from frappe.installer import remove_from_installed_apps def execute(): - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_tax_account') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_log') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_webhook_detail') + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings") + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_tax_account") + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_log") + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_webhook_detail") - if 'erpnext_shopify' in frappe.get_installed_apps(): - remove_from_installed_apps('erpnext_shopify') + if "erpnext_shopify" in frappe.get_installed_apps(): + remove_from_installed_apps("erpnext_shopify") - frappe.delete_doc("Module Def", 'erpnext_shopify') + frappe.delete_doc("Module Def", "erpnext_shopify") frappe.db.commit() @@ -22,11 +21,14 @@ def execute(): else: disable_shopify() + def setup_app_type(): try: shopify_settings = frappe.get_doc("Shopify Settings") - shopify_settings.app_type = 'Private' - shopify_settings.update_price_in_erpnext_price_list = 0 if getattr(shopify_settings, 'push_prices_to_shopify', None) else 1 + shopify_settings.app_type = "Private" + shopify_settings.update_price_in_erpnext_price_list = ( + 0 if getattr(shopify_settings, "push_prices_to_shopify", None) else 1 + ) shopify_settings.flags.ignore_mandatory = True shopify_settings.ignore_permissions = True shopify_settings.save() @@ -34,11 +36,15 @@ def setup_app_type(): frappe.db.set_value("Shopify Settings", None, "enable_shopify", 0) frappe.log_error(frappe.get_traceback()) + def disable_shopify(): # due to frappe.db.set_value wrongly written and enable_shopify being default 1 # Shopify Settings isn't properly configured and leads to error - shopify = frappe.get_doc('Shopify Settings') + shopify = frappe.get_doc("Shopify Settings") - if shopify.app_type == "Public" or shopify.app_type == None or \ - (shopify.enable_shopify and not (shopify.shopify_url or shopify.api_key)): + if ( + shopify.app_type == "Public" + or shopify.app_type == None + or (shopify.enable_shopify and not (shopify.shopify_url or shopify.api_key)) + ): frappe.db.set_value("Shopify Settings", None, "enable_shopify", 0) diff --git a/erpnext/patches/v11_0/refactor_naming_series.py b/erpnext/patches/v11_0/refactor_naming_series.py index 6b275e2b1ef..efc1540f288 100644 --- a/erpnext/patches/v11_0/refactor_naming_series.py +++ b/erpnext/patches/v11_0/refactor_naming_series.py @@ -6,88 +6,94 @@ import frappe from frappe.custom.doctype.property_setter.property_setter import make_property_setter doctype_series_map = { - 'Additional Salary': 'HR-ADS-.YY.-.MM.-', - 'Appraisal': 'HR-APR-.YY.-.MM.', - 'Asset': 'ACC-ASS-.YYYY.-', - 'Attendance': 'HR-ATT-.YYYY.-', - 'Auto Repeat': 'SYS-ARP-.YYYY.-', - 'Blanket Order': 'MFG-BLR-.YYYY.-', - 'C-Form': 'ACC-CF-.YYYY.-', - 'Campaign': 'SAL-CAM-.YYYY.-', - 'Clinical Procedure': 'HLC-CPR-.YYYY.-', - 'Course Schedule': 'EDU-CSH-.YYYY.-', - 'Customer': 'CUST-.YYYY.-', - 'Delivery Note': 'MAT-DN-.YYYY.-', - 'Delivery Trip': 'MAT-DT-.YYYY.-', - 'Driver': 'HR-DRI-.YYYY.-', - 'Employee': 'HR-EMP-', - 'Employee Advance': 'HR-EAD-.YYYY.-', - 'Expense Claim': 'HR-EXP-.YYYY.-', - 'Fee Schedule': 'EDU-FSH-.YYYY.-', - 'Fee Structure': 'EDU-FST-.YYYY.-', - 'Fees': 'EDU-FEE-.YYYY.-', - 'Inpatient Record': 'HLC-INP-.YYYY.-', - 'Installation Note': 'MAT-INS-.YYYY.-', - 'Instructor': 'EDU-INS-.YYYY.-', - 'Issue': 'ISS-.YYYY.-', - 'Journal Entry': 'ACC-JV-.YYYY.-', - 'Lab Test': 'HLC-LT-.YYYY.-', - 'Landed Cost Voucher': 'MAT-LCV-.YYYY.-', - 'Lead': 'CRM-LEAD-.YYYY.-', - 'Leave Allocation': 'HR-LAL-.YYYY.-', - 'Leave Application': 'HR-LAP-.YYYY.-', - 'Maintenance Schedule': 'MAT-MSH-.YYYY.-', - 'Maintenance Visit': 'MAT-MVS-.YYYY.-', - 'Material Request': 'MAT-MR-.YYYY.-', - 'Member': 'NPO-MEM-.YYYY.-', - 'Opportunity': 'CRM-OPP-.YYYY.-', - 'Packing Slip': 'MAT-PAC-.YYYY.-', - 'Patient': 'HLC-PAT-.YYYY.-', - 'Patient Encounter': 'HLC-ENC-.YYYY.-', - 'Patient Medical Record': 'HLC-PMR-.YYYY.-', - 'Payment Entry': 'ACC-PAY-.YYYY.-', - 'Payment Request': 'ACC-PRQ-.YYYY.-', - 'Production Plan': 'MFG-PP-.YYYY.-', - 'Project Update': 'PROJ-UPD-.YYYY.-', - 'Purchase Invoice': 'ACC-PINV-.YYYY.-', - 'Purchase Order': 'PUR-ORD-.YYYY.-', - 'Purchase Receipt': 'MAT-PRE-.YYYY.-', - 'Quality Inspection': 'MAT-QA-.YYYY.-', - 'Quotation': 'SAL-QTN-.YYYY.-', - 'Request for Quotation': 'PUR-RFQ-.YYYY.-', - 'Sales Invoice': 'ACC-SINV-.YYYY.-', - 'Sales Order': 'SAL-ORD-.YYYY.-', - 'Sample Collection': 'HLC-SC-.YYYY.-', - 'Shareholder': 'ACC-SH-.YYYY.-', - 'Stock Entry': 'MAT-STE-.YYYY.-', - 'Stock Reconciliation': 'MAT-RECO-.YYYY.-', - 'Student': 'EDU-STU-.YYYY.-', - 'Student Applicant': 'EDU-APP-.YYYY.-', - 'Supplier': 'SUP-.YYYY.-', - 'Supplier Quotation': 'PUR-SQTN-.YYYY.-', - 'Supplier Scorecard Period': 'PU-SSP-.YYYY.-', - 'Timesheet': 'TS-.YYYY.-', - 'Vehicle Log': 'HR-VLOG-.YYYY.-', - 'Warranty Claim': 'SER-WRN-.YYYY.-', - 'Work Order': 'MFG-WO-.YYYY.-' + "Additional Salary": "HR-ADS-.YY.-.MM.-", + "Appraisal": "HR-APR-.YY.-.MM.", + "Asset": "ACC-ASS-.YYYY.-", + "Attendance": "HR-ATT-.YYYY.-", + "Auto Repeat": "SYS-ARP-.YYYY.-", + "Blanket Order": "MFG-BLR-.YYYY.-", + "C-Form": "ACC-CF-.YYYY.-", + "Campaign": "SAL-CAM-.YYYY.-", + "Clinical Procedure": "HLC-CPR-.YYYY.-", + "Course Schedule": "EDU-CSH-.YYYY.-", + "Customer": "CUST-.YYYY.-", + "Delivery Note": "MAT-DN-.YYYY.-", + "Delivery Trip": "MAT-DT-.YYYY.-", + "Driver": "HR-DRI-.YYYY.-", + "Employee": "HR-EMP-", + "Employee Advance": "HR-EAD-.YYYY.-", + "Expense Claim": "HR-EXP-.YYYY.-", + "Fee Schedule": "EDU-FSH-.YYYY.-", + "Fee Structure": "EDU-FST-.YYYY.-", + "Fees": "EDU-FEE-.YYYY.-", + "Inpatient Record": "HLC-INP-.YYYY.-", + "Installation Note": "MAT-INS-.YYYY.-", + "Instructor": "EDU-INS-.YYYY.-", + "Issue": "ISS-.YYYY.-", + "Journal Entry": "ACC-JV-.YYYY.-", + "Lab Test": "HLC-LT-.YYYY.-", + "Landed Cost Voucher": "MAT-LCV-.YYYY.-", + "Lead": "CRM-LEAD-.YYYY.-", + "Leave Allocation": "HR-LAL-.YYYY.-", + "Leave Application": "HR-LAP-.YYYY.-", + "Maintenance Schedule": "MAT-MSH-.YYYY.-", + "Maintenance Visit": "MAT-MVS-.YYYY.-", + "Material Request": "MAT-MR-.YYYY.-", + "Member": "NPO-MEM-.YYYY.-", + "Opportunity": "CRM-OPP-.YYYY.-", + "Packing Slip": "MAT-PAC-.YYYY.-", + "Patient": "HLC-PAT-.YYYY.-", + "Patient Encounter": "HLC-ENC-.YYYY.-", + "Patient Medical Record": "HLC-PMR-.YYYY.-", + "Payment Entry": "ACC-PAY-.YYYY.-", + "Payment Request": "ACC-PRQ-.YYYY.-", + "Production Plan": "MFG-PP-.YYYY.-", + "Project Update": "PROJ-UPD-.YYYY.-", + "Purchase Invoice": "ACC-PINV-.YYYY.-", + "Purchase Order": "PUR-ORD-.YYYY.-", + "Purchase Receipt": "MAT-PRE-.YYYY.-", + "Quality Inspection": "MAT-QA-.YYYY.-", + "Quotation": "SAL-QTN-.YYYY.-", + "Request for Quotation": "PUR-RFQ-.YYYY.-", + "Sales Invoice": "ACC-SINV-.YYYY.-", + "Sales Order": "SAL-ORD-.YYYY.-", + "Sample Collection": "HLC-SC-.YYYY.-", + "Shareholder": "ACC-SH-.YYYY.-", + "Stock Entry": "MAT-STE-.YYYY.-", + "Stock Reconciliation": "MAT-RECO-.YYYY.-", + "Student": "EDU-STU-.YYYY.-", + "Student Applicant": "EDU-APP-.YYYY.-", + "Supplier": "SUP-.YYYY.-", + "Supplier Quotation": "PUR-SQTN-.YYYY.-", + "Supplier Scorecard Period": "PU-SSP-.YYYY.-", + "Timesheet": "TS-.YYYY.-", + "Vehicle Log": "HR-VLOG-.YYYY.-", + "Warranty Claim": "SER-WRN-.YYYY.-", + "Work Order": "MFG-WO-.YYYY.-", } + def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ update `tabProperty Setter` set name=concat(doc_type, '-', field_name, '-', property) where property='fetch_from' - """) + """ + ) series_to_set = get_series() for doctype, opts in series_to_set.items(): set_series(doctype, opts["options"], opts["default"]) + def set_series(doctype, options, default): def _make_property_setter(property_name, value): - property_setter = frappe.db.exists('Property Setter', - {'doc_type': doctype, 'field_name': 'naming_series', 'property': property_name}) + property_setter = frappe.db.exists( + "Property Setter", + {"doc_type": doctype, "field_name": "naming_series", "property": property_name}, + ) if property_setter: - frappe.db.set_value('Property Setter', property_setter, 'value', value) + frappe.db.set_value("Property Setter", property_setter, "value", value) else: make_property_setter(doctype, "naming_series", "options", value, "Text") @@ -95,17 +101,18 @@ def set_series(doctype, options, default): if default: _make_property_setter("default", default) + def get_series(): series_to_set = {} for doctype in doctype_series_map: - if not frappe.db.exists('DocType', doctype): + if not frappe.db.exists("DocType", doctype): continue if not frappe.db.a_row_exists(doctype): continue - if not frappe.db.has_column(doctype, 'naming_series'): + if not frappe.db.has_column(doctype, "naming_series"): continue - if not frappe.get_meta(doctype).has_field('naming_series'): + if not frappe.get_meta(doctype).has_field("naming_series"): continue series_to_preserve = list(filter(None, get_series_to_preserve(doctype))) default_series = get_default_series(doctype) @@ -123,12 +130,18 @@ def get_series(): return series_to_set + def get_series_to_preserve(doctype): - series_to_preserve = frappe.db.sql_list("""select distinct naming_series from `tab{doctype}` where ifnull(naming_series, '') != ''""".format(doctype=doctype)) + series_to_preserve = frappe.db.sql_list( + """select distinct naming_series from `tab{doctype}` where ifnull(naming_series, '') != ''""".format( + doctype=doctype + ) + ) series_to_preserve.sort() return series_to_preserve + def get_default_series(doctype): field = frappe.get_meta(doctype).get_field("naming_series") - default_series = field.get('default', '') if field else '' + default_series = field.get("default", "") if field else "" return default_series diff --git a/erpnext/patches/v11_0/remove_barcodes_field_from_copy_fields_to_variants.py b/erpnext/patches/v11_0/remove_barcodes_field_from_copy_fields_to_variants.py index caf74f578de..2e0204c22b9 100644 --- a/erpnext/patches/v11_0/remove_barcodes_field_from_copy_fields_to_variants.py +++ b/erpnext/patches/v11_0/remove_barcodes_field_from_copy_fields_to_variants.py @@ -2,7 +2,7 @@ import frappe def execute(): - '''Remove barcodes field from "Copy Fields to Variants" table because barcodes must be unique''' + """Remove barcodes field from "Copy Fields to Variants" table because barcodes must be unique""" - settings = frappe.get_doc('Item Variant Settings') + settings = frappe.get_doc("Item Variant Settings") settings.remove_invalid_fields_for_copy_fields_in_variants() diff --git a/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py b/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py index 85292e8d135..036ae8ebfc1 100644 --- a/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py +++ b/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py @@ -1,11 +1,11 @@ - import frappe # this patch should have been included with this PR https://github.com/frappe/erpnext/pull/14302 + def execute(): if frappe.db.table_exists("Additional Salary Component"): if not frappe.db.table_exists("Additional Salary"): frappe.rename_doc("DocType", "Additional Salary Component", "Additional Salary") - frappe.delete_doc('DocType', "Additional Salary Component") + frappe.delete_doc("DocType", "Additional Salary Component") diff --git a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py index c7a3aa2abd4..c444c16a59b 100644 --- a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py +++ b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py @@ -6,6 +6,8 @@ import frappe def execute(): - if frappe.db.table_exists("Asset Adjustment") and not frappe.db.table_exists("Asset Value Adjustment"): - frappe.rename_doc('DocType', 'Asset Adjustment', 'Asset Value Adjustment', force=True) - frappe.reload_doc('assets', 'doctype', 'asset_value_adjustment') + if frappe.db.table_exists("Asset Adjustment") and not frappe.db.table_exists( + "Asset Value Adjustment" + ): + frappe.rename_doc("DocType", "Asset Adjustment", "Asset Value Adjustment", force=True) + frappe.reload_doc("assets", "doctype", "asset_value_adjustment") diff --git a/erpnext/patches/v11_0/rename_bom_wo_fields.py b/erpnext/patches/v11_0/rename_bom_wo_fields.py index cab7d0a673f..fb25eeb6fcc 100644 --- a/erpnext/patches/v11_0/rename_bom_wo_fields.py +++ b/erpnext/patches/v11_0/rename_bom_wo_fields.py @@ -7,28 +7,36 @@ from frappe.model.utils.rename_field import rename_field def execute(): - # updating column value to handle field change from Data to Currency - changed_field = "base_scrap_material_cost" - frappe.db.sql(f"update `tabBOM` set {changed_field} = '0' where trim(coalesce({changed_field}, ''))= ''") + # updating column value to handle field change from Data to Currency + changed_field = "base_scrap_material_cost" + frappe.db.sql( + f"update `tabBOM` set {changed_field} = '0' where trim(coalesce({changed_field}, ''))= ''" + ) - for doctype in ['BOM Explosion Item', 'BOM Item', 'Work Order Item', 'Item']: - if frappe.db.has_column(doctype, 'allow_transfer_for_manufacture'): - if doctype != 'Item': - frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype)) - else: - frappe.reload_doc('stock', 'doctype', frappe.scrub(doctype)) + for doctype in ["BOM Explosion Item", "BOM Item", "Work Order Item", "Item"]: + if frappe.db.has_column(doctype, "allow_transfer_for_manufacture"): + if doctype != "Item": + frappe.reload_doc("manufacturing", "doctype", frappe.scrub(doctype)) + else: + frappe.reload_doc("stock", "doctype", frappe.scrub(doctype)) - rename_field(doctype, "allow_transfer_for_manufacture", "include_item_in_manufacturing") + rename_field(doctype, "allow_transfer_for_manufacture", "include_item_in_manufacturing") - for doctype in ['BOM', 'Work Order']: - frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype)) + for doctype in ["BOM", "Work Order"]: + frappe.reload_doc("manufacturing", "doctype", frappe.scrub(doctype)) - if frappe.db.has_column(doctype, 'transfer_material_against_job_card'): - frappe.db.sql(""" UPDATE `tab%s` + if frappe.db.has_column(doctype, "transfer_material_against_job_card"): + frappe.db.sql( + """ UPDATE `tab%s` SET transfer_material_against = CASE WHEN transfer_material_against_job_card = 1 then 'Job Card' Else 'Work Order' END - WHERE docstatus < 2""" % (doctype)) - else: - frappe.db.sql(""" UPDATE `tab%s` + WHERE docstatus < 2""" + % (doctype) + ) + else: + frappe.db.sql( + """ UPDATE `tab%s` SET transfer_material_against = 'Work Order' - WHERE docstatus < 2""" % (doctype)) + WHERE docstatus < 2""" + % (doctype) + ) diff --git a/erpnext/patches/v11_0/rename_duplicate_item_code_values.py b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py index 61f3856e8eb..1f65e14814d 100644 --- a/erpnext/patches/v11_0/rename_duplicate_item_code_values.py +++ b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py @@ -3,7 +3,9 @@ import frappe def execute(): items = [] - items = frappe.db.sql("""select item_code from `tabItem` group by item_code having count(*) > 1""", as_dict=True) + items = frappe.db.sql( + """select item_code from `tabItem` group by item_code having count(*) > 1""", as_dict=True + ) if items: for item in items: frappe.db.sql("""update `tabItem` set item_code=name where item_code = %s""", (item.item_code)) diff --git a/erpnext/patches/v11_0/rename_field_max_days_allowed.py b/erpnext/patches/v11_0/rename_field_max_days_allowed.py index fb08be8628b..0813770efcc 100644 --- a/erpnext/patches/v11_0/rename_field_max_days_allowed.py +++ b/erpnext/patches/v11_0/rename_field_max_days_allowed.py @@ -1,14 +1,15 @@ - import frappe from frappe.model.utils.rename_field import rename_field def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLeave Type` SET max_days_allowed = '0' WHERE trim(coalesce(max_days_allowed, '')) = '' - """) + """ + ) frappe.db.sql_ddl("""ALTER table `tabLeave Type` modify max_days_allowed int(8) NOT NULL""") frappe.reload_doc("hr", "doctype", "leave_type") rename_field("Leave Type", "max_days_allowed", "max_continuous_days_allowed") diff --git a/erpnext/patches/v11_0/rename_health_insurance.py b/erpnext/patches/v11_0/rename_health_insurance.py index 1b6db89101b..3509c6b104e 100644 --- a/erpnext/patches/v11_0/rename_health_insurance.py +++ b/erpnext/patches/v11_0/rename_health_insurance.py @@ -6,5 +6,5 @@ import frappe def execute(): - frappe.rename_doc('DocType', 'Health Insurance', 'Employee Health Insurance', force=True) - frappe.reload_doc('hr', 'doctype', 'employee_health_insurance') + frappe.rename_doc("DocType", "Health Insurance", "Employee Health Insurance", force=True) + frappe.reload_doc("hr", "doctype", "employee_health_insurance") diff --git a/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py b/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py index 55717f88ea1..f237937e9cd 100644 --- a/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py +++ b/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field from frappe.modules import get_doctype_module, scrub @@ -8,21 +7,15 @@ field_rename_map = { ["consultation_time", "encounter_time"], ["consultation_date", "encounter_date"], ["consultation_comment", "encounter_comment"], - ["physician", "practitioner"] - ], - "Fee Validity": [ - ["physician", "practitioner"] - ], - "Lab Test": [ - ["physician", "practitioner"] + ["physician", "practitioner"], ], + "Fee Validity": [["physician", "practitioner"]], + "Lab Test": [["physician", "practitioner"]], "Patient Appointment": [ ["physician", "practitioner"], - ["referring_physician", "referring_practitioner"] + ["referring_physician", "referring_practitioner"], ], - "Procedure Prescription": [ - ["physician", "practitioner"] - ] + "Procedure Prescription": [["physician", "practitioner"]], } doc_rename_map = { @@ -30,37 +23,40 @@ doc_rename_map = { "Physician Schedule": "Practitioner Schedule", "Physician Service Unit Schedule": "Practitioner Service Unit Schedule", "Consultation": "Patient Encounter", - "Physician": "Healthcare Practitioner" + "Physician": "Healthcare Practitioner", } + def execute(): for dt in doc_rename_map: - if frappe.db.exists('DocType', dt): - frappe.rename_doc('DocType', dt, doc_rename_map[dt], force=True) + if frappe.db.exists("DocType", dt): + frappe.rename_doc("DocType", dt, doc_rename_map[dt], force=True) for dn in field_rename_map: - if frappe.db.exists('DocType', dn): + if frappe.db.exists("DocType", dn): frappe.reload_doc(get_doctype_module(dn), "doctype", scrub(dn)) for dt, field_list in field_rename_map.items(): - if frappe.db.exists('DocType', dt): + if frappe.db.exists("DocType", dt): for field in field_list: if frappe.db.has_column(dt, field[0]): rename_field(dt, field[0], field[1]) - if frappe.db.exists('DocType', 'Practitioner Service Unit Schedule'): - if frappe.db.has_column('Practitioner Service Unit Schedule', 'parentfield'): - frappe.db.sql(""" + if frappe.db.exists("DocType", "Practitioner Service Unit Schedule"): + if frappe.db.has_column("Practitioner Service Unit Schedule", "parentfield"): + frappe.db.sql( + """ update `tabPractitioner Service Unit Schedule` set parentfield = 'practitioner_schedules' where parentfield = 'physician_schedules' and parenttype = 'Healthcare Practitioner' - """) + """ + ) if frappe.db.exists("DocType", "Healthcare Practitioner"): frappe.reload_doc("healthcare", "doctype", "healthcare_practitioner") frappe.reload_doc("healthcare", "doctype", "practitioner_service_unit_schedule") - if frappe.db.has_column('Healthcare Practitioner', 'physician_schedule'): - for doc in frappe.get_all('Healthcare Practitioner'): - _doc = frappe.get_doc('Healthcare Practitioner', doc.name) + if frappe.db.has_column("Healthcare Practitioner", "physician_schedule"): + for doc in frappe.get_all("Healthcare Practitioner"): + _doc = frappe.get_doc("Healthcare Practitioner", doc.name) if _doc.physician_schedule: - _doc.append('practitioner_schedules', {'schedule': _doc.physician_schedule}) + _doc.append("practitioner_schedules", {"schedule": _doc.physician_schedule}) _doc.save() diff --git a/erpnext/patches/v11_0/rename_healthcare_fields.py b/erpnext/patches/v11_0/rename_healthcare_fields.py index 88aac61333a..f2ac4d223ef 100644 --- a/erpnext/patches/v11_0/rename_healthcare_fields.py +++ b/erpnext/patches/v11_0/rename_healthcare_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field from frappe.modules import get_doctype_module, scrub @@ -18,36 +17,48 @@ lab_test_event = ["test_event", "lab_test_event"] lab_test_particulars = ["test_particulars", "lab_test_particulars"] field_rename_map = { - "Lab Test Template": [lab_test_name, lab_test_code, lab_test_rate, lab_test_description, - lab_test_group, lab_test_template_type, lab_test_uom, lab_test_normal_range], + "Lab Test Template": [ + lab_test_name, + lab_test_code, + lab_test_rate, + lab_test_description, + lab_test_group, + lab_test_template_type, + lab_test_uom, + lab_test_normal_range, + ], "Normal Test Items": [lab_test_name, lab_test_comment, lab_test_uom, lab_test_event], "Lab Test": [lab_test_name, lab_test_comment, lab_test_group], "Lab Prescription": [lab_test_name, lab_test_code, lab_test_comment, lab_test_created], "Lab Test Groups": [lab_test_template, lab_test_rate, lab_test_description], "Lab Test UOM": [lab_test_uom], "Normal Test Template": [lab_test_uom, lab_test_event], - "Special Test Items": [lab_test_particulars] + "Special Test Items": [lab_test_particulars], } def execute(): for dt, field_list in field_rename_map.items(): - if frappe.db.exists('DocType', dt): + if frappe.db.exists("DocType", dt): frappe.reload_doc(get_doctype_module(dt), "doctype", scrub(dt)) for field in field_list: if frappe.db.has_column(dt, field[0]): rename_field(dt, field[0], field[1]) - if frappe.db.exists('DocType', 'Lab Prescription'): - if frappe.db.has_column('Lab Prescription', 'parentfield'): - frappe.db.sql(""" + if frappe.db.exists("DocType", "Lab Prescription"): + if frappe.db.has_column("Lab Prescription", "parentfield"): + frappe.db.sql( + """ update `tabLab Prescription` set parentfield = 'lab_test_prescription' where parentfield = 'test_prescription' - """) + """ + ) - if frappe.db.exists('DocType', 'Lab Test Groups'): - if frappe.db.has_column('Lab Test Groups', 'parentfield'): - frappe.db.sql(""" + if frappe.db.exists("DocType", "Lab Test Groups"): + if frappe.db.has_column("Lab Test Groups", "parentfield"): + frappe.db.sql( + """ update `tabLab Test Groups` set parentfield = 'lab_test_groups' where parentfield = 'test_groups' - """) + """ + ) diff --git a/erpnext/patches/v11_0/rename_members_with_naming_series.py b/erpnext/patches/v11_0/rename_members_with_naming_series.py index 49dbc8a6dc8..4dffbc8fe81 100644 --- a/erpnext/patches/v11_0/rename_members_with_naming_series.py +++ b/erpnext/patches/v11_0/rename_members_with_naming_series.py @@ -1,11 +1,10 @@ - import frappe def execute(): frappe.reload_doc("non_profit", "doctype", "member") - old_named_members = frappe.get_all("Member", filters = {"name": ("not like", "MEM-%")}) - correctly_named_members = frappe.get_all("Member", filters = {"name": ("like", "MEM-%")}) + old_named_members = frappe.get_all("Member", filters={"name": ("not like", "MEM-%")}) + correctly_named_members = frappe.get_all("Member", filters={"name": ("like", "MEM-%")}) current_index = len(correctly_named_members) for member in old_named_members: diff --git a/erpnext/patches/v11_0/rename_overproduction_percent_field.py b/erpnext/patches/v11_0/rename_overproduction_percent_field.py index c78ec5d0128..74699db41ef 100644 --- a/erpnext/patches/v11_0/rename_overproduction_percent_field.py +++ b/erpnext/patches/v11_0/rename_overproduction_percent_field.py @@ -7,5 +7,9 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings') - rename_field('Manufacturing Settings', 'over_production_allowance_percentage', 'overproduction_percentage_for_sales_order') + frappe.reload_doc("manufacturing", "doctype", "manufacturing_settings") + rename_field( + "Manufacturing Settings", + "over_production_allowance_percentage", + "overproduction_percentage_for_sales_order", + ) diff --git a/erpnext/patches/v11_0/rename_production_order_to_work_order.py b/erpnext/patches/v11_0/rename_production_order_to_work_order.py index 453a5710a1d..b58ac4e72f1 100644 --- a/erpnext/patches/v11_0/rename_production_order_to_work_order.py +++ b/erpnext/patches/v11_0/rename_production_order_to_work_order.py @@ -7,22 +7,28 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.rename_doc('DocType', 'Production Order', 'Work Order', force=True) - frappe.reload_doc('manufacturing', 'doctype', 'work_order') + frappe.rename_doc("DocType", "Production Order", "Work Order", force=True) + frappe.reload_doc("manufacturing", "doctype", "work_order") - frappe.rename_doc('DocType', 'Production Order Item', 'Work Order Item', force=True) - frappe.reload_doc('manufacturing', 'doctype', 'work_order_item') + frappe.rename_doc("DocType", "Production Order Item", "Work Order Item", force=True) + frappe.reload_doc("manufacturing", "doctype", "work_order_item") - frappe.rename_doc('DocType', 'Production Order Operation', 'Work Order Operation', force=True) - frappe.reload_doc('manufacturing', 'doctype', 'work_order_operation') + frappe.rename_doc("DocType", "Production Order Operation", "Work Order Operation", force=True) + frappe.reload_doc("manufacturing", "doctype", "work_order_operation") - frappe.reload_doc('projects', 'doctype', 'timesheet') - frappe.reload_doc('stock', 'doctype', 'stock_entry') + frappe.reload_doc("projects", "doctype", "timesheet") + frappe.reload_doc("stock", "doctype", "stock_entry") rename_field("Timesheet", "production_order", "work_order") rename_field("Stock Entry", "production_order", "work_order") - frappe.rename_doc("Report", "Production Orders in Progress", "Work Orders in Progress", force=True) + frappe.rename_doc( + "Report", "Production Orders in Progress", "Work Orders in Progress", force=True + ) frappe.rename_doc("Report", "Completed Production Orders", "Completed Work Orders", force=True) frappe.rename_doc("Report", "Open Production Orders", "Open Work Orders", force=True) - frappe.rename_doc("Report", "Issued Items Against Production Order", "Issued Items Against Work Order", force=True) - frappe.rename_doc("Report", "Production Order Stock Report", "Work Order Stock Report", force=True) + frappe.rename_doc( + "Report", "Issued Items Against Production Order", "Issued Items Against Work Order", force=True + ) + frappe.rename_doc( + "Report", "Production Order Stock Report", "Work Order Stock Report", force=True + ) diff --git a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py index fd7e684c61a..96daba7d368 100644 --- a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py +++ b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ from frappe.model.utils.rename_field import rename_field @@ -7,10 +6,10 @@ from frappe.utils.nestedset import rebuild_tree def execute(): if frappe.db.table_exists("Supplier Group"): - frappe.reload_doc('setup', 'doctype', 'supplier_group') + frappe.reload_doc("setup", "doctype", "supplier_group") elif frappe.db.table_exists("Supplier Type"): frappe.rename_doc("DocType", "Supplier Type", "Supplier Group", force=True) - frappe.reload_doc('setup', 'doctype', 'supplier_group') + frappe.reload_doc("setup", "doctype", "supplier_group") frappe.reload_doc("accounts", "doctype", "pricing_rule") frappe.reload_doc("accounts", "doctype", "tax_rule") frappe.reload_doc("buying", "doctype", "buying_settings") @@ -23,16 +22,23 @@ def execute(): build_tree() -def build_tree(): - frappe.db.sql("""update `tabSupplier Group` set parent_supplier_group = '{0}' - where is_group = 0""".format(_('All Supplier Groups'))) - if not frappe.db.exists("Supplier Group", _('All Supplier Groups')): - frappe.get_doc({ - 'doctype': 'Supplier Group', - 'supplier_group_name': _('All Supplier Groups'), - 'is_group': 1, - 'parent_supplier_group': '' - }).insert(ignore_permissions=True) +def build_tree(): + frappe.db.sql( + """update `tabSupplier Group` set parent_supplier_group = '{0}' + where is_group = 0""".format( + _("All Supplier Groups") + ) + ) + + if not frappe.db.exists("Supplier Group", _("All Supplier Groups")): + frappe.get_doc( + { + "doctype": "Supplier Group", + "supplier_group_name": _("All Supplier Groups"), + "is_group": 1, + "parent_supplier_group": "", + } + ).insert(ignore_permissions=True) rebuild_tree("Supplier Group", "parent_supplier_group") diff --git a/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py b/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py index f23a81494be..4dc2521d391 100644 --- a/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py +++ b/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py @@ -7,8 +7,8 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('projects', 'doctype', 'project') + frappe.reload_doc("projects", "doctype", "project") - if frappe.db.has_column('Project', 'from'): - rename_field('Project', 'from', 'from_time') - rename_field('Project', 'to', 'to_time') + if frappe.db.has_column("Project", "from"): + rename_field("Project", "from", "from_time") + rename_field("Project", "to", "to_time") diff --git a/erpnext/patches/v11_0/reset_publish_in_hub_for_all_items.py b/erpnext/patches/v11_0/reset_publish_in_hub_for_all_items.py index 2f75c0826e5..1bdb53b7074 100644 --- a/erpnext/patches/v11_0/reset_publish_in_hub_for_all_items.py +++ b/erpnext/patches/v11_0/reset_publish_in_hub_for_all_items.py @@ -1,7 +1,6 @@ - import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "item") frappe.db.sql("""update `tabItem` set publish_in_hub = 0""") diff --git a/erpnext/patches/v11_0/set_default_email_template_in_hr.py b/erpnext/patches/v11_0/set_default_email_template_in_hr.py index a77dee93692..ee083ca4b80 100644 --- a/erpnext/patches/v11_0/set_default_email_template_in_hr.py +++ b/erpnext/patches/v11_0/set_default_email_template_in_hr.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ diff --git a/erpnext/patches/v11_0/set_department_for_doctypes.py b/erpnext/patches/v11_0/set_department_for_doctypes.py index a3ece7fc768..1e14b9ceb0f 100644 --- a/erpnext/patches/v11_0/set_department_for_doctypes.py +++ b/erpnext/patches/v11_0/set_department_for_doctypes.py @@ -1,24 +1,37 @@ - import frappe # Set department value based on employee value + def execute(): doctypes_to_update = { - 'hr': ['Appraisal', 'Leave Allocation', 'Expense Claim', 'Salary Slip', - 'Attendance', 'Training Feedback', 'Training Result Employee','Leave Application', - 'Employee Advance', 'Training Event Employee', 'Payroll Employee Detail'], - 'education': ['Instructor'], - 'projects': ['Activity Cost', 'Timesheet'], - 'setup': ['Sales Person'] + "hr": [ + "Appraisal", + "Leave Allocation", + "Expense Claim", + "Salary Slip", + "Attendance", + "Training Feedback", + "Training Result Employee", + "Leave Application", + "Employee Advance", + "Training Event Employee", + "Payroll Employee Detail", + ], + "education": ["Instructor"], + "projects": ["Activity Cost", "Timesheet"], + "setup": ["Sales Person"], } for module, doctypes in doctypes_to_update.items(): for doctype in doctypes: if frappe.db.table_exists(doctype): - frappe.reload_doc(module, 'doctype', frappe.scrub(doctype)) - frappe.db.sql(""" + frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) + frappe.db.sql( + """ update `tab%s` dt set department=(select department from `tabEmployee` where name=dt.employee) - """ % doctype) + """ + % doctype + ) diff --git a/erpnext/patches/v11_0/set_missing_gst_hsn_code.py b/erpnext/patches/v11_0/set_missing_gst_hsn_code.py index 262ca2d61f9..d9356e758d7 100644 --- a/erpnext/patches/v11_0/set_missing_gst_hsn_code.py +++ b/erpnext/patches/v11_0/set_missing_gst_hsn_code.py @@ -1,4 +1,3 @@ - import frappe from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_html @@ -9,15 +8,24 @@ def execute(): if not company: return - doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", - "Supplier Quotation", "Purchase Order", "Purchase Receipt", "Purchase Invoice"] + doctypes = [ + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ] for dt in doctypes: date_field = "posting_date" if dt in ["Quotation", "Sales Order", "Supplier Quotation", "Purchase Order"]: date_field = "transaction_date" - transactions = frappe.db.sql(""" + transactions = frappe.db.sql( + """ select dt.name, dt_item.name as child_name from `tab{dt}` dt, `tab{dt} Item` dt_item where dt.name = dt_item.parent @@ -26,18 +34,28 @@ def execute(): and ifnull(dt_item.gst_hsn_code, '') = '' and ifnull(dt_item.item_code, '') != '' and dt.company in ({company}) - """.format(dt=dt, date_field=date_field, company=", ".join(['%s']*len(company))), tuple(company), as_dict=1) + """.format( + dt=dt, date_field=date_field, company=", ".join(["%s"] * len(company)) + ), + tuple(company), + as_dict=1, + ) if not transactions: continue transaction_rows_name = [d.child_name for d in transactions] - frappe.db.sql(""" + frappe.db.sql( + """ update `tab{dt} Item` dt_item set dt_item.gst_hsn_code = (select gst_hsn_code from tabItem where name=dt_item.item_code) where dt_item.name in ({rows_name}) - """.format(dt=dt, rows_name=", ".join(['%s']*len(transaction_rows_name))), tuple(transaction_rows_name)) + """.format( + dt=dt, rows_name=", ".join(["%s"] * len(transaction_rows_name)) + ), + tuple(transaction_rows_name), + ) parent = set([d.name for d in transactions]) for t in list(parent): diff --git a/erpnext/patches/v11_0/set_salary_component_properties.py b/erpnext/patches/v11_0/set_salary_component_properties.py index 5ff9e4ab6fd..3ec9f8ab29a 100644 --- a/erpnext/patches/v11_0/set_salary_component_properties.py +++ b/erpnext/patches/v11_0/set_salary_component_properties.py @@ -1,17 +1,22 @@ - import frappe def execute(): - frappe.reload_doc('Payroll', 'doctype', 'salary_detail') - frappe.reload_doc('Payroll', 'doctype', 'salary_component') + frappe.reload_doc("Payroll", "doctype", "salary_detail") + frappe.reload_doc("Payroll", "doctype", "salary_component") frappe.db.sql("update `tabSalary Component` set is_tax_applicable=1 where type='Earning'") - frappe.db.sql("""update `tabSalary Component` set variable_based_on_taxable_salary=1 - where type='Deduction' and name in ('TDS', 'Tax Deducted at Source')""") + frappe.db.sql( + """update `tabSalary Component` set variable_based_on_taxable_salary=1 + where type='Deduction' and name in ('TDS', 'Tax Deducted at Source')""" + ) - frappe.db.sql("""update `tabSalary Detail` set is_tax_applicable=1 - where parentfield='earnings' and statistical_component=0""") - frappe.db.sql("""update `tabSalary Detail` set variable_based_on_taxable_salary=1 - where parentfield='deductions' and salary_component in ('TDS', 'Tax Deducted at Source')""") + frappe.db.sql( + """update `tabSalary Detail` set is_tax_applicable=1 + where parentfield='earnings' and statistical_component=0""" + ) + frappe.db.sql( + """update `tabSalary Detail` set variable_based_on_taxable_salary=1 + where parentfield='deductions' and salary_component in ('TDS', 'Tax Deducted at Source')""" + ) diff --git a/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py b/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py index f23018d0b4d..548a7cb158c 100644 --- a/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py +++ b/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py @@ -1,20 +1,21 @@ - import frappe from frappe.model.workflow import get_workflow_name def execute(): - for doctype in ['Expense Claim', 'Leave Application']: + for doctype in ["Expense Claim", "Leave Application"]: active_workflow = get_workflow_name(doctype) - if not active_workflow: continue + if not active_workflow: + continue - workflow_states = frappe.get_all('Workflow Document State', - filters=[['parent', '=', active_workflow]], - fields=['*']) + workflow_states = frappe.get_all( + "Workflow Document State", filters=[["parent", "=", active_workflow]], fields=["*"] + ) for state in workflow_states: - if state.update_field: continue - status_field = 'approval_status' if doctype=="Expense Claim" else 'status' - frappe.set_value('Workflow Document State', state.name, 'update_field', status_field) - frappe.set_value('Workflow Document State', state.name, 'update_value', state.state) + if state.update_field: + continue + status_field = "approval_status" if doctype == "Expense Claim" else "status" + frappe.set_value("Workflow Document State", state.name, "update_field", status_field) + frappe.set_value("Workflow Document State", state.name, "update_value", state.state) diff --git a/erpnext/patches/v11_0/set_user_permissions_for_department.py b/erpnext/patches/v11_0/set_user_permissions_for_department.py index cb38beb51c1..9b5cb243729 100644 --- a/erpnext/patches/v11_0/set_user_permissions_for_department.py +++ b/erpnext/patches/v11_0/set_user_permissions_for_department.py @@ -1,20 +1,25 @@ - import frappe def execute(): - user_permissions = frappe.db.sql("""select name, for_value from `tabUser Permission` - where allow='Department'""", as_dict=1) - for d in user_permissions: - user_permission = frappe.get_doc("User Permission", d.name) - for new_dept in frappe.db.sql("""select name from tabDepartment - where ifnull(company, '') != '' and department_name=%s""", d.for_value): - try: - new_user_permission = frappe.copy_doc(user_permission) - new_user_permission.for_value = new_dept[0] - new_user_permission.save() - except frappe.DuplicateEntryError: - pass + user_permissions = frappe.db.sql( + """select name, for_value from `tabUser Permission` + where allow='Department'""", + as_dict=1, + ) + for d in user_permissions: + user_permission = frappe.get_doc("User Permission", d.name) + for new_dept in frappe.db.sql( + """select name from tabDepartment + where ifnull(company, '') != '' and department_name=%s""", + d.for_value, + ): + try: + new_user_permission = frappe.copy_doc(user_permission) + new_user_permission.for_value = new_dept[0] + new_user_permission.save() + except frappe.DuplicateEntryError: + pass - frappe.reload_doc("hr", "doctype", "department") - frappe.db.sql("update tabDepartment set disabled=1 where ifnull(company, '') = ''") + frappe.reload_doc("hr", "doctype", "department") + frappe.db.sql("update tabDepartment set disabled=1 where ifnull(company, '') = ''") diff --git a/erpnext/patches/v11_0/skip_user_permission_check_for_department.py b/erpnext/patches/v11_0/skip_user_permission_check_for_department.py index 8e2aa47785d..1327da981e1 100644 --- a/erpnext/patches/v11_0/skip_user_permission_check_for_department.py +++ b/erpnext/patches/v11_0/skip_user_permission_check_for_department.py @@ -1,24 +1,40 @@ - import frappe from frappe.desk.form.linked_with import get_linked_doctypes # Skips user permission check for doctypes where department link field was recently added # https://github.com/frappe/erpnext/pull/14121 + def execute(): doctypes_to_skip = [] - for doctype in ['Appraisal', 'Leave Allocation', 'Expense Claim', 'Instructor', 'Salary Slip', - 'Attendance', 'Training Feedback', 'Training Result Employee', - 'Leave Application', 'Employee Advance', 'Activity Cost', 'Training Event Employee', - 'Timesheet', 'Sales Person', 'Payroll Employee Detail']: - if frappe.db.exists('Custom Field', { 'dt': doctype, 'fieldname': 'department'}): continue + for doctype in [ + "Appraisal", + "Leave Allocation", + "Expense Claim", + "Instructor", + "Salary Slip", + "Attendance", + "Training Feedback", + "Training Result Employee", + "Leave Application", + "Employee Advance", + "Activity Cost", + "Training Event Employee", + "Timesheet", + "Sales Person", + "Payroll Employee Detail", + ]: + if frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": "department"}): + continue doctypes_to_skip.append(doctype) - frappe.reload_doctype('User Permission') + frappe.reload_doctype("User Permission") - user_permissions = frappe.get_all("User Permission", - filters=[['allow', '=', 'Department'], ['applicable_for', 'in', [None] + doctypes_to_skip]], - fields=['name', 'applicable_for']) + user_permissions = frappe.get_all( + "User Permission", + filters=[["allow", "=", "Department"], ["applicable_for", "in", [None] + doctypes_to_skip]], + fields=["name", "applicable_for"], + ) user_permissions_to_delete = [] new_user_permissions_list = [] @@ -38,24 +54,32 @@ def execute(): for doctype in applicable_for_doctypes: if doctype: # Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes) - new_user_permissions_list.append(( - frappe.generate_hash("", 10), - user_permission.user, - user_permission.allow, - user_permission.for_value, - doctype, - 0 - )) + new_user_permissions_list.append( + ( + frappe.generate_hash("", 10), + user_permission.user, + user_permission.allow, + user_permission.for_value, + doctype, + 0, + ) + ) if new_user_permissions_list: - frappe.db.sql(''' + frappe.db.sql( + """ INSERT INTO `tabUser Permission` (`name`, `user`, `allow`, `for_value`, `applicable_for`, `apply_to_all_doctypes`) - VALUES {}'''.format(', '.join(['%s'] * len(new_user_permissions_list))), # nosec - tuple(new_user_permissions_list) + VALUES {}""".format( + ", ".join(["%s"] * len(new_user_permissions_list)) + ), # nosec + tuple(new_user_permissions_list), ) if user_permissions_to_delete: - frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `name` IN ({})'.format( # nosec - ','.join(['%s'] * len(user_permissions_to_delete)) - ), tuple(user_permissions_to_delete)) + frappe.db.sql( + "DELETE FROM `tabUser Permission` WHERE `name` IN ({})".format( # nosec + ",".join(["%s"] * len(user_permissions_to_delete)) + ), + tuple(user_permissions_to_delete), + ) diff --git a/erpnext/patches/v11_0/uom_conversion_data.py b/erpnext/patches/v11_0/uom_conversion_data.py index 854f5223470..5dee0840eb8 100644 --- a/erpnext/patches/v11_0/uom_conversion_data.py +++ b/erpnext/patches/v11_0/uom_conversion_data.py @@ -1,4 +1,3 @@ - import frappe @@ -15,8 +14,8 @@ def execute(): # delete conversion data and insert again frappe.db.sql("delete from `tabUOM Conversion Factor`") try: - frappe.delete_doc('UOM', 'Hundredweight') - frappe.delete_doc('UOM', 'Pound Cubic Yard') + frappe.delete_doc("UOM", "Hundredweight") + frappe.delete_doc("UOM", "Pound Cubic Yard") except frappe.LinkExistsError: pass diff --git a/erpnext/patches/v11_0/update_account_type_in_party_type.py b/erpnext/patches/v11_0/update_account_type_in_party_type.py index c66cef042d9..e55f9f20cc8 100644 --- a/erpnext/patches/v11_0/update_account_type_in_party_type.py +++ b/erpnext/patches/v11_0/update_account_type_in_party_type.py @@ -6,9 +6,15 @@ import frappe def execute(): - frappe.reload_doc('setup', 'doctype', 'party_type') - party_types = {'Customer': 'Receivable', 'Supplier': 'Payable', - 'Employee': 'Payable', 'Member': 'Receivable', 'Shareholder': 'Payable', 'Student': 'Receivable'} + frappe.reload_doc("setup", "doctype", "party_type") + party_types = { + "Customer": "Receivable", + "Supplier": "Payable", + "Employee": "Payable", + "Member": "Receivable", + "Shareholder": "Payable", + "Student": "Receivable", + } for party_type, account_type in party_types.items(): - frappe.db.set_value('Party Type', party_type, 'account_type', account_type) + frappe.db.set_value("Party Type", party_type, "account_type", account_type) diff --git a/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py b/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py index 3e36a4bb90d..a7351d27595 100644 --- a/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py +++ b/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py @@ -6,16 +6,22 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'item') - frappe.db.sql(""" update `tabItem` set include_item_in_manufacturing = 1 - where ifnull(is_stock_item, 0) = 1""") + frappe.reload_doc("stock", "doctype", "item") + frappe.db.sql( + """ update `tabItem` set include_item_in_manufacturing = 1 + where ifnull(is_stock_item, 0) = 1""" + ) - for doctype in ['BOM Item', 'Work Order Item', 'BOM Explosion Item']: - frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype)) + for doctype in ["BOM Item", "Work Order Item", "BOM Explosion Item"]: + frappe.reload_doc("manufacturing", "doctype", frappe.scrub(doctype)) - frappe.db.sql(""" update `tab{0}` child, tabItem item + frappe.db.sql( + """ update `tab{0}` child, tabItem item set child.include_item_in_manufacturing = 1 where child.item_code = item.name and ifnull(item.is_stock_item, 0) = 1 - """.format(doctype)) + """.format( + doctype + ) + ) diff --git a/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py b/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py index f3a2ac6a655..51ba706dcf0 100644 --- a/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py +++ b/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py @@ -6,15 +6,19 @@ import frappe def execute(): - frappe.reload_doc('buying', 'doctype', 'buying_settings') - frappe.db.set_value('Buying Settings', None, 'backflush_raw_materials_of_subcontract_based_on', 'BOM') + frappe.reload_doc("buying", "doctype", "buying_settings") + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) - frappe.reload_doc('stock', 'doctype', 'stock_entry_detail') - frappe.db.sql(""" update `tabStock Entry Detail` as sed, + frappe.reload_doc("stock", "doctype", "stock_entry_detail") + frappe.db.sql( + """ update `tabStock Entry Detail` as sed, `tabStock Entry` as se, `tabPurchase Order Item Supplied` as pois set sed.subcontracted_item = pois.main_item_code where se.purpose = 'Send to Subcontractor' and sed.parent = se.name and pois.rm_item_code = sed.item_code and se.docstatus = 1 - and pois.parenttype = 'Purchase Order'""") + and pois.parenttype = 'Purchase Order'""" + ) diff --git a/erpnext/patches/v11_0/update_brand_in_item_price.py b/erpnext/patches/v11_0/update_brand_in_item_price.py index ce1df78083f..f4859ae1c77 100644 --- a/erpnext/patches/v11_0/update_brand_in_item_price.py +++ b/erpnext/patches/v11_0/update_brand_in_item_price.py @@ -6,11 +6,13 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'item_price') + frappe.reload_doc("stock", "doctype", "item_price") - frappe.db.sql(""" update `tabItem Price`, `tabItem` + frappe.db.sql( + """ update `tabItem Price`, `tabItem` set `tabItem Price`.brand = `tabItem`.brand where `tabItem Price`.item_code = `tabItem`.name - and `tabItem`.brand is not null and `tabItem`.brand != ''""") + and `tabItem`.brand is not null and `tabItem`.brand != ''""" + ) diff --git a/erpnext/patches/v11_0/update_delivery_trip_status.py b/erpnext/patches/v11_0/update_delivery_trip_status.py index 35b95353b12..1badfab5025 100755 --- a/erpnext/patches/v11_0/update_delivery_trip_status.py +++ b/erpnext/patches/v11_0/update_delivery_trip_status.py @@ -6,18 +6,14 @@ import frappe def execute(): - frappe.reload_doc('setup', 'doctype', 'global_defaults', force=True) - frappe.reload_doc('stock', 'doctype', 'delivery_trip') - frappe.reload_doc('stock', 'doctype', 'delivery_stop', force=True) + frappe.reload_doc("setup", "doctype", "global_defaults", force=True) + frappe.reload_doc("stock", "doctype", "delivery_trip") + frappe.reload_doc("stock", "doctype", "delivery_stop", force=True) for trip in frappe.get_all("Delivery Trip"): trip_doc = frappe.get_doc("Delivery Trip", trip.name) - status = { - 0: "Draft", - 1: "Scheduled", - 2: "Cancelled" - }[trip_doc.docstatus] + status = {0: "Draft", 1: "Scheduled", 2: "Cancelled"}[trip_doc.docstatus] if trip_doc.docstatus == 1: visited_stops = [stop.visited for stop in trip_doc.delivery_stops] diff --git a/erpnext/patches/v11_0/update_department_lft_rgt.py b/erpnext/patches/v11_0/update_department_lft_rgt.py index 4cb2dccbb27..bca5e9e8825 100644 --- a/erpnext/patches/v11_0/update_department_lft_rgt.py +++ b/erpnext/patches/v11_0/update_department_lft_rgt.py @@ -1,20 +1,21 @@ - import frappe from frappe import _ from frappe.utils.nestedset import rebuild_tree def execute(): - """ assign lft and rgt appropriately """ + """assign lft and rgt appropriately""" frappe.reload_doc("hr", "doctype", "department") - if not frappe.db.exists("Department", _('All Departments')): - frappe.get_doc({ - 'doctype': 'Department', - 'department_name': _('All Departments'), - 'is_group': 1 - }).insert(ignore_permissions=True, ignore_mandatory=True) + if not frappe.db.exists("Department", _("All Departments")): + frappe.get_doc( + {"doctype": "Department", "department_name": _("All Departments"), "is_group": 1} + ).insert(ignore_permissions=True, ignore_mandatory=True) - frappe.db.sql("""update `tabDepartment` set parent_department = '{0}' - where is_group = 0""".format(_('All Departments'))) + frappe.db.sql( + """update `tabDepartment` set parent_department = '{0}' + where is_group = 0""".format( + _("All Departments") + ) + ) rebuild_tree("Department", "parent_department") diff --git a/erpnext/patches/v11_0/update_hub_url.py b/erpnext/patches/v11_0/update_hub_url.py index 9150581c580..8eec3f3f318 100644 --- a/erpnext/patches/v11_0/update_hub_url.py +++ b/erpnext/patches/v11_0/update_hub_url.py @@ -1,7 +1,8 @@ - import frappe def execute(): - frappe.reload_doc('hub_node', 'doctype', 'Marketplace Settings') - frappe.db.set_value('Marketplace Settings', 'Marketplace Settings', 'marketplace_url', 'https://hubmarket.org') + frappe.reload_doc("hub_node", "doctype", "Marketplace Settings") + frappe.db.set_value( + "Marketplace Settings", "Marketplace Settings", "marketplace_url", "https://hubmarket.org" + ) diff --git a/erpnext/patches/v11_0/update_sales_partner_type.py b/erpnext/patches/v11_0/update_sales_partner_type.py index c7937e532be..2d37fd69b19 100644 --- a/erpnext/patches/v11_0/update_sales_partner_type.py +++ b/erpnext/patches/v11_0/update_sales_partner_type.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ @@ -6,25 +5,28 @@ from frappe import _ def execute(): from erpnext.setup.setup_wizard.operations.install_fixtures import default_sales_partner_type - frappe.reload_doc('selling', 'doctype', 'sales_partner_type') + frappe.reload_doc("selling", "doctype", "sales_partner_type") - frappe.local.lang = frappe.db.get_default("lang") or 'en' + frappe.local.lang = frappe.db.get_default("lang") or "en" for s in default_sales_partner_type: insert_sales_partner_type(_(s)) # get partner type in existing forms (customized) # and create a document if not created - for d in ['Sales Partner']: - partner_type = frappe.db.sql_list('select distinct partner_type from `tab{0}`'.format(d)) + for d in ["Sales Partner"]: + partner_type = frappe.db.sql_list("select distinct partner_type from `tab{0}`".format(d)) for s in partner_type: if s and s not in default_sales_partner_type: insert_sales_partner_type(s) # remove customization for partner type - for p in frappe.get_all('Property Setter', {'doc_type':d, 'field_name':'partner_type', 'property':'options'}): - frappe.delete_doc('Property Setter', p.name) + for p in frappe.get_all( + "Property Setter", {"doc_type": d, "field_name": "partner_type", "property": "options"} + ): + frappe.delete_doc("Property Setter", p.name) + def insert_sales_partner_type(s): - if not frappe.db.exists('Sales Partner Type', s): - frappe.get_doc(dict(doctype='Sales Partner Type', sales_partner_type=s)).insert() + if not frappe.db.exists("Sales Partner Type", s): + frappe.get_doc(dict(doctype="Sales Partner Type", sales_partner_type=s)).insert() diff --git a/erpnext/patches/v11_0/update_total_qty_field.py b/erpnext/patches/v11_0/update_total_qty_field.py index 09bb02f9593..09fcdb8723d 100644 --- a/erpnext/patches/v11_0/update_total_qty_field.py +++ b/erpnext/patches/v11_0/update_total_qty_field.py @@ -1,35 +1,47 @@ - import frappe def execute(): - frappe.reload_doc('buying', 'doctype', 'purchase_order') - frappe.reload_doc('buying', 'doctype', 'supplier_quotation') - frappe.reload_doc('selling', 'doctype', 'sales_order') - frappe.reload_doc('selling', 'doctype', 'quotation') - frappe.reload_doc('stock', 'doctype', 'delivery_note') - frappe.reload_doc('stock', 'doctype', 'purchase_receipt') - frappe.reload_doc('accounts', 'doctype', 'sales_invoice') - frappe.reload_doc('accounts', 'doctype', 'purchase_invoice') + frappe.reload_doc("buying", "doctype", "purchase_order") + frappe.reload_doc("buying", "doctype", "supplier_quotation") + frappe.reload_doc("selling", "doctype", "sales_order") + frappe.reload_doc("selling", "doctype", "quotation") + frappe.reload_doc("stock", "doctype", "delivery_note") + frappe.reload_doc("stock", "doctype", "purchase_receipt") + frappe.reload_doc("accounts", "doctype", "sales_invoice") + frappe.reload_doc("accounts", "doctype", "purchase_invoice") - doctypes = ["Sales Order", "Sales Invoice", "Delivery Note",\ - "Purchase Order", "Purchase Invoice", "Purchase Receipt", "Quotation", "Supplier Quotation"] + doctypes = [ + "Sales Order", + "Sales Invoice", + "Delivery Note", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + "Quotation", + "Supplier Quotation", + ] for doctype in doctypes: - total_qty = frappe.db.sql(''' + total_qty = frappe.db.sql( + """ SELECT parent, SUM(qty) as qty FROM `tab{0} Item` where parenttype = '{0}' GROUP BY parent - '''.format(doctype), as_dict = True) + """.format( + doctype + ), + as_dict=True, + ) # Query to update total_qty might become too big, Update in batches # batch_size is chosen arbitrarily, Don't try too hard to reason about it batch_size = 100000 for i in range(0, len(total_qty), batch_size): - batch_transactions = total_qty[i:i + batch_size] + batch_transactions = total_qty[i : i + batch_size] # UPDATE with CASE for some reason cannot use PRIMARY INDEX, # causing all rows to be examined, leading to a very slow update @@ -43,7 +55,11 @@ def execute(): for d in batch_transactions: values.append("({0}, {1})".format(frappe.db.escape(d.parent), d.qty)) conditions = ",".join(values) - frappe.db.sql(""" + frappe.db.sql( + """ INSERT INTO `tab{}` (name, total_qty) VALUES {} ON DUPLICATE KEY UPDATE name = VALUES(name), total_qty = VALUES(total_qty) - """.format(doctype, conditions)) + """.format( + doctype, conditions + ) + ) diff --git a/erpnext/patches/v11_1/delete_bom_browser.py b/erpnext/patches/v11_1/delete_bom_browser.py index 9b5c169717a..09ee1695b9f 100644 --- a/erpnext/patches/v11_1/delete_bom_browser.py +++ b/erpnext/patches/v11_1/delete_bom_browser.py @@ -6,4 +6,4 @@ import frappe def execute(): - frappe.delete_doc_if_exists('Page', 'bom-browser') + frappe.delete_doc_if_exists("Page", "bom-browser") diff --git a/erpnext/patches/v11_1/make_job_card_time_logs.py b/erpnext/patches/v11_1/make_job_card_time_logs.py index 100cd64f8fe..beb2c4e5341 100644 --- a/erpnext/patches/v11_1/make_job_card_time_logs.py +++ b/erpnext/patches/v11_1/make_job_card_time_logs.py @@ -6,25 +6,45 @@ import frappe def execute(): - frappe.reload_doc('manufacturing', 'doctype', 'job_card_time_log') + frappe.reload_doc("manufacturing", "doctype", "job_card_time_log") - if (frappe.db.table_exists("Job Card") - and frappe.get_meta("Job Card").has_field("actual_start_date")): - time_logs = [] - for d in frappe.get_all('Job Card', - fields = ["actual_start_date", "actual_end_date", "time_in_mins", "name", "for_quantity"], - filters = {'docstatus': ("<", 2)}): - if d.actual_start_date: - time_logs.append([d.actual_start_date, d.actual_end_date, d.time_in_mins, - d.for_quantity, d.name, 'Job Card', 'time_logs', frappe.generate_hash("", 10)]) + if frappe.db.table_exists("Job Card") and frappe.get_meta("Job Card").has_field( + "actual_start_date" + ): + time_logs = [] + for d in frappe.get_all( + "Job Card", + fields=["actual_start_date", "actual_end_date", "time_in_mins", "name", "for_quantity"], + filters={"docstatus": ("<", 2)}, + ): + if d.actual_start_date: + time_logs.append( + [ + d.actual_start_date, + d.actual_end_date, + d.time_in_mins, + d.for_quantity, + d.name, + "Job Card", + "time_logs", + frappe.generate_hash("", 10), + ] + ) - if time_logs: - frappe.db.sql(""" INSERT INTO + if time_logs: + frappe.db.sql( + """ INSERT INTO `tabJob Card Time Log` (from_time, to_time, time_in_mins, completed_qty, parent, parenttype, parentfield, name) values {values} - """.format(values = ','.join(['%s'] * len(time_logs))), tuple(time_logs)) + """.format( + values=",".join(["%s"] * len(time_logs)) + ), + tuple(time_logs), + ) - frappe.reload_doc('manufacturing', 'doctype', 'job_card') - frappe.db.sql(""" update `tabJob Card` set total_completed_qty = for_quantity, - total_time_in_mins = time_in_mins where docstatus < 2 """) + frappe.reload_doc("manufacturing", "doctype", "job_card") + frappe.db.sql( + """ update `tabJob Card` set total_completed_qty = for_quantity, + total_time_in_mins = time_in_mins where docstatus < 2 """ + ) diff --git a/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py b/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py index d292d7ae432..b681f25d84e 100644 --- a/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py +++ b/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py @@ -8,8 +8,14 @@ import frappe def execute(): frappe.reload_doctype("Quotation") frappe.db.sql(""" UPDATE `tabQuotation` set party_name = lead WHERE quotation_to = 'Lead' """) - frappe.db.sql(""" UPDATE `tabQuotation` set party_name = customer WHERE quotation_to = 'Customer' """) + frappe.db.sql( + """ UPDATE `tabQuotation` set party_name = customer WHERE quotation_to = 'Customer' """ + ) frappe.reload_doctype("Opportunity") - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = lead WHERE opportunity_from = 'Lead' """) - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = customer WHERE opportunity_from = 'Customer' """) + frappe.db.sql( + """ UPDATE `tabOpportunity` set party_name = lead WHERE opportunity_from = 'Lead' """ + ) + frappe.db.sql( + """ UPDATE `tabOpportunity` set party_name = customer WHERE opportunity_from = 'Customer' """ + ) diff --git a/erpnext/patches/v11_1/renamed_delayed_item_report.py b/erpnext/patches/v11_1/renamed_delayed_item_report.py index c160b79d0e9..86247815e29 100644 --- a/erpnext/patches/v11_1/renamed_delayed_item_report.py +++ b/erpnext/patches/v11_1/renamed_delayed_item_report.py @@ -6,6 +6,6 @@ import frappe def execute(): - for report in ["Delayed Order Item Summary", "Delayed Order Summary"]: - if frappe.db.exists("Report", report): - frappe.delete_doc("Report", report) + for report in ["Delayed Order Item Summary", "Delayed Order Summary"]: + if frappe.db.exists("Report", report): + frappe.delete_doc("Report", report) diff --git a/erpnext/patches/v11_1/set_default_action_for_quality_inspection.py b/erpnext/patches/v11_1/set_default_action_for_quality_inspection.py index 672b7628bb4..39aa6dd8e8e 100644 --- a/erpnext/patches/v11_1/set_default_action_for_quality_inspection.py +++ b/erpnext/patches/v11_1/set_default_action_for_quality_inspection.py @@ -6,11 +6,13 @@ import frappe def execute(): - stock_settings = frappe.get_doc('Stock Settings') - if stock_settings.default_warehouse and not frappe.db.exists("Warehouse", stock_settings.default_warehouse): - stock_settings.default_warehouse = None - if stock_settings.stock_uom and not frappe.db.exists("UOM", stock_settings.stock_uom): - stock_settings.stock_uom = None - stock_settings.flags.ignore_mandatory = True - stock_settings.action_if_quality_inspection_is_not_submitted = "Stop" - stock_settings.save() + stock_settings = frappe.get_doc("Stock Settings") + if stock_settings.default_warehouse and not frappe.db.exists( + "Warehouse", stock_settings.default_warehouse + ): + stock_settings.default_warehouse = None + if stock_settings.stock_uom and not frappe.db.exists("UOM", stock_settings.stock_uom): + stock_settings.stock_uom = None + stock_settings.flags.ignore_mandatory = True + stock_settings.action_if_quality_inspection_is_not_submitted = "Stop" + stock_settings.save() diff --git a/erpnext/patches/v11_1/set_missing_opportunity_from.py b/erpnext/patches/v11_1/set_missing_opportunity_from.py index 7a041919a15..ae5f6200145 100644 --- a/erpnext/patches/v11_1/set_missing_opportunity_from.py +++ b/erpnext/patches/v11_1/set_missing_opportunity_from.py @@ -1,4 +1,3 @@ - import frappe @@ -6,13 +5,23 @@ def execute(): frappe.reload_doctype("Opportunity") if frappe.db.has_column("Opportunity", "enquiry_from"): - frappe.db.sql(""" UPDATE `tabOpportunity` set opportunity_from = enquiry_from - where ifnull(opportunity_from, '') = '' and ifnull(enquiry_from, '') != ''""") + frappe.db.sql( + """ UPDATE `tabOpportunity` set opportunity_from = enquiry_from + where ifnull(opportunity_from, '') = '' and ifnull(enquiry_from, '') != ''""" + ) - if frappe.db.has_column("Opportunity", "lead") and frappe.db.has_column("Opportunity", "enquiry_from"): - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = lead - where enquiry_from = 'Lead' and ifnull(party_name, '') = '' and ifnull(lead, '') != ''""") + if frappe.db.has_column("Opportunity", "lead") and frappe.db.has_column( + "Opportunity", "enquiry_from" + ): + frappe.db.sql( + """ UPDATE `tabOpportunity` set party_name = lead + where enquiry_from = 'Lead' and ifnull(party_name, '') = '' and ifnull(lead, '') != ''""" + ) - if frappe.db.has_column("Opportunity", "customer") and frappe.db.has_column("Opportunity", "enquiry_from"): - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = customer - where enquiry_from = 'Customer' and ifnull(party_name, '') = '' and ifnull(customer, '') != ''""") + if frappe.db.has_column("Opportunity", "customer") and frappe.db.has_column( + "Opportunity", "enquiry_from" + ): + frappe.db.sql( + """ UPDATE `tabOpportunity` set party_name = customer + where enquiry_from = 'Customer' and ifnull(party_name, '') = '' and ifnull(customer, '') != ''""" + ) diff --git a/erpnext/patches/v11_1/set_missing_title_for_quotation.py b/erpnext/patches/v11_1/set_missing_title_for_quotation.py index 93d9f0e7d89..6e7e2c9d8bf 100644 --- a/erpnext/patches/v11_1/set_missing_title_for_quotation.py +++ b/erpnext/patches/v11_1/set_missing_title_for_quotation.py @@ -4,7 +4,8 @@ import frappe def execute(): frappe.reload_doctype("Quotation") # update customer_name from Customer document if quotation_to is set to Customer - frappe.db.sql(''' + frappe.db.sql( + """ update tabQuotation, tabCustomer set tabQuotation.customer_name = tabCustomer.customer_name, @@ -13,11 +14,13 @@ def execute(): tabQuotation.customer_name is null and tabQuotation.party_name = tabCustomer.name and tabQuotation.quotation_to = 'Customer' - ''') + """ + ) # update customer_name from Lead document if quotation_to is set to Lead - frappe.db.sql(''' + frappe.db.sql( + """ update tabQuotation, tabLead set tabQuotation.customer_name = case when ifnull(tabLead.company_name, '') != '' then tabLead.company_name else tabLead.lead_name end, @@ -26,4 +29,5 @@ def execute(): tabQuotation.customer_name is null and tabQuotation.party_name = tabLead.name and tabQuotation.quotation_to = 'Lead' - ''') + """ + ) diff --git a/erpnext/patches/v11_1/set_salary_details_submittable.py b/erpnext/patches/v11_1/set_salary_details_submittable.py index 8ad8ff8c2ba..e5ecce6486a 100644 --- a/erpnext/patches/v11_1/set_salary_details_submittable.py +++ b/erpnext/patches/v11_1/set_salary_details_submittable.py @@ -1,10 +1,11 @@ - import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ update `tabSalary Structure` ss, `tabSalary Detail` sd set sd.docstatus=1 where ss.name=sd.parent and ss.docstatus=1 and sd.parenttype='Salary Structure' - """) + """ + ) diff --git a/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py b/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py index 2da1ecbda26..6e2edfea878 100644 --- a/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py +++ b/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py @@ -1,10 +1,11 @@ - import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ update `tabMaterial Request` set status='Manufactured' where docstatus=1 and material_request_type='Manufacture' and per_ordered=100 and status != 'Stopped' - """) + """ + ) diff --git a/erpnext/patches/v11_1/set_variant_based_on.py b/erpnext/patches/v11_1/set_variant_based_on.py index 2e06e63a8aa..1d57527b1f8 100644 --- a/erpnext/patches/v11_1/set_variant_based_on.py +++ b/erpnext/patches/v11_1/set_variant_based_on.py @@ -6,7 +6,9 @@ import frappe def execute(): - frappe.db.sql("""update tabItem set variant_based_on = 'Item Attribute' + frappe.db.sql( + """update tabItem set variant_based_on = 'Item Attribute' where ifnull(variant_based_on, '') = '' and (has_variants=1 or ifnull(variant_of, '') != '') - """) + """ + ) diff --git a/erpnext/patches/v11_1/setup_guardian_role.py b/erpnext/patches/v11_1/setup_guardian_role.py index 385bc209fa2..2b25e132540 100644 --- a/erpnext/patches/v11_1/setup_guardian_role.py +++ b/erpnext/patches/v11_1/setup_guardian_role.py @@ -1,13 +1,9 @@ - import frappe def execute(): - if 'Education' in frappe.get_active_domains() and not frappe.db.exists("Role", "Guardian"): + if "Education" in frappe.get_active_domains() and not frappe.db.exists("Role", "Guardian"): doc = frappe.new_doc("Role") - doc.update({ - "role_name": "Guardian", - "desk_access": 0 - }) + doc.update({"role_name": "Guardian", "desk_access": 0}) doc.insert(ignore_permissions=True) diff --git a/erpnext/patches/v11_1/update_bank_transaction_status.py b/erpnext/patches/v11_1/update_bank_transaction_status.py index 9b8be3de1b0..0615b04a57f 100644 --- a/erpnext/patches/v11_1/update_bank_transaction_status.py +++ b/erpnext/patches/v11_1/update_bank_transaction_status.py @@ -6,22 +6,26 @@ import frappe def execute(): - frappe.reload_doc("accounts", "doctype", "bank_transaction") + frappe.reload_doc("accounts", "doctype", "bank_transaction") - bank_transaction_fields = frappe.get_meta("Bank Transaction").get_valid_columns() + bank_transaction_fields = frappe.get_meta("Bank Transaction").get_valid_columns() - if 'debit' in bank_transaction_fields: - frappe.db.sql(""" UPDATE `tabBank Transaction` + if "debit" in bank_transaction_fields: + frappe.db.sql( + """ UPDATE `tabBank Transaction` SET status = 'Reconciled' WHERE status = 'Settled' and (debit = allocated_amount or credit = allocated_amount) and ifnull(allocated_amount, 0) > 0 - """) + """ + ) - elif 'deposit' in bank_transaction_fields: - frappe.db.sql(""" UPDATE `tabBank Transaction` + elif "deposit" in bank_transaction_fields: + frappe.db.sql( + """ UPDATE `tabBank Transaction` SET status = 'Reconciled' WHERE status = 'Settled' and (deposit = allocated_amount or withdrawal = allocated_amount) and ifnull(allocated_amount, 0) > 0 - """) + """ + ) diff --git a/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py b/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py index 902df201a42..16e11ed7be3 100644 --- a/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py +++ b/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py @@ -6,21 +6,23 @@ import frappe def execute(): - ''' - default supplier was not set in the item defaults for multi company instance, - this patch will set the default supplier + """ + default supplier was not set in the item defaults for multi company instance, + this patch will set the default supplier - ''' - if not frappe.db.has_column('Item', 'default_supplier'): + """ + if not frappe.db.has_column("Item", "default_supplier"): return - frappe.reload_doc('stock', 'doctype', 'item_default') - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "item_default") + frappe.reload_doc("stock", "doctype", "item") companies = frappe.get_all("Company") if len(companies) > 1: - frappe.db.sql(""" UPDATE `tabItem Default`, `tabItem` + frappe.db.sql( + """ UPDATE `tabItem Default`, `tabItem` SET `tabItem Default`.default_supplier = `tabItem`.default_supplier WHERE `tabItem Default`.parent = `tabItem`.name and `tabItem Default`.default_supplier is null - and `tabItem`.default_supplier is not null and `tabItem`.default_supplier != '' """) + and `tabItem`.default_supplier is not null and `tabItem`.default_supplier != '' """ + ) diff --git a/erpnext/patches/v11_1/woocommerce_set_creation_user.py b/erpnext/patches/v11_1/woocommerce_set_creation_user.py index 1de25bb739c..e2d9e3ef38a 100644 --- a/erpnext/patches/v11_1/woocommerce_set_creation_user.py +++ b/erpnext/patches/v11_1/woocommerce_set_creation_user.py @@ -1,10 +1,9 @@ - import frappe from frappe.utils import cint def execute(): - frappe.reload_doc("erpnext_integrations", "doctype","woocommerce_settings") + frappe.reload_doc("erpnext_integrations", "doctype", "woocommerce_settings") doc = frappe.get_doc("Woocommerce Settings") if cint(doc.enable_sync): diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py index 53d363f233f..1778a45049b 100644 --- a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py +++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py @@ -1,21 +1,23 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return frappe.reload_doc("regional", "doctype", "e_invoice_user") - if not frappe.db.count('E Invoice User'): + if not frappe.db.count("E Invoice User"): return - for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']): - company_name = frappe.db.sql(""" + for creds in frappe.db.get_all("E Invoice User", fields=["name", "gstin"]): + company_name = frappe.db.sql( + """ select dl.link_name from `tabAddress` a, `tabDynamic Link` dl where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' - """, (creds.get('gstin'))) + """, + (creds.get("gstin")), + ) if company_name and len(company_name) > 0: - frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0]) + frappe.db.set_value("E Invoice User", creds.get("name"), "company", company_name[0][0]) diff --git a/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py b/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py index 80187d834a4..284b616bbda 100644 --- a/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py +++ b/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py @@ -8,12 +8,16 @@ from frappe.model.utils.rename_field import rename_field def execute(): frappe.reload_doc("setup", "doctype", "company") - if frappe.db.has_column('Company', 'default_terms'): - rename_field('Company', "default_terms", "default_selling_terms") + if frappe.db.has_column("Company", "default_terms"): + rename_field("Company", "default_terms", "default_selling_terms") - for company in frappe.get_all("Company", ["name", "default_selling_terms", "default_buying_terms"]): + for company in frappe.get_all( + "Company", ["name", "default_selling_terms", "default_buying_terms"] + ): if company.default_selling_terms and not company.default_buying_terms: - frappe.db.set_value("Company", company.name, "default_buying_terms", company.default_selling_terms) + frappe.db.set_value( + "Company", company.name, "default_buying_terms", company.default_selling_terms + ) frappe.reload_doc("setup", "doctype", "terms_and_conditions") frappe.db.sql("update `tabTerms and Conditions` set selling=1, buying=1, hr=1") diff --git a/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py b/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py index 41264819ef3..a98976a968c 100644 --- a/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py +++ b/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py @@ -1,18 +1,21 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'Italy'}) + company = frappe.get_all("Company", filters={"country": "Italy"}) if not company: return custom_fields = { - 'Sales Invoice': [ - dict(fieldname='type_of_document', label='Type of Document', - fieldtype='Select', insert_after='customer_fiscal_code', - options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'), + "Sales Invoice": [ + dict( + fieldname="type_of_document", + label="Type of Document", + fieldtype="Select", + insert_after="customer_fiscal_code", + options="\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27", + ), ] } diff --git a/erpnext/patches/v12_0/add_einvoice_status_field.py b/erpnext/patches/v12_0/add_einvoice_status_field.py index ae908573236..0948e822974 100644 --- a/erpnext/patches/v12_0/add_einvoice_status_field.py +++ b/erpnext/patches/v12_0/add_einvoice_status_field.py @@ -1,4 +1,3 @@ - import json import frappe @@ -6,66 +5,154 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return # move hidden einvoice fields to a different section custom_fields = { - 'Sales Invoice': [ - dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', - print_hide=1, hidden=1), - - dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', - no_copy=1, print_hide=1), - - dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), - - dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', - no_copy=1, print_hide=1), - - dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', - options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', - hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) + "Sales Invoice": [ + dict( + fieldname="einvoice_section", + label="E-Invoice Fields", + fieldtype="Section Break", + insert_after="gst_vehicle_type", + print_hide=1, + hidden=1, + ), + dict( + fieldname="ack_no", + label="Ack. No.", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="einvoice_section", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="ack_date", + label="Ack. Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_no", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="irn_cancel_date", + label="Cancel Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_date", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="signed_einvoice", + label="Signed E-Invoice", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="irn_cancel_date", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="signed_qr_code", + label="Signed QRCode", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="signed_einvoice", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="qrcode_image", + label="QRCode", + fieldtype="Attach Image", + hidden=1, + insert_after="signed_qr_code", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="einvoice_status", + label="E-Invoice Status", + fieldtype="Select", + insert_after="qrcode_image", + options="\nPending\nGenerated\nCancelled\nFailed", + default=None, + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="failure_description", + label="E-Invoice Failure Description", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="einvoice_status", + no_copy=1, + print_hide=1, + read_only=1, + ), ] } create_custom_fields(custom_fields, update=True) - if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'): - frappe.db.sql(''' + if frappe.db.exists("E Invoice Settings") and frappe.db.get_single_value( + "E Invoice Settings", "enable" + ): + frappe.db.sql( + """ UPDATE `tabSales Invoice` SET einvoice_status = 'Pending' WHERE posting_date >= '2021-04-01' AND ifnull(irn, '') = '' AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '') AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export') - ''') + """ + ) # set appropriate statuses - frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated' - WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''') + frappe.db.sql( + """UPDATE `tabSales Invoice` SET einvoice_status = 'Generated' + WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0""" + ) - frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled' - WHERE ifnull(irn_cancelled, 0) = 1''') + frappe.db.sql( + """UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled' + WHERE ifnull(irn_cancelled, 0) = 1""" + ) # set correct acknowledgement in e-invoices - einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice']) + einvoices = frappe.get_all("Sales Invoice", {"irn": ["is", "set"]}, ["name", "signed_einvoice"]) if einvoices: for inv in einvoices: - signed_einvoice = inv.get('signed_einvoice') + signed_einvoice = inv.get("signed_einvoice") if signed_einvoice: signed_einvoice = json.loads(signed_einvoice) - frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False) - frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False) + frappe.db.set_value( + "Sales Invoice", + inv.get("name"), + "ack_no", + signed_einvoice.get("AckNo"), + update_modified=False, + ) + frappe.db.set_value( + "Sales Invoice", + inv.get("name"), + "ack_date", + signed_einvoice.get("AckDt"), + update_modified=False, + ) diff --git a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py index b5d8493ae0b..d15f4d1327e 100644 --- a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py +++ b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py @@ -1,19 +1,18 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - if frappe.db.exists('Report', 'E-Invoice Summary') and \ - not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')): - frappe.get_doc(dict( - doctype='Custom Role', - report='E-Invoice Summary', - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if frappe.db.exists("Report", "E-Invoice Summary") and not frappe.db.get_value( + "Custom Role", dict(report="E-Invoice Summary") + ): + frappe.get_doc( + dict( + doctype="Custom Role", + report="E-Invoice Summary", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() diff --git a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py index 973da895623..db9fa247ed5 100644 --- a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py +++ b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py @@ -3,18 +3,21 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) - if not company: - return + if not company: + return - create_custom_field('Delivery Note', { - 'fieldname': 'ewaybill', - 'label': 'E-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', - 'allow_on_submit': 1, - 'insert_after': 'customer_name_in_arabic', - 'translatable': 0, - 'owner': 'Administrator' - }) + create_custom_field( + "Delivery Note", + { + "fieldname": "ewaybill", + "label": "E-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:(doc.docstatus === 1)", + "allow_on_submit": 1, + "insert_after": "customer_name_in_arabic", + "translatable": 0, + "owner": "Administrator", + }, + ) diff --git a/erpnext/patches/v12_0/add_ewaybill_validity_field.py b/erpnext/patches/v12_0/add_ewaybill_validity_field.py index 1c8a68a7517..19b96159ef7 100644 --- a/erpnext/patches/v12_0/add_ewaybill_validity_field.py +++ b/erpnext/patches/v12_0/add_ewaybill_validity_field.py @@ -1,17 +1,25 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return custom_fields = { - 'Sales Invoice': [ - dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, - depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill') + "Sales Invoice": [ + dict( + fieldname="eway_bill_validity", + label="E-Way Bill Validity", + fieldtype="Data", + no_copy=1, + print_hide=1, + depends_on="ewaybill", + read_only=1, + allow_on_submit=1, + insert_after="ewaybill", + ) ] } create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py index df9bbea344a..b14ffd2d349 100644 --- a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py +++ b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py @@ -1,4 +1,3 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields @@ -6,37 +5,38 @@ from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return make_custom_fields() - frappe.reload_doctype('Tax Category') - frappe.reload_doctype('Sales Taxes and Charges Template') - frappe.reload_doctype('Purchase Taxes and Charges Template') + frappe.reload_doctype("Tax Category") + frappe.reload_doctype("Sales Taxes and Charges Template") + frappe.reload_doctype("Purchase Taxes and Charges Template") # Create tax category with inter state field checked - tax_category = frappe.db.get_value('Tax Category', {'name': 'OUT OF STATE'}, 'name') + tax_category = frappe.db.get_value("Tax Category", {"name": "OUT OF STATE"}, "name") if not tax_category: - inter_state_category = frappe.get_doc({ - 'doctype': 'Tax Category', - 'title': 'OUT OF STATE', - 'is_inter_state': 1 - }).insert() + inter_state_category = frappe.get_doc( + {"doctype": "Tax Category", "title": "OUT OF STATE", "is_inter_state": 1} + ).insert() tax_category = inter_state_category.name - for doctype in ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template'): - if not frappe.get_meta(doctype).has_field('is_inter_state'): continue + for doctype in ("Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"): + if not frappe.get_meta(doctype).has_field("is_inter_state"): + continue - template = frappe.db.get_value(doctype, {'is_inter_state': 1, 'disabled': 0}, ['name']) + template = frappe.db.get_value(doctype, {"is_inter_state": 1, "disabled": 0}, ["name"]) if template: - frappe.db.set_value(doctype, template, 'tax_category', tax_category) + frappe.db.set_value(doctype, template, "tax_category", tax_category) - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabCustom Field` WHERE fieldname = 'is_inter_state' AND dt IN ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template') - """) + """ + ) diff --git a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py index 77b63487ca7..5944889b63d 100644 --- a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py +++ b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py @@ -1,19 +1,24 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return custom_fields = { - 'Delivery Note': [ - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1), + "Delivery Note": [ + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_vehicle_type", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + fetch_from="customer.gst_category", + fetch_if_empty=1, + ), ] } diff --git a/erpnext/patches/v12_0/add_item_name_in_work_orders.py b/erpnext/patches/v12_0/add_item_name_in_work_orders.py index d765b93d218..0e5cd4eed52 100644 --- a/erpnext/patches/v12_0/add_item_name_in_work_orders.py +++ b/erpnext/patches/v12_0/add_item_name_in_work_orders.py @@ -4,11 +4,13 @@ import frappe def execute(): frappe.reload_doc("manufacturing", "doctype", "work_order") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabWork Order` wo JOIN `tabItem` item ON wo.production_item = item.item_code SET wo.item_name = item.item_name - """) + """ + ) frappe.db.commit() diff --git a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py index 1d77e5ad3d1..24748b2cc65 100644 --- a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py +++ b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py @@ -3,12 +3,12 @@ from frappe.permissions import add_permission, update_permission_property def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('regional', 'doctype', 'Lower Deduction Certificate') + frappe.reload_doc("regional", "doctype", "Lower Deduction Certificate") - add_permission('Lower Deduction Certificate', 'Accounts Manager', 0) - update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'write', 1) - update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'create', 1) + add_permission("Lower Deduction Certificate", "Accounts Manager", 0) + update_permission_property("Lower Deduction Certificate", "Accounts Manager", 0, "write", 1) + update_permission_property("Lower Deduction Certificate", "Accounts Manager", 0, "create", 1) diff --git a/erpnext/patches/v12_0/add_state_code_for_ladakh.py b/erpnext/patches/v12_0/add_state_code_for_ladakh.py index 6722b7bcef0..4b06eb1921e 100644 --- a/erpnext/patches/v12_0/add_state_code_for_ladakh.py +++ b/erpnext/patches/v12_0/add_state_code_for_ladakh.py @@ -5,15 +5,15 @@ from erpnext.regional.india import states def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - custom_fields = ['Address-gst_state', 'Tax Category-gst_state'] + custom_fields = ["Address-gst_state", "Tax Category-gst_state"] # Update options in gst_state custom fields for field in custom_fields: - if frappe.db.exists('Custom Field', field): - gst_state_field = frappe.get_doc('Custom Field', field) - gst_state_field.options = '\n'.join(states) + if frappe.db.exists("Custom Field", field): + gst_state_field = frappe.get_doc("Custom Field", field) + gst_state_field.options = "\n".join(states) gst_state_field.save() diff --git a/erpnext/patches/v12_0/add_taxjar_integration_field.py b/erpnext/patches/v12_0/add_taxjar_integration_field.py index fbaf6f83a7b..9217384b813 100644 --- a/erpnext/patches/v12_0/add_taxjar_integration_field.py +++ b/erpnext/patches/v12_0/add_taxjar_integration_field.py @@ -1,11 +1,10 @@ - import frappe from erpnext.regional.united_states.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters={'country': 'United States'}) + company = frappe.get_all("Company", filters={"country": "United States"}) if not company: return diff --git a/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py b/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py index c3a422c49e5..7f044c8d479 100644 --- a/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py +++ b/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py @@ -2,9 +2,11 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'item_variant_attribute') - frappe.db.sql(''' + frappe.reload_doc("stock", "doctype", "item_variant_attribute") + frappe.db.sql( + """ UPDATE `tabItem Variant Attribute` t1 INNER JOIN `tabItem` t2 ON t2.name = t1.parent SET t1.variant_of = t2.variant_of - ''') + """ + ) diff --git a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py index e0ed9d8c147..744ea1ccd8a 100644 --- a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py +++ b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py @@ -1,14 +1,16 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field def execute(): - frappe.reload_doc('accounts', 'doctype', 'accounting_dimension') + frappe.reload_doc("accounts", "doctype", "accounting_dimension") - accounting_dimensions = frappe.db.sql("""select fieldname, label, document_type, disabled from - `tabAccounting Dimension`""", as_dict=1) + accounting_dimensions = frappe.db.sql( + """select fieldname, label, document_type, disabled from + `tabAccounting Dimension`""", + as_dict=1, + ) if not accounting_dimensions: return @@ -16,13 +18,19 @@ def execute(): count = 1 for d in accounting_dimensions: - if count%2 == 0: - insert_after_field = 'dimension_col_break' + if count % 2 == 0: + insert_after_field = "dimension_col_break" else: - insert_after_field = 'accounting_dimensions_section' + insert_after_field = "accounting_dimensions_section" - for doctype in ["Subscription Plan", "Subscription", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", - "Expense Claim Detail", "Expense Taxes and Charges"]: + for doctype in [ + "Subscription Plan", + "Subscription", + "Opening Invoice Creation Tool", + "Opening Invoice Creation Tool Item", + "Expense Claim Detail", + "Expense Taxes and Charges", + ]: field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) @@ -34,7 +42,7 @@ def execute(): "label": d.label, "fieldtype": "Link", "options": d.document_type, - "insert_after": insert_after_field + "insert_after": insert_after_field, } create_custom_field(doctype, df) diff --git a/erpnext/patches/v12_0/create_default_energy_point_rules.py b/erpnext/patches/v12_0/create_default_energy_point_rules.py index 35eaca7f400..da200de9b77 100644 --- a/erpnext/patches/v12_0/create_default_energy_point_rules.py +++ b/erpnext/patches/v12_0/create_default_energy_point_rules.py @@ -4,5 +4,5 @@ from erpnext.setup.install import create_default_energy_point_rules def execute(): - frappe.reload_doc('social', 'doctype', 'energy_point_rule') + frappe.reload_doc("social", "doctype", "energy_point_rule") create_default_energy_point_rules() diff --git a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py index 0f5e07b5e53..80e9047cf06 100644 --- a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py +++ b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py @@ -1,4 +1,3 @@ - import frappe from erpnext.regional.united_states.setup import make_custom_fields @@ -6,12 +5,12 @@ from erpnext.regional.united_states.setup import make_custom_fields def execute(): - frappe.reload_doc('accounts', 'doctype', 'allowed_to_transact_with', force=True) - frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail', force=True) - frappe.reload_doc('crm', 'doctype', 'lost_reason_detail', force=True) - frappe.reload_doc('setup', 'doctype', 'quotation_lost_reason_detail', force=True) + frappe.reload_doc("accounts", "doctype", "allowed_to_transact_with", force=True) + frappe.reload_doc("accounts", "doctype", "pricing_rule_detail", force=True) + frappe.reload_doc("crm", "doctype", "lost_reason_detail", force=True) + frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail", force=True) - company = frappe.get_all('Company', filters = {'country': 'United States'}) + company = frappe.get_all("Company", filters={"country": "United States"}) if not company: return diff --git a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py index 3dc1115a20a..906baf22ccb 100644 --- a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py +++ b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -7,44 +6,76 @@ from erpnext.regional.india.utils import get_gst_accounts def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name']) + company = frappe.get_all("Company", filters={"country": "India"}, fields=["name"]) if not company: return frappe.reload_doc("regional", "doctype", "gst_settings") frappe.reload_doc("accounts", "doctype", "gst_account") - journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] - make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '') + journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + [ + "Reversal Of ITC" + ] + make_property_setter( + "Journal Entry", "voucher_type", "options", "\n".join(journal_entry_types), "" + ) custom_fields = { - 'Journal Entry': [ - dict(fieldname='reversal_type', label='Reversal Type', - fieldtype='Select', insert_after='voucher_type', print_hide=1, + "Journal Entry": [ + dict( + fieldname="reversal_type", + label="Reversal Type", + fieldtype="Select", + insert_after="voucher_type", + print_hide=1, options="As per rules 42 & 43 of CGST Rules\nOthers", depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_address', label='Company Address', - fieldtype='Link', options='Address', insert_after='reversal_type', - print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1, - fetch_from='company_address.gstin', + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_address", + label="Company Address", + fieldtype="Link", + options="Address", + insert_after="reversal_type", + print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'") + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + read_only=1, + insert_after="company_address", + print_hide=1, + fetch_from="company_address.gstin", + depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), ], - 'Purchase Invoice': [ - dict(fieldname='eligibility_for_itc', label='Eligibility For ITC', - fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1, - options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC', - default="All Other ITC") + "Purchase Invoice": [ + dict( + fieldname="eligibility_for_itc", + label="Eligibility For ITC", + fieldtype="Select", + insert_after="reason_for_issuing_document", + print_hide=1, + options="Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC", + default="All Other ITC", + ) + ], + "Purchase Invoice Item": [ + dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) ], - 'Purchase Invoice Item': [ - dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) - ] } create_custom_fields(custom_fields, update=True) @@ -54,28 +85,40 @@ def execute(): gst_accounts = get_gst_accounts(only_non_reverse_charge=1) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustom Field` SET fieldtype='Currency', options='Company:company:default_currency' WHERE dt = 'Purchase Invoice' and fieldname in ('itc_integrated_tax', 'itc_state_tax', 'itc_central_tax', 'itc_cess_amount') - """) + """ + ) - frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_integrated_tax = '0' - WHERE trim(coalesce(itc_integrated_tax, '')) = '' """) + frappe.db.sql( + """UPDATE `tabPurchase Invoice` set itc_integrated_tax = '0' + WHERE trim(coalesce(itc_integrated_tax, '')) = '' """ + ) - frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_state_tax = '0' - WHERE trim(coalesce(itc_state_tax, '')) = '' """) + frappe.db.sql( + """UPDATE `tabPurchase Invoice` set itc_state_tax = '0' + WHERE trim(coalesce(itc_state_tax, '')) = '' """ + ) - frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_central_tax = '0' - WHERE trim(coalesce(itc_central_tax, '')) = '' """) + frappe.db.sql( + """UPDATE `tabPurchase Invoice` set itc_central_tax = '0' + WHERE trim(coalesce(itc_central_tax, '')) = '' """ + ) - frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_cess_amount = '0' - WHERE trim(coalesce(itc_cess_amount, '')) = '' """) + frappe.db.sql( + """UPDATE `tabPurchase Invoice` set itc_cess_amount = '0' + WHERE trim(coalesce(itc_cess_amount, '')) = '' """ + ) # Get purchase invoices - invoices = frappe.get_all('Purchase Invoice', - {'posting_date': ('>=', '2021-04-01'), 'eligibility_for_itc': ('!=', 'Ineligible')}, - ['name']) + invoices = frappe.get_all( + "Purchase Invoice", + {"posting_date": (">=", "2021-04-01"), "eligibility_for_itc": ("!=", "Ineligible")}, + ["name"], + ) amount_map = {} @@ -83,37 +126,42 @@ def execute(): invoice_list = set([d.name for d in invoices]) # Get GST applied - amounts = frappe.db.sql(""" + amounts = frappe.db.sql( + """ SELECT parent, account_head, sum(base_tax_amount_after_discount_amount) as amount FROM `tabPurchase Taxes and Charges` where parent in %s GROUP BY parent, account_head - """, (invoice_list), as_dict=1) + """, + (invoice_list), + as_dict=1, + ) for d in amounts: - amount_map.setdefault(d.parent, - { - 'itc_integrated_tax': 0, - 'itc_state_tax': 0, - 'itc_central_tax': 0, - 'itc_cess_amount': 0 - }) + amount_map.setdefault( + d.parent, + {"itc_integrated_tax": 0, "itc_state_tax": 0, "itc_central_tax": 0, "itc_cess_amount": 0}, + ) if not gst_accounts: continue - if d.account_head in gst_accounts.get('igst_account'): - amount_map[d.parent]['itc_integrated_tax'] += d.amount - if d.account_head in gst_accounts.get('cgst_account'): - amount_map[d.parent]['itc_central_tax'] += d.amount - if d.account_head in gst_accounts.get('sgst_account'): - amount_map[d.parent]['itc_state_tax'] += d.amount - if d.account_head in gst_accounts.get('cess_account'): - amount_map[d.parent]['itc_cess_amount'] += d.amount + if d.account_head in gst_accounts.get("igst_account"): + amount_map[d.parent]["itc_integrated_tax"] += d.amount + if d.account_head in gst_accounts.get("cgst_account"): + amount_map[d.parent]["itc_central_tax"] += d.amount + if d.account_head in gst_accounts.get("sgst_account"): + amount_map[d.parent]["itc_state_tax"] += d.amount + if d.account_head in gst_accounts.get("cess_account"): + amount_map[d.parent]["itc_cess_amount"] += d.amount for invoice, values in amount_map.items(): - frappe.db.set_value('Purchase Invoice', invoice, { - 'itc_integrated_tax': values.get('itc_integrated_tax'), - 'itc_central_tax': values.get('itc_central_tax'), - 'itc_state_tax': values['itc_state_tax'], - 'itc_cess_amount': values['itc_cess_amount'], - }) + frappe.db.set_value( + "Purchase Invoice", + invoice, + { + "itc_integrated_tax": values.get("itc_integrated_tax"), + "itc_central_tax": values.get("itc_central_tax"), + "itc_state_tax": values["itc_state_tax"], + "itc_cess_amount": values["itc_cess_amount"], + }, + ) diff --git a/erpnext/patches/v12_0/create_taxable_value_field.py b/erpnext/patches/v12_0/create_taxable_value_field.py index df0269d3a8c..ad953032e7c 100644 --- a/erpnext/patches/v12_0/create_taxable_value_field.py +++ b/erpnext/patches/v12_0/create_taxable_value_field.py @@ -1,18 +1,23 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return custom_fields = { - 'Sales Invoice Item': [ - dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) + "Sales Invoice Item": [ + dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) ] } diff --git a/erpnext/patches/v12_0/delete_priority_property_setter.py b/erpnext/patches/v12_0/delete_priority_property_setter.py index cacc463d4b4..fbb02430f49 100644 --- a/erpnext/patches/v12_0/delete_priority_property_setter.py +++ b/erpnext/patches/v12_0/delete_priority_property_setter.py @@ -2,9 +2,11 @@ import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabProperty Setter` WHERE `tabProperty Setter`.doc_type='Issue' AND `tabProperty Setter`.field_name='priority' AND `tabProperty Setter`.property='options' - """) + """ + ) diff --git a/erpnext/patches/v12_0/fix_percent_complete_for_projects.py b/erpnext/patches/v12_0/fix_percent_complete_for_projects.py index 36f51bca60d..1fbcfa4e59a 100644 --- a/erpnext/patches/v12_0/fix_percent_complete_for_projects.py +++ b/erpnext/patches/v12_0/fix_percent_complete_for_projects.py @@ -4,10 +4,13 @@ from frappe.utils import flt def execute(): for project in frappe.get_all("Project", fields=["name", "percent_complete_method"]): - total = frappe.db.count('Task', dict(project=project.name)) + total = frappe.db.count("Task", dict(project=project.name)) if project.percent_complete_method == "Task Completion" and total > 0: - completed = frappe.db.sql("""select count(name) from tabTask where - project=%s and status in ('Cancelled', 'Completed')""", project.name)[0][0] + completed = frappe.db.sql( + """select count(name) from tabTask where + project=%s and status in ('Cancelled', 'Completed')""", + project.name, + )[0][0] percent_complete = flt(flt(completed) / total * 100, 2) if project.percent_complete != percent_complete: frappe.db.set_value("Project", project.name, "percent_complete", percent_complete) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index e5c4b8c524f..285183bfa25 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -13,12 +13,13 @@ def execute(): so_item.docstatus = 1 and so.docstatus = 1 and so_item.parent = so.name and so_item.prevdoc_docname = qo.name - and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired + and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""" - .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo) + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""".format( + cond=cond, invalid_so_against_quo=invalid_so_against_quo ) + ) valid_so_against_quo = """ SELECT @@ -27,9 +28,10 @@ def execute(): so_item.docstatus = 1 and so.docstatus = 1 and so_item.parent = so.name and so_item.prevdoc_docname = qo.name - and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired + and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and exists({valid_so_against_quo})""" - .format(cond=cond, valid_so_against_quo=valid_so_against_quo) + """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and exists({valid_so_against_quo})""".format( + cond=cond, valid_so_against_quo=valid_so_against_quo ) + ) diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index 90e46d07e40..354c5096c0f 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -7,8 +7,8 @@ from frappe.utils import getdate, today def execute(): - """ Generates leave ledger entries for leave allocation/application/encashment - for last allocation """ + """Generates leave ledger entries for leave allocation/application/encashment + for last allocation""" frappe.reload_doc("HR", "doctype", "Leave Ledger Entry") frappe.reload_doc("HR", "doctype", "Leave Encashment") frappe.reload_doc("HR", "doctype", "Leave Type") @@ -22,55 +22,79 @@ def execute(): generate_encashment_leave_ledger_entries() generate_expiry_allocation_ledger_entries() + def update_leave_allocation_fieldname(): - ''' maps data from old field to the new field ''' - frappe.db.sql(""" + """maps data from old field to the new field""" + frappe.db.sql( + """ UPDATE `tabLeave Allocation` SET `unused_leaves` = `carry_forwarded_leaves` - """) + """ + ) + def generate_allocation_ledger_entries(): - ''' fix ledger entries for missing leave allocation transaction ''' + """fix ledger entries for missing leave allocation transaction""" allocation_list = get_allocation_records() for allocation in allocation_list: - if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}): + if not frappe.db.exists( + "Leave Ledger Entry", + {"transaction_type": "Leave Allocation", "transaction_name": allocation.name}, + ): allocation_obj = frappe.get_doc("Leave Allocation", allocation) allocation_obj.create_leave_ledger_entry() + def generate_application_leave_ledger_entries(): - ''' fix ledger entries for missing leave application transaction ''' + """fix ledger entries for missing leave application transaction""" leave_applications = get_leaves_application_records() for application in leave_applications: - if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}): + if not frappe.db.exists( + "Leave Ledger Entry", + {"transaction_type": "Leave Application", "transaction_name": application.name}, + ): frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry() + def generate_encashment_leave_ledger_entries(): - ''' fix ledger entries for missing leave encashment transaction ''' + """fix ledger entries for missing leave encashment transaction""" leave_encashments = get_leave_encashment_records() for encashment in leave_encashments: - if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): + if not frappe.db.exists( + "Leave Ledger Entry", + {"transaction_type": "Leave Encashment", "transaction_name": encashment.name}, + ): frappe.get_doc("Leave Encashment", encashment).create_leave_ledger_entry() + def generate_expiry_allocation_ledger_entries(): - ''' fix ledger entries for missing leave allocation transaction ''' + """fix ledger entries for missing leave allocation transaction""" from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation + allocation_list = get_allocation_records() for allocation in allocation_list: - if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): + if not frappe.db.exists( + "Leave Ledger Entry", + {"transaction_type": "Leave Allocation", "transaction_name": allocation.name, "is_expired": 1}, + ): allocation_obj = frappe.get_doc("Leave Allocation", allocation) if allocation_obj.to_date <= getdate(today()): expire_allocation(allocation_obj) + def get_allocation_records(): - return frappe.get_all("Leave Allocation", filters={"docstatus": 1}, - fields=['name'], order_by='to_date ASC') + return frappe.get_all( + "Leave Allocation", filters={"docstatus": 1}, fields=["name"], order_by="to_date ASC" + ) + def get_leaves_application_records(): - return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name']) + return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=["name"]) + def get_leave_encashment_records(): - return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name']) + return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=["name"]) diff --git a/erpnext/patches/v12_0/make_item_manufacturer.py b/erpnext/patches/v12_0/make_item_manufacturer.py index d66f429de3c..3f233659d01 100644 --- a/erpnext/patches/v12_0/make_item_manufacturer.py +++ b/erpnext/patches/v12_0/make_item_manufacturer.py @@ -9,20 +9,29 @@ def execute(): frappe.reload_doc("stock", "doctype", "item_manufacturer") item_manufacturer = [] - for d in frappe.db.sql(""" SELECT name, manufacturer, manufacturer_part_no, creation, owner - FROM `tabItem` WHERE manufacturer is not null and manufacturer != ''""", as_dict=1): - item_manufacturer.append(( - frappe.generate_hash("", 10), - d.name, - d.manufacturer, - d.manufacturer_part_no, - d.creation, - d.owner - )) + for d in frappe.db.sql( + """ SELECT name, manufacturer, manufacturer_part_no, creation, owner + FROM `tabItem` WHERE manufacturer is not null and manufacturer != ''""", + as_dict=1, + ): + item_manufacturer.append( + ( + frappe.generate_hash("", 10), + d.name, + d.manufacturer, + d.manufacturer_part_no, + d.creation, + d.owner, + ) + ) if item_manufacturer: - frappe.db.sql(''' + frappe.db.sql( + """ INSERT INTO `tabItem Manufacturer` (`name`, `item_code`, `manufacturer`, `manufacturer_part_no`, `creation`, `owner`) - VALUES {}'''.format(', '.join(['%s'] * len(item_manufacturer))), tuple(item_manufacturer) + VALUES {}""".format( + ", ".join(["%s"] * len(item_manufacturer)) + ), + tuple(item_manufacturer), ) diff --git a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py index c0b262395d8..c069c24cfa5 100644 --- a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py +++ b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py @@ -1,15 +1,23 @@ - import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'bank', force=1) + frappe.reload_doc("accounts", "doctype", "bank", force=1) - if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'): - frappe.db.sql(""" - UPDATE `tabBank` b, `tabBank Account` ba - SET b.swift_number = ba.swift_number WHERE b.name = ba.bank - """) + if ( + frappe.db.table_exists("Bank") + and frappe.db.table_exists("Bank Account") + and frappe.db.has_column("Bank Account", "swift_number") + ): + try: + frappe.db.sql( + """ + UPDATE `tabBank` b, `tabBank Account` ba + SET b.swift_number = ba.swift_number WHERE b.name = ba.bank + """ + ) + except Exception as e: + frappe.log_error(e, title="Patch Migration Failed") - frappe.reload_doc('accounts', 'doctype', 'bank_account') - frappe.reload_doc('accounts', 'doctype', 'payment_request') + frappe.reload_doc("accounts", "doctype", "bank_account") + frappe.reload_doc("accounts", "doctype", "payment_request") diff --git a/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py b/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py index 82dfba52c9f..17c1966624e 100644 --- a/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py +++ b/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py @@ -6,7 +6,7 @@ import frappe def execute(): - ''' Move credit limit and bypass credit limit to the child table of customer credit limit ''' + """Move credit limit and bypass credit limit to the child table of customer credit limit""" frappe.reload_doc("Selling", "doctype", "Customer Credit Limit") frappe.reload_doc("Selling", "doctype", "Customer") frappe.reload_doc("Setup", "doctype", "Customer Group") @@ -16,28 +16,32 @@ def execute(): move_credit_limit_to_child_table() -def move_credit_limit_to_child_table(): - ''' maps data from old field to the new field in the child table ''' - companies = frappe.get_all("Company", 'name') +def move_credit_limit_to_child_table(): + """maps data from old field to the new field in the child table""" + + companies = frappe.get_all("Company", "name") for doctype in ("Customer", "Customer Group"): fields = "" - if doctype == "Customer" \ - and frappe.db.has_column('Customer', 'bypass_credit_limit_check_at_sales_order'): + if doctype == "Customer" and frappe.db.has_column( + "Customer", "bypass_credit_limit_check_at_sales_order" + ): fields = ", bypass_credit_limit_check_at_sales_order" - credit_limit_records = frappe.db.sql(''' + credit_limit_records = frappe.db.sql( + """ SELECT name, credit_limit {0} FROM `tab{1}` where credit_limit > 0 - '''.format(fields, doctype), as_dict=1) #nosec + """.format( + fields, doctype + ), + as_dict=1, + ) # nosec for record in credit_limit_records: doc = frappe.get_doc(doctype, record.name) for company in companies: - row = frappe._dict({ - 'credit_limit': record.credit_limit, - 'company': company.name - }) + row = frappe._dict({"credit_limit": record.credit_limit, "company": company.name}) if doctype == "Customer": row.bypass_credit_limit_check = record.bypass_credit_limit_check_at_sales_order diff --git a/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py index 5de7e69620b..8b8d9637f22 100644 --- a/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py +++ b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py @@ -6,7 +6,7 @@ import frappe def execute(): - ''' Move from due_advance_amount to pending_amount ''' + """Move from due_advance_amount to pending_amount""" - if frappe.db.has_column("Employee Advance", "due_advance_amount"): - frappe.db.sql(''' UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount ''') + if frappe.db.has_column("Employee Advance", "due_advance_amount"): + frappe.db.sql(""" UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount """) diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index 677a564af0d..35354dade75 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -13,17 +13,22 @@ def execute(): frappe.reload_doc("accounts", "doctype", "item_tax_template_detail", force=1) frappe.reload_doc("accounts", "doctype", "item_tax_template", force=1) - existing_templates = frappe.db.sql("""select template.name, details.tax_type, details.tax_rate + existing_templates = frappe.db.sql( + """select template.name, details.tax_type, details.tax_rate from `tabItem Tax Template` template, `tabItem Tax Template Detail` details where details.parent=template.name - """, as_dict=1) + """, + as_dict=1, + ) if len(existing_templates): for d in existing_templates: item_tax_templates.setdefault(d.name, {}) item_tax_templates[d.name][d.tax_type] = d.tax_rate - for d in frappe.db.sql("""select parent as item_code, tax_type, tax_rate from `tabItem Tax`""", as_dict=1): + for d in frappe.db.sql( + """select parent as item_code, tax_type, tax_rate from `tabItem Tax`""", as_dict=1 + ): old_item_taxes.setdefault(d.item_code, []) old_item_taxes[d.item_code].append(d) @@ -50,7 +55,9 @@ def execute(): item_tax_map[d.tax_type] = d.tax_rate tax_types = [] - item_tax_template_name = get_item_tax_template(item_tax_templates, item_tax_map, item_code, tax_types=tax_types) + item_tax_template_name = get_item_tax_template( + item_tax_templates, item_tax_map, item_code, tax_types=tax_types + ) # update the item tax table frappe.db.sql("delete from `tabItem Tax` where parent=%s and parenttype='Item'", item_code) @@ -62,17 +69,29 @@ def execute(): d.db_insert() doctypes = [ - 'Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice', - 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice' + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", ] for dt in doctypes: - for d in frappe.db.sql("""select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item` + for d in frappe.db.sql( + """select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item` where ifnull(item_tax_rate, '') not in ('', '{{}}') - and item_tax_template is NULL""".format(dt), as_dict=1): + and item_tax_template is NULL""".format( + dt + ), + as_dict=1, + ): item_tax_map = json.loads(d.item_tax_rate) - item_tax_template_name = get_item_tax_template(item_tax_templates, - item_tax_map, d.item_code, d.parenttype, d.parent, tax_types=tax_types) + item_tax_template_name = get_item_tax_template( + item_tax_templates, item_tax_map, d.item_code, d.parenttype, d.parent, tax_types=tax_types + ) frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name) frappe.db.auto_commit_on_many_writes = False @@ -82,7 +101,10 @@ def execute(): settings.determine_address_tax_category_from = "Billing Address" settings.save() -def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None, tax_types=None): + +def get_item_tax_template( + item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None, tax_types=None +): # search for previously created item tax template by comparing tax maps for template, item_tax_template_map in iteritems(item_tax_templates): if item_tax_map == item_tax_template_map: @@ -91,13 +113,26 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp # if no item tax template found, create one item_tax_template = frappe.new_doc("Item Tax Template") item_tax_template.title = make_autoname("Item Tax Template-.####") + item_tax_template_name = item_tax_template.title for tax_type, tax_rate in iteritems(item_tax_map): - account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type', 'company'], as_dict=1) + account_details = frappe.db.get_value( + "Account", tax_type, ["name", "account_type", "company"], as_dict=1 + ) if account_details: item_tax_template.company = account_details.company - if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): - frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable') + if not item_tax_template_name: + # set name once company is set as name is generated from company & title + # setting name is required to update `item_tax_templates` dict + item_tax_template_name = item_tax_template.set_new_name() + if account_details.account_type not in ( + "Tax", + "Chargeable", + "Income Account", + "Expense Account", + "Expenses Included In Valuation", + ): + frappe.db.set_value("Account", account_details.name, "account_type", "Chargeable") else: parts = tax_type.strip().split(" - ") account_name = " - ".join(parts[:-1]) @@ -105,18 +140,25 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp tax_type = None else: company = get_company(parts[-1], parenttype, parent) - parent_account = frappe.get_value("Account", {"account_name": account_name, "company": company}, "parent_account") + parent_account = frappe.get_value( + "Account", {"account_name": account_name, "company": company}, "parent_account" + ) if not parent_account: - parent_account = frappe.db.get_value("Account", - filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account") + parent_account = frappe.db.get_value( + "Account", + filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, + fieldname="parent_account", + ) if not parent_account: - parent_account = frappe.db.get_value("Account", - filters={"account_type": "Tax", "root_type": "Liability", "is_group": 1, "company": company}) + parent_account = frappe.db.get_value( + "Account", + filters={"account_type": "Tax", "root_type": "Liability", "is_group": 1, "company": company}, + ) filters = { "account_name": account_name, "company": company, "account_type": "Tax", - "parent_account": parent_account + "parent_account": parent_account, } tax_type = frappe.db.get_value("Account", filters) if not tax_type: @@ -126,28 +168,38 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp account.insert() tax_type = account.name except frappe.DuplicateEntryError: - tax_type = frappe.db.get_value("Account", {"account_name": account_name, "company": company}, "name") + tax_type = frappe.db.get_value( + "Account", {"account_name": account_name, "company": company}, "name" + ) account_type = frappe.get_cached_value("Account", tax_type, "account_type") - if tax_type and account_type in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): + if tax_type and account_type in ( + "Tax", + "Chargeable", + "Income Account", + "Expense Account", + "Expenses Included In Valuation", + ): if tax_type not in tax_types: item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate}) tax_types.append(tax_type) - item_tax_templates.setdefault(item_tax_template.title, {}) - item_tax_templates[item_tax_template.title][tax_type] = tax_rate + item_tax_templates.setdefault(item_tax_template_name, {}) + item_tax_templates[item_tax_template_name][tax_type] = tax_rate + if item_tax_template.get("taxes"): item_tax_template.save() return item_tax_template.name + def get_company(company_abbr, parenttype=None, parent=None): if parenttype and parent: - company = frappe.get_cached_value(parenttype, parent, 'company') + company = frappe.get_cached_value(parenttype, parent, "company") else: company = frappe.db.get_value("Company", filters={"abbr": company_abbr}) if not company: - companies = frappe.get_all('Company') + companies = frappe.get_all("Company") if len(companies) == 1: company = companies[0].name diff --git a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py index c396891b59e..6788cb2e264 100644 --- a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py +++ b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py @@ -12,10 +12,12 @@ def execute(): if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret): plaid_settings.enabled = 0 else: - plaid_settings.update({ - "plaid_client_id": frappe.conf.plaid_client_id, - "plaid_env": frappe.conf.plaid_env, - "plaid_secret": frappe.conf.plaid_secret - }) + plaid_settings.update( + { + "plaid_client_id": frappe.conf.plaid_client_id, + "plaid_env": frappe.conf.plaid_env, + "plaid_secret": frappe.conf.plaid_secret, + } + ) plaid_settings.flags.ignore_mandatory = True plaid_settings.save() diff --git a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py index 36fe18d8b71..71926107bd5 100644 --- a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py +++ b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py @@ -6,19 +6,23 @@ import frappe def execute(): - frappe.reload_doc("setup", "doctype", "target_detail") - frappe.reload_doc("core", "doctype", "prepared_report") + frappe.reload_doc("setup", "doctype", "target_detail") + frappe.reload_doc("core", "doctype", "prepared_report") - for d in ['Sales Person', 'Sales Partner', 'Territory']: - frappe.db.sql(""" + for d in ["Sales Person", "Sales Partner", "Territory"]: + frappe.db.sql( + """ UPDATE `tab{child_doc}`, `tab{parent_doc}` SET `tab{child_doc}`.distribution_id = `tab{parent_doc}`.distribution_id WHERE `tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.distribution_id is not null and `tab{parent_doc}`.distribution_id != '' - """.format(parent_doc = d, child_doc = "Target Detail")) + """.format( + parent_doc=d, child_doc="Target Detail" + ) + ) - frappe.delete_doc("Report", "Sales Partner-wise Transaction Summary") - frappe.delete_doc("Report", "Sales Person Target Variance Item Group-Wise") - frappe.delete_doc("Report", "Territory Target Variance Item Group-Wise") + frappe.delete_doc("Report", "Sales Partner-wise Transaction Summary") + frappe.delete_doc("Report", "Sales Person Target Variance Item Group-Wise") + frappe.delete_doc("Report", "Territory Target Variance Item Group-Wise") diff --git a/erpnext/patches/v12_0/purchase_receipt_status.py b/erpnext/patches/v12_0/purchase_receipt_status.py index 459221e7695..3b828d69cb2 100644 --- a/erpnext/patches/v12_0/purchase_receipt_status.py +++ b/erpnext/patches/v12_0/purchase_receipt_status.py @@ -7,6 +7,7 @@ import frappe logger = frappe.logger("patch", allow_site=True, file_count=50) + def execute(): affected_purchase_receipts = frappe.db.sql( """select name from `tabPurchase Receipt` @@ -16,13 +17,13 @@ def execute(): if not affected_purchase_receipts: return - logger.info("purchase_receipt_status: begin patch, PR count: {}" - .format(len(affected_purchase_receipts))) + 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] logger.info("purchase_receipt_status: patching PR - {}".format(pr_name)) diff --git a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py index 50d97c4830d..661152bef34 100644 --- a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py +++ b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py @@ -1,17 +1,21 @@ - import frappe from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty def execute(): - bin_details = frappe.db.sql(""" + bin_details = frappe.db.sql( + """ SELECT item_code, warehouse - FROM `tabBin`""",as_dict=1) + FROM `tabBin`""", + as_dict=1, + ) for entry in bin_details: if not (entry.item_code and entry.warehouse): continue - update_bin_qty(entry.get("item_code"), entry.get("warehouse"), { - "indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse")) - }) + update_bin_qty( + entry.get("item_code"), + entry.get("warehouse"), + {"indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse"))}, + ) diff --git a/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py b/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py index 0f4a366c657..b18f4ebe2ed 100644 --- a/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py +++ b/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py @@ -1,14 +1,18 @@ - import frappe def execute(): frappe.reload_doc("accounts", "doctype", "tax_category") frappe.reload_doc("stock", "doctype", "item_manufacturer") - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return if frappe.db.exists("Custom Field", "Company-bank_remittance_section"): - deprecated_fields = ['bank_remittance_section', 'client_code', 'remittance_column_break', 'product_code'] + deprecated_fields = [ + "bank_remittance_section", + "client_code", + "remittance_column_break", + "product_code", + ] for i in range(len(deprecated_fields)): - frappe.delete_doc("Custom Field", 'Company-'+deprecated_fields[i]) + frappe.delete_doc("Custom Field", "Company-" + deprecated_fields[i]) diff --git a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py index d1d4bcc140f..4029a3f0e22 100644 --- a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py +++ b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py @@ -6,8 +6,8 @@ import frappe def execute(): - ''' Delete leave ledger entry created - via leave applications with status != Approved ''' + """Delete leave ledger entry created + via leave applications with status != Approved""" if not frappe.db.a_row_exists("Leave Ledger Entry"): return @@ -15,14 +15,21 @@ def execute(): if leave_application_list: delete_denied_leaves_from_leave_ledger_entry(leave_application_list) + def get_denied_leave_application_list(): - return frappe.db.sql_list(''' Select name from `tabLeave Application` where status <> 'Approved' ''') + return frappe.db.sql_list( + """ Select name from `tabLeave Application` where status <> 'Approved' """ + ) + def delete_denied_leaves_from_leave_ledger_entry(leave_application_list): if leave_application_list: - frappe.db.sql(''' Delete + frappe.db.sql( + """ Delete FROM `tabLeave Ledger Entry` WHERE transaction_type = 'Leave Application' - AND transaction_name in (%s) ''' % (', '.join(['%s'] * len(leave_application_list))), #nosec - tuple(leave_application_list)) + AND transaction_name in (%s) """ + % (", ".join(["%s"] * len(leave_application_list))), # nosec + tuple(leave_application_list), + ) diff --git a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py index 6ad68ccc6e4..8247734a4a1 100644 --- a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py @@ -7,16 +7,18 @@ import frappe def execute(): """Delete duplicate leave ledger entries of type allocation created.""" - frappe.reload_doc('hr', 'doctype', 'leave_ledger_entry') + frappe.reload_doc("hr", "doctype", "leave_ledger_entry") if not frappe.db.a_row_exists("Leave Ledger Entry"): return duplicate_records_list = get_duplicate_records() delete_duplicate_ledger_entries(duplicate_records_list) + def get_duplicate_records(): """Fetch all but one duplicate records from the list of expired leave allocation.""" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT name, employee, transaction_name, leave_type, is_carry_forward, from_date, to_date FROM `tabLeave Ledger Entry` WHERE @@ -29,13 +31,17 @@ def get_duplicate_records(): count(name) > 1 ORDER BY creation - """) + """ + ) + def delete_duplicate_ledger_entries(duplicate_records_list): """Delete duplicate leave ledger entries.""" - if not duplicate_records_list: return + if not duplicate_records_list: + return for d in duplicate_records_list: - frappe.db.sql(''' + frappe.db.sql( + """ DELETE FROM `tabLeave Ledger Entry` WHERE name != %s AND employee = %s @@ -44,4 +50,6 @@ def delete_duplicate_ledger_entries(duplicate_records_list): AND is_carry_forward = %s AND from_date = %s AND to_date = %s - ''', tuple(d)) + """, + tuple(d), + ) diff --git a/erpnext/patches/v12_0/rename_account_type_doctype.py b/erpnext/patches/v12_0/rename_account_type_doctype.py index c2c834bf98c..ab195549a42 100644 --- a/erpnext/patches/v12_0/rename_account_type_doctype.py +++ b/erpnext/patches/v12_0/rename_account_type_doctype.py @@ -1,8 +1,7 @@ - import frappe def execute(): - frappe.rename_doc('DocType', 'Account Type', 'Bank Account Type', force=True) - frappe.rename_doc('DocType', 'Account Subtype', 'Bank Account Subtype', force=True) - frappe.reload_doc('accounts', 'doctype', 'bank_account') + frappe.rename_doc("DocType", "Account Type", "Bank Account Type", force=True) + frappe.rename_doc("DocType", "Account Subtype", "Bank Account Subtype", force=True) + frappe.reload_doc("accounts", "doctype", "bank_account") diff --git a/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py b/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py index a5d986a0a16..92687530ebc 100644 --- a/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py +++ b/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py @@ -7,12 +7,13 @@ from frappe.model.utils.rename_field import rename_field def execute(): - ''' Change the fieldname from bank_account_no to bank_account ''' + """Change the fieldname from bank_account_no to bank_account""" if not frappe.get_meta("Journal Entry Account").has_field("bank_account"): frappe.reload_doc("Accounts", "doctype", "Journal Entry Account") update_journal_entry_account_fieldname() + def update_journal_entry_account_fieldname(): - ''' maps data from old field to the new field ''' - if frappe.db.has_column('Journal Entry Account', 'bank_account_no'): + """maps data from old field to the new field""" + if frappe.db.has_column("Journal Entry Account", "bank_account_no"): rename_field("Journal Entry Account", "bank_account_no", "bank_account") diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation.py b/erpnext/patches/v12_0/rename_bank_reconciliation.py index 51ff0c8c949..aacd6e8161d 100644 --- a/erpnext/patches/v12_0/rename_bank_reconciliation.py +++ b/erpnext/patches/v12_0/rename_bank_reconciliation.py @@ -7,8 +7,8 @@ import frappe def execute(): if frappe.db.table_exists("Bank Reconciliation"): - frappe.rename_doc('DocType', 'Bank Reconciliation', 'Bank Clearance', force=True) - frappe.reload_doc('Accounts', 'doctype', 'Bank Clearance') + frappe.rename_doc("DocType", "Bank Reconciliation", "Bank Clearance", force=True) + frappe.reload_doc("Accounts", "doctype", "Bank Clearance") - frappe.rename_doc('DocType', 'Bank Reconciliation Detail', 'Bank Clearance Detail', force=True) - frappe.reload_doc('Accounts', 'doctype', 'Bank Clearance Detail') + frappe.rename_doc("DocType", "Bank Reconciliation Detail", "Bank Clearance Detail", force=True) + frappe.reload_doc("Accounts", "doctype", "Bank Clearance Detail") diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py index 629cd5bda66..e2a3887b9ac 100644 --- a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py +++ b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py @@ -5,11 +5,24 @@ import frappe def _rename_single_field(**kwargs): - count = frappe.db.sql("SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format(**kwargs))[0][0] #nosec + count = frappe.db.sql( + "SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format( + **kwargs + ) + )[0][ + 0 + ] # nosec if count == 0: - frappe.db.sql("UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format(**kwargs)) #nosec + frappe.db.sql( + "UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format( + **kwargs + ) + ) # nosec + def execute(): - _rename_single_field(doctype = "Bank Clearance", old_name = "bank_account" , new_name = "account") - _rename_single_field(doctype = "Bank Clearance", old_name = "bank_account_no", new_name = "bank_account") + _rename_single_field(doctype="Bank Clearance", old_name="bank_account", new_name="account") + _rename_single_field( + doctype="Bank Clearance", old_name="bank_account_no", new_name="bank_account" + ) frappe.reload_doc("Accounts", "doctype", "Bank Clearance") diff --git a/erpnext/patches/v12_0/rename_lost_reason_detail.py b/erpnext/patches/v12_0/rename_lost_reason_detail.py index 55bf6f223fb..2f7f8428482 100644 --- a/erpnext/patches/v12_0/rename_lost_reason_detail.py +++ b/erpnext/patches/v12_0/rename_lost_reason_detail.py @@ -1,19 +1,24 @@ - import frappe def execute(): - if frappe.db.exists("DocType", "Lost Reason Detail"): - frappe.reload_doc("crm", "doctype", "opportunity_lost_reason") - frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail") - frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail") + if frappe.db.exists("DocType", "Lost Reason Detail"): + frappe.reload_doc("crm", "doctype", "opportunity_lost_reason") + frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail") + frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail") - frappe.db.sql("""INSERT INTO `tabOpportunity Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Opportunity'""") + frappe.db.sql( + """INSERT INTO `tabOpportunity Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Opportunity'""" + ) - frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""") + frappe.db.sql( + """INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""" + ) - frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) + frappe.db.sql( + """INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` - FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""") + FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""" + ) - frappe.delete_doc("DocType", "Lost Reason Detail") + frappe.delete_doc("DocType", "Lost Reason Detail") diff --git a/erpnext/patches/v12_0/rename_mws_settings_fields.py b/erpnext/patches/v12_0/rename_mws_settings_fields.py index d5bf38d204d..97c89e0dbf9 100644 --- a/erpnext/patches/v12_0/rename_mws_settings_fields.py +++ b/erpnext/patches/v12_0/rename_mws_settings_fields.py @@ -5,8 +5,12 @@ import frappe def execute(): - count = frappe.db.sql("SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';")[0][0] + count = frappe.db.sql( + "SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';" + )[0][0] if count == 0: - frappe.db.sql("UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';") + frappe.db.sql( + "UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';" + ) frappe.reload_doc("ERPNext Integrations", "doctype", "Amazon MWS Settings") diff --git a/erpnext/patches/v12_0/rename_pos_closing_doctype.py b/erpnext/patches/v12_0/rename_pos_closing_doctype.py index f5f0112e036..fb80f8dc615 100644 --- a/erpnext/patches/v12_0/rename_pos_closing_doctype.py +++ b/erpnext/patches/v12_0/rename_pos_closing_doctype.py @@ -7,17 +7,19 @@ import frappe def execute(): if frappe.db.table_exists("POS Closing Voucher"): if not frappe.db.exists("DocType", "POS Closing Entry"): - frappe.rename_doc('DocType', 'POS Closing Voucher', 'POS Closing Entry', force=True) + frappe.rename_doc("DocType", "POS Closing Voucher", "POS Closing Entry", force=True) - if not frappe.db.exists('DocType', 'POS Closing Entry Taxes'): - frappe.rename_doc('DocType', 'POS Closing Voucher Taxes', 'POS Closing Entry Taxes', force=True) + if not frappe.db.exists("DocType", "POS Closing Entry Taxes"): + frappe.rename_doc("DocType", "POS Closing Voucher Taxes", "POS Closing Entry Taxes", force=True) - if not frappe.db.exists('DocType', 'POS Closing Voucher Details'): - frappe.rename_doc('DocType', 'POS Closing Voucher Details', 'POS Closing Entry Detail', force=True) + if not frappe.db.exists("DocType", "POS Closing Voucher Details"): + frappe.rename_doc( + "DocType", "POS Closing Voucher Details", "POS Closing Entry Detail", force=True + ) - frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry') - frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry Taxes') - frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry Detail') + frappe.reload_doc("Accounts", "doctype", "POS Closing Entry") + frappe.reload_doc("Accounts", "doctype", "POS Closing Entry Taxes") + frappe.reload_doc("Accounts", "doctype", "POS Closing Entry Detail") if frappe.db.exists("DocType", "POS Closing Voucher"): frappe.delete_doc("DocType", "POS Closing Voucher") diff --git a/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py b/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py index 87630fbcaf9..8d4c01359d4 100644 --- a/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py +++ b/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py @@ -5,16 +5,17 @@ import frappe doctypes = { - 'Price Discount Slab': 'Promotional Scheme Price Discount', - 'Product Discount Slab': 'Promotional Scheme Product Discount', - 'Apply Rule On Item Code': 'Pricing Rule Item Code', - 'Apply Rule On Item Group': 'Pricing Rule Item Group', - 'Apply Rule On Brand': 'Pricing Rule Brand' + "Price Discount Slab": "Promotional Scheme Price Discount", + "Product Discount Slab": "Promotional Scheme Product Discount", + "Apply Rule On Item Code": "Pricing Rule Item Code", + "Apply Rule On Item Group": "Pricing Rule Item Group", + "Apply Rule On Brand": "Pricing Rule Brand", } + def execute(): - for old_doc, new_doc in doctypes.items(): - if not frappe.db.table_exists(new_doc) and frappe.db.table_exists(old_doc): - frappe.rename_doc('DocType', old_doc, new_doc) - frappe.reload_doc("accounts", "doctype", frappe.scrub(new_doc)) - frappe.delete_doc("DocType", old_doc) + for old_doc, new_doc in doctypes.items(): + if not frappe.db.table_exists(new_doc) and frappe.db.table_exists(old_doc): + frappe.rename_doc("DocType", old_doc, new_doc) + frappe.reload_doc("accounts", "doctype", frappe.scrub(new_doc)) + frappe.delete_doc("DocType", old_doc) diff --git a/erpnext/patches/v12_0/rename_tolerance_fields.py b/erpnext/patches/v12_0/rename_tolerance_fields.py index ca2427bc3dd..ef1ba655a9f 100644 --- a/erpnext/patches/v12_0/rename_tolerance_fields.py +++ b/erpnext/patches/v12_0/rename_tolerance_fields.py @@ -7,8 +7,8 @@ def execute(): frappe.reload_doc("stock", "doctype", "stock_settings") frappe.reload_doc("accounts", "doctype", "accounts_settings") - rename_field('Stock Settings', "tolerance", "over_delivery_receipt_allowance") - rename_field('Item', "tolerance", "over_delivery_receipt_allowance") + rename_field("Stock Settings", "tolerance", "over_delivery_receipt_allowance") + rename_field("Item", "tolerance", "over_delivery_receipt_allowance") qty_allowance = frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") frappe.db.set_value("Accounts Settings", None, "over_delivery_receipt_allowance", qty_allowance) diff --git a/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py b/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py index ff332f771d3..21dd258eadc 100644 --- a/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py +++ b/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py @@ -2,5 +2,7 @@ import frappe def execute(): - frappe.db.sql("""UPDATE `tabUser` SET `home_settings` = REPLACE(`home_settings`, 'Accounting', 'Accounts')""") - frappe.cache().delete_key('home_settings') + frappe.db.sql( + """UPDATE `tabUser` SET `home_settings` = REPLACE(`home_settings`, 'Accounting', 'Accounts')""" + ) + frappe.cache().delete_key("home_settings") diff --git a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py index 198963df711..a4a85871ea2 100644 --- a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py +++ b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py @@ -6,51 +6,79 @@ import frappe def execute(): - warehouse_perm = frappe.get_all("User Permission", - fields=["count(*) as p_count", "is_default", "user"], filters={"allow": "Warehouse"}, group_by="user") + warehouse_perm = frappe.get_all( + "User Permission", + fields=["count(*) as p_count", "is_default", "user"], + filters={"allow": "Warehouse"}, + group_by="user", + ) if not warehouse_perm: return execute_patch = False for perm_data in warehouse_perm: - if perm_data.p_count == 1 or (perm_data.p_count > 1 and frappe.get_all("User Permission", - filters = {"user": perm_data.user, "allow": "warehouse", "is_default": 1}, limit=1)): + if perm_data.p_count == 1 or ( + perm_data.p_count > 1 + and frappe.get_all( + "User Permission", + filters={"user": perm_data.user, "allow": "warehouse", "is_default": 1}, + limit=1, + ) + ): execute_patch = True break - if not execute_patch: return + if not execute_patch: + return for doctype in ["Sales Invoice", "Delivery Note"]: - if not frappe.get_meta(doctype + ' Item').get_field("target_warehouse").hidden: continue + if not frappe.get_meta(doctype + " Item").get_field("target_warehouse").hidden: + continue cond = "" if doctype == "Sales Invoice": cond = " AND parent_doc.update_stock = 1" - data = frappe.db.sql(""" SELECT parent_doc.name as name, child_doc.name as child_name + data = frappe.db.sql( + """ SELECT parent_doc.name as name, child_doc.name as child_name FROM `tab{doctype}` parent_doc, `tab{doctype} Item` child_doc WHERE parent_doc.name = child_doc.parent AND parent_doc.docstatus < 2 AND child_doc.target_warehouse is not null AND child_doc.target_warehouse != '' AND child_doc.creation > '2020-04-16' {cond} - """.format(doctype=doctype, cond=cond), as_dict=1) + """.format( + doctype=doctype, cond=cond + ), + as_dict=1, + ) if data: names = [d.child_name for d in data] - frappe.db.sql(""" UPDATE `tab{0} Item` set target_warehouse = null - WHERE name in ({1}) """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names)) + frappe.db.sql( + """ UPDATE `tab{0} Item` set target_warehouse = null + WHERE name in ({1}) """.format( + doctype, ",".join(["%s"] * len(names)) + ), + tuple(names), + ) - frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null + frappe.db.sql( + """ UPDATE `tabPacked Item` set target_warehouse = null WHERE parenttype = '{0}' and parent_detail_docname in ({1}) - """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names)) + """.format( + doctype, ",".join(["%s"] * len(names)) + ), + tuple(names), + ) parent_names = list(set([d.name for d in data])) for d in parent_names: doc = frappe.get_doc(doctype, d) - if doc.docstatus != 1: continue + if doc.docstatus != 1: + continue doc.docstatus = 2 doc.update_stock_ledger() @@ -61,9 +89,13 @@ def execute(): doc.update_stock_ledger() doc.make_gl_entries() - if frappe.get_meta('Sales Order Item').get_field("target_warehouse").hidden: - frappe.db.sql(""" UPDATE `tabSales Order Item` set target_warehouse = null - WHERE creation > '2020-04-16' and docstatus < 2 """) + if frappe.get_meta("Sales Order Item").get_field("target_warehouse").hidden: + frappe.db.sql( + """ UPDATE `tabSales Order Item` set target_warehouse = null + WHERE creation > '2020-04-16' and docstatus < 2 """ + ) - frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null - WHERE creation > '2020-04-16' and docstatus < 2 and parenttype = 'Sales Order' """) + frappe.db.sql( + """ UPDATE `tabPacked Item` set target_warehouse = null + WHERE creation > '2020-04-16' and docstatus < 2 and parenttype = 'Sales Order' """ + ) diff --git a/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py index b76e34abe13..d88593b4984 100644 --- a/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py +++ b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py @@ -3,12 +3,16 @@ import frappe def execute(): - frappe.reload_doc('selling', 'doctype', 'sales_order_item', force=True) - frappe.reload_doc('buying', 'doctype', 'purchase_order_item', force=True) + frappe.reload_doc("selling", "doctype", "sales_order_item", force=True) + frappe.reload_doc("buying", "doctype", "purchase_order_item", force=True) - for doctype in ('Sales Order Item', 'Purchase Order Item'): - frappe.db.sql(""" + for doctype in ("Sales Order Item", "Purchase Order Item"): + frappe.db.sql( + """ UPDATE `tab{0}` SET against_blanket_order = 1 WHERE ifnull(blanket_order, '') != '' - """.format(doctype)) + """.format( + doctype + ) + ) diff --git a/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py b/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py index b4f8a0631a6..37af989549f 100644 --- a/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py +++ b/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py @@ -1,8 +1,9 @@ - import frappe def execute(): frappe.reload_doc("accounts", "doctype", "accounts_settings") - frappe.db.set_value("Accounts Settings", None, "automatically_process_deferred_accounting_entry", 1) + frappe.db.set_value( + "Accounts Settings", None, "automatically_process_deferred_accounting_entry", 1 + ) diff --git a/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py index d3045a1a576..a5b4c66ce87 100644 --- a/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py +++ b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py @@ -2,9 +2,11 @@ import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'expense_claim_detail') - frappe.db.sql(""" + frappe.reload_doc("hr", "doctype", "expense_claim_detail") + frappe.db.sql( + """ UPDATE `tabExpense Claim Detail` child, `tabExpense Claim` par SET child.cost_center = par.cost_center WHERE child.parent = par.name - """) + """ + ) diff --git a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py index fe580ce0236..952f64be2e7 100644 --- a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py +++ b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py @@ -1,11 +1,10 @@ - import frappe from frappe.utils import cint def execute(): - '''Get 'Disable CWIP Accounting value' from Asset Settings, set it in 'Enable Capital Work in Progress Accounting' field - in Company, delete Asset Settings ''' + """Get 'Disable CWIP Accounting value' from Asset Settings, set it in 'Enable Capital Work in Progress Accounting' field + in Company, delete Asset Settings""" if frappe.db.exists("DocType", "Asset Settings"): frappe.reload_doctype("Asset Category") diff --git a/erpnext/patches/v12_0/set_default_batch_size.py b/erpnext/patches/v12_0/set_default_batch_size.py index 6fb69456dd1..ac3e2f47ee5 100644 --- a/erpnext/patches/v12_0/set_default_batch_size.py +++ b/erpnext/patches/v12_0/set_default_batch_size.py @@ -2,18 +2,22 @@ import frappe def execute(): - frappe.reload_doc("manufacturing", "doctype", "bom_operation") - frappe.reload_doc("manufacturing", "doctype", "work_order_operation") + frappe.reload_doc("manufacturing", "doctype", "bom_operation") + frappe.reload_doc("manufacturing", "doctype", "work_order_operation") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabBOM Operation` bo SET bo.batch_size = 1 - """) - frappe.db.sql(""" + """ + ) + frappe.db.sql( + """ UPDATE `tabWork Order Operation` wop SET wop.batch_size = 1 - """) + """ + ) diff --git a/erpnext/patches/v12_0/set_default_homepage_type.py b/erpnext/patches/v12_0/set_default_homepage_type.py index 1e4333aa466..d70b28efd85 100644 --- a/erpnext/patches/v12_0/set_default_homepage_type.py +++ b/erpnext/patches/v12_0/set_default_homepage_type.py @@ -2,4 +2,4 @@ import frappe def execute(): - frappe.db.set_value('Homepage', 'Homepage', 'hero_section_based_on', 'Default') + frappe.db.set_value("Homepage", "Homepage", "hero_section_based_on", "Default") diff --git a/erpnext/patches/v12_0/set_default_payroll_based_on.py b/erpnext/patches/v12_0/set_default_payroll_based_on.py index b70bb18b60b..de641c65a1c 100644 --- a/erpnext/patches/v12_0/set_default_payroll_based_on.py +++ b/erpnext/patches/v12_0/set_default_payroll_based_on.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v12_0/set_default_shopify_app_type.py b/erpnext/patches/v12_0/set_default_shopify_app_type.py index c712287c0dc..41516ba850c 100644 --- a/erpnext/patches/v12_0/set_default_shopify_app_type.py +++ b/erpnext/patches/v12_0/set_default_shopify_app_type.py @@ -1,7 +1,6 @@ - import frappe def execute(): - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings') - frappe.db.set_value('Shopify Settings', None, 'app_type', 'Private') + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings") + frappe.db.set_value("Shopify Settings", None, "app_type", "Private") diff --git a/erpnext/patches/v12_0/set_employee_preferred_emails.py b/erpnext/patches/v12_0/set_employee_preferred_emails.py index f6eb12e2b9f..a6159c6b6b0 100644 --- a/erpnext/patches/v12_0/set_employee_preferred_emails.py +++ b/erpnext/patches/v12_0/set_employee_preferred_emails.py @@ -2,9 +2,11 @@ import frappe def execute(): - employees = frappe.get_all("Employee", + employees = frappe.get_all( + "Employee", filters={"prefered_email": ""}, - fields=["name", "prefered_contact_email", "company_email", "personal_email", "user_id"]) + fields=["name", "prefered_contact_email", "company_email", "personal_email", "user_id"], + ) for employee in employees: if not employee.prefered_contact_email: @@ -13,4 +15,6 @@ def execute(): preferred_email_field = frappe.scrub(employee.prefered_contact_email) preferred_email = employee.get(preferred_email_field) - frappe.db.set_value("Employee", employee.name, "prefered_email", preferred_email, update_modified=False) + frappe.db.set_value( + "Employee", employee.name, "prefered_email", preferred_email, update_modified=False + ) diff --git a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py index 47d4eb599b8..96f80a057b4 100644 --- a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py +++ b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py @@ -1,17 +1,21 @@ - import frappe from six import iteritems def execute(): - frappe.reload_doctype('Landed Cost Taxes and Charges') + frappe.reload_doctype("Landed Cost Taxes and Charges") - company_account_map = frappe._dict(frappe.db.sql(""" + company_account_map = frappe._dict( + frappe.db.sql( + """ SELECT name, expenses_included_in_valuation from `tabCompany` - """)) + """ + ) + ) for company, account in iteritems(company_account_map): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLanded Cost Taxes and Charges` t, `tabLanded Cost Voucher` l SET @@ -20,9 +24,12 @@ def execute(): l.docstatus = 1 AND l.company = %s AND t.parent = l.name - """, (account, company)) + """, + (account, company), + ) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLanded Cost Taxes and Charges` t, `tabStock Entry` s SET @@ -31,4 +38,6 @@ def execute(): s.docstatus = 1 AND s.company = %s AND t.parent = s.name - """, (account, company)) + """, + (account, company), + ) diff --git a/erpnext/patches/v12_0/set_gst_category.py b/erpnext/patches/v12_0/set_gst_category.py index 094e2a3134b..126a73bd3d0 100644 --- a/erpnext/patches/v12_0/set_gst_category.py +++ b/erpnext/patches/v12_0/set_gst_category.py @@ -5,48 +5,62 @@ from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('accounts', 'doctype', 'Tax Category') + frappe.reload_doc("accounts", "doctype", "Tax Category") make_custom_fields() - for doctype in ['Sales Invoice', 'Purchase Invoice']: - has_column = frappe.db.has_column(doctype,'invoice_type') + for doctype in ["Sales Invoice", "Purchase Invoice"]: + has_column = frappe.db.has_column(doctype, "invoice_type") if has_column: update_map = { - 'Regular': 'Registered Regular', - 'Export': 'Overseas', - 'SEZ': 'SEZ', - 'Deemed Export': 'Deemed Export', + "Regular": "Registered Regular", + "Export": "Overseas", + "SEZ": "SEZ", + "Deemed Export": "Deemed Export", } for old, new in update_map.items(): - frappe.db.sql("UPDATE `tab{doctype}` SET gst_category = %s where invoice_type = %s".format(doctype=doctype), (new, old)) #nosec + frappe.db.sql( + "UPDATE `tab{doctype}` SET gst_category = %s where invoice_type = %s".format(doctype=doctype), + (new, old), + ) # nosec - frappe.delete_doc('Custom Field', 'Sales Invoice-invoice_type') - frappe.delete_doc('Custom Field', 'Purchase Invoice-invoice_type') + frappe.delete_doc("Custom Field", "Sales Invoice-invoice_type") + frappe.delete_doc("Custom Field", "Purchase Invoice-invoice_type") itc_update_map = { "ineligible": "Ineligible", "input service": "Input Service Distributor", "capital goods": "Import Of Capital Goods", - "input": "All Other ITC" + "input": "All Other ITC", } - has_gst_fields = frappe.db.has_column('Purchase Invoice','eligibility_for_itc') + has_gst_fields = frappe.db.has_column("Purchase Invoice", "eligibility_for_itc") if has_gst_fields: for old, new in itc_update_map.items(): - frappe.db.sql("UPDATE `tabPurchase Invoice` SET eligibility_for_itc = %s where eligibility_for_itc = %s ", (new, old)) + frappe.db.sql( + "UPDATE `tabPurchase Invoice` SET eligibility_for_itc = %s where eligibility_for_itc = %s ", + (new, old), + ) for doctype in ["Customer", "Supplier"]: - frappe.db.sql(""" UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Registered Regular" - where t3.link_name = t1.name and t3.parent = t2.name and t2.gstin IS NOT NULL and t2.gstin != '' """.format(doctype=doctype)) #nosec + frappe.db.sql( + """ UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Registered Regular" + where t3.link_name = t1.name and t3.parent = t2.name and t2.gstin IS NOT NULL and t2.gstin != '' """.format( + doctype=doctype + ) + ) # nosec - frappe.db.sql(""" UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Overseas" - where t3.link_name = t1.name and t3.parent = t2.name and t2.country != 'India' """.format(doctype=doctype)) #nosec + frappe.db.sql( + """ UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Overseas" + where t3.link_name = t1.name and t3.parent = t2.name and t2.country != 'India' """.format( + doctype=doctype + ) + ) # nosec diff --git a/erpnext/patches/v12_0/set_job_offer_applicant_email.py b/erpnext/patches/v12_0/set_job_offer_applicant_email.py index 7dd8492081e..0e3b5c4d2aa 100644 --- a/erpnext/patches/v12_0/set_job_offer_applicant_email.py +++ b/erpnext/patches/v12_0/set_job_offer_applicant_email.py @@ -4,9 +4,11 @@ import frappe def execute(): frappe.reload_doc("hr", "doctype", "job_offer") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabJob Offer` AS offer SET applicant_email = (SELECT email_id FROM `tabJob Applicant` WHERE name = offer.job_applicant) - """) + """ + ) diff --git a/erpnext/patches/v12_0/set_lead_title_field.py b/erpnext/patches/v12_0/set_lead_title_field.py index 86e00038f6c..eda3007e29b 100644 --- a/erpnext/patches/v12_0/set_lead_title_field.py +++ b/erpnext/patches/v12_0/set_lead_title_field.py @@ -3,9 +3,11 @@ import frappe def execute(): frappe.reload_doc("crm", "doctype", "lead") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLead` SET title = IF(organization_lead = 1, company_name, lead_name) - """) + """ + ) diff --git a/erpnext/patches/v12_0/set_multi_uom_in_rfq.py b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py index a8e0ec1f816..4d19007313d 100644 --- a/erpnext/patches/v12_0/set_multi_uom_in_rfq.py +++ b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py @@ -6,10 +6,12 @@ import frappe def execute(): - frappe.reload_doc('buying', 'doctype', 'request_for_quotation_item') + frappe.reload_doc("buying", "doctype", "request_for_quotation_item") - frappe.db.sql("""UPDATE `tabRequest for Quotation Item` + frappe.db.sql( + """UPDATE `tabRequest for Quotation Item` SET stock_uom = uom, conversion_factor = 1, - stock_qty = qty""") + stock_qty = qty""" + ) diff --git a/erpnext/patches/v12_0/set_payment_entry_status.py b/erpnext/patches/v12_0/set_payment_entry_status.py index f8792952d8b..2a3a3ad45e4 100644 --- a/erpnext/patches/v12_0/set_payment_entry_status.py +++ b/erpnext/patches/v12_0/set_payment_entry_status.py @@ -3,8 +3,10 @@ import frappe def execute(): frappe.reload_doctype("Payment Entry") - frappe.db.sql("""update `tabPayment Entry` set status = CASE + frappe.db.sql( + """update `tabPayment Entry` set status = CASE WHEN docstatus = 1 THEN 'Submitted' WHEN docstatus = 2 THEN 'Cancelled' ELSE 'Draft' - END;""") + END;""" + ) diff --git a/erpnext/patches/v12_0/set_permission_einvoicing.py b/erpnext/patches/v12_0/set_permission_einvoicing.py index 01cab14db9d..65d70977d20 100644 --- a/erpnext/patches/v12_0/set_permission_einvoicing.py +++ b/erpnext/patches/v12_0/set_permission_einvoicing.py @@ -5,7 +5,7 @@ from erpnext.regional.italy.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'Italy'}) + company = frappe.get_all("Company", filters={"country": "Italy"}) if not company: return @@ -14,6 +14,6 @@ def execute(): frappe.reload_doc("regional", "doctype", "import_supplier_invoice") - add_permission('Import Supplier Invoice', 'Accounts Manager', 0) - update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'write', 1) - update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'create', 1) + add_permission("Import Supplier Invoice", "Accounts Manager", 0) + update_permission_property("Import Supplier Invoice", "Accounts Manager", 0, "write", 1) + update_permission_property("Import Supplier Invoice", "Accounts Manager", 0, "create", 1) diff --git a/erpnext/patches/v12_0/set_priority_for_support.py b/erpnext/patches/v12_0/set_priority_for_support.py index 6d7d0993460..a8a07e76eab 100644 --- a/erpnext/patches/v12_0/set_priority_for_support.py +++ b/erpnext/patches/v12_0/set_priority_for_support.py @@ -4,21 +4,20 @@ import frappe def execute(): frappe.reload_doc("support", "doctype", "issue_priority") frappe.reload_doc("support", "doctype", "service_level_priority") - frappe.reload_doc('support', 'doctype', 'issue') + frappe.reload_doc("support", "doctype", "issue") set_issue_priority() set_priority_for_issue() set_priorities_service_level() set_priorities_service_level_agreement() + def set_issue_priority(): # Adds priority from issue to Issue Priority DocType as Priority is a new DocType. for priority in frappe.get_meta("Issue").get_field("priority").options.split("\n"): if priority and not frappe.db.exists("Issue Priority", priority): - frappe.get_doc({ - "doctype": "Issue Priority", - "name": priority - }).insert(ignore_permissions=True) + frappe.get_doc({"doctype": "Issue Priority", "name": priority}).insert(ignore_permissions=True) + def set_priority_for_issue(): # Sets priority for Issues as Select field is changed to Link field. @@ -28,38 +27,63 @@ def set_priority_for_issue(): for issue in issue_priority: frappe.db.set_value("Issue", issue.name, "priority", issue.priority) + def set_priorities_service_level(): # Migrates "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period" to Child Table # as a Service Level can have multiple priorities try: - service_level_priorities = frappe.get_list("Service Level", fields=["name", "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period"]) + service_level_priorities = frappe.get_list( + "Service Level", + fields=[ + "name", + "priority", + "response_time", + "response_time_period", + "resolution_time", + "resolution_time_period", + ], + ) frappe.reload_doc("support", "doctype", "service_level") frappe.reload_doc("support", "doctype", "support_settings") - frappe.db.set_value('Support Settings', None, 'track_service_level_agreement', 1) + frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) for service_level in service_level_priorities: if service_level: doc = frappe.get_doc("Service Level", service_level.name) if not doc.priorities: - doc.append("priorities", { - "priority": service_level.priority, - "default_priority": 1, - "response_time": service_level.response_time, - "response_time_period": service_level.response_time_period, - "resolution_time": service_level.resolution_time, - "resolution_time_period": service_level.resolution_time_period - }) + doc.append( + "priorities", + { + "priority": service_level.priority, + "default_priority": 1, + "response_time": service_level.response_time, + "response_time_period": service_level.response_time_period, + "resolution_time": service_level.resolution_time, + "resolution_time_period": service_level.resolution_time_period, + }, + ) doc.flags.ignore_validate = True doc.save(ignore_permissions=True) except frappe.db.TableMissingError: frappe.reload_doc("support", "doctype", "service_level") + def set_priorities_service_level_agreement(): # Migrates "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period" to Child Table # as a Service Level Agreement can have multiple priorities try: - service_level_agreement_priorities = frappe.get_list("Service Level Agreement", fields=["name", "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period"]) + service_level_agreement_priorities = frappe.get_list( + "Service Level Agreement", + fields=[ + "name", + "priority", + "response_time", + "response_time_period", + "resolution_time", + "resolution_time_period", + ], + ) frappe.reload_doc("support", "doctype", "service_level_agreement") @@ -71,14 +95,17 @@ def set_priorities_service_level_agreement(): doc.entity_type = "Customer" doc.entity = doc.customer - doc.append("priorities", { - "priority": service_level_agreement.priority, - "default_priority": 1, - "response_time": service_level_agreement.response_time, - "response_time_period": service_level_agreement.response_time_period, - "resolution_time": service_level_agreement.resolution_time, - "resolution_time_period": service_level_agreement.resolution_time_period - }) + doc.append( + "priorities", + { + "priority": service_level_agreement.priority, + "default_priority": 1, + "response_time": service_level_agreement.response_time, + "response_time_period": service_level_agreement.response_time_period, + "resolution_time": service_level_agreement.resolution_time, + "resolution_time_period": service_level_agreement.resolution_time_period, + }, + ) doc.flags.ignore_validate = True doc.save(ignore_permissions=True) except frappe.db.TableMissingError: diff --git a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py index 9c851ddcee1..562ebed757b 100644 --- a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py +++ b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py @@ -4,12 +4,14 @@ from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_ def execute(): - frappe.reload_doctype('Sales Order Item') - frappe.reload_doctype('Sales Order') + frappe.reload_doctype("Sales Order Item") + frappe.reload_doctype("Sales Order") - for d in frappe.get_all('Work Order', - fields = ['sales_order', 'sales_order_item'], - filters={'sales_order': ('!=', ''), 'sales_order_item': ('!=', '')}): + for d in frappe.get_all( + "Work Order", + fields=["sales_order", "sales_order_item"], + filters={"sales_order": ("!=", ""), "sales_order_item": ("!=", "")}, + ): # update produced qty in sales order update_produced_qty_in_so_item(d.sales_order, d.sales_order_item) diff --git a/erpnext/patches/v12_0/set_production_capacity_in_workstation.py b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py index 14956a23b43..0246c35447b 100644 --- a/erpnext/patches/v12_0/set_production_capacity_in_workstation.py +++ b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py @@ -1,9 +1,10 @@ - import frappe def execute(): - frappe.reload_doc("manufacturing", "doctype", "workstation") + frappe.reload_doc("manufacturing", "doctype", "workstation") - frappe.db.sql(""" UPDATE `tabWorkstation` - SET production_capacity = 1 """) + frappe.db.sql( + """ UPDATE `tabWorkstation` + SET production_capacity = 1 """ + ) diff --git a/erpnext/patches/v12_0/set_published_in_hub_tracked_item.py b/erpnext/patches/v12_0/set_published_in_hub_tracked_item.py index a0fe8aa2fe8..d90464ea8e5 100644 --- a/erpnext/patches/v12_0/set_published_in_hub_tracked_item.py +++ b/erpnext/patches/v12_0/set_published_in_hub_tracked_item.py @@ -1,4 +1,3 @@ - import frappe @@ -7,7 +6,9 @@ def execute(): if not frappe.db.a_row_exists("Hub Tracked Item"): return - frappe.db.sql(''' + frappe.db.sql( + """ Update `tabHub Tracked Item` SET published = 1 - ''') + """ + ) diff --git a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py index 05b86f3204a..2edf0f54fcf 100644 --- a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py +++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py @@ -1,4 +1,3 @@ - from collections import defaultdict import frappe @@ -6,29 +5,36 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'delivery_note_item', force=True) - frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item', force=True) + frappe.reload_doc("stock", "doctype", "delivery_note_item", force=True) + frappe.reload_doc("stock", "doctype", "purchase_receipt_item", force=True) def map_rows(doc_row, return_doc_row, detail_field, doctype): """Map rows after identifying similar ones.""" - frappe.db.sql(""" UPDATE `tab{doctype} Item` set {detail_field} = '{doc_row_name}' - where name = '{return_doc_row_name}'""" \ - .format(doctype=doctype, - detail_field=detail_field, - doc_row_name=doc_row.get('name'), - return_doc_row_name=return_doc_row.get('name'))) #nosec + frappe.db.sql( + """ UPDATE `tab{doctype} Item` set {detail_field} = '{doc_row_name}' + where name = '{return_doc_row_name}'""".format( + doctype=doctype, + detail_field=detail_field, + doc_row_name=doc_row.get("name"), + return_doc_row_name=return_doc_row.get("name"), + ) + ) # nosec def row_is_mappable(doc_row, return_doc_row, detail_field): """Checks if two rows are similar enough to be mapped.""" if doc_row.item_code == return_doc_row.item_code and not return_doc_row.get(detail_field): - if doc_row.get('batch_no') and return_doc_row.get('batch_no') and doc_row.batch_no == return_doc_row.batch_no: + if ( + doc_row.get("batch_no") + and return_doc_row.get("batch_no") + and doc_row.batch_no == return_doc_row.batch_no + ): return True - elif doc_row.get('serial_no') and return_doc_row.get('serial_no'): - doc_sn = doc_row.serial_no.split('\n') - return_doc_sn = return_doc_row.serial_no.split('\n') + elif doc_row.get("serial_no") and return_doc_row.get("serial_no"): + doc_sn = doc_row.serial_no.split("\n") + return_doc_sn = return_doc_row.serial_no.split("\n") if set(doc_sn) & set(return_doc_sn): # if two rows have serial nos in common, map them @@ -43,12 +49,17 @@ def execute(): """Returns a map of documents and it's return documents. Format => { 'document' : ['return_document_1','return_document_2'] }""" - return_against_documents = frappe.db.sql(""" + return_against_documents = frappe.db.sql( + """ SELECT return_against as document, name as return_document FROM `tab{doctype}` WHERE - is_return = 1 and docstatus = 1""".format(doctype=doctype),as_dict=1) #nosec + is_return = 1 and docstatus = 1""".format( + doctype=doctype + ), + as_dict=1, + ) # nosec for entry in return_against_documents: return_document_map[entry.document].append(entry.return_document) @@ -59,7 +70,7 @@ def execute(): """Map each row of the original document in the return document.""" mapped = [] return_document_map = defaultdict(list) - detail_field = "purchase_receipt_item" if doctype=="Purchase Receipt" else "dn_detail" + detail_field = "purchase_receipt_item" if doctype == "Purchase Receipt" else "dn_detail" child_doc = frappe.scrub("{0} Item".format(doctype)) frappe.reload_doc("stock", "doctype", child_doc) @@ -68,25 +79,27 @@ def execute(): count = 0 - #iterate through original documents and its return documents + # iterate through original documents and its return documents for docname in return_document_map: doc_items = frappe.get_cached_doc(doctype, docname).get("items") for return_doc in return_document_map[docname]: return_doc_items = frappe.get_cached_doc(doctype, return_doc).get("items") - #iterate through return document items and original document items for mapping + # iterate through return document items and original document items for mapping for return_item in return_doc_items: for doc_item in doc_items: - if row_is_mappable(doc_item, return_item, detail_field) and doc_item.get('name') not in mapped: + if ( + row_is_mappable(doc_item, return_item, detail_field) and doc_item.get("name") not in mapped + ): map_rows(doc_item, return_item, detail_field, doctype) - mapped.append(doc_item.get('name')) + mapped.append(doc_item.get("name")) break else: continue # commit after every 100 sql updates count += 1 - if count%100 == 0: + if count % 100 == 0: frappe.db.commit() set_document_detail_in_return_document("Purchase Receipt") diff --git a/erpnext/patches/v12_0/set_quotation_status.py b/erpnext/patches/v12_0/set_quotation_status.py index adfc5e96798..bebedd3a498 100644 --- a/erpnext/patches/v12_0/set_quotation_status.py +++ b/erpnext/patches/v12_0/set_quotation_status.py @@ -1,8 +1,9 @@ - import frappe def execute(): - frappe.db.sql(""" UPDATE `tabQuotation` set status = 'Open' - where docstatus = 1 and status = 'Submitted' """) + frappe.db.sql( + """ UPDATE `tabQuotation` set status = 'Open' + where docstatus = 1 and status = 'Submitted' """ + ) diff --git a/erpnext/patches/v12_0/set_received_qty_in_material_request_as_per_stock_uom.py b/erpnext/patches/v12_0/set_received_qty_in_material_request_as_per_stock_uom.py index 83db7961d9d..dcdd19fbb65 100644 --- a/erpnext/patches/v12_0/set_received_qty_in_material_request_as_per_stock_uom.py +++ b/erpnext/patches/v12_0/set_received_qty_in_material_request_as_per_stock_uom.py @@ -1,15 +1,17 @@ - import frappe def execute(): - purchase_receipts = frappe.db.sql(""" + purchase_receipts = frappe.db.sql( + """ SELECT parent from `tabPurchase Receipt Item` WHERE material_request is not null AND docstatus=1 - """,as_dict=1) + """, + as_dict=1, + ) purchase_receipts = set([d.parent for d in purchase_receipts]) @@ -17,15 +19,15 @@ def execute(): doc = frappe.get_doc("Purchase Receipt", pr) doc.status_updater = [ { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Material Request Item', - 'join_field': 'material_request_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Material Request', - 'target_parent_field': 'per_received', - 'target_ref_field': 'stock_qty', - 'source_field': 'stock_qty', - 'percent_join_field': 'material_request' + "source_dt": "Purchase Receipt Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "received_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_received", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + "percent_join_field": "material_request", } ] doc.update_qty() diff --git a/erpnext/patches/v12_0/set_serial_no_status.py b/erpnext/patches/v12_0/set_serial_no_status.py index 57206ced3e2..8ab342e9f2b 100644 --- a/erpnext/patches/v12_0/set_serial_no_status.py +++ b/erpnext/patches/v12_0/set_serial_no_status.py @@ -1,20 +1,24 @@ - import frappe from frappe.utils import getdate, nowdate def execute(): - frappe.reload_doc('stock', 'doctype', 'serial_no') + frappe.reload_doc("stock", "doctype", "serial_no") - serial_no_list = frappe.db.sql("""select name, delivery_document_type, warranty_expiry_date, warehouse from `tabSerial No` - where (status is NULL OR status='')""", as_dict = 1) + serial_no_list = frappe.db.sql( + """select name, delivery_document_type, warranty_expiry_date, warehouse from `tabSerial No` + where (status is NULL OR status='')""", + as_dict=1, + ) if len(serial_no_list) > 20000: frappe.db.auto_commit_on_many_writes = True for serial_no in serial_no_list: if serial_no.get("delivery_document_type"): status = "Delivered" - elif serial_no.get("warranty_expiry_date") and getdate(serial_no.get("warranty_expiry_date")) <= getdate(nowdate()): + elif serial_no.get("warranty_expiry_date") and getdate( + serial_no.get("warranty_expiry_date") + ) <= getdate(nowdate()): status = "Expired" elif not serial_no.get("warehouse"): status = "Inactive" diff --git a/erpnext/patches/v12_0/set_task_status.py b/erpnext/patches/v12_0/set_task_status.py index 1b4955a75be..1c6654e57ac 100644 --- a/erpnext/patches/v12_0/set_task_status.py +++ b/erpnext/patches/v12_0/set_task_status.py @@ -2,14 +2,16 @@ import frappe def execute(): - frappe.reload_doctype('Task') + frappe.reload_doctype("Task") # add "Completed" if customized - property_setter_name = frappe.db.exists('Property Setter', dict(doc_type='Task', field_name = 'status', property = 'options')) + property_setter_name = frappe.db.exists( + "Property Setter", dict(doc_type="Task", field_name="status", property="options") + ) if property_setter_name: - property_setter = frappe.get_doc('Property Setter', property_setter_name) + property_setter = frappe.get_doc("Property Setter", property_setter_name) if not "Completed" in property_setter.value: - property_setter.value = property_setter.value + '\nCompleted' + property_setter.value = property_setter.value + "\nCompleted" property_setter.save() # renamed default status to Completed as status "Closed" is ambiguous diff --git a/erpnext/patches/v12_0/set_total_batch_quantity.py b/erpnext/patches/v12_0/set_total_batch_quantity.py index 7296eaa33d8..068e0a6a4c1 100644 --- a/erpnext/patches/v12_0/set_total_batch_quantity.py +++ b/erpnext/patches/v12_0/set_total_batch_quantity.py @@ -5,7 +5,12 @@ def execute(): frappe.reload_doc("stock", "doctype", "batch") for batch in frappe.get_all("Batch", fields=["name", "batch_id"]): - batch_qty = frappe.db.get_value("Stock Ledger Entry", - {"docstatus": 1, "batch_no": batch.batch_id, "is_cancelled": 0}, - "sum(actual_qty)") or 0.0 + batch_qty = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"docstatus": 1, "batch_no": batch.batch_id, "is_cancelled": 0}, + "sum(actual_qty)", + ) + or 0.0 + ) frappe.db.set_value("Batch", batch.name, "batch_qty", batch_qty, update_modified=False) diff --git a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py index 300d0f2ba47..1e390819cd5 100644 --- a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py +++ b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py @@ -6,6 +6,8 @@ import frappe def execute(): - frappe.reload_doc("stock", "doctype", "pick_list") - frappe.db.sql("""UPDATE `tabPick List` set purpose = 'Delivery' - WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """) + frappe.reload_doc("stock", "doctype", "pick_list") + frappe.db.sql( + """UPDATE `tabPick List` set purpose = 'Delivery' + WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """ + ) diff --git a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py index d79628f2a90..94322cd1971 100644 --- a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py +++ b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py @@ -1,9 +1,10 @@ - import frappe def execute(): frappe.reload_doc("buying", "doctype", "supplier_quotation") - frappe.db.sql("""UPDATE `tabSupplier Quotation` + frappe.db.sql( + """UPDATE `tabSupplier Quotation` SET valid_till = DATE_ADD(transaction_date , INTERVAL 1 MONTH) - WHERE docstatus < 2""") + WHERE docstatus < 2""" + ) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index af2e60fd79a..f704f977aa3 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -6,53 +5,128 @@ from erpnext.regional.india.setup import add_permissions, add_print_formats def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return frappe.reload_doc("custom", "doctype", "custom_field") frappe.reload_doc("regional", "doctype", "e_invoice_settings") custom_fields = { - 'Sales Invoice': [ - dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, - depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - - dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), - - dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), - - dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), - - dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), - - dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) + "Sales Invoice": [ + dict( + fieldname="irn", + label="IRN", + fieldtype="Data", + read_only=1, + insert_after="customer", + no_copy=1, + print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0', + ), + dict( + fieldname="ack_no", + label="Ack. No.", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="irn", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="ack_date", + label="Ack. Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_no", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="irn_cancelled", + label="IRN Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval:(doc.irn_cancelled === 1)", + read_only=1, + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="eway_bill_cancelled", + label="E-Way Bill Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval:(doc.eway_bill_cancelled === 1)", + read_only=1, + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="signed_einvoice", + fieldtype="Code", + options="JSON", + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="signed_qr_code", + fieldtype="Code", + options="JSON", + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="qrcode_image", + label="QRCode", + fieldtype="Attach Image", + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), ] } create_custom_fields(custom_fields, update=True) add_permissions() add_print_formats() - einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + einvoice_cond = ( + 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + ) t = { - 'mode_of_transport': [{'default': None}], - 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}], - 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], - 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], - 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], - 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], - 'ewaybill': [ - {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'}, - {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'} - ] + "mode_of_transport": [{"default": None}], + "distance": [{"mandatory_depends_on": f"eval:{einvoice_cond} && doc.transporter"}], + "gst_vehicle_type": [ + {"mandatory_depends_on": f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'} + ], + "lr_date": [ + { + "mandatory_depends_on": f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)' + } + ], + "lr_no": [ + { + "mandatory_depends_on": f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)' + } + ], + "vehicle_no": [ + {"mandatory_depends_on": f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'} + ], + "ewaybill": [ + {"read_only_depends_on": "eval:doc.irn && doc.ewaybill"}, + {"depends_on": "eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)"}, + ], } for field, conditions in t.items(): for c in conditions: [(prop, value)] = c.items() - frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value) + frappe.db.set_value("Custom Field", {"fieldname": field}, prop, value) diff --git a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py index 630a9046a4e..5508d260660 100644 --- a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py +++ b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py @@ -1,13 +1,14 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'}) + irn_cancelled_field = frappe.db.exists( + "Custom Field", {"dt": "Sales Invoice", "fieldname": "irn_cancelled"} + ) if irn_cancelled_field: - frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn') - frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0) + frappe.db.set_value("Custom Field", irn_cancelled_field, "depends_on", "eval: doc.irn") + frappe.db.set_value("Custom Field", irn_cancelled_field, "read_only", 0) diff --git a/erpnext/patches/v12_0/stock_entry_enhancements.py b/erpnext/patches/v12_0/stock_entry_enhancements.py index 94d8ff9cde3..db099a304cf 100644 --- a/erpnext/patches/v12_0/stock_entry_enhancements.py +++ b/erpnext/patches/v12_0/stock_entry_enhancements.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -10,44 +9,61 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): create_stock_entry_types() - company = frappe.db.get_value("Company", {'country': 'India'}, 'name') + company = frappe.db.get_value("Company", {"country": "India"}, "name") if company: add_gst_hsn_code_field() + def create_stock_entry_types(): - frappe.reload_doc('stock', 'doctype', 'stock_entry_type') - frappe.reload_doc('stock', 'doctype', 'stock_entry') + frappe.reload_doc("stock", "doctype", "stock_entry_type") + frappe.reload_doc("stock", "doctype", "stock_entry") - for purpose in ["Material Issue", "Material Receipt", "Material Transfer", - "Material Transfer for Manufacture", "Material Consumption for Manufacture", "Manufacture", - "Repack", "Send to Subcontractor"]: + for purpose in [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + ]: - ste_type = frappe.get_doc({ - 'doctype': 'Stock Entry Type', - 'name': purpose, - 'purpose': purpose - }) + ste_type = frappe.get_doc({"doctype": "Stock Entry Type", "name": purpose, "purpose": purpose}) try: ste_type.insert() except frappe.DuplicateEntryError: pass - frappe.db.sql(" UPDATE `tabStock Entry` set purpose = 'Send to Subcontractor' where purpose = 'Subcontract'") + frappe.db.sql( + " UPDATE `tabStock Entry` set purpose = 'Send to Subcontractor' where purpose = 'Subcontract'" + ) frappe.db.sql(" UPDATE `tabStock Entry` set stock_entry_type = purpose ") + def add_gst_hsn_code_field(): custom_fields = { - 'Stock Entry Detail': [dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Data', fetch_from='item_code.gst_hsn_code', - insert_after='description', allow_on_submit=1, print_hide=0)] + "Stock Entry Detail": [ + dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Data", + fetch_from="item_code.gst_hsn_code", + insert_after="description", + allow_on_submit=1, + print_hide=0, + ) + ] } - create_custom_fields(custom_fields, ignore_validate = frappe.flags.in_patch, update=True) + create_custom_fields(custom_fields, ignore_validate=frappe.flags.in_patch, update=True) - frappe.db.sql(""" update `tabStock Entry Detail`, `tabItem` + frappe.db.sql( + """ update `tabStock Entry Detail`, `tabItem` SET `tabStock Entry Detail`.gst_hsn_code = `tabItem`.gst_hsn_code Where `tabItem`.name = `tabStock Entry Detail`.item_code and `tabItem`.gst_hsn_code is not null - """) + """ + ) diff --git a/erpnext/patches/v12_0/unhide_cost_center_field.py b/erpnext/patches/v12_0/unhide_cost_center_field.py index 72450212872..5f91ef6260a 100644 --- a/erpnext/patches/v12_0/unhide_cost_center_field.py +++ b/erpnext/patches/v12_0/unhide_cost_center_field.py @@ -6,9 +6,11 @@ import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabProperty Setter` WHERE doc_type in ('Sales Invoice', 'Purchase Invoice', 'Payment Entry') AND field_name = 'cost_center' AND property = 'hidden' - """) + """ + ) diff --git a/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py index 1a677f91672..332609b8466 100644 --- a/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py +++ b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py @@ -1,29 +1,30 @@ - import frappe def execute(): - """ - set proper customer and supplier details for item price - based on selling and buying values - """ + """ + set proper customer and supplier details for item price + based on selling and buying values + """ - # update for selling - frappe.db.sql( - """UPDATE `tabItem Price` ip, `tabPrice List` pl + # update for selling + frappe.db.sql( + """UPDATE `tabItem Price` ip, `tabPrice List` pl SET ip.`reference` = ip.`customer`, ip.`supplier` = NULL WHERE ip.`selling` = 1 AND ip.`buying` = 0 AND (ip.`supplier` IS NOT NULL OR ip.`supplier` = '') AND ip.`price_list` = pl.`name` - AND pl.`enabled` = 1""") + AND pl.`enabled` = 1""" + ) - # update for buying - frappe.db.sql( - """UPDATE `tabItem Price` ip, `tabPrice List` pl + # update for buying + frappe.db.sql( + """UPDATE `tabItem Price` ip, `tabPrice List` pl SET ip.`reference` = ip.`supplier`, ip.`customer` = NULL WHERE ip.`selling` = 0 AND ip.`buying` = 1 AND (ip.`customer` IS NOT NULL OR ip.`customer` = '') AND ip.`price_list` = pl.`name` - AND pl.`enabled` = 1""") + AND pl.`enabled` = 1""" + ) diff --git a/erpnext/patches/v12_0/update_address_template_for_india.py b/erpnext/patches/v12_0/update_address_template_for_india.py index 64a2e41587f..27b1bb6dc18 100644 --- a/erpnext/patches/v12_0/update_address_template_for_india.py +++ b/erpnext/patches/v12_0/update_address_template_for_india.py @@ -8,7 +8,7 @@ from erpnext.regional.address_template.setup import set_up_address_templates def execute(): - if frappe.db.get_value('Company', {'country': 'India'}, 'name'): - address_template = frappe.db.get_value('Address Template', 'India', 'template') + if frappe.db.get_value("Company", {"country": "India"}, "name"): + address_template = frappe.db.get_value("Address Template", "India", "template") if not address_template or "gstin" not in address_template: - set_up_address_templates(default_country='India') + set_up_address_templates(default_country="India") diff --git a/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py index 024cb2b7630..725ff1e4797 100644 --- a/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py +++ b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py @@ -2,7 +2,9 @@ import frappe def execute(): - job = frappe.db.exists('Scheduled Job Type', 'patient_appointment.send_appointment_reminder') + job = frappe.db.exists("Scheduled Job Type", "patient_appointment.send_appointment_reminder") if job: - method = 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder' - frappe.db.set_value('Scheduled Job Type', job, 'method', method) + method = ( + "erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" + ) + frappe.db.set_value("Scheduled Job Type", job, "method", method) diff --git a/erpnext/patches/v12_0/update_bom_in_so_mr.py b/erpnext/patches/v12_0/update_bom_in_so_mr.py index ee9f90df2a4..114f65d100e 100644 --- a/erpnext/patches/v12_0/update_bom_in_so_mr.py +++ b/erpnext/patches/v12_0/update_bom_in_so_mr.py @@ -1,4 +1,3 @@ - import frappe @@ -11,11 +10,15 @@ def execute(): if doctype == "Material Request": condition = " and doc.per_ordered < 100 and doc.material_request_type = 'Manufacture'" - frappe.db.sql(""" UPDATE `tab{doc}` as doc, `tab{doc} Item` as child_doc, tabItem as item + frappe.db.sql( + """ UPDATE `tab{doc}` as doc, `tab{doc} Item` as child_doc, tabItem as item SET child_doc.bom_no = item.default_bom WHERE child_doc.item_code = item.name and child_doc.docstatus < 2 and child_doc.parent = doc.name and item.default_bom is not null and item.default_bom != '' {cond} - """.format(doc = doctype, cond = condition)) + """.format( + doc=doctype, cond=condition + ) + ) diff --git a/erpnext/patches/v12_0/update_due_date_in_gle.py b/erpnext/patches/v12_0/update_due_date_in_gle.py index 032c2bb9538..a1c4f51ad01 100644 --- a/erpnext/patches/v12_0/update_due_date_in_gle.py +++ b/erpnext/patches/v12_0/update_due_date_in_gle.py @@ -1,18 +1,20 @@ - import frappe def execute(): - frappe.reload_doc("accounts", "doctype", "gl_entry") + frappe.reload_doc("accounts", "doctype", "gl_entry") - for doctype in ["Sales Invoice", "Purchase Invoice", "Journal Entry"]: - frappe.reload_doc("accounts", "doctype", frappe.scrub(doctype)) + for doctype in ["Sales Invoice", "Purchase Invoice", "Journal Entry"]: + frappe.reload_doc("accounts", "doctype", frappe.scrub(doctype)) - frappe.db.sql(""" UPDATE `tabGL Entry`, `tab{doctype}` + frappe.db.sql( + """ UPDATE `tabGL Entry`, `tab{doctype}` SET `tabGL Entry`.due_date = `tab{doctype}`.due_date WHERE `tabGL Entry`.voucher_no = `tab{doctype}`.name and `tabGL Entry`.party is not null and `tabGL Entry`.voucher_type in ('Sales Invoice', 'Purchase Invoice', 'Journal Entry') - and `tabGL Entry`.account in (select name from `tabAccount` where account_type in ('Receivable', 'Payable'))""" #nosec - .format(doctype=doctype)) + and `tabGL Entry`.account in (select name from `tabAccount` where account_type in ('Receivable', 'Payable'))""".format( # nosec + doctype=doctype + ) + ) diff --git a/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py index 48febc5aa45..570b77b88e1 100644 --- a/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py +++ b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py @@ -1,25 +1,24 @@ - import frappe from frappe.utils import add_days, getdate, today def execute(): - if frappe.db.exists('DocType', 'Email Campaign'): - email_campaign = frappe.get_all('Email Campaign') - for campaign in email_campaign: - doc = frappe.get_doc("Email Campaign",campaign["name"]) - send_after_days = [] + if frappe.db.exists("DocType", "Email Campaign"): + email_campaign = frappe.get_all("Email Campaign") + for campaign in email_campaign: + doc = frappe.get_doc("Email Campaign", campaign["name"]) + send_after_days = [] - camp = frappe.get_doc("Campaign", doc.campaign_name) - for entry in camp.get("campaign_schedules"): - send_after_days.append(entry.send_after_days) - if send_after_days: - end_date = add_days(getdate(doc.start_date), max(send_after_days)) - doc.db_set("end_date", end_date) - today_date = getdate(today()) - if doc.start_date > today_date: - doc.db_set("status", "Scheduled") - elif end_date >= today_date: - doc.db_set("status", "In Progress") - elif end_date < today_date: - doc.db_set("status", "Completed") + camp = frappe.get_doc("Campaign", doc.campaign_name) + for entry in camp.get("campaign_schedules"): + send_after_days.append(entry.send_after_days) + if send_after_days: + end_date = add_days(getdate(doc.start_date), max(send_after_days)) + doc.db_set("end_date", end_date) + today_date = getdate(today()) + if doc.start_date > today_date: + doc.db_set("status", "Scheduled") + elif end_date >= today_date: + doc.db_set("status", "In Progress") + elif end_date < today_date: + doc.db_set("status", "Completed") diff --git a/erpnext/patches/v12_0/update_ewaybill_field_position.py b/erpnext/patches/v12_0/update_ewaybill_field_position.py index ace3aceebba..24a834b05c7 100644 --- a/erpnext/patches/v12_0/update_ewaybill_field_position.py +++ b/erpnext/patches/v12_0/update_ewaybill_field_position.py @@ -1,9 +1,8 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return @@ -15,14 +14,16 @@ def execute(): ewaybill_field.flags.ignore_validate = True - ewaybill_field.update({ - 'fieldname': 'ewaybill', - 'label': 'e-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', - 'allow_on_submit': 1, - 'insert_after': 'tax_id', - 'translatable': 0 - }) + ewaybill_field.update( + { + "fieldname": "ewaybill", + "label": "e-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:(doc.docstatus === 1)", + "allow_on_submit": 1, + "insert_after": "tax_id", + "translatable": 0, + } + ) ewaybill_field.save() diff --git a/erpnext/patches/v12_0/update_gst_category.py b/erpnext/patches/v12_0/update_gst_category.py index 20dce94f2ff..16168f022cd 100644 --- a/erpnext/patches/v12_0/update_gst_category.py +++ b/erpnext/patches/v12_0/update_gst_category.py @@ -1,20 +1,23 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company: - return + company = frappe.get_all("Company", filters={"country": "India"}) + if not company: + return - frappe.db.sql(""" UPDATE `tabSales Invoice` set gst_category = 'Unregistered' + frappe.db.sql( + """ UPDATE `tabSales Invoice` set gst_category = 'Unregistered' where gst_category = 'Registered Regular' and ifnull(customer_gstin, '')='' and ifnull(billing_address_gstin,'')='' - """) + """ + ) - frappe.db.sql(""" UPDATE `tabPurchase Invoice` set gst_category = 'Unregistered' + frappe.db.sql( + """ UPDATE `tabPurchase Invoice` set gst_category = 'Unregistered' where gst_category = 'Registered Regular' and ifnull(supplier_gstin, '')='' - """) + """ + ) diff --git a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py index 4e24a638f98..5ca0d5d47d9 100644 --- a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py +++ b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py @@ -1,79 +1,81 @@ - import frappe from frappe.model.utils.rename_field import rename_field from frappe.modules import get_doctype_module, scrub field_rename_map = { - 'Healthcare Settings': [ - ['patient_master_name', 'patient_name_by'], - ['max_visit', 'max_visits'], - ['reg_sms', 'send_registration_msg'], - ['reg_msg', 'registration_msg'], - ['app_con', 'send_appointment_confirmation'], - ['app_con_msg', 'appointment_confirmation_msg'], - ['no_con', 'avoid_confirmation'], - ['app_rem', 'send_appointment_reminder'], - ['app_rem_msg', 'appointment_reminder_msg'], - ['rem_before', 'remind_before'], - ['manage_customer', 'link_customer_to_patient'], - ['create_test_on_si_submit', 'create_lab_test_on_si_submit'], - ['require_sample_collection', 'create_sample_collection_for_lab_test'], - ['require_test_result_approval', 'lab_test_approval_required'], - ['manage_appointment_invoice_automatically', 'automate_appointment_invoicing'] + "Healthcare Settings": [ + ["patient_master_name", "patient_name_by"], + ["max_visit", "max_visits"], + ["reg_sms", "send_registration_msg"], + ["reg_msg", "registration_msg"], + ["app_con", "send_appointment_confirmation"], + ["app_con_msg", "appointment_confirmation_msg"], + ["no_con", "avoid_confirmation"], + ["app_rem", "send_appointment_reminder"], + ["app_rem_msg", "appointment_reminder_msg"], + ["rem_before", "remind_before"], + ["manage_customer", "link_customer_to_patient"], + ["create_test_on_si_submit", "create_lab_test_on_si_submit"], + ["require_sample_collection", "create_sample_collection_for_lab_test"], + ["require_test_result_approval", "lab_test_approval_required"], + ["manage_appointment_invoice_automatically", "automate_appointment_invoicing"], ], - 'Drug Prescription':[ - ['use_interval', 'usage_interval'], - ['in_every', 'interval_uom'] + "Drug Prescription": [["use_interval", "usage_interval"], ["in_every", "interval_uom"]], + "Lab Test Template": [ + ["sample_quantity", "sample_qty"], + ["sample_collection_details", "sample_details"], ], - 'Lab Test Template':[ - ['sample_quantity', 'sample_qty'], - ['sample_collection_details', 'sample_details'] + "Sample Collection": [ + ["sample_quantity", "sample_qty"], + ["sample_collection_details", "sample_details"], ], - 'Sample Collection':[ - ['sample_quantity', 'sample_qty'], - ['sample_collection_details', 'sample_details'] - ], - 'Fee Validity': [ - ['max_visit', 'max_visits'] - ] + "Fee Validity": [["max_visit", "max_visits"]], } + def execute(): for dn in field_rename_map: - if frappe.db.exists('DocType', dn): - if dn == 'Healthcare Settings': - frappe.reload_doctype('Healthcare Settings') + if frappe.db.exists("DocType", dn): + if dn == "Healthcare Settings": + frappe.reload_doctype("Healthcare Settings") else: frappe.reload_doc(get_doctype_module(dn), "doctype", scrub(dn)) for dt, field_list in field_rename_map.items(): - if frappe.db.exists('DocType', dt): + if frappe.db.exists("DocType", dt): for field in field_list: - if dt == 'Healthcare Settings': + if dt == "Healthcare Settings": rename_field(dt, field[0], field[1]) elif frappe.db.has_column(dt, field[0]): rename_field(dt, field[0], field[1]) # first name mandatory in Patient - if frappe.db.exists('DocType', 'Patient'): + if frappe.db.exists("DocType", "Patient"): patients = frappe.db.sql("select name, patient_name from `tabPatient`", as_dict=1) - frappe.reload_doc('healthcare', 'doctype', 'patient') + frappe.reload_doc("healthcare", "doctype", "patient") for entry in patients: - name = entry.patient_name.split(' ') - frappe.db.set_value('Patient', entry.name, 'first_name', name[0]) + name = entry.patient_name.split(" ") + frappe.db.set_value("Patient", entry.name, "first_name", name[0]) # mark Healthcare Practitioner status as Disabled - if frappe.db.exists('DocType', 'Healthcare Practitioner'): - practitioners = frappe.db.sql("select name from `tabHealthcare Practitioner` where 'active'= 0", as_dict=1) + if frappe.db.exists("DocType", "Healthcare Practitioner"): + practitioners = frappe.db.sql( + "select name from `tabHealthcare Practitioner` where 'active'= 0", as_dict=1 + ) practitioners_lst = [p.name for p in practitioners] - frappe.reload_doc('healthcare', 'doctype', 'healthcare_practitioner') + frappe.reload_doc("healthcare", "doctype", "healthcare_practitioner") if practitioners_lst: - frappe.db.sql("update `tabHealthcare Practitioner` set status = 'Disabled' where name IN %(practitioners)s""", {"practitioners": practitioners_lst}) + frappe.db.sql( + "update `tabHealthcare Practitioner` set status = 'Disabled' where name IN %(practitioners)s" + "", + {"practitioners": practitioners_lst}, + ) # set Clinical Procedure status - if frappe.db.exists('DocType', 'Clinical Procedure'): - frappe.reload_doc('healthcare', 'doctype', 'clinical_procedure') - frappe.db.sql(""" + if frappe.db.exists("DocType", "Clinical Procedure"): + frappe.reload_doc("healthcare", "doctype", "clinical_procedure") + frappe.db.sql( + """ UPDATE `tabClinical Procedure` SET @@ -81,57 +83,49 @@ def execute(): WHEN status = 'Draft' THEN 0 ELSE 1 END) - """) + """ + ) # set complaints and diagnosis in table multiselect in Patient Encounter - if frappe.db.exists('DocType', 'Patient Encounter'): - field_list = [ - ['visit_department', 'medical_department'], - ['type', 'appointment_type'] - ] - encounter_details = frappe.db.sql("""select symptoms, diagnosis, name from `tabPatient Encounter`""", as_dict=True) - frappe.reload_doc('healthcare', 'doctype', 'patient_encounter') - frappe.reload_doc('healthcare', 'doctype', 'patient_encounter_symptom') - frappe.reload_doc('healthcare', 'doctype', 'patient_encounter_diagnosis') + if frappe.db.exists("DocType", "Patient Encounter"): + field_list = [["visit_department", "medical_department"], ["type", "appointment_type"]] + encounter_details = frappe.db.sql( + """select symptoms, diagnosis, name from `tabPatient Encounter`""", as_dict=True + ) + frappe.reload_doc("healthcare", "doctype", "patient_encounter") + frappe.reload_doc("healthcare", "doctype", "patient_encounter_symptom") + frappe.reload_doc("healthcare", "doctype", "patient_encounter_diagnosis") for field in field_list: if frappe.db.has_column(dt, field[0]): rename_field(dt, field[0], field[1]) for entry in encounter_details: - doc = frappe.get_doc('Patient Encounter', entry.name) - symptoms = entry.symptoms.split('\n') if entry.symptoms else [] + doc = frappe.get_doc("Patient Encounter", entry.name) + symptoms = entry.symptoms.split("\n") if entry.symptoms else [] for symptom in symptoms: - if not frappe.db.exists('Complaint', symptom): - frappe.get_doc({ - 'doctype': 'Complaint', - 'complaints': symptom - }).insert() - row = doc.append('symptoms', { - 'complaint': symptom - }) + if not frappe.db.exists("Complaint", symptom): + frappe.get_doc({"doctype": "Complaint", "complaints": symptom}).insert() + row = doc.append("symptoms", {"complaint": symptom}) row.db_update() - diagnosis = entry.diagnosis.split('\n') if entry.diagnosis else [] + diagnosis = entry.diagnosis.split("\n") if entry.diagnosis else [] for d in diagnosis: - if not frappe.db.exists('Diagnosis', d): - frappe.get_doc({ - 'doctype': 'Diagnosis', - 'diagnosis': d - }).insert() - row = doc.append('diagnosis', { - 'diagnosis': d - }) + if not frappe.db.exists("Diagnosis", d): + frappe.get_doc({"doctype": "Diagnosis", "diagnosis": d}).insert() + row = doc.append("diagnosis", {"diagnosis": d}) row.db_update() doc.db_update() - if frappe.db.exists('DocType', 'Fee Validity'): + if frappe.db.exists("DocType", "Fee Validity"): # update fee validity status - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabFee Validity` SET status = (CASE WHEN visited >= max_visits THEN 'Completed' ELSE 'Pending' END) - """) + """ + ) diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py index 06b6673a5d2..398dd700eda 100644 --- a/erpnext/patches/v12_0/update_is_cancelled_field.py +++ b/erpnext/patches/v12_0/update_is_cancelled_field.py @@ -1,30 +1,36 @@ - import frappe def execute(): - #handle type casting for is_cancelled field + # handle type casting for is_cancelled field module_doctypes = ( - ('stock', 'Stock Ledger Entry'), - ('stock', 'Serial No'), - ('accounts', 'GL Entry') + ("stock", "Stock Ledger Entry"), + ("stock", "Serial No"), + ("accounts", "GL Entry"), ) for module, doctype in module_doctypes: - if (not frappe.db.has_column(doctype, "is_cancelled") + if ( + not frappe.db.has_column(doctype, "is_cancelled") or frappe.db.get_column_type(doctype, "is_cancelled").lower() == "int(1)" ): continue - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{doctype}` SET is_cancelled = 0 - where is_cancelled in ('', NULL, 'No')""" - .format(doctype=doctype)) - frappe.db.sql(""" + where is_cancelled in ('', 'No') or is_cancelled is NULL""".format( + doctype=doctype + ) + ) + frappe.db.sql( + """ UPDATE `tab{doctype}` SET is_cancelled = 1 - where is_cancelled = 'Yes'""" - .format(doctype=doctype)) + where is_cancelled = 'Yes'""".format( + doctype=doctype + ) + ) frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) diff --git a/erpnext/patches/v12_0/update_item_tax_template_company.py b/erpnext/patches/v12_0/update_item_tax_template_company.py index a737cb2dfa5..489f70d4497 100644 --- a/erpnext/patches/v12_0/update_item_tax_template_company.py +++ b/erpnext/patches/v12_0/update_item_tax_template_company.py @@ -1,14 +1,13 @@ - import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'item_tax_template') + frappe.reload_doc("accounts", "doctype", "item_tax_template") - item_tax_template_list = frappe.get_list('Item Tax Template') - for template in item_tax_template_list: - doc = frappe.get_doc('Item Tax Template', template.name) - for tax in doc.taxes: - doc.company = frappe.get_value('Account', tax.tax_type, 'company') - break - doc.save() + item_tax_template_list = frappe.get_list("Item Tax Template") + for template in item_tax_template_list: + doc = frappe.get_doc("Item Tax Template", template.name) + for tax in doc.taxes: + doc.company = frappe.get_value("Account", tax.tax_type, "company") + break + doc.save() diff --git a/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py b/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py index 53e83216673..7dc0af9a1aa 100644 --- a/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py +++ b/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py @@ -1,4 +1,3 @@ - import frappe from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -7,15 +6,21 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( def execute(): - accounting_dimensions = frappe.db.sql("""select fieldname from - `tabAccounting Dimension`""", as_dict=1) + accounting_dimensions = frappe.db.sql( + """select fieldname from + `tabAccounting Dimension`""", + as_dict=1, + ) doclist = get_doctypes_with_dimensions() for dimension in accounting_dimensions: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustom Field` SET owner = 'Administrator' WHERE fieldname = %s - AND dt IN (%s)""" % #nosec - ('%s', ', '.join(['%s']* len(doclist))), tuple([dimension.fieldname] + doclist)) + AND dt IN (%s)""" + % ("%s", ", ".join(["%s"] * len(doclist))), # nosec + tuple([dimension.fieldname] + doclist), + ) diff --git a/erpnext/patches/v12_0/update_price_list_currency_in_bom.py b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py index e0382d818ea..5710320e62d 100644 --- a/erpnext/patches/v12_0/update_price_list_currency_in_bom.py +++ b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py @@ -1,4 +1,3 @@ - import frappe from frappe.utils import getdate @@ -9,16 +8,19 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "bom") frappe.reload_doc("manufacturing", "doctype", "bom_item") - frappe.db.sql(""" UPDATE `tabBOM`, `tabPrice List` + frappe.db.sql( + """ UPDATE `tabBOM`, `tabPrice List` SET `tabBOM`.price_list_currency = `tabPrice List`.currency, `tabBOM`.plc_conversion_rate = 1.0 WHERE `tabBOM`.buying_price_list = `tabPrice List`.name AND `tabBOM`.docstatus < 2 AND `tabBOM`.rm_cost_as_per = 'Price List' - """) + """ + ) - for d in frappe.db.sql(""" + for d in frappe.db.sql( + """ SELECT bom.creation, bom.name, bom.price_list_currency as currency, company.default_currency as company_currency @@ -26,8 +28,11 @@ def execute(): `tabBOM` as bom, `tabCompany` as company WHERE bom.company = company.name AND bom.rm_cost_as_per = 'Price List' AND - bom.price_list_currency != company.default_currency AND bom.docstatus < 2""", as_dict=1): - plc_conversion_rate = get_exchange_rate(d.currency, - d.company_currency, getdate(d.creation), "for_buying") + bom.price_list_currency != company.default_currency AND bom.docstatus < 2""", + as_dict=1, + ): + plc_conversion_rate = get_exchange_rate( + d.currency, d.company_currency, getdate(d.creation), "for_buying" + ) - frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) + frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) diff --git a/erpnext/patches/v12_0/update_price_or_product_discount.py b/erpnext/patches/v12_0/update_price_or_product_discount.py index 86105a469db..64344c8cd42 100644 --- a/erpnext/patches/v12_0/update_price_or_product_discount.py +++ b/erpnext/patches/v12_0/update_price_or_product_discount.py @@ -1,9 +1,10 @@ - import frappe def execute(): frappe.reload_doc("accounts", "doctype", "pricing_rule") - frappe.db.sql(""" UPDATE `tabPricing Rule` SET price_or_product_discount = 'Price' - WHERE ifnull(price_or_product_discount,'') = '' """) + frappe.db.sql( + """ UPDATE `tabPricing Rule` SET price_or_product_discount = 'Price' + WHERE ifnull(price_or_product_discount,'') = '' """ + ) diff --git a/erpnext/patches/v12_0/update_pricing_rule_fields.py b/erpnext/patches/v12_0/update_pricing_rule_fields.py index b7c36ae7780..8da06b0bda0 100644 --- a/erpnext/patches/v12_0/update_pricing_rule_fields.py +++ b/erpnext/patches/v12_0/update_pricing_rule_fields.py @@ -4,68 +4,120 @@ import frappe -parentfield = { - 'item_code': 'items', - 'item_group': 'item_groups', - 'brand': 'brands' -} +parentfield = {"item_code": "items", "item_group": "item_groups", "brand": "brands"} + def execute(): - if not frappe.get_all('Pricing Rule', limit=1): + if not frappe.get_all("Pricing Rule", limit=1): return - frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail') - doctypes = {'Supplier Quotation': 'buying', 'Purchase Order': 'buying', 'Purchase Invoice': 'accounts', - 'Purchase Receipt': 'stock', 'Quotation': 'selling', 'Sales Order': 'selling', - 'Sales Invoice': 'accounts', 'Delivery Note': 'stock'} + frappe.reload_doc("accounts", "doctype", "pricing_rule_detail") + doctypes = { + "Supplier Quotation": "buying", + "Purchase Order": "buying", + "Purchase Invoice": "accounts", + "Purchase Receipt": "stock", + "Quotation": "selling", + "Sales Order": "selling", + "Sales Invoice": "accounts", + "Delivery Note": "stock", + } for doctype, module in doctypes.items(): - frappe.reload_doc(module, 'doctype', frappe.scrub(doctype)) + frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) - child_doc = frappe.scrub(doctype) + '_item' - frappe.reload_doc(module, 'doctype', child_doc, force=True) + child_doc = frappe.scrub(doctype) + "_item" + frappe.reload_doc(module, "doctype", child_doc, force=True) - child_doctype = doctype + ' Item' + child_doctype = doctype + " Item" - frappe.db.sql(""" UPDATE `tab{child_doctype}` SET pricing_rules = pricing_rule + frappe.db.sql( + """ UPDATE `tab{child_doctype}` SET pricing_rules = pricing_rule WHERE docstatus < 2 and pricing_rule is not null and pricing_rule != '' - """.format(child_doctype= child_doctype)) + """.format( + child_doctype=child_doctype + ) + ) - data = frappe.db.sql(""" SELECT pricing_rule, name, parent, + data = frappe.db.sql( + """ SELECT pricing_rule, name, parent, parenttype, creation, modified, docstatus, modified_by, owner, name FROM `tab{child_doc}` where docstatus < 2 and pricing_rule is not null - and pricing_rule != ''""".format(child_doc=child_doctype), as_dict=1) + and pricing_rule != ''""".format( + child_doc=child_doctype + ), + as_dict=1, + ) values = [] for d in data: - values.append((d.pricing_rule, d.name, d.parent, 'pricing_rules', d.parenttype, - d.creation, d.modified, d.docstatus, d.modified_by, d.owner, frappe.generate_hash("", 10))) + values.append( + ( + d.pricing_rule, + d.name, + d.parent, + "pricing_rules", + d.parenttype, + d.creation, + d.modified, + d.docstatus, + d.modified_by, + d.owner, + frappe.generate_hash("", 10), + ) + ) if values: - frappe.db.sql(""" INSERT INTO + frappe.db.sql( + """ INSERT INTO `tabPricing Rule Detail` (`pricing_rule`, `child_docname`, `parent`, `parentfield`, `parenttype`, `creation`, `modified`, `docstatus`, `modified_by`, `owner`, `name`) - VALUES {values} """.format(values=', '.join(['%s'] * len(values))), tuple(values)) + VALUES {values} """.format( + values=", ".join(["%s"] * len(values)) + ), + tuple(values), + ) - frappe.reload_doc('accounts', 'doctype', 'pricing_rule') + frappe.reload_doc("accounts", "doctype", "pricing_rule") - for doctype, apply_on in {'Pricing Rule Item Code': 'Item Code', - 'Pricing Rule Item Group': 'Item Group', 'Pricing Rule Brand': 'Brand'}.items(): - frappe.reload_doc('accounts', 'doctype', frappe.scrub(doctype)) + for doctype, apply_on in { + "Pricing Rule Item Code": "Item Code", + "Pricing Rule Item Group": "Item Group", + "Pricing Rule Brand": "Brand", + }.items(): + frappe.reload_doc("accounts", "doctype", frappe.scrub(doctype)) field = frappe.scrub(apply_on) - data = frappe.get_all('Pricing Rule', fields=[field, "name", "creation", "modified", - "owner", "modified_by"], filters= {'apply_on': apply_on}) + data = frappe.get_all( + "Pricing Rule", + fields=[field, "name", "creation", "modified", "owner", "modified_by"], + filters={"apply_on": apply_on}, + ) values = [] for d in data: - values.append((d.get(field), d.name, parentfield.get(field), 'Pricing Rule', - d.creation, d.modified, d.owner, d.modified_by, frappe.generate_hash("", 10))) + values.append( + ( + d.get(field), + d.name, + parentfield.get(field), + "Pricing Rule", + d.creation, + d.modified, + d.owner, + d.modified_by, + frappe.generate_hash("", 10), + ) + ) if values: - frappe.db.sql(""" INSERT INTO + frappe.db.sql( + """ INSERT INTO `tab{doctype}` ({field}, parent, parentfield, parenttype, creation, modified, owner, modified_by, name) - VALUES {values} """.format(doctype=doctype, - field=field, values=', '.join(['%s'] * len(values))), tuple(values)) + VALUES {values} """.format( + doctype=doctype, field=field, values=", ".join(["%s"] * len(values)) + ), + tuple(values), + ) diff --git a/erpnext/patches/v12_0/update_production_plan_status.py b/erpnext/patches/v12_0/update_production_plan_status.py index 06fc503a33f..dc65ec25f21 100644 --- a/erpnext/patches/v12_0/update_production_plan_status.py +++ b/erpnext/patches/v12_0/update_production_plan_status.py @@ -6,7 +6,8 @@ import frappe def execute(): frappe.reload_doc("manufacturing", "doctype", "production_plan") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabProduction Plan` ppl SET status = "Completed" WHERE ppl.name IN ( @@ -28,4 +29,5 @@ def execute(): HAVING should_set = 1 ) ss ) - """) + """ + ) diff --git a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py index 25cf6b97e3b..d7e96fafd60 100644 --- a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py +++ b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py @@ -5,20 +5,22 @@ from erpnext.regional.india import states def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return # Update options in gst_state custom field - gst_state = frappe.get_doc('Custom Field', 'Address-gst_state') - gst_state.options = '\n'.join(states) + gst_state = frappe.get_doc("Custom Field", "Address-gst_state") + gst_state.options = "\n".join(states) gst_state.save() # Update gst_state and state code in existing address - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabAddress` SET gst_state = 'Dadra and Nagar Haveli and Daman and Diu', gst_state_number = 26 WHERE gst_state = 'Daman and Diu' - """) + """ + ) diff --git a/erpnext/patches/v12_0/update_uom_conversion_factor.py b/erpnext/patches/v12_0/update_uom_conversion_factor.py index 3184d1195f0..a09ac190e2e 100644 --- a/erpnext/patches/v12_0/update_uom_conversion_factor.py +++ b/erpnext/patches/v12_0/update_uom_conversion_factor.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py index 57fbaae9d8d..38a8500ac73 100644 --- a/erpnext/patches/v13_0/add_bin_unique_constraint.py +++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py @@ -14,13 +14,16 @@ def execute(): delete_broken_bins() delete_and_patch_duplicate_bins() + def delete_broken_bins(): # delete useless bins frappe.db.sql("delete from `tabBin` where item_code is null or warehouse is null") + def delete_and_patch_duplicate_bins(): - duplicate_bins = frappe.db.sql(""" + duplicate_bins = frappe.db.sql( + """ SELECT item_code, warehouse, count(*) as bin_count FROM @@ -29,18 +32,19 @@ def delete_and_patch_duplicate_bins(): item_code, warehouse HAVING bin_count > 1 - """, as_dict=1) + """, + as_dict=1, + ) for duplicate_bin in duplicate_bins: item_code = duplicate_bin.item_code warehouse = duplicate_bin.warehouse - existing_bins = frappe.get_list("Bin", - filters={ - "item_code": item_code, - "warehouse": warehouse - }, - fields=["name"], - order_by="creation",) + existing_bins = frappe.get_list( + "Bin", + filters={"item_code": item_code, "warehouse": warehouse}, + fields=["name"], + order_by="creation", + ) # keep last one existing_bins.pop() @@ -53,7 +57,7 @@ def delete_and_patch_duplicate_bins(): "indented_qty": get_indented_qty(item_code, warehouse), "ordered_qty": get_ordered_qty(item_code, warehouse), "planned_qty": get_planned_qty(item_code, warehouse), - "actual_qty": get_balance_qty_from_sle(item_code, warehouse) + "actual_qty": get_balance_qty_from_sle(item_code, warehouse), } bin = get_bin(item_code, warehouse) diff --git a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py index b34b5c1801f..353376b6038 100644 --- a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py +++ b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py @@ -7,13 +7,13 @@ from erpnext.regional.south_africa.setup import add_permissions, make_custom_fie def execute(): - company = frappe.get_all('Company', filters = {'country': 'South Africa'}) + company = frappe.get_all("Company", filters={"country": "South Africa"}) if not company: return - frappe.reload_doc('regional', 'doctype', 'south_africa_vat_settings') - frappe.reload_doc('regional', 'report', 'vat_audit_report') - frappe.reload_doc('accounts', 'doctype', 'south_africa_vat_account') + frappe.reload_doc("regional", "doctype", "south_africa_vat_settings") + frappe.reload_doc("regional", "report", "vat_audit_report") + frappe.reload_doc("accounts", "doctype", "south_africa_vat_account") make_custom_fields() add_permissions() diff --git a/erpnext/patches/v13_0/add_default_interview_notification_templates.py b/erpnext/patches/v13_0/add_default_interview_notification_templates.py index fa1b31c34bb..9a47efe8704 100644 --- a/erpnext/patches/v13_0/add_default_interview_notification_templates.py +++ b/erpnext/patches/v13_0/add_default_interview_notification_templates.py @@ -1,4 +1,3 @@ - import os import frappe @@ -6,32 +5,40 @@ from frappe import _ def execute(): - if not frappe.db.exists('Email Template', _('Interview Reminder')): - base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') - response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html')) + if not frappe.db.exists("Email Template", _("Interview Reminder")): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file( + os.path.join(base_path, "interview/interview_reminder_notification_template.html") + ) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _('Interview Reminder'), - 'response': response, - 'subject': _('Interview Reminder'), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Interview Reminder"), + "response": response, + "subject": _("Interview Reminder"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')): - base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') - response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html')) + if not frappe.db.exists("Email Template", _("Interview Feedback Reminder")): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file( + os.path.join(base_path, "interview/interview_feedback_reminder_template.html") + ) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _('Interview Feedback Reminder'), - 'response': response, - 'subject': _('Interview Feedback Reminder'), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Interview Feedback Reminder"), + "response": response, + "subject": _("Interview Feedback Reminder"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - hr_settings = frappe.get_doc('HR Settings') - hr_settings.interview_reminder_template = _('Interview Reminder') - hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder') + hr_settings = frappe.get_doc("HR Settings") + hr_settings.interview_reminder_template = _("Interview Reminder") + hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") hr_settings.flags.ignore_links = True hr_settings.save() diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py index bd18b9bd173..517a14a8300 100644 --- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -9,31 +9,34 @@ from erpnext.stock.stock_ledger import make_sl_entries def execute(): - if not frappe.db.has_column('Work Order', 'has_batch_no'): + if not frappe.db.has_column("Work Order", "has_batch_no"): return - frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings') - if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): + frappe.reload_doc("manufacturing", "doctype", "manufacturing_settings") + if cint( + frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") + ): return - frappe.reload_doc('manufacturing', 'doctype', 'work_order') + frappe.reload_doc("manufacturing", "doctype", "work_order") filters = { - 'docstatus': 1, - 'produced_qty': ('>', 0), - 'creation': ('>=', '2021-06-29 00:00:00'), - 'has_batch_no': 1 + "docstatus": 1, + "produced_qty": (">", 0), + "creation": (">=", "2021-06-29 00:00:00"), + "has_batch_no": 1, } - fields = ['name', 'production_item'] + fields = ["name", "production_item"] - work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)] + work_orders = [d.name for d in frappe.get_all("Work Order", filters=filters, fields=fields)] if not work_orders: return repost_stock_entries = [] - stock_entries = frappe.db.sql_list(''' + stock_entries = frappe.db.sql_list( + """ SELECT se.name FROM @@ -45,18 +48,20 @@ def execute(): ) ORDER BY se.posting_date, se.posting_time - ''', (work_orders,)) + """, + (work_orders,), + ) if stock_entries: - print('Length of stock entries', len(stock_entries)) + print("Length of stock entries", len(stock_entries)) for stock_entry in stock_entries: - doc = frappe.get_doc('Stock Entry', stock_entry) + doc = frappe.get_doc("Stock Entry", stock_entry) doc.set_work_order_details() doc.load_items_from_bom() doc.calculate_rate_and_amount() set_expense_account(doc) - doc.make_batches('t_warehouse') + doc.make_batches("t_warehouse") if doc.docstatus == 0: doc.save() @@ -67,10 +72,14 @@ def execute(): for repost_doc in repost_stock_entries: repost_future_sle_and_gle(repost_doc) + def set_expense_account(doc): for row in doc.items: if row.is_finished_item and not row.expense_account: - row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account') + row.expense_account = frappe.get_cached_value( + "Company", doc.company, "stock_adjustment_account" + ) + def repost_stock_entry(doc): doc.db_update() @@ -86,29 +95,36 @@ def repost_stock_entry(doc): try: make_sl_entries(sl_entries, True) except Exception: - print(f'SLE entries not posted for the stock entry {doc.name}') + print(f"SLE entries not posted for the stock entry {doc.name}") traceback = frappe.get_traceback() frappe.log_error(traceback) + def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row): - for d in doc.get('items'): + for d in doc.get("items"): if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name: - sle = doc.get_sl_entries(d, { - "warehouse": cstr(d.t_warehouse), - "actual_qty": flt(d.transfer_qty), - "incoming_rate": flt(d.valuation_rate) - }) + sle = doc.get_sl_entries( + d, + { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate), + }, + ) sle.recalculate_rate = 1 sl_entries.append(sle) + def repost_future_sle_and_gle(doc): - args = frappe._dict({ - "posting_date": doc.posting_date, - "posting_time": doc.posting_time, - "voucher_type": doc.doctype, - "voucher_no": doc.name, - "company": doc.company - }) + args = frappe._dict( + { + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "company": doc.company, + } + ) create_repost_item_valuation_entry(args) diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py index 3bf2762456e..7dce95c1b82 100644 --- a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py +++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py @@ -1,12 +1,13 @@ - import frappe def execute(): frappe.reload_doc("projects", "doctype", "project") - frappe.db.sql("""UPDATE `tabProject` + frappe.db.sql( + """UPDATE `tabProject` SET naming_series = 'PROJ-.####' WHERE - naming_series is NULL""") + naming_series is NULL""" + ) diff --git a/erpnext/patches/v13_0/add_po_to_global_search.py b/erpnext/patches/v13_0/add_po_to_global_search.py index 396d3343ba6..514cd343900 100644 --- a/erpnext/patches/v13_0/add_po_to_global_search.py +++ b/erpnext/patches/v13_0/add_po_to_global_search.py @@ -1,17 +1,14 @@ - import frappe def execute(): - global_search_settings = frappe.get_single("Global Search Settings") + global_search_settings = frappe.get_single("Global Search Settings") - if "Purchase Order" in ( - dt.document_type for dt in global_search_settings.allowed_in_global_search - ): - return + if "Purchase Order" in ( + dt.document_type for dt in global_search_settings.allowed_in_global_search + ): + return - global_search_settings.append( - "allowed_in_global_search", {"document_type": "Purchase Order"} - ) + global_search_settings.append("allowed_in_global_search", {"document_type": "Purchase Order"}) - global_search_settings.save(ignore_permissions=True) + global_search_settings.save(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/add_standard_navbar_items.py b/erpnext/patches/v13_0/add_standard_navbar_items.py index c739a7a93f9..24141b7862d 100644 --- a/erpnext/patches/v13_0/add_standard_navbar_items.py +++ b/erpnext/patches/v13_0/add_standard_navbar_items.py @@ -1,4 +1,3 @@ - # import frappe from erpnext.setup.install import add_standard_navbar_items diff --git a/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py b/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py index 5eb6ff44702..cc424c6d3e0 100644 --- a/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py +++ b/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py @@ -12,4 +12,4 @@ def execute(): "Amazon MWS Integration is moved to a separate app and will be removed from ERPNext in version-14.\n" "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations", fg="yellow", - ) \ No newline at end of file + ) diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py index a95f822d281..ee23747cc04 100644 --- a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py +++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py b/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py new file mode 100644 index 00000000000..0b00188e6a8 --- /dev/null +++ b/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py @@ -0,0 +1,16 @@ +import frappe + + +def execute(): + + # Erase all default item manufacturers that dont exist. + item = frappe.qb.DocType("Item") + manufacturer = frappe.qb.DocType("Manufacturer") + + ( + frappe.qb.update(item) + .set(item.default_item_manufacturer, None) + .left_join(manufacturer) + .on(item.default_item_manufacturer == manufacturer.name) + .where(manufacturer.name.isnull() & item.default_item_manufacturer.isnotnull()) + ).run() diff --git a/erpnext/patches/v13_0/change_default_pos_print_format.py b/erpnext/patches/v13_0/change_default_pos_print_format.py index 9664247ab41..be478a2b67f 100644 --- a/erpnext/patches/v13_0/change_default_pos_print_format.py +++ b/erpnext/patches/v13_0/change_default_pos_print_format.py @@ -1,4 +1,3 @@ - import frappe @@ -6,4 +5,5 @@ def execute(): frappe.db.sql( """UPDATE `tabPOS Profile` profile SET profile.`print_format` = 'POS Invoice' - WHERE profile.`print_format` = 'Point of Sale'""") + WHERE profile.`print_format` = 'Point of Sale'""" + ) diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py index 5e1df14d4e0..0ae3a3e3bd7 100644 --- a/erpnext/patches/v13_0/check_is_income_tax_component.py +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -10,33 +10,36 @@ import erpnext def execute(): - doctypes = ['salary_component', - 'Employee Tax Exemption Declaration', - 'Employee Tax Exemption Proof Submission', - 'Employee Tax Exemption Declaration Category', - 'Employee Tax Exemption Proof Submission Detail', - 'gratuity_rule', - 'gratuity_rule_slab', - 'gratuity_applicable_component' + doctypes = [ + "salary_component", + "Employee Tax Exemption Declaration", + "Employee Tax Exemption Proof Submission", + "Employee Tax Exemption Declaration Category", + "Employee Tax Exemption Proof Submission Detail", + "gratuity_rule", + "gratuity_rule_slab", + "gratuity_applicable_component", ] for doctype in doctypes: - frappe.reload_doc('Payroll', 'doctype', doctype, force=True) + frappe.reload_doc("Payroll", "doctype", doctype, force=True) - - reports = ['Professional Tax Deductions', 'Provident Fund Deductions', 'E-Invoice Summary'] + reports = ["Professional Tax Deductions", "Provident Fund Deductions", "E-Invoice Summary"] for report in reports: - frappe.reload_doc('Regional', 'Report', report) - frappe.reload_doc('Regional', 'Report', report) + frappe.reload_doc("Regional", "Report", report) + frappe.reload_doc("Regional", "Report", report) if erpnext.get_region() == "India": - create_custom_field('Salary Component', - dict(fieldname='component_type', - label='Component Type', - fieldtype='Select', - insert_after='description', - options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax', - depends_on='eval:doc.type == "Deduction"') + create_custom_field( + "Salary Component", + dict( + fieldname="component_type", + label="Component Type", + fieldtype="Select", + insert_after="description", + options="\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax", + depends_on='eval:doc.type == "Deduction"', + ), ) if frappe.db.exists("Salary Component", "Income Tax"): @@ -44,7 +47,9 @@ def execute(): if frappe.db.exists("Salary Component", "TDS"): frappe.db.set_value("Salary Component", "TDS", "is_income_tax_component", 1) - components = frappe.db.sql("select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1) + components = frappe.db.sql( + "select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1 + ) for component in components: frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1) @@ -52,4 +57,6 @@ def execute(): if frappe.db.exists("Salary Component", "Provident Fund"): frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund") if frappe.db.exists("Salary Component", "Professional Tax"): - frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax") + frappe.db.set_value( + "Salary Component", "Professional Tax", "component_type", "Professional Tax" + ) diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py index 7ef154e6066..efbb96c100a 100644 --- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -1,24 +1,25 @@ - import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter') + frappe.reload_doc("stock", "doctype", "quality_inspection_parameter") # get all distinct parameters from QI readigs table - reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"]) + reading_params = frappe.db.get_all( + "Quality Inspection Reading", fields=["distinct specification"] + ) reading_params = [d.specification for d in reading_params] # get all distinct parameters from QI Template as some may be unused in QI - template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"]) + template_params = frappe.db.get_all( + "Item Quality Inspection Parameter", fields=["distinct specification"] + ) template_params = [d.specification for d in template_params] params = list(set(reading_params + template_params)) for parameter in params: if not frappe.db.exists("Quality Inspection Parameter", parameter): - frappe.get_doc({ - "doctype": "Quality Inspection Parameter", - "parameter": parameter, - "description": parameter - }).insert(ignore_permissions=True) + frappe.get_doc( + {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py index d3ee3f8276c..020521d5b95 100644 --- a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py +++ b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py @@ -7,51 +7,57 @@ from erpnext.e_commerce.doctype.website_item.website_item import make_website_it def execute(): - """ - Convert all Item links to Website Item link values in - exisitng 'Item Card Group' Web Page Block data. - """ - frappe.reload_doc("e_commerce", "web_template", "item_card_group") + """ + Convert all Item links to Website Item link values in + exisitng 'Item Card Group' Web Page Block data. + """ + frappe.reload_doc("e_commerce", "web_template", "item_card_group") - blocks = frappe.db.get_all( - "Web Page Block", - filters={"web_template": "Item Card Group"}, - fields=["parent", "web_template_values", "name"] - ) + blocks = frappe.db.get_all( + "Web Page Block", + filters={"web_template": "Item Card Group"}, + fields=["parent", "web_template_values", "name"], + ) - fields = generate_fields_to_edit() + fields = generate_fields_to_edit() - for block in blocks: - web_template_value = json.loads(block.get('web_template_values')) + for block in blocks: + web_template_value = json.loads(block.get("web_template_values")) - for field in fields: - item = web_template_value.get(field) - if not item: - continue + for field in fields: + item = web_template_value.get(field) + if not item: + continue - if frappe.db.exists("Website Item", {"item_code": item}): - website_item = frappe.db.get_value("Website Item", {"item_code": item}) - else: - website_item = make_new_website_item(item) + if frappe.db.exists("Website Item", {"item_code": item}): + website_item = frappe.db.get_value("Website Item", {"item_code": item}) + else: + website_item = make_new_website_item(item) - if website_item: - web_template_value[field] = website_item + if website_item: + web_template_value[field] = website_item + + frappe.db.set_value( + "Web Page Block", block.name, "web_template_values", json.dumps(web_template_value) + ) - frappe.db.set_value("Web Page Block", block.name, "web_template_values", json.dumps(web_template_value)) def generate_fields_to_edit() -> List: - fields = [] - for i in range(1, 13): - fields.append(f"card_{i}_item") # fields like 'card_1_item', etc. + fields = [] + for i in range(1, 13): + fields.append(f"card_{i}_item") # fields like 'card_1_item', etc. + + return fields - return fields def make_new_website_item(item: str) -> Union[str, None]: - try: - doc = frappe.get_doc("Item", item) - web_item = make_website_item(doc) # returns [website_item.name, item_name] - return web_item[0] - except Exception: - title = f"{item}: Error while converting to Website Item " - frappe.log_error(title + "for Item Card Group Template" + "\n\n" + frappe.get_traceback(), title=title) - return None + try: + doc = frappe.get_doc("Item", item) + web_item = make_website_item(doc) # returns [website_item.name, item_name] + return web_item[0] + except Exception: + title = f"{item}: Error while converting to Website Item " + frappe.log_error( + title + "for Item Card Group Template" + "\n\n" + frappe.get_traceback(), title=title + ) + return None diff --git a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py new file mode 100644 index 00000000000..e8d0b593e6f --- /dev/null +++ b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py @@ -0,0 +1,94 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + "Add Field Filters, that are not standard fields in Website Item, as Custom Fields." + + def move_table_multiselect_data(docfield): + "Copy child table data (Table Multiselect) from Item to Website Item for a docfield." + table_multiselect_data = get_table_multiselect_data(docfield) + field = docfield.fieldname + + for row in table_multiselect_data: + # add copied multiselect data rows in Website Item + web_item = frappe.db.get_value("Website Item", {"item_code": row.parent}) + web_item_doc = frappe.get_doc("Website Item", web_item) + + child_doc = frappe.new_doc(docfield.options, web_item_doc, field) + + for field in ["name", "creation", "modified", "idx"]: + row[field] = None + + child_doc.update(row) + + child_doc.parenttype = "Website Item" + child_doc.parent = web_item + + child_doc.insert() + + def get_table_multiselect_data(docfield): + child_table = frappe.qb.DocType(docfield.options) + item = frappe.qb.DocType("Item") + + table_multiselect_data = ( # query table data for field + frappe.qb.from_(child_table) + .join(item) + .on(item.item_code == child_table.parent) + .select(child_table.star) + .where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1)) + ).run(as_dict=True) + + return table_multiselect_data + + settings = frappe.get_doc("E Commerce Settings") + + if not (settings.enable_field_filters or settings.filter_fields): + return + + item_meta = frappe.get_meta("Item") + valid_item_fields = [ + df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] + ] + + web_item_meta = frappe.get_meta("Website Item") + valid_web_item_fields = [ + df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] + ] + + for row in settings.filter_fields: + # skip if illegal field + if row.fieldname not in valid_item_fields: + continue + + # if Item field is not in Website Item, add it as a custom field + if row.fieldname not in valid_web_item_fields: + df = item_meta.get_field(row.fieldname) + create_custom_field( + "Website Item", + dict( + owner="Administrator", + fieldname=df.fieldname, + label=df.label, + fieldtype=df.fieldtype, + options=df.options, + description=df.description, + read_only=df.read_only, + no_copy=df.no_copy, + insert_after="on_backorder", + ), + ) + + # map field values + if df.fieldtype == "Table MultiSelect": + move_table_multiselect_data(df) + else: + frappe.db.sql( # nosemgrep + """ + UPDATE `tabWebsite Item` wi, `tabItem` i + SET wi.{0} = i.{0} + WHERE wi.item_code = i.item_code + """.format( + row.fieldname + ) + ) diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py b/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py new file mode 100644 index 00000000000..8a3f1d0a58f --- /dev/null +++ b/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py @@ -0,0 +1,39 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + count = 1 + for d in accounting_dimensions: + + if count % 2 == 0: + insert_after_field = "dimension_col_break" + else: + insert_after_field = "accounting_dimensions_section" + + for doctype in ["Purchase Order", "Purchase Receipt", "Sales Order"]: + + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": insert_after_field, + } + + create_custom_field(doctype, df, ignore_validate=False) + frappe.clear_cache(doctype=doctype) + + count += 1 diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py b/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py index 44501088102..51ab0e8b65c 100644 --- a/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py +++ b/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py @@ -3,9 +3,12 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field def execute(): - frappe.reload_doc('accounts', 'doctype', 'accounting_dimension') - accounting_dimensions = frappe.db.sql("""select fieldname, label, document_type, disabled from - `tabAccounting Dimension`""", as_dict=1) + frappe.reload_doc("accounts", "doctype", "accounting_dimension") + accounting_dimensions = frappe.db.sql( + """select fieldname, label, document_type, disabled from + `tabAccounting Dimension`""", + as_dict=1, + ) if not accounting_dimensions: return @@ -14,9 +17,9 @@ def execute(): for d in accounting_dimensions: if count % 2 == 0: - insert_after_field = 'dimension_col_break' + insert_after_field = "dimension_col_break" else: - insert_after_field = 'accounting_dimensions_section' + insert_after_field = "accounting_dimensions_section" for doctype in ["POS Invoice", "POS Invoice Item"]: @@ -32,10 +35,10 @@ def execute(): "label": d.label, "fieldtype": "Link", "options": d.document_type, - "insert_after": insert_after_field + "insert_after": insert_after_field, } - if df['fieldname'] not in fieldnames: + if df["fieldname"] not in fieldnames: create_custom_field(doctype, df) frappe.clear_cache(doctype=doctype) diff --git a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py index 313b0e9a2eb..2b8666d21b6 100644 --- a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py +++ b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py @@ -3,18 +3,18 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return custom_field = { - 'Finance Book': [ + "Finance Book": [ { - 'fieldname': 'for_income_tax', - 'label': 'For Income Tax', - 'fieldtype': 'Check', - 'insert_after': 'finance_book_name', - 'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.' + "fieldname": "for_income_tax", + "label": "For Income Tax", + "fieldtype": "Check", + "insert_after": "finance_book_name", + "description": "If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.", } ] } diff --git a/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py b/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py new file mode 100644 index 00000000000..3217eab43d6 --- /dev/null +++ b/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py @@ -0,0 +1,53 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + company = frappe.get_all("Company", filters={"country": "India"}, fields=["name"]) + if not company: + return + + sales_invoice_gst_fields = [ + dict( + fieldname="billing_address_gstin", + label="Billing Address GSTIN", + fieldtype="Data", + insert_after="customer_address", + read_only=1, + fetch_from="customer_address.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="shipping_address_name", + fetch_from="shipping_address_name.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="customer_gstin", + print_hide=1, + read_only=1, + length=50, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + length=15, + ), + ] + + custom_fields = {"Quotation": sales_invoice_gst_fields} + + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py index 416694559cb..bef2516f6d0 100644 --- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py +++ b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py @@ -6,32 +6,75 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges') - frappe.reload_doc('accounts', 'doctype', 'payment_entry') + frappe.reload_doc("accounts", "doctype", "advance_taxes_and_charges") + frappe.reload_doc("accounts", "doctype", "payment_entry") - if frappe.db.exists('Company', {'country': 'India'}): + if frappe.db.exists("Company", {"country": "India"}): custom_fields = { - 'Payment Entry': [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', - print_hide=1, collapsible=1), - dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', - print_hide=1, options='Address'), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='company_gstin', - print_hide=1, read_only=1), - dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', - print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='customer_address', - fetch_from='customer_address.gstin', print_hide=1, read_only=1) + "Payment Entry": [ + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="deductions", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="company_address", + label="Company Address", + fieldtype="Link", + insert_after="gst_section", + print_hide=1, + options="Address", + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="company_gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="customer_address", + label="Customer Address", + fieldtype="Link", + insert_after="place_of_supply", + print_hide=1, + options="Address", + depends_on='eval:doc.party_type == "Customer"', + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="customer_address", + fetch_from="customer_address.gstin", + print_hide=1, + read_only=1, + ), ] } create_custom_fields(custom_fields, update=True) else: - fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin'] + fields = [ + "gst_section", + "company_address", + "company_gstin", + "place_of_supply", + "customer_address", + "customer_gstin", + ] for field in fields: - frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}") \ No newline at end of file + frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}") diff --git a/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py index 543faeb74ac..3fe27b5c1a9 100644 --- a/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py +++ b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py @@ -5,8 +5,8 @@ from erpnext.domains.healthcare import data def execute(): - if 'Healthcare' not in frappe.get_active_domains(): + if "Healthcare" not in frappe.get_active_domains(): return - if data['custom_fields']: - create_custom_fields(data['custom_fields']) + if data["custom_fields"]: + create_custom_fields(data["custom_fields"]) diff --git a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py b/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py index f33b4b3ea0d..093463a12e9 100644 --- a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py +++ b/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py @@ -4,9 +4,8 @@ from erpnext.regional.saudi_arabia.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) if not company: return make_custom_fields() - diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py index 55125431b52..59b17eea9fe 100644 --- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -6,71 +6,89 @@ import frappe def execute(): - if "leave_policy" in frappe.db.get_table_columns("Employee"): - employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1) + frappe.reload_doc("hr", "doctype", "leave_policy_assignment") + frappe.reload_doc("hr", "doctype", "employee_grade") + employee_with_assignment = [] + leave_policy = [] - employee_with_assignment = [] - leave_policy =[] + if "leave_policy" in frappe.db.get_table_columns("Employee"): + employees_with_leave_policy = frappe.db.sql( + "SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", + as_dict=1, + ) - #for employee + for employee in employees_with_leave_policy: + alloc = frappe.db.exists( + "Leave Allocation", + {"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}, + ) + if not alloc: + create_assignment(employee.name, employee.leave_policy) - for employee in employees_with_leave_policy: - alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}) - if not alloc: - create_assignment(employee.name, employee.leave_policy) + employee_with_assignment.append(employee.name) + leave_policy.append(employee.leave_policy) - employee_with_assignment.append(employee.name) - leave_policy.append(employee.leave_policy) + if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"): + employee_grade_with_leave_policy = frappe.db.sql( + "SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", + as_dict=1, + ) + + # for whole employee Grade + for grade in employee_grade_with_leave_policy: + employees = get_employee_with_grade(grade.name) + for employee in employees: + + if employee not in employee_with_assignment: # Will ensure no duplicate + alloc = frappe.db.exists( + "Leave Allocation", + {"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}, + ) + if not alloc: + create_assignment(employee.name, grade.default_leave_policy) + leave_policy.append(grade.default_leave_policy) + + # for old Leave allocation and leave policy from allocation, which may got updated in employee grade. + leave_allocations = frappe.db.sql( + "SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", + as_dict=1, + ) + + for allocation in leave_allocations: + if allocation.leave_policy not in leave_policy: + create_assignment( + allocation.employee, + allocation.leave_policy, + leave_period=allocation.leave_period, + allocation_exists=True, + ) - if "default_leave_policy" in frappe.db.get_table_columns("Employee"): - employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1) +def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False): + if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2: + return - #for whole employee Grade + filters = {"employee": employee, "leave_policy": leave_policy} + if leave_period: + filters["leave_period"] = leave_period - for grade in employee_grade_with_leave_policy: - employees = get_employee_with_grade(grade.name) - for employee in employees: + if not frappe.db.exists("Leave Policy Assignment", filters): + lpa = frappe.new_doc("Leave Policy Assignment") + lpa.employee = employee + lpa.leave_policy = leave_policy - if employee not in employee_with_assignment: #Will ensure no duplicate - alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}) - if not alloc: - create_assignment(employee.name, grade.default_leave_policy) - leave_policy.append(grade.default_leave_policy) + lpa.flags.ignore_mandatory = True + if allocation_exists: + lpa.assignment_based_on = "Leave Period" + lpa.leave_period = leave_period + lpa.leaves_allocated = 1 - #for old Leave allocation and leave policy from allocation, which may got updated in employee grade. - leave_allocations = frappe.db.sql("SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", as_dict = 1) - - for allocation in leave_allocations: - if allocation.leave_policy not in leave_policy: - create_assignment(allocation.employee, allocation.leave_policy, leave_period=allocation.leave_period, - allocation_exists=True) - -def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False): - - filters = {"employee":employee, "leave_policy": leave_policy} - if leave_period: - filters["leave_period"] = leave_period - - frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment') - - if not frappe.db.exists("Leave Policy Assignment" , filters): - lpa = frappe.new_doc("Leave Policy Assignment") - lpa.employee = employee - lpa.leave_policy = leave_policy - - lpa.flags.ignore_mandatory = True - if allocation_exists: - lpa.assignment_based_on = 'Leave Period' - lpa.leave_period = leave_period - lpa.leaves_allocated = 1 - - lpa.save() - if allocation_exists: - lpa.submit() - #Updating old Leave Allocation - frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) + lpa.save() + if allocation_exists: + lpa.submit() + # Updating old Leave Allocation + frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) def get_employee_with_grade(grade): - return frappe.get_list("Employee", filters = {"grade": grade}) + return frappe.get_list("Employee", filters={"grade": grade}) diff --git a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py index 87c9cf1ebd5..66aae9a30af 100644 --- a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py +++ b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py @@ -8,12 +8,13 @@ from erpnext.regional.united_arab_emirates.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': ['in', ['Saudi Arabia', 'United Arab Emirates']]}) + company = frappe.get_all( + "Company", filters={"country": ["in", ["Saudi Arabia", "United Arab Emirates"]]} + ) if not company: return - - frappe.reload_doc('accounts', 'doctype', 'pos_invoice') - frappe.reload_doc('accounts', 'doctype', 'pos_invoice_item') + frappe.reload_doc("accounts", "doctype", "pos_invoice") + frappe.reload_doc("accounts", "doctype", "pos_invoice_item") make_custom_fields() diff --git a/erpnext/patches/v13_0/create_website_items.py b/erpnext/patches/v13_0/create_website_items.py index 3baa34b71c0..1a1d79ca828 100644 --- a/erpnext/patches/v13_0/create_website_items.py +++ b/erpnext/patches/v13_0/create_website_items.py @@ -11,14 +11,31 @@ def execute(): frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings") frappe.reload_doc("stock", "doctype", "item") - item_fields = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image", - "has_variants", "variant_of", "description", "weightage"] - web_fields_to_map = ["route", "slideshow", "website_image_alt", - "website_warehouse", "web_long_description", "website_content", "thumbnail"] + item_fields = [ + "item_code", + "item_name", + "item_group", + "stock_uom", + "brand", + "image", + "has_variants", + "variant_of", + "description", + "weightage", + ] + web_fields_to_map = [ + "route", + "slideshow", + "website_image_alt", + "website_warehouse", + "web_long_description", + "website_content", + "thumbnail", + ] # get all valid columns (fields) from Item master DB schema item_table_fields = frappe.db.sql("desc `tabItem`", as_dict=1) - item_table_fields = [d.get('Field') for d in item_table_fields] + item_table_fields = [d.get("Field") for d in item_table_fields] # prepare fields to query from Item, check if the web field exists in Item master web_query_fields = [] @@ -38,11 +55,7 @@ def execute(): # most likely a fresh installation that doesnt need this patch return - items = frappe.db.get_all( - "Item", - fields=item_fields, - or_filters=or_filters - ) + items = frappe.db.get_all("Item", fields=item_fields, or_filters=or_filters) total_count = len(items) for count, item in enumerate(items, start=1): @@ -62,11 +75,11 @@ def execute(): for doctype in ("Website Item Group", "Item Website Specification"): frappe.db.set_value( doctype, - {"parenttype": "Item", "parent": item.item_code}, # filters - {"parenttype": "Website Item", "parent": website_item.name} # value dict + {"parenttype": "Item", "parent": item.item_code}, # filters + {"parenttype": "Website Item", "parent": website_item.name}, # value dict ) - if count % 20 == 0: # commit after every 20 items + if count % 20 == 0: # commit after every 20 items frappe.db.commit() - frappe.utils.update_progress_bar('Creating Website Items', count, total_count) \ No newline at end of file + frappe.utils.update_progress_bar("Creating Website Items", count, total_count) diff --git a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py index ed46e7a60a5..5cbd0b5fcb5 100644 --- a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py +++ b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -6,35 +5,68 @@ from erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings import def execute(): - company = frappe.get_all('Company', filters = {'country': 'United States'}, fields=['name']) + company = frappe.get_all("Company", filters={"country": "United States"}, fields=["name"]) if not company: return - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value( + "TaxJar Settings", "taxjar_create_transactions" + ) TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox") - if (not TAXJAR_CREATE_TRANSACTIONS and not TAXJAR_CALCULATE_TAX and not TAXJAR_SANDBOX_MODE): + if not TAXJAR_CREATE_TRANSACTIONS and not TAXJAR_CALCULATE_TAX and not TAXJAR_SANDBOX_MODE: return custom_fields = { - 'Sales Invoice Item': [ - dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category', - label='Product Tax Category', fetch_from='item_code.product_tax_category'), - dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount', - label='Tax Collectable', read_only=1, options='currency'), - dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable', - label='Taxable Amount', read_only=1, options='currency') + "Sales Invoice Item": [ + dict( + fieldname="product_tax_category", + fieldtype="Link", + insert_after="description", + options="Product Tax Category", + label="Product Tax Category", + fetch_from="item_code.product_tax_category", + ), + dict( + fieldname="tax_collectable", + fieldtype="Currency", + insert_after="net_amount", + label="Tax Collectable", + read_only=1, + options="currency", + ), + dict( + fieldname="taxable_amount", + fieldtype="Currency", + insert_after="tax_collectable", + label="Taxable Amount", + read_only=1, + options="currency", + ), ], - 'Item': [ - dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category', - label='Product Tax Category') + "Item": [ + dict( + fieldname="product_tax_category", + fieldtype="Link", + insert_after="item_group", + options="Product Tax Category", + label="Product Tax Category", + ) + ], + "TaxJar Settings": [ + dict( + fieldname="company", + fieldtype="Link", + insert_after="configuration", + options="Company", + label="Company", + ) ], - 'TaxJar Settings': [ - dict(fieldname='company', fieldtype='Link', insert_after='configuration', options='Company', - label='Company') - ] } create_custom_fields(custom_fields, update=True) add_permissions() - frappe.enqueue('erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories', now=True) + frappe.enqueue( + "erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories", + now=True, + ) diff --git a/erpnext/patches/v13_0/datev_deprecation_warning.py b/erpnext/patches/v13_0/datev_deprecation_warning.py new file mode 100644 index 00000000000..bf58440a610 --- /dev/null +++ b/erpnext/patches/v13_0/datev_deprecation_warning.py @@ -0,0 +1,9 @@ +import click + + +def execute(): + click.secho( + "DATEV reports are moved to a separate app and will be removed from ERPNext in version-14.\n" + "Please install the app to continue using them: https://github.com/alyf-de/erpnext_datev", + fg="yellow", + ) diff --git a/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py index 75953b0e304..c53eb794378 100644 --- a/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py +++ b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py @@ -7,7 +7,8 @@ import frappe def execute(): - if frappe.db.exists('DocType', 'Bank Reconciliation Detail') and \ - frappe.db.exists('DocType', 'Bank Clearance Detail'): + if frappe.db.exists("DocType", "Bank Reconciliation Detail") and frappe.db.exists( + "DocType", "Bank Clearance Detail" + ): - frappe.delete_doc("DocType", 'Bank Reconciliation Detail', force=1) + frappe.delete_doc("DocType", "Bank Reconciliation Detail", force=1) diff --git a/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py index 2c5c577978e..3755315813f 100644 --- a/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py +++ b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py @@ -22,7 +22,7 @@ def execute(): frappe.delete_doc("Page", "bank-reconciliation", force=1) - frappe.reload_doc('accounts', 'doctype', 'bank_transaction') + frappe.reload_doc("accounts", "doctype", "bank_transaction") rename_field("Bank Transaction", "debit", "deposit") rename_field("Bank Transaction", "credit", "withdrawal") diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index e57d6d0d3e2..987f53f37c1 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -8,9 +8,12 @@ from erpnext.accounts.utils import check_and_delete_linked_reports def execute(): - reports_to_delete = ["Requested Items To Be Ordered", - "Purchase Order Items To Be Received or Billed","Purchase Order Items To Be Received", - "Purchase Order Items To Be Billed"] + reports_to_delete = [ + "Requested Items To Be Ordered", + "Purchase Order Items To Be Received or Billed", + "Purchase Order Items To Be Received", + "Purchase Order Items To Be Billed", + ] for report in reports_to_delete: if frappe.db.exists("Report", report): @@ -19,8 +22,9 @@ def execute(): frappe.delete_doc("Report", report) + def delete_auto_email_reports(report): - """ Check for one or multiple Auto Email Reports and delete """ + """Check for one or multiple Auto Email Reports and delete""" auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index e6eba0a6085..b31c9d17d71 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -18,14 +18,16 @@ def execute(): frappe.delete_doc("Report", report) + def delete_auto_email_reports(report): - """ Check for one or multiple Auto Email Reports and delete """ + """Check for one or multiple Auto Email Reports and delete""" auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) + def delete_links_from_desktop_icons(report): - """ Check for one or multiple Desktop Icons and delete """ + """Check for one or multiple Desktop Icons and delete""" desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"]) for desktop_icon in desktop_icons: - frappe.delete_doc("Desktop Icon", desktop_icon[0]) \ No newline at end of file + frappe.delete_doc("Desktop Icon", desktop_icon[0]) diff --git a/erpnext/patches/v13_0/delete_orphaned_tables.py b/erpnext/patches/v13_0/delete_orphaned_tables.py index c32f83067bc..794be098a9f 100644 --- a/erpnext/patches/v13_0/delete_orphaned_tables.py +++ b/erpnext/patches/v13_0/delete_orphaned_tables.py @@ -7,63 +7,66 @@ from frappe.utils import getdate def execute(): - frappe.reload_doc('setup', 'doctype', 'transaction_deletion_record') + frappe.reload_doc("setup", "doctype", "transaction_deletion_record") - if has_deleted_company_transactions(): - child_doctypes = get_child_doctypes_whose_parent_doctypes_were_affected() + if has_deleted_company_transactions(): + child_doctypes = get_child_doctypes_whose_parent_doctypes_were_affected() - for doctype in child_doctypes: - docs = frappe.get_all(doctype, fields=['name', 'parent', 'parenttype', 'creation']) + for doctype in child_doctypes: + docs = frappe.get_all(doctype, fields=["name", "parent", "parenttype", "creation"]) - for doc in docs: - if not frappe.db.exists(doc['parenttype'], doc['parent']): - frappe.db.delete(doctype, {'name': doc['name']}) + for doc in docs: + if not frappe.db.exists(doc["parenttype"], doc["parent"]): + frappe.db.delete(doctype, {"name": doc["name"]}) + + elif check_for_new_doc_with_same_name_as_deleted_parent(doc): + frappe.db.delete(doctype, {"name": doc["name"]}) - elif check_for_new_doc_with_same_name_as_deleted_parent(doc): - frappe.db.delete(doctype, {'name': doc['name']}) def has_deleted_company_transactions(): - return frappe.get_all('Transaction Deletion Record') + return frappe.get_all("Transaction Deletion Record") + def get_child_doctypes_whose_parent_doctypes_were_affected(): - parent_doctypes = get_affected_doctypes() - child_doctypes = frappe.get_all( - 'DocField', - filters={ - 'fieldtype': 'Table', - 'parent':['in', parent_doctypes] - }, pluck='options') + parent_doctypes = get_affected_doctypes() + child_doctypes = frappe.get_all( + "DocField", filters={"fieldtype": "Table", "parent": ["in", parent_doctypes]}, pluck="options" + ) + + return child_doctypes - return child_doctypes def get_affected_doctypes(): - affected_doctypes = [] - tdr_docs = frappe.get_all('Transaction Deletion Record', pluck="name") + affected_doctypes = [] + tdr_docs = frappe.get_all("Transaction Deletion Record", pluck="name") - for tdr in tdr_docs: - tdr_doc = frappe.get_doc("Transaction Deletion Record", tdr) + for tdr in tdr_docs: + tdr_doc = frappe.get_doc("Transaction Deletion Record", tdr) - for doctype in tdr_doc.doctypes: - if is_not_child_table(doctype.doctype_name): - affected_doctypes.append(doctype.doctype_name) + for doctype in tdr_doc.doctypes: + if is_not_child_table(doctype.doctype_name): + affected_doctypes.append(doctype.doctype_name) + + affected_doctypes = remove_duplicate_items(affected_doctypes) + return affected_doctypes - affected_doctypes = remove_duplicate_items(affected_doctypes) - return affected_doctypes def is_not_child_table(doctype): - return not bool(frappe.get_value('DocType', doctype, 'istable')) + return not bool(frappe.get_value("DocType", doctype, "istable")) + def remove_duplicate_items(affected_doctypes): - return list(set(affected_doctypes)) + return list(set(affected_doctypes)) + def check_for_new_doc_with_same_name_as_deleted_parent(doc): - """ - Compares creation times of parent and child docs. - Since Transaction Deletion Record resets the naming series after deletion, - it allows the creation of new docs with the same names as the deleted ones. - """ + """ + Compares creation times of parent and child docs. + Since Transaction Deletion Record resets the naming series after deletion, + it allows the creation of new docs with the same names as the deleted ones. + """ - parent_creation_time = frappe.db.get_value(doc['parenttype'], doc['parent'], 'creation') - child_creation_time = doc['creation'] + parent_creation_time = frappe.db.get_value(doc["parenttype"], doc["parent"], "creation") + child_creation_time = doc["creation"] - return getdate(parent_creation_time) > getdate(child_creation_time) + return getdate(parent_creation_time) > getdate(child_creation_time) diff --git a/erpnext/patches/v13_0/delete_report_requested_items_to_order.py b/erpnext/patches/v13_0/delete_report_requested_items_to_order.py index 87565f0fe42..430a3056cd1 100644 --- a/erpnext/patches/v13_0/delete_report_requested_items_to_order.py +++ b/erpnext/patches/v13_0/delete_report_requested_items_to_order.py @@ -2,12 +2,16 @@ import frappe def execute(): - """ Check for one or multiple Auto Email Reports and delete """ - auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": "Requested Items to Order"}, ["name"]) + """Check for one or multiple Auto Email Reports and delete""" + auto_email_reports = frappe.db.get_values( + "Auto Email Report", {"report": "Requested Items to Order"}, ["name"] + ) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabReport` WHERE name = 'Requested Items to Order' - """) + """ + ) diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py index aa2a2d3b785..84b6c37dd9d 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -7,13 +7,13 @@ from erpnext.regional.saudi_arabia.setup import add_print_formats def execute(): - company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) if company: add_print_formats() return - if frappe.db.exists('DocType', 'Print Format'): + if frappe.db.exists("DocType", "Print Format"): frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - for d in ('KSA VAT Invoice', 'KSA POS Invoice'): + for d in ("KSA VAT Invoice", "KSA POS Invoice"): frappe.db.set_value("Print Format", d, "disabled", 1) diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py index aea498d8d30..ca166cee74f 100644 --- a/erpnext/patches/v13_0/drop_razorpay_payload_column.py +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -1,8 +1,7 @@ - import frappe def execute(): if frappe.db.exists("DocType", "Membership"): - if 'webhook_payload' in frappe.db.get_table_columns("Membership"): + if "webhook_payload" in frappe.db.get_table_columns("Membership"): frappe.db.sql("alter table `tabMembership` drop column webhook_payload") diff --git a/erpnext/patches/v13_0/education_deprecation_warning.py b/erpnext/patches/v13_0/education_deprecation_warning.py new file mode 100644 index 00000000000..96602ebdf94 --- /dev/null +++ b/erpnext/patches/v13_0/education_deprecation_warning.py @@ -0,0 +1,10 @@ +import click + + +def execute(): + + click.secho( + "Education Domain is moved to a separate app and will be removed from ERPNext in version-14.\n" + "When upgrading to ERPNext version-14, please install the app to continue using the Education domain: https://github.com/frappe/education", + fg="yellow", + ) diff --git a/erpnext/patches/v13_0/enable_ksa_vat_docs.py b/erpnext/patches/v13_0/enable_ksa_vat_docs.py new file mode 100644 index 00000000000..4adf4d71db7 --- /dev/null +++ b/erpnext/patches/v13_0/enable_ksa_vat_docs.py @@ -0,0 +1,12 @@ +import frappe + +from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_formats + + +def execute(): + company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) + if not company: + return + + add_print_formats() + add_permissions() diff --git a/erpnext/patches/v13_0/enable_provisional_accounting.py b/erpnext/patches/v13_0/enable_provisional_accounting.py index 85bbaed89df..7212146e458 100644 --- a/erpnext/patches/v13_0/enable_provisional_accounting.py +++ b/erpnext/patches/v13_0/enable_provisional_accounting.py @@ -9,14 +9,11 @@ def execute(): company = frappe.qb.DocType("Company") - frappe.qb.update( - company - ).set( - company.enable_provisional_accounting_for_non_stock_items, company.enable_perpetual_inventory_for_non_stock_items - ).set( - company.default_provisional_account, company.service_received_but_not_billed - ).where( + frappe.qb.update(company).set( + company.enable_provisional_accounting_for_non_stock_items, + company.enable_perpetual_inventory_for_non_stock_items, + ).set(company.default_provisional_account, company.service_received_but_not_billed).where( company.enable_perpetual_inventory_for_non_stock_items == 1 ).where( company.service_received_but_not_billed.isnotnull() - ).run() \ No newline at end of file + ).run() diff --git a/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py b/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py index 7a51b432117..68b5cde9203 100644 --- a/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py +++ b/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py @@ -2,7 +2,6 @@ import frappe def execute(): - frappe.reload_doc('core', 'doctype', 'scheduled_job_type') - if frappe.db.exists('Scheduled Job Type', 'repost_item_valuation.repost_entries'): - frappe.db.set_value('Scheduled Job Type', - 'repost_item_valuation.repost_entries', 'stopped', 0) + frappe.reload_doc("core", "doctype", "scheduled_job_type") + if frappe.db.exists("Scheduled Job Type", "repost_item_valuation.repost_entries"): + frappe.db.set_value("Scheduled Job Type", "repost_item_valuation.repost_entries", "stopped", 0) diff --git a/erpnext/patches/v13_0/enable_uoms.py b/erpnext/patches/v13_0/enable_uoms.py index 4d3f6376303..8efd67e2806 100644 --- a/erpnext/patches/v13_0/enable_uoms.py +++ b/erpnext/patches/v13_0/enable_uoms.py @@ -2,12 +2,12 @@ import frappe def execute(): - frappe.reload_doc('setup', 'doctype', 'uom') + frappe.reload_doc("setup", "doctype", "uom") uom = frappe.qb.DocType("UOM") - (frappe.qb - .update(uom) + ( + frappe.qb.update(uom) .set(uom.enabled, 1) .where(uom.creation >= "2021-10-18") # date when this field was released ).run() diff --git a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py index 32ad542cf88..9197d86058d 100644 --- a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py +++ b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py @@ -2,15 +2,10 @@ import frappe def execute(): - if frappe.db.has_column("Item", "thumbnail"): - website_item = frappe.qb.DocType("Website Item").as_("wi") - item = frappe.qb.DocType("Item") + if frappe.db.has_column("Item", "thumbnail"): + website_item = frappe.qb.DocType("Website Item").as_("wi") + item = frappe.qb.DocType("Item") - frappe.qb.update(website_item).inner_join(item).on( - website_item.item_code == item.item_code - ).set( - website_item.thumbnail, item.thumbnail - ).where( - website_item.website_image.notnull() - & website_item.thumbnail.isnull() - ).run() + frappe.qb.update(website_item).inner_join(item).on(website_item.item_code == item.item_code).set( + website_item.thumbnail, item.thumbnail + ).where(website_item.website_image.notnull() & website_item.thumbnail.isnull()).run() diff --git a/erpnext/patches/v13_0/fix_invoice_statuses.py b/erpnext/patches/v13_0/fix_invoice_statuses.py index 4395757159f..253b425c58b 100644 --- a/erpnext/patches/v13_0/fix_invoice_statuses.py +++ b/erpnext/patches/v13_0/fix_invoice_statuses.py @@ -8,6 +8,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( TODAY = getdate() + def execute(): # This fix is not related to Party Specific Item, # but it is needed for code introduced after Party Specific Item was @@ -35,39 +36,28 @@ def execute(): fields=fields, filters={ "docstatus": 1, - "status": ("in", ( - "Overdue", - "Overdue and Discounted", - "Partly Paid", - "Partly Paid and Discounted" - )), + "status": ( + "in", + ("Overdue", "Overdue and Discounted", "Partly Paid", "Partly Paid and Discounted"), + ), "outstanding_amount": (">", 0), "modified": (">", "2021-01-01") # an assumption is being made that only invoices modified # after 2021 got affected as incorrectly overdue. # required for performance reasons. - } + }, ) - invoices_to_update = { - invoice.name: invoice for invoice in invoices_to_update - } + invoices_to_update = {invoice.name: invoice for invoice in invoices_to_update} payment_schedule_items = frappe.get_all( "Payment Schedule", - fields=( - "due_date", - "payment_amount", - "base_payment_amount", - "parent" - ), - filters={"parent": ("in", invoices_to_update)} + fields=("due_date", "payment_amount", "base_payment_amount", "parent"), + filters={"parent": ("in", invoices_to_update)}, ) for item in payment_schedule_items: - invoices_to_update[item.parent].setdefault( - "payment_schedule", [] - ).append(item) + invoices_to_update[item.parent].setdefault("payment_schedule", []).append(item) status_map = {} @@ -81,19 +71,11 @@ def execute(): status_map.setdefault(correct_status, []).append(doc.name) for status, docs in status_map.items(): - frappe.db.set_value( - doctype, {"name": ("in", docs)}, - "status", - status, - update_modified=False - ) - + frappe.db.set_value(doctype, {"name": ("in", docs)}, "status", status, update_modified=False) def get_correct_status(doc): - outstanding_amount = flt( - doc.outstanding_amount, doc.precision("outstanding_amount") - ) + outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount")) total = get_total_in_party_account_currency(doc) status = "" diff --git a/erpnext/patches/v13_0/fix_non_unique_represents_company.py b/erpnext/patches/v13_0/fix_non_unique_represents_company.py index e91c1db4dd4..c604f9cb24f 100644 --- a/erpnext/patches/v13_0/fix_non_unique_represents_company.py +++ b/erpnext/patches/v13_0/fix_non_unique_represents_company.py @@ -2,8 +2,10 @@ import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ update tabCustomer set represents_company = NULL where represents_company = '' - """) + """ + ) diff --git a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py index 72cda751e6c..fc3e68ac677 100644 --- a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py +++ b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py @@ -13,18 +13,24 @@ def execute(): "DATEV". This is no longer necessary. The reference ID for DATEV will be stored in a new custom field "debtor_creditor_number". """ - company_list = frappe.get_all('Company', filters={'country': 'Germany'}) + company_list = frappe.get_all("Company", filters={"country": "Germany"}) for company in company_list: - party_account_list = frappe.get_all('Party Account', filters={'company': company.name}, fields=['name', 'account', 'debtor_creditor_number']) + party_account_list = frappe.get_all( + "Party Account", + filters={"company": company.name}, + fields=["name", "account", "debtor_creditor_number"], + ) for party_account in party_account_list: if (not party_account.account) or party_account.debtor_creditor_number: # account empty or debtor_creditor_number already filled continue - account_number = frappe.db.get_value('Account', party_account.account, 'account_number') + account_number = frappe.db.get_value("Account", party_account.account, "account_number") if not account_number: continue - frappe.db.set_value('Party Account', party_account.name, 'debtor_creditor_number', account_number) - frappe.db.set_value('Party Account', party_account.name, 'account', '') + frappe.db.set_value( + "Party Account", party_account.name, "debtor_creditor_number", account_number + ) + frappe.db.set_value("Party Account", party_account.name, "account", "") diff --git a/erpnext/patches/v13_0/germany_make_custom_fields.py b/erpnext/patches/v13_0/germany_make_custom_fields.py index 80b6a3954a6..cc358135acd 100644 --- a/erpnext/patches/v13_0/germany_make_custom_fields.py +++ b/erpnext/patches/v13_0/germany_make_custom_fields.py @@ -13,7 +13,7 @@ def execute(): It is usually run once at setup of a new company. Since it's new, run it once for existing companies as well. """ - company_list = frappe.get_all('Company', filters = {'country': 'Germany'}) + company_list = frappe.get_all("Company", filters={"country": "Germany"}) if not company_list: return diff --git a/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py b/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py index cb3df3c5ddc..efd2c21d6a0 100644 --- a/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py +++ b/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py @@ -1,43 +1,87 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name']) + company = frappe.get_all("Company", filters={"country": "India"}, fields=["name"]) if not company: return - hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description', - allow_on_submit=1, print_hide=1, fetch_if_empty=1) - nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code', - print_hide=1) - is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', - fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt', - print_hide=1) - taxable_value = dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) + hsn_sac_field = dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Data", + fetch_from="item_code.gst_hsn_code", + insert_after="description", + allow_on_submit=1, + print_hide=1, + fetch_if_empty=1, + ) + nil_rated_exempt = dict( + fieldname="is_nil_exempt", + label="Is Nil Rated or Exempted", + fieldtype="Check", + fetch_from="item_code.is_nil_exempt", + insert_after="gst_hsn_code", + print_hide=1, + ) + is_non_gst = dict( + fieldname="is_non_gst", + label="Is Non GST", + fieldtype="Check", + fetch_from="item_code.is_non_gst", + insert_after="is_nil_exempt", + print_hide=1, + ) + taxable_value = dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) sales_invoice_gst_fields = [ - dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', read_only=1, - fetch_from='customer_address.gstin', print_hide=1), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='shipping_address_name', - fetch_from='shipping_address_name.gstin', print_hide=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=1), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1), - ] + dict( + fieldname="billing_address_gstin", + label="Billing Address GSTIN", + fieldtype="Data", + insert_after="customer_address", + read_only=1, + fetch_from="customer_address.gstin", + print_hide=1, + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="shipping_address_name", + fetch_from="shipping_address_name.gstin", + print_hide=1, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="customer_gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + ), + ] custom_fields = { - 'POS Invoice': sales_invoice_gst_fields, - 'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "POS Invoice": sales_invoice_gst_fields, + "POS Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py b/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py index d43e793b9a9..30b84accf3f 100644 --- a/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py +++ b/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py @@ -1,91 +1,94 @@ - import frappe from frappe.model.utils.rename_field import rename_field def execute(): - if frappe.db.exists('DocType', 'Lab Test') and frappe.db.exists('DocType', 'Lab Test Template'): + if frappe.db.exists("DocType", "Lab Test") and frappe.db.exists("DocType", "Lab Test Template"): # rename child doctypes doctypes = { - 'Lab Test Groups': 'Lab Test Group Template', - 'Normal Test Items': 'Normal Test Result', - 'Sensitivity Test Items': 'Sensitivity Test Result', - 'Special Test Items': 'Descriptive Test Result', - 'Special Test Template': 'Descriptive Test Template' + "Lab Test Groups": "Lab Test Group Template", + "Normal Test Items": "Normal Test Result", + "Sensitivity Test Items": "Sensitivity Test Result", + "Special Test Items": "Descriptive Test Result", + "Special Test Template": "Descriptive Test Template", } - frappe.reload_doc('healthcare', 'doctype', 'lab_test') - frappe.reload_doc('healthcare', 'doctype', 'lab_test_template') + frappe.reload_doc("healthcare", "doctype", "lab_test") + frappe.reload_doc("healthcare", "doctype", "lab_test_template") for old_dt, new_dt in doctypes.items(): frappe.flags.link_fields = {} - should_rename = ( - frappe.db.table_exists(old_dt) - and not frappe.db.table_exists(new_dt) - ) + should_rename = frappe.db.table_exists(old_dt) and not frappe.db.table_exists(new_dt) if should_rename: - frappe.reload_doc('healthcare', 'doctype', frappe.scrub(old_dt)) - frappe.rename_doc('DocType', old_dt, new_dt, force=True) - frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt)) - frappe.delete_doc_if_exists('DocType', old_dt) + frappe.reload_doc("healthcare", "doctype", frappe.scrub(old_dt)) + frappe.rename_doc("DocType", old_dt, new_dt, force=True) + frappe.reload_doc("healthcare", "doctype", frappe.scrub(new_dt)) + frappe.delete_doc_if_exists("DocType", old_dt) parent_fields = { - 'Lab Test Group Template': 'lab_test_groups', - 'Descriptive Test Template': 'descriptive_test_templates', - 'Normal Test Result': 'normal_test_items', - 'Sensitivity Test Result': 'sensitivity_test_items', - 'Descriptive Test Result': 'descriptive_test_items' + "Lab Test Group Template": "lab_test_groups", + "Descriptive Test Template": "descriptive_test_templates", + "Normal Test Result": "normal_test_items", + "Sensitivity Test Result": "sensitivity_test_items", + "Descriptive Test Result": "descriptive_test_items", } for doctype, parentfield in parent_fields.items(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{0}` SET parentfield = %(parentfield)s - """.format(doctype), {'parentfield': parentfield}) + """.format( + doctype + ), + {"parentfield": parentfield}, + ) # copy renamed child table fields (fields were already renamed in old doctype json, hence sql) rename_fields = { - 'lab_test_name': 'test_name', - 'lab_test_event': 'test_event', - 'lab_test_uom': 'test_uom', - 'lab_test_comment': 'test_comment' + "lab_test_name": "test_name", + "lab_test_event": "test_event", + "lab_test_uom": "test_uom", + "lab_test_comment": "test_comment", } for new, old in rename_fields.items(): - if frappe.db.has_column('Normal Test Result', old): - frappe.db.sql("""UPDATE `tabNormal Test Result` SET {} = {}""" - .format(new, old)) + if frappe.db.has_column("Normal Test Result", old): + frappe.db.sql("""UPDATE `tabNormal Test Result` SET {} = {}""".format(new, old)) - if frappe.db.has_column('Normal Test Template', 'test_event'): + if frappe.db.has_column("Normal Test Template", "test_event"): frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""") - if frappe.db.has_column('Normal Test Template', 'test_uom'): + if frappe.db.has_column("Normal Test Template", "test_uom"): frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""") - if frappe.db.has_column('Descriptive Test Result', 'test_particulars'): - frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""") + if frappe.db.has_column("Descriptive Test Result", "test_particulars"): + frappe.db.sql( + """UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""" + ) rename_fields = { - 'lab_test_template': 'test_template', - 'lab_test_description': 'test_description', - 'lab_test_rate': 'test_rate' + "lab_test_template": "test_template", + "lab_test_description": "test_description", + "lab_test_rate": "test_rate", } for new, old in rename_fields.items(): - if frappe.db.has_column('Lab Test Group Template', old): - frappe.db.sql("""UPDATE `tabLab Test Group Template` SET {} = {}""" - .format(new, old)) + if frappe.db.has_column("Lab Test Group Template", old): + frappe.db.sql("""UPDATE `tabLab Test Group Template` SET {} = {}""".format(new, old)) # rename field - frappe.reload_doc('healthcare', 'doctype', 'lab_test') - if frappe.db.has_column('Lab Test', 'special_toggle'): - rename_field('Lab Test', 'special_toggle', 'descriptive_toggle') + frappe.reload_doc("healthcare", "doctype", "lab_test") + if frappe.db.has_column("Lab Test", "special_toggle"): + rename_field("Lab Test", "special_toggle", "descriptive_toggle") - if frappe.db.exists('DocType', 'Lab Test Group Template'): + if frappe.db.exists("DocType", "Lab Test Group Template"): # fix select field option - frappe.reload_doc('healthcare', 'doctype', 'lab_test_group_template') - frappe.db.sql(""" + frappe.reload_doc("healthcare", "doctype", "lab_test_group_template") + frappe.db.sql( + """ UPDATE `tabLab Test Group Template` SET template_or_new_line = 'Add New Line' WHERE template_or_new_line = 'Add new line' - """) + """ + ) diff --git a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py index 5fe85a48308..33fb8f963c5 100644 --- a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py +++ b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py @@ -7,5 +7,10 @@ def execute(): stock_settings = frappe.get_doc("Stock Settings") - set_by_naming_series("Item", "item_code", - stock_settings.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0) + set_by_naming_series( + "Item", + "item_code", + stock_settings.get("item_naming_by") == "Naming Series", + hide_name_field=True, + make_mandatory=0, + ) diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index 0f2ac4b4514..f6427ca55a6 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -7,18 +7,18 @@ from erpnext.stock.stock_ledger import update_entries_after def execute(): doctypes_to_reload = [ - ("stock", "repost_item_valuation"), - ("stock", "stock_entry_detail"), - ("stock", "purchase_receipt_item"), - ("stock", "delivery_note_item"), - ("stock", "packed_item"), - ("accounts", "sales_invoice_item"), - ("accounts", "purchase_invoice_item"), - ("buying", "purchase_receipt_item_supplied") - ] + ("stock", "repost_item_valuation"), + ("stock", "stock_entry_detail"), + ("stock", "purchase_receipt_item"), + ("stock", "delivery_note_item"), + ("stock", "packed_item"), + ("accounts", "sales_invoice_item"), + ("accounts", "purchase_invoice_item"), + ("buying", "purchase_receipt_item_supplied"), + ] for module, doctype in doctypes_to_reload: - frappe.reload_doc(module, 'doctype', doctype) + frappe.reload_doc(module, "doctype", doctype) reposting_project_deployed_on = get_creation_time() posting_date = getdate(reposting_project_deployed_on) @@ -32,7 +32,8 @@ def execute(): company_list = [] - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company FROM @@ -41,7 +42,10 @@ def execute(): creation > %s and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - ''', reposting_project_deployed_on, as_dict=1) + """, + reposting_project_deployed_on, + as_dict=1, + ) frappe.db.auto_commit_on_many_writes = 1 print("Reposting Stock Ledger Entries...") @@ -51,30 +55,36 @@ def execute(): if d.company not in company_list: company_list.append(d.company) - update_entries_after({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": d.posting_date, - "posting_time": d.posting_time, - "voucher_type": d.voucher_type, - "voucher_no": d.voucher_no, - "sle_id": d.name - }, allow_negative_stock=True) + update_entries_after( + { + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": d.posting_date, + "posting_time": d.posting_time, + "voucher_type": d.voucher_type, + "voucher_no": d.voucher_no, + "sle_id": d.name, + }, + allow_negative_stock=True, + ) i += 1 - if i%100 == 0: + if i % 100 == 0: print(i, "/", total_sle) - print("Reposting General Ledger Entries...") if data: - for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): + for row in frappe.get_all("Company", filters={"enable_perpetual_inventory": 1}): if row.name in company_list: update_gl_entries_after(posting_date, posting_time, company=row.name) frappe.db.auto_commit_on_many_writes = 0 + def get_creation_time(): - return frappe.db.sql(''' SELECT create_time FROM - INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0] + return frappe.db.sql( + """ SELECT create_time FROM + INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" """, + as_list=1, + )[0][0] diff --git a/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py b/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py index 68bcd8a8da5..69a695ef301 100644 --- a/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py +++ b/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py @@ -6,15 +6,16 @@ import frappe def execute(): - '''`sales_invoice` field from loyalty point entry is splitted into `invoice_type` & `invoice` fields''' + """`sales_invoice` field from loyalty point entry is splitted into `invoice_type` & `invoice` fields""" frappe.reload_doc("Accounts", "doctype", "loyalty_point_entry") - if not frappe.db.has_column('Loyalty Point Entry', 'sales_invoice'): + if not frappe.db.has_column("Loyalty Point Entry", "sales_invoice"): return frappe.db.sql( """UPDATE `tabLoyalty Point Entry` lpe SET lpe.`invoice_type` = 'Sales Invoice', lpe.`invoice` = lpe.`sales_invoice` WHERE lpe.`sales_invoice` IS NOT NULL - AND (lpe.`invoice` IS NULL OR lpe.`invoice` = '')""") + AND (lpe.`invoice` IS NULL OR lpe.`invoice` = '')""" + ) diff --git a/erpnext/patches/v13_0/make_homepage_products_website_items.py b/erpnext/patches/v13_0/make_homepage_products_website_items.py index 3ca20e2da86..3f23e9cdd5d 100644 --- a/erpnext/patches/v13_0/make_homepage_products_website_items.py +++ b/erpnext/patches/v13_0/make_homepage_products_website_items.py @@ -1,4 +1,3 @@ - import frappe @@ -15,4 +14,4 @@ def execute(): homepage.flags.ignore_mandatory = True homepage.flags.ignore_links = True - homepage.save() \ No newline at end of file + homepage.save() diff --git a/erpnext/patches/v13_0/make_non_standard_user_type.py b/erpnext/patches/v13_0/make_non_standard_user_type.py index 9faf298c262..ba34cb98172 100644 --- a/erpnext/patches/v13_0/make_non_standard_user_type.py +++ b/erpnext/patches/v13_0/make_non_standard_user_type.py @@ -10,15 +10,24 @@ from erpnext.setup.install import add_non_standard_user_types def execute(): doctype_dict = { - 'projects': ['Timesheet'], - 'payroll': ['Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission'], - 'hr': ['Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request'] + "projects": ["Timesheet"], + "payroll": [ + "Salary Slip", + "Employee Tax Exemption Declaration", + "Employee Tax Exemption Proof Submission", + ], + "hr": [ + "Employee", + "Expense Claim", + "Leave Application", + "Attendance Request", + "Compensatory Leave Request", + ], } for module, doctypes in iteritems(doctype_dict): for doctype in doctypes: - frappe.reload_doc(module, 'doctype', doctype) - + frappe.reload_doc(module, "doctype", doctype) frappe.flags.ignore_select_perm = True frappe.flags.update_select_perm_after_migrate = True diff --git a/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py index ad79c811c06..492e0403ec4 100644 --- a/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py +++ b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py @@ -1,14 +1,14 @@ - import json import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'purchase_invoice_advance') - frappe.reload_doc('accounts', 'doctype', 'sales_invoice_advance') + frappe.reload_doc("accounts", "doctype", "purchase_invoice_advance") + frappe.reload_doc("accounts", "doctype", "sales_invoice_advance") - purchase_invoices = frappe.db.sql(""" + purchase_invoices = frappe.db.sql( + """ select parenttype as type, parent as name from @@ -19,9 +19,12 @@ def execute(): and ifnull(exchange_gain_loss, 0) != 0 group by parent - """, as_dict=1) + """, + as_dict=1, + ) - sales_invoices = frappe.db.sql(""" + sales_invoices = frappe.db.sql( + """ select parenttype as type, parent as name from @@ -32,14 +35,16 @@ def execute(): and ifnull(exchange_gain_loss, 0) != 0 group by parent - """, as_dict=1) + """, + as_dict=1, + ) if purchase_invoices + sales_invoices: frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log") - acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto') + acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto") if acc_frozen_upto: - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) for invoice in purchase_invoices + sales_invoices: try: @@ -48,13 +53,13 @@ def execute(): doc.make_gl_entries() for advance in doc.advances: if advance.ref_exchange_rate == 1: - advance.db_set('exchange_gain_loss', 0, False) + advance.db_set("exchange_gain_loss", 0, False) doc.docstatus = 1 doc.make_gl_entries() frappe.db.commit() except Exception: frappe.db.rollback() - print(f'Failed to correct gl entries of {invoice.name}') + print(f"Failed to correct gl entries of {invoice.name}") if acc_frozen_upto: - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', acc_frozen_upto) \ No newline at end of file + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", acc_frozen_upto) diff --git a/erpnext/patches/v13_0/move_branch_code_to_bank_account.py b/erpnext/patches/v13_0/move_branch_code_to_bank_account.py index 350744fd41f..24061271484 100644 --- a/erpnext/patches/v13_0/move_branch_code_to_bank_account.py +++ b/erpnext/patches/v13_0/move_branch_code_to_bank_account.py @@ -7,11 +7,15 @@ import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'bank_account') - frappe.reload_doc('accounts', 'doctype', 'bank') + frappe.reload_doc("accounts", "doctype", "bank_account") + frappe.reload_doc("accounts", "doctype", "bank") - if frappe.db.has_column('Bank', 'branch_code') and frappe.db.has_column('Bank Account', 'branch_code'): - frappe.db.sql("""UPDATE `tabBank` b, `tabBank Account` ba + if frappe.db.has_column("Bank", "branch_code") and frappe.db.has_column( + "Bank Account", "branch_code" + ): + frappe.db.sql( + """UPDATE `tabBank` b, `tabBank Account` ba SET ba.branch_code = b.branch_code WHERE ba.bank = b.name AND - ifnull(b.branch_code, '') != '' AND ifnull(ba.branch_code, '') = ''""") + ifnull(b.branch_code, '') != '' AND ifnull(ba.branch_code, '') = ''""" + ) diff --git a/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py b/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py index c07caaef661..0290af0f73e 100644 --- a/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py +++ b/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py @@ -6,47 +6,47 @@ import frappe def execute(): - frappe.db.sql("""UPDATE `tabPrint Format` + frappe.db.sql( + """UPDATE `tabPrint Format` SET module = 'Payroll' WHERE name IN ('Salary Slip Based On Timesheet', 'Salary Slip Standard')""" - ) + ) - frappe.db.sql("""UPDATE `tabNotification` SET module='Payroll' WHERE name='Retention Bonus';""" - ) + frappe.db.sql("""UPDATE `tabNotification` SET module='Payroll' WHERE name='Retention Bonus';""") - doctypes_moved = [ - 'Employee Benefit Application Detail', - 'Employee Tax Exemption Declaration Category', - 'Salary Component', - 'Employee Tax Exemption Proof Submission Detail', - 'Income Tax Slab Other Charges', - 'Taxable Salary Slab', - 'Payroll Period Date', - 'Salary Slip Timesheet', - 'Payroll Employee Detail', - 'Salary Detail', - 'Employee Tax Exemption Sub Category', - 'Employee Tax Exemption Category', - 'Employee Benefit Claim', - 'Employee Benefit Application', - 'Employee Other Income', - 'Employee Tax Exemption Proof Submission', - 'Employee Tax Exemption Declaration', - 'Employee Incentive', - 'Retention Bonus', - 'Additional Salary', - 'Income Tax Slab', - 'Payroll Period', - 'Salary Slip', - 'Payroll Entry', - 'Salary Structure Assignment', - 'Salary Structure' - ] + doctypes_moved = [ + "Employee Benefit Application Detail", + "Employee Tax Exemption Declaration Category", + "Salary Component", + "Employee Tax Exemption Proof Submission Detail", + "Income Tax Slab Other Charges", + "Taxable Salary Slab", + "Payroll Period Date", + "Salary Slip Timesheet", + "Payroll Employee Detail", + "Salary Detail", + "Employee Tax Exemption Sub Category", + "Employee Tax Exemption Category", + "Employee Benefit Claim", + "Employee Benefit Application", + "Employee Other Income", + "Employee Tax Exemption Proof Submission", + "Employee Tax Exemption Declaration", + "Employee Incentive", + "Retention Bonus", + "Additional Salary", + "Income Tax Slab", + "Payroll Period", + "Salary Slip", + "Payroll Entry", + "Salary Structure Assignment", + "Salary Structure", + ] - for doctype in doctypes_moved: - frappe.delete_doc_if_exists("DocType", doctype) + for doctype in doctypes_moved: + frappe.delete_doc_if_exists("DocType", doctype) - reports = ["Salary Register", "Bank Remittance"] + reports = ["Salary Register", "Bank Remittance"] - for report in reports: - frappe.delete_doc_if_exists("Report", report) + for report in reports: + frappe.delete_doc_if_exists("Report", report) diff --git a/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py b/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py index fca7c09c91a..37a3c357b32 100644 --- a/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py +++ b/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py @@ -6,7 +6,8 @@ import frappe def execute(): - data = frappe.db.sql('''SELECT * + data = frappe.db.sql( + """SELECT * FROM `tabSingles` WHERE doctype = "HR Settings" @@ -21,7 +22,9 @@ def execute(): "payroll_based_on", "password_policy" ) - ''', as_dict=1) + """, + as_dict=1, + ) - for d in data: - frappe.db.set_value("Payroll Settings", None, d.field, d.value) + for d in data: + frappe.db.set_value("Payroll Settings", None, d.field, d.value) diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index d1ea22f7f26..f84a739d741 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -6,28 +6,42 @@ import frappe def execute(): - if not (frappe.db.table_exists("Payroll Period") and frappe.db.table_exists("Taxable Salary Slab")): + if not ( + frappe.db.table_exists("Payroll Period") and frappe.db.table_exists("Taxable Salary Slab") + ): return - for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): + for doctype in ( + "income_tax_slab", + "salary_structure_assignment", + "employee_other_income", + "income_tax_slab_other_charges", + ): frappe.reload_doc("Payroll", "doctype", doctype) - - standard_tax_exemption_amount_exists = frappe.db.has_column("Payroll Period", "standard_tax_exemption_amount") + standard_tax_exemption_amount_exists = frappe.db.has_column( + "Payroll Period", "standard_tax_exemption_amount" + ) select_fields = "name, start_date, end_date" if standard_tax_exemption_amount_exists: select_fields = "name, start_date, end_date, standard_tax_exemption_amount" for company in frappe.get_all("Company"): - payroll_periods = frappe.db.sql(""" + payroll_periods = frappe.db.sql( + """ SELECT {0} FROM `tabPayroll Period` WHERE company=%s ORDER BY start_date DESC - """.format(select_fields), company.name, as_dict = 1) + """.format( + select_fields + ), + company.name, + as_dict=1, + ) for i, period in enumerate(payroll_periods): income_tax_slab = frappe.new_doc("Income Tax Slab") @@ -48,13 +62,17 @@ def execute(): income_tax_slab.submit() frappe.db.sql( - """ UPDATE `tabTaxable Salary Slab` + """ UPDATE `tabTaxable Salary Slab` SET parent = %s , parentfield = 'slabs' , parenttype = "Income Tax Slab" WHERE parent = %s - """, (income_tax_slab.name, period.name), as_dict = 1) + """, + (income_tax_slab.name, period.name), + as_dict=1, + ) if i == 0: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSalary Structure Assignment` set @@ -63,16 +81,19 @@ def execute(): company = %s and from_date >= %s and docstatus < 2 - """, (income_tax_slab.name, company.name, period.start_date)) + """, + (income_tax_slab.name, company.name, period.start_date), + ) # move other incomes to separate document if not frappe.db.table_exists("Employee Tax Exemption Proof Submission"): return migrated = [] - proofs = frappe.get_all("Employee Tax Exemption Proof Submission", - filters = {'docstatus': 1}, - fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + proofs = frappe.get_all( + "Employee Tax Exemption Proof Submission", + filters={"docstatus": 1}, + fields=["payroll_period", "employee", "company", "income_from_other_sources"], ) for proof in proofs: if proof.income_from_other_sources: @@ -91,14 +112,17 @@ def execute(): if not frappe.db.table_exists("Employee Tax Exemption Declaration"): return - declerations = frappe.get_all("Employee Tax Exemption Declaration", - filters = {'docstatus': 1}, - fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + declerations = frappe.get_all( + "Employee Tax Exemption Declaration", + filters={"docstatus": 1}, + fields=["payroll_period", "employee", "company", "income_from_other_sources"], ) for declaration in declerations: - if declaration.income_from_other_sources \ - and [declaration.employee, declaration.payroll_period] not in migrated: + if ( + declaration.income_from_other_sources + and [declaration.employee, declaration.payroll_period] not in migrated + ): employee_other_income = frappe.new_doc("Employee Other Income") employee_other_income.employee = declaration.employee employee_other_income.payroll_period = declaration.payroll_period diff --git a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py index 3d999bf3240..45acf49205b 100644 --- a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py +++ b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py @@ -1,4 +1,3 @@ - import frappe @@ -11,43 +10,58 @@ def execute(): frappe.reload_doc("hr", "doctype", "Leave Encashment") - additional_salaries = frappe.get_all("Additional Salary", - fields = ['name', "salary_slip", "type", "salary_component"], - filters = {'salary_slip': ['!=', '']}, - group_by = 'salary_slip' - ) - leave_encashments = frappe.get_all("Leave Encashment", - fields = ["name","additional_salary"], - filters = {'additional_salary': ['!=', '']} - ) - employee_incentives = frappe.get_all("Employee Incentive", - fields= ["name", "additional_salary"], - filters = {'additional_salary': ['!=', '']} - ) + if frappe.db.has_column("Leave Encashment", "additional_salary"): + leave_encashments = frappe.get_all( + "Leave Encashment", + fields=["name", "additional_salary"], + filters={"additional_salary": ["!=", ""]}, + ) + for leave_encashment in leave_encashments: + frappe.db.sql( + """ UPDATE `tabAdditional Salary` + SET ref_doctype = 'Leave Encashment', ref_docname = %s + WHERE name = %s + """, + (leave_encashment["name"], leave_encashment["additional_salary"]), + ) - for incentive in employee_incentives: - frappe.db.sql(""" UPDATE `tabAdditional Salary` - SET ref_doctype = 'Employee Incentive', ref_docname = %s - WHERE name = %s - """, (incentive['name'], incentive['additional_salary'])) + if frappe.db.has_column("Employee Incentive", "additional_salary"): + employee_incentives = frappe.get_all( + "Employee Incentive", + fields=["name", "additional_salary"], + filters={"additional_salary": ["!=", ""]}, + ) + for incentive in employee_incentives: + frappe.db.sql( + """ UPDATE `tabAdditional Salary` + SET ref_doctype = 'Employee Incentive', ref_docname = %s + WHERE name = %s + """, + (incentive["name"], incentive["additional_salary"]), + ) - for leave_encashment in leave_encashments: - frappe.db.sql(""" UPDATE `tabAdditional Salary` - SET ref_doctype = 'Leave Encashment', ref_docname = %s - WHERE name = %s - """, (leave_encashment['name'], leave_encashment['additional_salary'])) + if frappe.db.has_column("Additional Salary", "salary_slip"): + additional_salaries = frappe.get_all( + "Additional Salary", + fields=["name", "salary_slip", "type", "salary_component"], + filters={"salary_slip": ["!=", ""]}, + group_by="salary_slip", + ) - salary_slips = [sal["salary_slip"] for sal in additional_salaries] + salary_slips = [sal["salary_slip"] for sal in additional_salaries] - for salary in additional_salaries: - comp_type = "earnings" if salary['type'] == 'Earning' else 'deductions' - if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1: - frappe.db.sql(""" - UPDATE `tabSalary Detail` - SET additional_salary = %s - WHERE parenttype = 'Salary Slip' - and parentfield = %s - and parent = %s - and salary_component = %s - """, (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"])) + for salary in additional_salaries: + comp_type = "earnings" if salary["type"] == "Earning" else "deductions" + if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1: + frappe.db.sql( + """ + UPDATE `tabSalary Detail` + SET additional_salary = %s + WHERE parenttype = 'Salary Slip' + and parentfield = %s + and parent = %s + and salary_component = %s + """, + (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"]), + ) diff --git a/erpnext/patches/v13_0/populate_e_commerce_settings.py b/erpnext/patches/v13_0/populate_e_commerce_settings.py index 024619c4dba..29427da2bc5 100644 --- a/erpnext/patches/v13_0/populate_e_commerce_settings.py +++ b/erpnext/patches/v13_0/populate_e_commerce_settings.py @@ -1,4 +1,3 @@ - import frappe from frappe.utils import cint @@ -9,23 +8,37 @@ def execute(): frappe.reload_doc("portal", "doctype", "website_attribute") products_settings_fields = [ - "hide_variants", "products_per_page", - "enable_attribute_filters", "enable_field_filters" + "hide_variants", + "products_per_page", + "enable_attribute_filters", + "enable_field_filters", ] shopping_cart_settings_fields = [ - "enabled", "show_attachments", "show_price", - "show_stock_availability", "enable_variants", "show_contact_us_button", - "show_quantity_in_website", "show_apply_coupon_code_in_website", - "allow_items_not_in_stock", "company", "price_list", "default_customer_group", - "quotation_series", "enable_checkout", "payment_success_url", - "payment_gateway_account", "save_quotations_as_draft" + "enabled", + "show_attachments", + "show_price", + "show_stock_availability", + "enable_variants", + "show_contact_us_button", + "show_quantity_in_website", + "show_apply_coupon_code_in_website", + "allow_items_not_in_stock", + "company", + "price_list", + "default_customer_group", + "quotation_series", + "enable_checkout", + "payment_success_url", + "payment_gateway_account", + "save_quotations_as_draft", ] settings = frappe.get_doc("E Commerce Settings") def map_into_e_commerce_settings(doctype, fields): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ Select field, value from `tabSingles` @@ -33,9 +46,11 @@ def execute(): doctype='{doctype}' and field in ({fields}) """.format( - doctype=doctype, - fields=(",").join(['%s'] * len(fields)) - ), tuple(fields), as_dict=1) + doctype=doctype, fields=(",").join(["%s"] * len(fields)) + ), + tuple(fields), + as_dict=1, + ) # {'enable_attribute_filters': '1', ...} mapper = {row.field: row.value for row in data} @@ -52,10 +67,14 @@ def execute(): # move filters and attributes tables to E Commerce Settings from Products Settings for doctype in ("Website Filter Field", "Website Attribute"): - frappe.db.sql("""Update `tab{doctype}` + frappe.db.sql( + """Update `tab{doctype}` set parenttype = 'E Commerce Settings', parent = 'E Commerce Settings' where parent = 'Products Settings' - """.format(doctype=doctype)) \ No newline at end of file + """.format( + doctype=doctype + ) + ) diff --git a/erpnext/patches/v13_0/print_uom_after_quantity_patch.py b/erpnext/patches/v13_0/print_uom_after_quantity_patch.py index 3da6f749aff..a16f909fc38 100644 --- a/erpnext/patches/v13_0/print_uom_after_quantity_patch.py +++ b/erpnext/patches/v13_0/print_uom_after_quantity_patch.py @@ -6,4 +6,4 @@ from erpnext.setup.install import create_print_uom_after_qty_custom_field def execute(): - create_print_uom_after_qty_custom_field() + create_print_uom_after_qty_custom_field() diff --git a/erpnext/patches/v13_0/remove_attribute_field_from_item_variant_setting.py b/erpnext/patches/v13_0/remove_attribute_field_from_item_variant_setting.py index bbe3eb5815b..4efbe4df96a 100644 --- a/erpnext/patches/v13_0/remove_attribute_field_from_item_variant_setting.py +++ b/erpnext/patches/v13_0/remove_attribute_field_from_item_variant_setting.py @@ -5,5 +5,7 @@ def execute(): """Remove has_variants and attribute fields from item variant settings.""" frappe.reload_doc("stock", "doctype", "Item Variant Settings") - frappe.db.sql("""delete from `tabVariant Field` - where field_name in ('attributes', 'has_variants')""") + frappe.db.sql( + """delete from `tabVariant Field` + where field_name in ('attributes', 'has_variants')""" + ) diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py index 381c3902da0..efd2098d9ed 100644 --- a/erpnext/patches/v13_0/remove_bad_selling_defaults.py +++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py @@ -12,5 +12,5 @@ def execute(): if selling_settings.territory in (_("All Territories"), "All Territories"): selling_settings.territory = None - selling_settings.flags.ignore_mandatory=True + selling_settings.flags.ignore_mandatory = True selling_settings.save(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py new file mode 100644 index 00000000000..35c93805c8b --- /dev/null +++ b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py @@ -0,0 +1,35 @@ +import frappe + + +def execute(): + """ + Remove "production_plan_item" field where linked field doesn't exist in tha table. + """ + frappe.reload_doc("manufacturing", "doctype", "production_plan_item") + + work_order = frappe.qb.DocType("Work Order") + pp_item = frappe.qb.DocType("Production Plan Item") + + broken_work_orders = ( + frappe.qb.from_(work_order) + .left_join(pp_item) + .on(work_order.production_plan_item == pp_item.name) + .select(work_order.name) + .where( + (work_order.docstatus == 0) + & (work_order.production_plan_item.notnull()) + & (work_order.production_plan_item.like("new-production-plan%")) + & (pp_item.name.isnull()) + ) + ).run() + + if not broken_work_orders: + return + + broken_work_order_names = [d[0] for d in broken_work_orders] + + ( + frappe.qb.update(work_order) + .set(work_order.production_plan_item, None) + .where(work_order.name.isin(broken_work_order_names)) + ).run() diff --git a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py index e9778043229..3bd717d77b8 100644 --- a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py +++ b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field diff --git a/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py index 002166409dc..7010f47275b 100644 --- a/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py +++ b/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index bf5438c4d2e..a9b6df70985 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -7,63 +7,78 @@ from frappe.model.utils.rename_field import rename_field def execute(): - if frappe.db.exists('DocType', 'Issue'): - issues = frappe.db.get_all('Issue', fields=['name', 'response_by_variance', 'resolution_by_variance', 'mins_to_first_response'], - order_by='creation desc') - frappe.reload_doc('support', 'doctype', 'issue') + if frappe.db.exists("DocType", "Issue"): + issues = frappe.db.get_all( + "Issue", + fields=["name", "response_by_variance", "resolution_by_variance", "mins_to_first_response"], + order_by="creation desc", + ) + frappe.reload_doc("support", "doctype", "issue") # rename fields rename_map = { - 'agreement_fulfilled': 'agreement_status', - 'mins_to_first_response': 'first_response_time' + "agreement_fulfilled": "agreement_status", + "mins_to_first_response": "first_response_time", } for old, new in rename_map.items(): - rename_field('Issue', old, new) + rename_field("Issue", old, new) # change fieldtype to duration count = 0 for entry in issues: - response_by_variance = convert_to_seconds(entry.response_by_variance, 'Hours') - resolution_by_variance = convert_to_seconds(entry.resolution_by_variance, 'Hours') - mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') - frappe.db.set_value('Issue', entry.name, { - 'response_by_variance': response_by_variance, - 'resolution_by_variance': resolution_by_variance, - 'first_response_time': mins_to_first_response - }, update_modified=False) + response_by_variance = convert_to_seconds(entry.response_by_variance, "Hours") + resolution_by_variance = convert_to_seconds(entry.resolution_by_variance, "Hours") + mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, "Minutes") + frappe.db.set_value( + "Issue", + entry.name, + { + "response_by_variance": response_by_variance, + "resolution_by_variance": resolution_by_variance, + "first_response_time": mins_to_first_response, + }, + update_modified=False, + ) # commit after every 100 updates count += 1 - if count%100 == 0: + if count % 100 == 0: frappe.db.commit() - if frappe.db.exists('DocType', 'Opportunity'): - opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') - frappe.reload_doctype('Opportunity', force=True) - rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') + if frappe.db.exists("DocType", "Opportunity"): + opportunities = frappe.db.get_all( + "Opportunity", fields=["name", "mins_to_first_response"], order_by="creation desc" + ) + frappe.reload_doctype("Opportunity", force=True) + rename_field("Opportunity", "mins_to_first_response", "first_response_time") # change fieldtype to duration - frappe.reload_doc('crm', 'doctype', 'opportunity', force=True) + frappe.reload_doc("crm", "doctype", "opportunity", force=True) count = 0 for entry in opportunities: - mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') - frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response, update_modified=False) + mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, "Minutes") + frappe.db.set_value( + "Opportunity", entry.name, "first_response_time", mins_to_first_response, update_modified=False + ) # commit after every 100 updates count += 1 - if count%100 == 0: + if count % 100 == 0: frappe.db.commit() # renamed reports from "Minutes to First Response for Issues" to "First Response Time for Issues". Same for Opportunity - for report in ['Minutes to First Response for Issues', 'Minutes to First Response for Opportunity']: - if frappe.db.exists('Report', report): - frappe.delete_doc('Report', report, ignore_permissions=True) + for report in [ + "Minutes to First Response for Issues", + "Minutes to First Response for Opportunity", + ]: + if frappe.db.exists("Report", report): + frappe.delete_doc("Report", report, ignore_permissions=True) def convert_to_seconds(value, unit): seconds = 0 - if value == 0: + if not value: return seconds - if unit == 'Hours': + if unit == "Hours": seconds = value * 3600 - if unit == 'Minutes': + if unit == "Minutes": seconds = value * 60 return seconds 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 index b129cbe80bc..d3896dd0a84 100644 --- 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 @@ -6,16 +6,19 @@ import frappe def execute(): - if frappe.db.exists('DocType', 'Issue'): + if frappe.db.exists("DocType", "Issue"): frappe.reload_doc("support", "doctype", "issue") rename_status() + def rename_status(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabIssue` SET status = 'On Hold' WHERE status = 'Hold' - """) + """ + ) diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py index f4f9b17fb81..e4b91412ee1 100644 --- a/erpnext/patches/v13_0/rename_ksa_qr_field.py +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -7,26 +7,30 @@ from frappe.model.utils.rename_field import rename_field def execute(): - company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) if not company: return - if frappe.db.exists('DocType', 'Sales Invoice'): - frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True) + if frappe.db.exists("DocType", "Sales Invoice"): + frappe.reload_doc("accounts", "doctype", "sales_invoice", force=True) # rename_field method assumes that the field already exists or the doc is synced - if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'): - create_custom_fields({ - 'Sales Invoice': [ - dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 - ) - ] - }) + if not frappe.db.has_column("Sales Invoice", "ksa_einv_qr"): + create_custom_fields( + { + "Sales Invoice": [ + dict( + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, + ) + ] + } + ) - if frappe.db.has_column('Sales Invoice', 'qr_code'): - rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') + if frappe.db.has_column("Sales Invoice", "qr_code"): + rename_field("Sales Invoice", "qr_code", "ksa_einv_qr") frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code") diff --git a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py index ecd03441e0a..0e5423444aa 100644 --- a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py +++ b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -16,7 +15,7 @@ def execute(): "enable_razorpay": "enable_razorpay_for_memberships", "debit_account": "membership_debit_account", "payment_account": "membership_payment_account", - "webhook_secret": "membership_webhook_secret" + "webhook_secret": "membership_webhook_secret", } for old_name, new_name in rename_fields_map.items(): diff --git a/erpnext/patches/v13_0/rename_non_profit_fields.py b/erpnext/patches/v13_0/rename_non_profit_fields.py new file mode 100644 index 00000000000..285403c004f --- /dev/null +++ b/erpnext/patches/v13_0/rename_non_profit_fields.py @@ -0,0 +1,16 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + if frappe.db.table_exists("Donation"): + frappe.reload_doc("non_profit", "doctype", "Donation") + + rename_field("Donation", "razorpay_payment_id", "payment_id") + + if frappe.db.table_exists("Tax Exemption 80G Certificate"): + frappe.reload_doc("regional", "doctype", "Tax Exemption 80G Certificate") + frappe.reload_doc("regional", "doctype", "Tax Exemption 80G Certificate Detail") + + rename_field("Tax Exemption 80G Certificate", "razorpay_payment_id", "payment_id") + rename_field("Tax Exemption 80G Certificate Detail", "razorpay_payment_id", "payment_id") diff --git a/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py index 28054317ad3..434dbb47e76 100644 --- a/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py +++ b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py @@ -3,22 +3,19 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('hr', 'doctype', 'hr_settings') + frappe.reload_doc("hr", "doctype", "hr_settings") try: # Rename the field - rename_field('HR Settings', 'stop_birthday_reminders', 'send_birthday_reminders') + rename_field("HR Settings", "stop_birthday_reminders", "send_birthday_reminders") # Reverse the value - old_value = frappe.db.get_single_value('HR Settings', 'send_birthday_reminders') + old_value = frappe.db.get_single_value("HR Settings", "send_birthday_reminders") frappe.db.set_value( - 'HR Settings', - 'HR Settings', - 'send_birthday_reminders', - 1 if old_value == 0 else 0 + "HR Settings", "HR Settings", "send_birthday_reminders", 1 if old_value == 0 else 0 ) except Exception as e: if e.args[0] != 1054: - raise \ No newline at end of file + raise diff --git a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py index f49b1e4bd8c..7d757b7a0fb 100644 --- a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py +++ b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py index a2c960c8f37..ba2feb3a4d0 100644 --- a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py +++ b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py @@ -10,9 +10,13 @@ def execute(): pos_profiles = frappe.get_all("POS Profile") for pos_profile in pos_profiles: - payments = frappe.db.sql(""" + payments = frappe.db.sql( + """ select idx, parentfield, parenttype, parent, mode_of_payment, `default` from `tabSales Invoice Payment` where parent=%s - """, pos_profile.name, as_dict=1) + """, + pos_profile.name, + as_dict=1, + ) if payments: for payment_mode in payments: pos_payment_method = frappe.new_doc("POS Payment Method") diff --git a/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py b/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py index ba96fdd2266..bf82f443082 100644 --- a/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py +++ b/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py @@ -5,7 +5,7 @@ import frappe def execute(): - if frappe.db.table_exists('Supplier Item Group'): + if frappe.db.table_exists("Supplier Item Group"): frappe.reload_doc("selling", "doctype", "party_specific_item") sig = frappe.db.get_all("Supplier Item Group", fields=["name", "supplier", "item_group"]) for item in sig: diff --git a/erpnext/patches/v13_0/requeue_failed_reposts.py b/erpnext/patches/v13_0/requeue_failed_reposts.py index 213cb9e26e4..752490da304 100644 --- a/erpnext/patches/v13_0/requeue_failed_reposts.py +++ b/erpnext/patches/v13_0/requeue_failed_reposts.py @@ -4,9 +4,11 @@ from frappe.utils import cstr def execute(): - reposts = frappe.get_all("Repost Item Valuation", - {"status": "Failed", "modified": [">", "2021-10-05"] }, - ["name", "modified", "error_log"]) + reposts = frappe.get_all( + "Repost Item Valuation", + {"status": "Failed", "modified": [">", "2021-10-05"]}, + ["name", "modified", "error_log"], + ) for repost in reposts: if "check_freezing_date" in cstr(repost.error_log): diff --git a/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py b/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py index e6717c57db3..5dfea5eaed7 100644 --- a/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py +++ b/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py @@ -6,40 +6,35 @@ import frappe def execute(): - """ - Reset Clearance Date for Payment Entries of type Internal Transfer that have only been reconciled with one Bank Transaction. - This will allow the Payment Entries to be reconciled with the second Bank Transaction using the Bank Reconciliation Tool. - """ + """ + Reset Clearance Date for Payment Entries of type Internal Transfer that have only been reconciled with one Bank Transaction. + This will allow the Payment Entries to be reconciled with the second Bank Transaction using the Bank Reconciliation Tool. + """ - intra_company_pe = get_intra_company_payment_entries_with_clearance_dates() - reconciled_bank_transactions = get_reconciled_bank_transactions(intra_company_pe) + intra_company_pe = get_intra_company_payment_entries_with_clearance_dates() + reconciled_bank_transactions = get_reconciled_bank_transactions(intra_company_pe) + + for payment_entry in reconciled_bank_transactions: + if len(reconciled_bank_transactions[payment_entry]) == 1: + frappe.db.set_value("Payment Entry", payment_entry, "clearance_date", None) - for payment_entry in reconciled_bank_transactions: - if len(reconciled_bank_transactions[payment_entry]) == 1: - frappe.db.set_value('Payment Entry', payment_entry, 'clearance_date', None) def get_intra_company_payment_entries_with_clearance_dates(): - return frappe.get_all( - 'Payment Entry', - filters = { - 'payment_type': 'Internal Transfer', - 'clearance_date': ["not in", None] - }, - pluck = 'name' - ) + return frappe.get_all( + "Payment Entry", + filters={"payment_type": "Internal Transfer", "clearance_date": ["not in", None]}, + pluck="name", + ) + def get_reconciled_bank_transactions(intra_company_pe): - """Returns dictionary where each key:value pair is Payment Entry : List of Bank Transactions reconciled with Payment Entry""" + """Returns dictionary where each key:value pair is Payment Entry : List of Bank Transactions reconciled with Payment Entry""" - reconciled_bank_transactions = {} + reconciled_bank_transactions = {} - for payment_entry in intra_company_pe: - reconciled_bank_transactions[payment_entry] = frappe.get_all( - 'Bank Transaction Payments', - filters = { - 'payment_entry': payment_entry - }, - pluck='parent' - ) + for payment_entry in intra_company_pe: + reconciled_bank_transactions[payment_entry] = frappe.get_all( + "Bank Transaction Payments", filters={"payment_entry": payment_entry}, pluck="parent" + ) - return reconciled_bank_transactions \ No newline at end of file + return reconciled_bank_transactions diff --git a/erpnext/patches/v13_0/set_available_for_use_date_if_missing.py b/erpnext/patches/v13_0/set_available_for_use_date_if_missing.py new file mode 100644 index 00000000000..3cfbd6e7fd0 --- /dev/null +++ b/erpnext/patches/v13_0/set_available_for_use_date_if_missing.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + """ + Sets available-for-use date for Assets created in older versions of ERPNext, + before the field was introduced. + """ + + assets = get_assets_without_available_for_use_date() + + for asset in assets: + frappe.db.set_value("Asset", asset.name, "available_for_use_date", asset.purchase_date) + +def get_assets_without_available_for_use_date(): + return frappe.get_all( + "Asset", + filters = { + "available_for_use_date": ["in", ["", None]] + }, + fields = ["name", "purchase_date"] + ) diff --git a/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py b/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py index b955686a17e..bc2d1b94f79 100644 --- a/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py +++ b/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py @@ -1,11 +1,25 @@ - import frappe def execute(): - company = frappe.db.get_single_value('Global Defaults', 'default_company') - doctypes = ['Clinical Procedure', 'Inpatient Record', 'Lab Test', 'Sample Collection', 'Patient Appointment', 'Patient Encounter', 'Vital Signs', 'Therapy Session', 'Therapy Plan', 'Patient Assessment'] + company = frappe.db.get_single_value("Global Defaults", "default_company") + doctypes = [ + "Clinical Procedure", + "Inpatient Record", + "Lab Test", + "Sample Collection", + "Patient Appointment", + "Patient Encounter", + "Vital Signs", + "Therapy Session", + "Therapy Plan", + "Patient Assessment", + ] for entry in doctypes: - if frappe.db.exists('DocType', entry): - frappe.reload_doc('Healthcare', 'doctype', entry) - frappe.db.sql("update `tab{dt}` set company = {company} where ifnull(company, '') = ''".format(dt=entry, company=frappe.db.escape(company))) + if frappe.db.exists("DocType", entry): + frappe.reload_doc("Healthcare", "doctype", entry) + frappe.db.sql( + "update `tab{dt}` set company = {company} where ifnull(company, '') = ''".format( + dt=entry, company=frappe.db.escape(company) + ) + ) diff --git a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py index c744f35b72f..adc8784b5ba 100644 --- a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py +++ b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py @@ -2,7 +2,11 @@ import frappe def execute(): - frappe.reload_doc('HR', 'doctype', 'Leave Allocation') - frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry') - frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""") - frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""") + frappe.reload_doc("HR", "doctype", "Leave Allocation") + frappe.reload_doc("HR", "doctype", "Leave Ledger Entry") + frappe.db.sql( + """update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""" + ) + frappe.db.sql( + """update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""" + ) diff --git a/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py index 0366d4902dc..ef02e250c89 100644 --- a/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py +++ b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py @@ -2,10 +2,11 @@ import frappe def execute(): - frappe.reload_doc('manufacturing', 'doctype', 'bom') - frappe.reload_doc('manufacturing', 'doctype', 'bom_operation') + frappe.reload_doc("manufacturing", "doctype", "bom") + frappe.reload_doc("manufacturing", "doctype", "bom_operation") - frappe.db.sql(''' + frappe.db.sql( + """ UPDATE `tabBOM Operation` SET @@ -13,4 +14,5 @@ def execute(): WHERE time_in_mins = 0 AND operating_cost > 0 AND hour_rate > 0 AND docstatus = 1 AND parenttype = "BOM" - ''') \ No newline at end of file + """ + ) diff --git a/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py b/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py index 3b751415eed..0cefa028ec4 100644 --- a/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py +++ b/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py @@ -1,4 +1,3 @@ - import frappe @@ -11,8 +10,11 @@ def execute(): frappe.reload_doc("Accounts", "doctype", "Payment Gateway Account") set_payment_channel_as_email() + def set_payment_channel_as_email(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabPayment Gateway Account` SET `payment_channel` = "Email" - """) + """ + ) diff --git a/erpnext/patches/v13_0/set_pos_closing_as_failed.py b/erpnext/patches/v13_0/set_pos_closing_as_failed.py index a838ce07b93..e2226c1cf0a 100644 --- a/erpnext/patches/v13_0/set_pos_closing_as_failed.py +++ b/erpnext/patches/v13_0/set_pos_closing_as_failed.py @@ -1,8 +1,7 @@ - import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'pos_closing_entry') + frappe.reload_doc("accounts", "doctype", "pos_closing_entry") - frappe.db.sql("update `tabPOS Closing Entry` set `status` = 'Failed' where `status` = 'Queued'") + frappe.db.sql("update `tabPOS Closing Entry` set `status` = 'Failed' where `status` = 'Queued'") diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py new file mode 100644 index 00000000000..fe9eb8b2cc1 --- /dev/null +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -0,0 +1,40 @@ +import frappe + + +def execute(): + """ + Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table. + """ + + POSClosingEntry = frappe.qb.DocType("POS Closing Entry") + open_pos_closing_entries = ( + frappe.qb.from_(POSClosingEntry) + .select(POSClosingEntry.name) + .where(POSClosingEntry.docstatus == 0) + .run() + ) + if open_pos_closing_entries: + open_pos_closing_entries = [d[0] for d in open_pos_closing_entries] + + if not open_pos_closing_entries: + return + + frappe.reload_doc("Accounts", "doctype", "pos_invoice_reference") + + POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") + POSInvoice = frappe.qb.DocType("POS Invoice") + pos_invoice_references = ( + frappe.qb.from_(POSInvoiceReference) + .join(POSInvoice) + .on(POSInvoiceReference.pos_invoice == POSInvoice.name) + .select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against) + .where(POSInvoiceReference.parent.isin(open_pos_closing_entries)) + .run(as_dict=True) + ) + + for row in pos_invoice_references: + frappe.db.set_value("POS Invoice Reference", row.name, "is_return", row.is_return) + if row.is_return: + frappe.db.set_value("POS Invoice Reference", row.name, "return_against", row.return_against) + else: + frappe.db.set_value("POS Invoice Reference", row.name, "return_against", None) diff --git a/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py index 9887ad9df0c..e1c8526f10d 100644 --- a/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py +++ b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py @@ -3,8 +3,10 @@ import frappe def execute(): frappe.reload_doc("maintenance", "doctype", "Maintenance Schedule Detail") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabMaintenance Schedule Detail` SET completion_status = 'Pending' WHERE docstatus < 2 - """) + """ + ) diff --git a/erpnext/patches/v13_0/set_training_event_attendance.py b/erpnext/patches/v13_0/set_training_event_attendance.py index 27a9d3ff089..7b557589723 100644 --- a/erpnext/patches/v13_0/set_training_event_attendance.py +++ b/erpnext/patches/v13_0/set_training_event_attendance.py @@ -1,10 +1,11 @@ - import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'training_event') - frappe.reload_doc('hr', 'doctype', 'training_event_employee') + frappe.reload_doc("hr", "doctype", "training_event") + frappe.reload_doc("hr", "doctype", "training_event_employee") - frappe.db.sql("update `tabTraining Event Employee` set `attendance` = 'Present'") - frappe.db.sql("update `tabTraining Event Employee` set `is_mandatory` = 1 where `attendance` = 'Mandatory'") + frappe.db.sql("update `tabTraining Event Employee` set `attendance` = 'Present'") + frappe.db.sql( + "update `tabTraining Event Employee` set `is_mandatory` = 1 where `attendance` = 'Mandatory'" + ) diff --git a/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py index f097ab9297f..1adf0d84538 100644 --- a/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py +++ b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py @@ -2,35 +2,37 @@ import frappe def execute(): - """ - 1. Get submitted Work Orders with MR, MR Item and SO set - 2. Get SO Item detail from MR Item detail in WO, and set in WO - 3. Update work_order_qty in SO - """ - work_order = frappe.qb.DocType("Work Order") - query = ( - frappe.qb.from_(work_order) - .select( - work_order.name, work_order.produced_qty, - work_order.material_request, - work_order.material_request_item, - work_order.sales_order - ).where( - (work_order.material_request.isnotnull()) - & (work_order.material_request_item.isnotnull()) - & (work_order.sales_order.isnotnull()) - & (work_order.docstatus == 1) - & (work_order.produced_qty > 0) - ) - ) - results = query.run(as_dict=True) + """ + 1. Get submitted Work Orders with MR, MR Item and SO set + 2. Get SO Item detail from MR Item detail in WO, and set in WO + 3. Update work_order_qty in SO + """ + work_order = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(work_order) + .select( + work_order.name, + work_order.produced_qty, + work_order.material_request, + work_order.material_request_item, + work_order.sales_order, + ) + .where( + (work_order.material_request.isnotnull()) + & (work_order.material_request_item.isnotnull()) + & (work_order.sales_order.isnotnull()) + & (work_order.docstatus == 1) + & (work_order.produced_qty > 0) + ) + ) + results = query.run(as_dict=True) - for row in results: - so_item = frappe.get_value( - "Material Request Item", row.material_request_item, "sales_order_item" - ) - frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item) + for row in results: + so_item = frappe.get_value( + "Material Request Item", row.material_request_item, "sales_order_item" + ) + frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item) - if so_item: - wo = frappe.get_doc("Work Order", row.name) - wo.update_work_order_qty_in_so() + if so_item: + wo = frappe.get_doc("Work Order", row.name) + wo.update_work_order_qty_in_so() diff --git a/erpnext/patches/v13_0/set_youtube_video_id.py b/erpnext/patches/v13_0/set_youtube_video_id.py index 76aaaea279c..9766bb871cf 100644 --- a/erpnext/patches/v13_0/set_youtube_video_id.py +++ b/erpnext/patches/v13_0/set_youtube_video_id.py @@ -1,11 +1,10 @@ - import frappe from erpnext.utilities.doctype.video.video import get_id_from_url def execute(): - frappe.reload_doc("utilities", "doctype","video") + frappe.reload_doc("utilities", "doctype", "video") for video in frappe.get_all("Video", fields=["name", "url", "youtube_video_id"]): if video.url and not video.youtube_video_id: diff --git a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py index 36bedf4f9ba..40c10f30354 100644 --- a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py +++ b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py @@ -1,12 +1,11 @@ - import frappe from erpnext.regional.india.setup import add_custom_roles_for_reports def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company: - return + company = frappe.get_all("Company", filters={"country": "India"}) + if not company: + return - add_custom_roles_for_reports() + add_custom_roles_for_reports() diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py index 2d35ea34587..1c36b536841 100644 --- a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py +++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py @@ -4,15 +4,13 @@ from erpnext.regional.india.setup import make_custom_fields def execute(): - if frappe.get_all('Company', filters = {'country': 'India'}): - frappe.reload_doc('accounts', 'doctype', 'POS Invoice') - frappe.reload_doc('accounts', 'doctype', 'POS Invoice Item') + if frappe.get_all("Company", filters={"country": "India"}): + frappe.reload_doc("accounts", "doctype", "POS Invoice") + frappe.reload_doc("accounts", "doctype", "POS Invoice Item") make_custom_fields() - if not frappe.db.exists('Party Type', 'Donor'): - frappe.get_doc({ - 'doctype': 'Party Type', - 'party_type': 'Donor', - 'account_type': 'Receivable' - }).insert(ignore_permissions=True) + if not frappe.db.exists("Party Type", "Donor"): + frappe.get_doc( + {"doctype": "Party Type", "party_type": "Donor", "account_type": "Receivable"} + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py index 82cc1ff771c..093e8a7646f 100644 --- a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py +++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py @@ -6,12 +6,14 @@ import frappe def execute(): - frappe.reload_doc('payroll', 'doctype', 'gratuity_rule') - frappe.reload_doc('payroll', 'doctype', 'gratuity_rule_slab') - frappe.reload_doc('payroll', 'doctype', 'gratuity_applicable_component') - if frappe.db.exists("Company", {"country": "India"}): - from erpnext.regional.india.setup import create_gratuity_rule - create_gratuity_rule() - if frappe.db.exists("Company", {"country": "United Arab Emirates"}): - from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule - create_gratuity_rule() + frappe.reload_doc("payroll", "doctype", "gratuity_rule") + frappe.reload_doc("payroll", "doctype", "gratuity_rule_slab") + frappe.reload_doc("payroll", "doctype", "gratuity_applicable_component") + if frappe.db.exists("Company", {"country": "India"}): + from erpnext.regional.india.setup import create_gratuity_rule + + create_gratuity_rule() + if frappe.db.exists("Company", {"country": "United Arab Emirates"}): + from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule + + create_gratuity_rule() diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py index ee8f96d0070..339acfe426b 100644 --- a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py +++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py @@ -1,4 +1,3 @@ - import frappe from erpnext.healthcare.setup import setup_patient_history_settings diff --git a/erpnext/patches/v13_0/setup_uae_vat_fields.py b/erpnext/patches/v13_0/setup_uae_vat_fields.py index d89e0521d8d..70466465e42 100644 --- a/erpnext/patches/v13_0/setup_uae_vat_fields.py +++ b/erpnext/patches/v13_0/setup_uae_vat_fields.py @@ -7,12 +7,12 @@ from erpnext.regional.united_arab_emirates.setup import setup def execute(): - company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'}) + company = frappe.get_all("Company", filters={"country": "United Arab Emirates"}) if not company: return - frappe.reload_doc('regional', 'report', 'uae_vat_201') - frappe.reload_doc('regional', 'doctype', 'uae_vat_settings') - frappe.reload_doc('regional', 'doctype', 'uae_vat_account') + frappe.reload_doc("regional", "report", "uae_vat_201") + frappe.reload_doc("regional", "doctype", "uae_vat_settings") + frappe.reload_doc("regional", "doctype", "uae_vat_account") setup() diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py index 968a83a4212..005980e80a5 100644 --- a/erpnext/patches/v13_0/stock_entry_enhancements.py +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -6,27 +6,31 @@ import frappe def execute(): - frappe.reload_doc("stock", "doctype", "stock_entry") - if frappe.db.has_column("Stock Entry", "add_to_transit"): - frappe.db.sql(""" + frappe.reload_doc("stock", "doctype", "stock_entry") + if frappe.db.has_column("Stock Entry", "add_to_transit"): + frappe.db.sql( + """ UPDATE `tabStock Entry` SET stock_entry_type = 'Material Transfer', purpose = 'Material Transfer', add_to_transit = 1 WHERE stock_entry_type = 'Send to Warehouse' - """) + """ + ) - frappe.db.sql("""UPDATE `tabStock Entry` SET + frappe.db.sql( + """UPDATE `tabStock Entry` SET stock_entry_type = 'Material Transfer', purpose = 'Material Transfer' WHERE stock_entry_type = 'Receive at Warehouse' - """) + """ + ) - frappe.reload_doc("stock", "doctype", "warehouse_type") - if not frappe.db.exists('Warehouse Type', 'Transit'): - doc = frappe.new_doc('Warehouse Type') - doc.name = 'Transit' - doc.insert() + frappe.reload_doc("stock", "doctype", "warehouse_type") + if not frappe.db.exists("Warehouse Type", "Transit"): + doc = frappe.new_doc("Warehouse Type") + doc.name = "Transit" + doc.insert() - frappe.reload_doc("stock", "doctype", "stock_entry_type") - frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") - frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") + frappe.reload_doc("stock", "doctype", "stock_entry_type") + frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") + frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") diff --git a/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py b/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py index fd48c0d902d..5f3fc5761ac 100644 --- a/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py +++ b/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py @@ -7,12 +7,10 @@ from erpnext.regional.india.setup import create_custom_fields, get_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - custom_fields = { - 'Sales Invoice': get_custom_fields().get('Sales Invoice') - } + custom_fields = {"Sales Invoice": get_custom_fields().get("Sales Invoice")} create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py index 4ec22e9d0e1..b69a408e65b 100644 --- a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py +++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py @@ -4,7 +4,8 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos def execute(): - broken_sles = frappe.db.sql(""" + broken_sles = frappe.db.sql( + """ select name, serial_no from `tabStock Ledger Entry` where @@ -12,15 +13,15 @@ def execute(): and ( serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s or serial_no = %s ) """, - ( - " %", # leading whitespace - "% ", # trailing whitespace - "%\n %", # leading whitespace on newline - "% \n%", # trailing whitespace on newline - "\n", # just new line - ), - as_dict=True, - ) + ( + " %", # leading whitespace + "% ", # trailing whitespace + "%\n %", # leading whitespace on newline + "% \n%", # trailing whitespace on newline + "\n", # just new line + ), + as_dict=True, + ) frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles) @@ -37,7 +38,9 @@ def execute(): if correct_sr_no == sle.serial_no: continue - frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False) + frappe.db.set_value( + "Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False + ) broken_serial_nos.update(serial_no_list) if not broken_serial_nos: @@ -45,14 +48,15 @@ def execute(): # Patch serial No documents if they don't have purchase info # Purchase info is used for fetching incoming rate - broken_sr_no_records = frappe.get_list("Serial No", - filters={ - "status":"Active", - "name": ("in", broken_serial_nos), - "purchase_document_type": ("is", "not set") - }, - pluck="name", - ) + broken_sr_no_records = frappe.get_list( + "Serial No", + filters={ + "status": "Active", + "name": ("in", broken_serial_nos), + "purchase_document_type": ("is", "not set"), + }, + pluck="name", + ) frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records) diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py new file mode 100644 index 00000000000..f593dc28410 --- /dev/null +++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py @@ -0,0 +1,24 @@ +import frappe + + +def execute(): + + frappe.reload_doc("loan_management", "doctype", "loan") + frappe.reload_doc("loan_management", "doctype", "loan_disbursement") + frappe.reload_doc("loan_management", "doctype", "loan_repayment") + + ld = frappe.qb.DocType("Loan Disbursement").as_("ld") + lr = frappe.qb.DocType("Loan Repayment").as_("lr") + loan = frappe.qb.DocType("Loan") + + frappe.qb.update(ld).inner_join(loan).on(loan.name == ld.against_loan).set( + ld.disbursement_account, loan.disbursement_account + ).set(ld.loan_account, loan.loan_account).where(ld.docstatus < 2).run() + + frappe.qb.update(lr).inner_join(loan).on(loan.name == lr.against_loan).set( + lr.payment_account, loan.payment_account + ).set(lr.loan_account, loan.loan_account).set( + lr.penalty_income_account, loan.penalty_income_account + ).where( + lr.docstatus < 2 + ).run() diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py index 9993063e485..3c6c5b5b75f 100644 --- a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py +++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py @@ -1,4 +1,3 @@ - # Copyright (c) 2019, Frappe and Contributors # License: GNU General Public License v3. See license.txt @@ -12,11 +11,9 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "work_order_item") frappe.reload_doc("manufacturing", "doctype", "job_card") - data = frappe.get_all("Work Order", - filters = { - "docstatus": 1, - "status": ("in", ["In Process", "Completed"]) - }) + data = frappe.get_all( + "Work Order", filters={"docstatus": 1, "status": ("in", ["In Process", "Completed"])} + ) for d in data: doc = frappe.get_doc("Work Order", d.name) @@ -24,18 +21,22 @@ def execute(): doc.db_set("actual_start_date", doc.actual_start_date, update_modified=False) if doc.status == "Completed": - frappe.db.set_value("Work Order", d.name, { - "actual_end_date": doc.actual_end_date, - "lead_time": doc.lead_time - }, update_modified=False) + frappe.db.set_value( + "Work Order", + d.name, + {"actual_end_date": doc.actual_end_date, "lead_time": doc.lead_time}, + update_modified=False, + ) if not doc.planned_end_date: planned_end_date = add_to_date(doc.planned_start_date, minutes=doc.lead_time) doc.db_set("planned_end_date", doc.actual_start_date, update_modified=False) - frappe.db.sql(""" UPDATE `tabJob Card` as jc, `tabWork Order` as wo + frappe.db.sql( + """ UPDATE `tabJob Card` as jc, `tabWork Order` as wo SET jc.production_item = wo.production_item, jc.item_name = wo.item_name WHERE jc.work_order = wo.name and IFNULL(jc.production_item, "") = "" - """) \ No newline at end of file + """ + ) diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py index dc973a9d451..e37f291233e 100644 --- a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py +++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py @@ -2,7 +2,7 @@ import frappe def execute(): - """ Correct amount in child table of required items table.""" + """Correct amount in child table of required items table.""" frappe.reload_doc("manufacturing", "doctype", "work_order") frappe.reload_doc("manufacturing", "doctype", "work_order_item") diff --git a/erpnext/patches/v13_0/update_category_in_ltds_certificate.py b/erpnext/patches/v13_0/update_category_in_ltds_certificate.py index a5f5a23449a..5a0873e0e53 100644 --- a/erpnext/patches/v13_0/update_category_in_ltds_certificate.py +++ b/erpnext/patches/v13_0/update_category_in_ltds_certificate.py @@ -2,19 +2,15 @@ import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('regional', 'doctype', 'lower_deduction_certificate') + frappe.reload_doc("regional", "doctype", "lower_deduction_certificate") ldc = frappe.qb.DocType("Lower Deduction Certificate").as_("ldc") supplier = frappe.qb.DocType("Supplier") - frappe.qb.update(ldc).inner_join(supplier).on( - ldc.supplier == supplier.name - ).set( + frappe.qb.update(ldc).inner_join(supplier).on(ldc.supplier == supplier.name).set( ldc.tax_withholding_category, supplier.tax_withholding_category - ).where( - ldc.tax_withholding_category.isnull() - ).run() \ No newline at end of file + ).where(ldc.tax_withholding_category.isnull()).run() diff --git a/erpnext/patches/v13_0/update_custom_fields_for_shopify.py b/erpnext/patches/v13_0/update_custom_fields_for_shopify.py index 8c724a8cb31..53eb6e3c788 100644 --- a/erpnext/patches/v13_0/update_custom_fields_for_shopify.py +++ b/erpnext/patches/v13_0/update_custom_fields_for_shopify.py @@ -10,5 +10,5 @@ from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings impo def execute(): - if frappe.db.get_single_value('Shopify Settings', 'enable_shopify'): + if frappe.db.get_single_value("Shopify Settings", "enable_shopify"): setup_custom_fields() diff --git a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py index 90fb50fb42c..c538476edb3 100644 --- a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py +++ b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py @@ -5,22 +5,23 @@ import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate') + frappe.reload_doc("accounts", "doctype", "Tax Withholding Rate") - if frappe.db.has_column('Tax Withholding Rate', 'fiscal_year'): - tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year']) + if frappe.db.has_column("Tax Withholding Rate", "fiscal_year"): + tds_category_rates = frappe.get_all("Tax Withholding Rate", fields=["name", "fiscal_year"]) fiscal_year_map = {} - fiscal_year_details = frappe.get_all('Fiscal Year', fields=['name', 'year_start_date', 'year_end_date']) + fiscal_year_details = frappe.get_all( + "Fiscal Year", fields=["name", "year_start_date", "year_end_date"] + ) for d in fiscal_year_details: fiscal_year_map.setdefault(d.name, d) for rate in tds_category_rates: - from_date = fiscal_year_map.get(rate.fiscal_year).get('year_start_date') - to_date = fiscal_year_map.get(rate.fiscal_year).get('year_end_date') + from_date = fiscal_year_map.get(rate.fiscal_year).get("year_start_date") + to_date = fiscal_year_map.get(rate.fiscal_year).get("year_end_date") - frappe.db.set_value('Tax Withholding Rate', rate.name, { - 'from_date': from_date, - 'to_date': to_date - }) \ No newline at end of file + frappe.db.set_value( + "Tax Withholding Rate", rate.name, {"from_date": from_date, "to_date": to_date} + ) diff --git a/erpnext/patches/v13_0/update_deferred_settings.py b/erpnext/patches/v13_0/update_deferred_settings.py index 1b63635b678..03fe66ffe89 100644 --- a/erpnext/patches/v13_0/update_deferred_settings.py +++ b/erpnext/patches/v13_0/update_deferred_settings.py @@ -5,8 +5,8 @@ import frappe def execute(): - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.book_deferred_entries_based_on = 'Days' + accounts_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + accounts_settings.book_deferred_entries_based_on = "Days" accounts_settings.book_deferred_entries_via_journal_entry = 0 accounts_settings.submit_journal_entries = 0 accounts_settings.save() diff --git a/erpnext/patches/v13_0/update_disbursement_account.py b/erpnext/patches/v13_0/update_disbursement_account.py index c56fa8fdc62..d6aba4720ee 100644 --- a/erpnext/patches/v13_0/update_disbursement_account.py +++ b/erpnext/patches/v13_0/update_disbursement_account.py @@ -9,14 +9,6 @@ def execute(): loan_type = frappe.qb.DocType("Loan Type") loan = frappe.qb.DocType("Loan") - frappe.qb.update( - loan_type - ).set( - loan_type.disbursement_account, loan_type.payment_account - ).run() + frappe.qb.update(loan_type).set(loan_type.disbursement_account, loan_type.payment_account).run() - frappe.qb.update( - loan - ).set( - loan.disbursement_account, loan.payment_account - ).run() \ No newline at end of file + frappe.qb.update(loan).set(loan.disbursement_account, loan.payment_account).run() diff --git a/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py b/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py new file mode 100644 index 00000000000..2bc17ae86bd --- /dev/null +++ b/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py @@ -0,0 +1,25 @@ +import frappe + + +def execute(): + """ + Update Expense Claim status to Paid if: + - the entire required amount is already covered via linked advances + - the claim is partially paid via advances and the rest is reimbursed + """ + + ExpenseClaim = frappe.qb.DocType("Expense Claim") + + ( + frappe.qb.update(ExpenseClaim) + .set(ExpenseClaim.status, "Paid") + .where( + ( + (ExpenseClaim.grand_total == 0) + | (ExpenseClaim.grand_total == ExpenseClaim.total_amount_reimbursed) + ) + & (ExpenseClaim.approval_status == "Approved") + & (ExpenseClaim.docstatus == 1) + & (ExpenseClaim.total_sanctioned_amount > 0) + ) + ).run() diff --git a/erpnext/patches/v13_0/update_export_type_for_gst.py b/erpnext/patches/v13_0/update_export_type_for_gst.py index de578612f7d..62368584dc3 100644 --- a/erpnext/patches/v13_0/update_export_type_for_gst.py +++ b/erpnext/patches/v13_0/update_export_type_for_gst.py @@ -2,32 +2,39 @@ import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return # Update custom fields - fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'}) + fieldname = frappe.db.get_value("Custom Field", {"dt": "Customer", "fieldname": "export_type"}) if fieldname: - frappe.db.set_value('Custom Field', fieldname, + frappe.db.set_value( + "Custom Field", + fieldname, { - 'default': '', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' - }) + "default": "", + "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + }, + ) - fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'}) + fieldname = frappe.db.get_value("Custom Field", {"dt": "Supplier", "fieldname": "export_type"}) if fieldname: - frappe.db.set_value('Custom Field', fieldname, - { - 'default': '', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' - }) + frappe.db.set_value( + "Custom Field", + fieldname, + {"default": "", "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)'}, + ) # Update Customer/Supplier Masters - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export') - """) + """ + ) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas') - """) + """ + ) diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py index 12f9006b76e..73baecf0dfa 100644 --- a/erpnext/patches/v13_0/update_job_card_details.py +++ b/erpnext/patches/v13_0/update_job_card_details.py @@ -10,8 +10,10 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "job_card_item") frappe.reload_doc("manufacturing", "doctype", "work_order_operation") - frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo + frappe.db.sql( + """ update `tabJob Card` jc, `tabWork Order Operation` wo SET jc.hour_rate = wo.hour_rate WHERE jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0 - """) + """ + ) diff --git a/erpnext/patches/v13_0/update_job_card_status.py b/erpnext/patches/v13_0/update_job_card_status.py index 797a3e2ae35..f2d12da119a 100644 --- a/erpnext/patches/v13_0/update_job_card_status.py +++ b/erpnext/patches/v13_0/update_job_card_status.py @@ -7,8 +7,8 @@ import frappe def execute(): job_card = frappe.qb.DocType("Job Card") - (frappe.qb - .update(job_card) + ( + frappe.qb.update(job_card) .set(job_card.status, "Completed") .where( (job_card.docstatus == 1) diff --git a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py index 43096991943..b631c0bab73 100644 --- a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py +++ b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py @@ -1,4 +1,3 @@ - import frappe @@ -7,18 +6,14 @@ def execute(): # Updates the Maintenance Schedule link to fetch serial nos from frappe.query_builder.functions import Coalesce - mvp = frappe.qb.DocType('Maintenance Visit Purpose') - mv = frappe.qb.DocType('Maintenance Visit') - frappe.qb.update( - mv - ).join( - mvp - ).on(mvp.parent == mv.name).set( - mv.maintenance_schedule, - Coalesce(mvp.prevdoc_docname, '') + mvp = frappe.qb.DocType("Maintenance Visit Purpose") + mv = frappe.qb.DocType("Maintenance Visit") + + frappe.qb.update(mv).join(mvp).on(mvp.parent == mv.name).set( + mv.maintenance_schedule, Coalesce(mvp.prevdoc_docname, "") ).where( - (mv.maintenance_type == "Scheduled") - & (mvp.prevdoc_docname.notnull()) - & (mv.docstatus < 2) - ).run(as_dict=1) + (mv.maintenance_type == "Scheduled") & (mvp.prevdoc_docname.notnull()) & (mv.docstatus < 2) + ).run( + as_dict=1 + ) diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index bcd80d4c5c4..a1d40b739eb 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ from frappe.model.naming import make_autoname @@ -17,69 +16,110 @@ def execute(): # Create a penalty account for loan types - frappe.reload_doc('loan_management', 'doctype', 'loan_type') - frappe.reload_doc('loan_management', 'doctype', 'loan') - frappe.reload_doc('loan_management', 'doctype', 'repayment_schedule') - frappe.reload_doc('loan_management', 'doctype', 'process_loan_interest_accrual') - frappe.reload_doc('loan_management', 'doctype', 'loan_repayment') - frappe.reload_doc('loan_management', 'doctype', 'loan_repayment_detail') - frappe.reload_doc('loan_management', 'doctype', 'loan_interest_accrual') - frappe.reload_doc('accounts', 'doctype', 'gl_entry') - frappe.reload_doc('accounts', 'doctype', 'journal_entry_account') + frappe.reload_doc("loan_management", "doctype", "loan_type") + frappe.reload_doc("loan_management", "doctype", "loan") + frappe.reload_doc("loan_management", "doctype", "repayment_schedule") + frappe.reload_doc("loan_management", "doctype", "process_loan_interest_accrual") + frappe.reload_doc("loan_management", "doctype", "loan_repayment") + frappe.reload_doc("loan_management", "doctype", "loan_repayment_detail") + frappe.reload_doc("loan_management", "doctype", "loan_interest_accrual") + frappe.reload_doc("accounts", "doctype", "gl_entry") + frappe.reload_doc("accounts", "doctype", "journal_entry_account") updated_loan_types = [] loans_to_close = [] # Update old loan status as closed - if frappe.db.has_column('Repayment Schedule', 'paid'): - loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule` - where paid = 0 and docstatus = 1""", as_dict=1) + if frappe.db.has_column("Repayment Schedule", "paid"): + loans_list = frappe.db.sql( + """SELECT distinct parent from `tabRepayment Schedule` + where paid = 0 and docstatus = 1""", + as_dict=1, + ) loans_to_close = [d.parent for d in loans_list] if loans_to_close: - frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close)) + frappe.db.sql( + "UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" + % (", ".join(["%s"] * len(loans_to_close))), + tuple(loans_to_close), + ) - loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment', - 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'], - filters={'docstatus': 1, 'status': ('!=', 'Closed')}) + loans = frappe.get_all( + "Loan", + fields=[ + "name", + "loan_type", + "company", + "status", + "mode_of_payment", + "applicant_type", + "applicant", + "loan_account", + "payment_account", + "interest_income_account", + ], + filters={"docstatus": 1, "status": ("!=", "Closed")}, + ) for loan in loans: # Update details in Loan Types and Loan - loan_type_company = frappe.db.get_value('Loan Type', loan.loan_type, 'company') + loan_type_company = frappe.db.get_value("Loan Type", loan.loan_type, "company") loan_type = loan.loan_type - group_income_account = frappe.get_value('Account', {'company': loan.company, - 'is_group': 1, 'root_type': 'Income', 'account_name': _('Indirect Income')}) + group_income_account = frappe.get_value( + "Account", + { + "company": loan.company, + "is_group": 1, + "root_type": "Income", + "account_name": _("Indirect Income"), + }, + ) if not group_income_account: - group_income_account = frappe.get_value('Account', {'company': loan.company, - 'is_group': 1, 'root_type': 'Income'}) + group_income_account = frappe.get_value( + "Account", {"company": loan.company, "is_group": 1, "root_type": "Income"} + ) - penalty_account = create_account(company=loan.company, account_type='Income Account', - account_name='Penalty Account', parent_account=group_income_account) + penalty_account = create_account( + company=loan.company, + account_type="Income Account", + account_name="Penalty Account", + parent_account=group_income_account, + ) # Same loan type used for multiple companies if loan_type_company and loan_type_company != loan.company: # get loan type for appropriate company - loan_type_name = frappe.get_value('Loan Type', {'company': loan.company, - 'mode_of_payment': loan.mode_of_payment, 'loan_account': loan.loan_account, - 'payment_account': loan.payment_account, 'interest_income_account': loan.interest_income_account, - 'penalty_income_account': loan.penalty_income_account}, 'name') + loan_type_name = frappe.get_value( + "Loan Type", + { + "company": loan.company, + "mode_of_payment": loan.mode_of_payment, + "loan_account": loan.loan_account, + "payment_account": loan.payment_account, + "interest_income_account": loan.interest_income_account, + "penalty_income_account": loan.penalty_income_account, + }, + "name", + ) if not loan_type_name: loan_type_name = create_loan_type(loan, loan_type_name, penalty_account) # update loan type in loan - frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name, - loan.name)) + frappe.db.sql( + "UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name, loan.name) + ) loan_type = loan_type_name if loan_type_name not in updated_loan_types: updated_loan_types.append(loan_type_name) elif not loan_type_company: - loan_type_doc = frappe.get_doc('Loan Type', loan.loan_type) + loan_type_doc = frappe.get_doc("Loan Type", loan.loan_type) loan_type_doc.is_term_loan = 1 loan_type_doc.company = loan.company loan_type_doc.mode_of_payment = loan.mode_of_payment @@ -92,26 +132,29 @@ def execute(): loan_type = loan.loan_type if loan_type in updated_loan_types: - if loan.status == 'Fully Disbursed': - status = 'Disbursed' - elif loan.status == 'Repaid/Closed': - status = 'Closed' + if loan.status == "Fully Disbursed": + status = "Disbursed" + elif loan.status == "Repaid/Closed": + status = "Closed" else: status = loan.status - frappe.db.set_value('Loan', loan.name, { - 'is_term_loan': 1, - 'penalty_income_account': penalty_account, - 'status': status - }) + frappe.db.set_value( + "Loan", + loan.name, + {"is_term_loan": 1, "penalty_income_account": penalty_account, "status": status}, + ) - process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type, - loan=loan.name) + process_loan_interest_accrual_for_term_loans( + posting_date=nowdate(), loan_type=loan_type, loan=loan.name + ) - - if frappe.db.has_column('Repayment Schedule', 'paid'): - total_principal, total_interest = frappe.db.get_value('Repayment Schedule', {'paid': 1, 'parent': loan.name}, - ['sum(principal_amount) as total_principal', 'sum(interest_amount) as total_interest']) + if frappe.db.has_column("Repayment Schedule", "paid"): + total_principal, total_interest = frappe.db.get_value( + "Repayment Schedule", + {"paid": 1, "parent": loan.name}, + ["sum(principal_amount) as total_principal", "sum(interest_amount) as total_interest"], + ) accrued_entries = get_accrued_interest_entries(loan.name) for entry in accrued_entries: @@ -128,17 +171,20 @@ def execute(): else: principal_paid = flt(total_principal) - frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` + frappe.db.sql( + """ UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` + %s, paid_interest_amount = `paid_interest_amount` + %s WHERE name = %s""", - (principal_paid, interest_paid, entry.name)) + (principal_paid, interest_paid, entry.name), + ) total_principal = flt(total_principal) - principal_paid total_interest = flt(total_interest) - interest_paid + def create_loan_type(loan, loan_type_name, penalty_account): - loan_type_doc = frappe.new_doc('Loan Type') + loan_type_doc = frappe.new_doc("Loan Type") loan_type_doc.loan_name = make_autoname("Loan Type-.####") loan_type_doc.is_term_loan = 1 loan_type_doc.company = loan.company diff --git a/erpnext/patches/v13_0/update_payment_terms_outstanding.py b/erpnext/patches/v13_0/update_payment_terms_outstanding.py index aea09ad7a37..d0c25f37ad7 100644 --- a/erpnext/patches/v13_0/update_payment_terms_outstanding.py +++ b/erpnext/patches/v13_0/update_payment_terms_outstanding.py @@ -7,10 +7,12 @@ import frappe def execute(): frappe.reload_doc("accounts", "doctype", "Payment Schedule") - if frappe.db.count('Payment Schedule'): - frappe.db.sql(''' + if frappe.db.count("Payment Schedule"): + frappe.db.sql( + """ UPDATE `tabPayment Schedule` ps SET ps.outstanding = (ps.payment_amount - ps.paid_amount) - ''') + """ + ) diff --git a/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py index b2e35591970..49826dfd261 100644 --- a/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py +++ b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py @@ -8,8 +8,9 @@ import frappe def execute(): frappe.reload_doc("accounts", "doctype", "POS Invoice Merge Log") frappe.reload_doc("accounts", "doctype", "POS Closing Entry") - if frappe.db.count('POS Invoice Merge Log'): - frappe.db.sql(''' + if frappe.db.count("POS Invoice Merge Log"): + frappe.db.sql( + """ UPDATE `tabPOS Invoice Merge Log` log, `tabPOS Invoice Reference` log_ref SET @@ -20,7 +21,8 @@ def execute(): ) WHERE log_ref.parent = log.name - ''') + """ + ) - frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Submitted' where docstatus = 1''') - frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Cancelled' where docstatus = 2''') + frappe.db.sql("""UPDATE `tabPOS Closing Entry` SET status = 'Submitted' where docstatus = 1""") + frappe.db.sql("""UPDATE `tabPOS Closing Entry` SET status = 'Cancelled' where docstatus = 2""") diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 29debc6ad14..c9a23222424 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -11,38 +11,39 @@ def execute(): frappe.reload_doc("projects", "doctype", "task") # Update property setter status if any - property_setter = frappe.db.get_value('Property Setter', {'doc_type': 'Task', - 'field_name': 'status', 'property': 'options'}) + property_setter = frappe.db.get_value( + "Property Setter", {"doc_type": "Task", "field_name": "status", "property": "options"} + ) if property_setter: - property_setter_doc = frappe.get_doc('Property Setter', {'doc_type': 'Task', - 'field_name': 'status', 'property': 'options'}) + property_setter_doc = frappe.get_doc( + "Property Setter", {"doc_type": "Task", "field_name": "status", "property": "options"} + ) property_setter_doc.value += "\nTemplate" property_setter_doc.save() - for template_name in frappe.get_all('Project Template'): + for template_name in frappe.get_all("Project Template"): template = frappe.get_doc("Project Template", template_name.name) replace_tasks = False new_tasks = [] for task in template.tasks: if task.subject: replace_tasks = True - new_task = frappe.get_doc(dict( - doctype = "Task", - subject = task.subject, - start = task.start, - duration = task.duration, - task_weight = task.task_weight, - description = task.description, - is_template = 1 - )).insert() + new_task = frappe.get_doc( + dict( + doctype="Task", + subject=task.subject, + start=task.start, + duration=task.duration, + task_weight=task.task_weight, + description=task.description, + is_template=1, + ) + ).insert() new_tasks.append(new_task) if replace_tasks: template.tasks = [] for tsk in new_tasks: - template.append("tasks", { - "task": tsk.name, - "subject": tsk.subject - }) + template.append("tasks", {"task": tsk.name, "subject": tsk.subject}) template.save() diff --git a/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py b/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py index f9bfc54502f..31aa29274d1 100644 --- a/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py +++ b/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py @@ -6,10 +6,12 @@ import frappe def execute(): - frappe.reload_doc("hr", "doctype", "employee") + frappe.reload_doc("hr", "doctype", "employee") - if frappe.db.has_column("Employee", "reason_for_resignation"): - frappe.db.sql(""" UPDATE `tabEmployee` + if frappe.db.has_column("Employee", "reason_for_resignation"): + frappe.db.sql( + """ UPDATE `tabEmployee` SET reason_for_leaving = reason_for_resignation WHERE status = 'Left' and reason_for_leaving is null and reason_for_resignation is not null - """) + """ + ) diff --git a/erpnext/patches/v13_0/update_recipient_email_digest.py b/erpnext/patches/v13_0/update_recipient_email_digest.py index e3528cc06bb..69961a87f78 100644 --- a/erpnext/patches/v13_0/update_recipient_email_digest.py +++ b/erpnext/patches/v13_0/update_recipient_email_digest.py @@ -8,16 +8,18 @@ import frappe def execute(): frappe.reload_doc("setup", "doctype", "Email Digest") frappe.reload_doc("setup", "doctype", "Email Digest Recipient") - email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list']) + email_digests = frappe.db.get_list("Email Digest", fields=["name", "recipient_list"]) for email_digest in email_digests: if email_digest.recipient_list: for recipient in email_digest.recipient_list.split("\n"): - if frappe.db.exists('User', recipient): - doc = frappe.get_doc({ - 'doctype': 'Email Digest Recipient', - 'parenttype': 'Email Digest', - 'parentfield': 'recipients', - 'parent': email_digest.name, - 'recipient': recipient - }) + if frappe.db.exists("User", recipient): + doc = frappe.get_doc( + { + "doctype": "Email Digest Recipient", + "parenttype": "Email Digest", + "parentfield": "recipients", + "parent": email_digest.name, + "recipient": recipient, + } + ) doc.insert() diff --git a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py index 00926b09241..72e77fe2161 100644 --- a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py +++ b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py @@ -9,15 +9,12 @@ def execute(): wo_item = frappe.qb.DocType("Work Order Item") incorrect_item_wh = ( - frappe.qb - .from_(wo) - .join(wo_item).on(wo.name == wo_item.parent) - .select(wo_item.item_code, wo.source_warehouse).distinct() - .where( - (wo.status == "Closed") - & (wo.docstatus == 1) - & (wo.source_warehouse.notnull()) - ) + frappe.qb.from_(wo) + .join(wo_item) + .on(wo.name == wo_item.parent) + .select(wo_item.item_code, wo.source_warehouse) + .distinct() + .where((wo.status == "Closed") & (wo.docstatus == 1) & (wo.source_warehouse.notnull())) ).run() for item_code, warehouse in incorrect_item_wh: diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py index dd64e05ec16..9b5845f494d 100644 --- a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py +++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py @@ -6,14 +6,16 @@ from erpnext.controllers.status_updater import OverAllowanceError def execute(): - frappe.reload_doc('stock', 'doctype', 'purchase_receipt') - frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') - frappe.reload_doc('stock', 'doctype', 'delivery_note') - frappe.reload_doc('stock', 'doctype', 'delivery_note_item') - frappe.reload_doc('stock', 'doctype', 'stock_settings') + frappe.reload_doc("stock", "doctype", "purchase_receipt") + frappe.reload_doc("stock", "doctype", "purchase_receipt_item") + frappe.reload_doc("stock", "doctype", "delivery_note") + frappe.reload_doc("stock", "doctype", "delivery_note_item") + frappe.reload_doc("stock", "doctype", "stock_settings") def update_from_return_docs(doctype): - for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1, 'return_against': ('!=', '')}): + for return_doc in frappe.get_all( + doctype, filters={"is_return": 1, "docstatus": 1, "return_against": ("!=", "")} + ): # Update original receipt/delivery document from return return_doc = frappe.get_cached_doc(doctype, return_doc.name) try: @@ -27,9 +29,11 @@ def execute(): frappe.db.commit() # Set received qty in stock uom in PR, as returned qty is checked against it - frappe.db.sql(""" update `tabPurchase Receipt Item` + frappe.db.sql( + """ update `tabPurchase Receipt Item` set received_stock_qty = received_qty * conversion_factor - where docstatus = 1 """) + where docstatus = 1 """ + ) - for doctype in ('Purchase Receipt', 'Delivery Note'): + for doctype in ("Purchase Receipt", "Delivery Note"): update_from_return_docs(doctype) diff --git a/erpnext/patches/v13_0/update_sane_transfer_against.py b/erpnext/patches/v13_0/update_sane_transfer_against.py index a163d385843..45691e2ded7 100644 --- a/erpnext/patches/v13_0/update_sane_transfer_against.py +++ b/erpnext/patches/v13_0/update_sane_transfer_against.py @@ -4,8 +4,8 @@ import frappe def execute(): bom = frappe.qb.DocType("BOM") - (frappe.qb - .update(bom) + ( + frappe.qb.update(bom) .set(bom.transfer_material_against, "Work Order") .where(bom.with_operations == 0) ).run() diff --git a/erpnext/patches/v13_0/update_shipment_status.py b/erpnext/patches/v13_0/update_shipment_status.py index f2d7d1d1e3f..d21caf70add 100644 --- a/erpnext/patches/v13_0/update_shipment_status.py +++ b/erpnext/patches/v13_0/update_shipment_status.py @@ -5,11 +5,15 @@ def execute(): frappe.reload_doc("stock", "doctype", "shipment") # update submitted status - frappe.db.sql("""UPDATE `tabShipment` + frappe.db.sql( + """UPDATE `tabShipment` SET status = "Submitted" - WHERE status = "Draft" AND docstatus = 1""") + WHERE status = "Draft" AND docstatus = 1""" + ) # update cancelled status - frappe.db.sql("""UPDATE `tabShipment` + frappe.db.sql( + """UPDATE `tabShipment` SET status = "Cancelled" - WHERE status = "Draft" AND docstatus = 2""") + WHERE status = "Draft" AND docstatus = 2""" + ) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 7f61020309d..84c683acd2c 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -8,78 +8,94 @@ import frappe def execute(): # add holiday list and employee group fields in SLA # change response and resolution time in priorities child table - if frappe.db.exists('DocType', 'Service Level Agreement'): - sla_details = frappe.db.get_all('Service Level Agreement', fields=['name', 'service_level']) - priorities = frappe.db.get_all('Service Level Priority', fields=['*'], filters={ - 'parenttype': ('in', ['Service Level Agreement', 'Service Level']) - }) + if frappe.db.exists("DocType", "Service Level Agreement"): + sla_details = frappe.db.get_all("Service Level Agreement", fields=["name", "service_level"]) + priorities = frappe.db.get_all( + "Service Level Priority", + fields=["*"], + filters={"parenttype": ("in", ["Service Level Agreement", "Service Level"])}, + ) - frappe.reload_doc('support', 'doctype', 'service_level_agreement') - frappe.reload_doc('support', 'doctype', 'pause_sla_on_status') - frappe.reload_doc('support', 'doctype', 'service_level_priority') - frappe.reload_doc('support', 'doctype', 'service_day') + frappe.reload_doc("support", "doctype", "service_level_agreement") + frappe.reload_doc("support", "doctype", "pause_sla_on_status") + frappe.reload_doc("support", "doctype", "service_level_priority") + frappe.reload_doc("support", "doctype", "service_day") for entry in sla_details: - values = frappe.db.get_value('Service Level', entry.service_level, ['holiday_list', 'employee_group']) + values = frappe.db.get_value( + "Service Level", entry.service_level, ["holiday_list", "employee_group"] + ) if values: holiday_list = values[0] employee_group = values[1] - frappe.db.set_value('Service Level Agreement', entry.name, { - 'holiday_list': holiday_list, - 'employee_group': employee_group - }) + frappe.db.set_value( + "Service Level Agreement", + entry.name, + {"holiday_list": holiday_list, "employee_group": employee_group}, + ) priority_dict = {} for priority in priorities: - if priority.parenttype == 'Service Level Agreement': + if priority.parenttype == "Service Level Agreement": response_time = convert_to_seconds(priority.response_time, priority.response_time_period) resolution_time = convert_to_seconds(priority.resolution_time, priority.resolution_time_period) - frappe.db.set_value('Service Level Priority', priority.name, { - 'response_time': response_time, - 'resolution_time': resolution_time - }) - if priority.parenttype == 'Service Level': + frappe.db.set_value( + "Service Level Priority", + priority.name, + {"response_time": response_time, "resolution_time": resolution_time}, + ) + if priority.parenttype == "Service Level": if not priority.parent in priority_dict: priority_dict[priority.parent] = [] priority_dict[priority.parent].append(priority) - # copy Service Levels to Service Level Agreements sl = [entry.service_level for entry in sla_details] - if frappe.db.exists('DocType', 'Service Level'): - service_levels = frappe.db.get_all('Service Level', filters={'service_level': ('not in', sl)}, fields=['*']) + if frappe.db.exists("DocType", "Service Level"): + service_levels = frappe.db.get_all( + "Service Level", filters={"service_level": ("not in", sl)}, fields=["*"] + ) for entry in service_levels: - sla = frappe.new_doc('Service Level Agreement') + sla = frappe.new_doc("Service Level Agreement") sla.service_level = entry.service_level sla.holiday_list = entry.holiday_list sla.employee_group = entry.employee_group sla.flags.ignore_validate = True sla = sla.insert(ignore_mandatory=True) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabService Day` SET parent = %(new_parent)s , parentfield = 'support_and_resolution', parenttype = 'Service Level Agreement' WHERE parent = %(old_parent)s - """, {'new_parent': sla.name, 'old_parent': entry.name}, as_dict = 1) + """, + {"new_parent": sla.name, "old_parent": entry.name}, + as_dict=1, + ) priority_list = priority_dict.get(entry.name) if priority_list: - sla = frappe.get_doc('Service Level Agreement', sla.name) + sla = frappe.get_doc("Service Level Agreement", sla.name) for priority in priority_list: - row = sla.append('priorities', { - 'priority': priority.priority, - 'default_priority': priority.default_priority, - 'response_time': convert_to_seconds(priority.response_time, priority.response_time_period), - 'resolution_time': convert_to_seconds(priority.resolution_time, priority.resolution_time_period) - }) + row = sla.append( + "priorities", + { + "priority": priority.priority, + "default_priority": priority.default_priority, + "response_time": convert_to_seconds(priority.response_time, priority.response_time_period), + "resolution_time": convert_to_seconds( + priority.resolution_time, priority.resolution_time_period + ), + }, + ) row.db_update() sla.db_update() - frappe.delete_doc_if_exists('DocType', 'Service Level') + frappe.delete_doc_if_exists("DocType", "Service Level") def convert_to_seconds(value, unit): diff --git a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py index 665cc39923a..6d26ac543fc 100644 --- a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py +++ b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py @@ -6,8 +6,10 @@ import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'shift_assignment') - if frappe.db.has_column('Shift Assignment', 'date'): - frappe.db.sql("""update `tabShift Assignment` + frappe.reload_doc("hr", "doctype", "shift_assignment") + if frappe.db.has_column("Shift Assignment", "date"): + frappe.db.sql( + """update `tabShift Assignment` set end_date=date, start_date=date - where date IS NOT NULL and start_date IS NULL and end_date IS NULL;""") + where date IS NOT NULL and start_date IS NULL and end_date IS NULL;""" + ) diff --git a/erpnext/patches/v13_0/update_subscription.py b/erpnext/patches/v13_0/update_subscription.py index b0bb1c86eea..95783849ab6 100644 --- a/erpnext/patches/v13_0/update_subscription.py +++ b/erpnext/patches/v13_0/update_subscription.py @@ -8,12 +8,13 @@ from six import iteritems def execute(): - frappe.reload_doc('accounts', 'doctype', 'subscription') - frappe.reload_doc('accounts', 'doctype', 'subscription_invoice') - frappe.reload_doc('accounts', 'doctype', 'subscription_plan') + frappe.reload_doc("accounts", "doctype", "subscription") + frappe.reload_doc("accounts", "doctype", "subscription_invoice") + frappe.reload_doc("accounts", "doctype", "subscription_plan") - if frappe.db.has_column('Subscription', 'customer'): - frappe.db.sql(""" + if frappe.db.has_column("Subscription", "customer"): + frappe.db.sql( + """ UPDATE `tabSubscription` SET start_date = start, @@ -21,22 +22,28 @@ def execute(): party = customer, sales_tax_template = tax_template WHERE IFNULL(party,'') = '' - """) + """ + ) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSubscription Invoice` SET document_type = 'Sales Invoice' WHERE IFNULL(document_type, '') = '' - """) + """ + ) price_determination_map = { - 'Fixed rate': 'Fixed Rate', - 'Based on price list': 'Based On Price List' + "Fixed rate": "Fixed Rate", + "Based on price list": "Based On Price List", } for key, value in iteritems(price_determination_map): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSubscription Plan` SET price_determination = %s WHERE price_determination = %s - """, (value, key)) + """, + (value, key), + ) diff --git a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py index e21fe578212..d7c849956e9 100644 --- a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py +++ b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py @@ -2,9 +2,11 @@ import frappe def execute(): - if frappe.db.exists('DocType', 'Member'): - frappe.reload_doc('Non Profit', 'doctype', 'Member') + if frappe.db.exists("DocType", "Member"): + frappe.reload_doc("Non Profit", "doctype", "Member") - if frappe.db.has_column('Member', 'subscription_activated'): - frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1') - frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated') + if frappe.db.has_column("Member", "subscription_activated"): + frappe.db.sql( + 'UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1' + ) + frappe.db.sql_ddl("ALTER table `tabMember` DROP COLUMN subscription_activated") diff --git a/erpnext/patches/v13_0/update_tax_category_for_rcm.py b/erpnext/patches/v13_0/update_tax_category_for_rcm.py index 7af2366bf0a..8ac95348894 100644 --- a/erpnext/patches/v13_0/update_tax_category_for_rcm.py +++ b/erpnext/patches/v13_0/update_tax_category_for_rcm.py @@ -5,27 +5,46 @@ from erpnext.regional.india import states def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - create_custom_fields({ - 'Tax Category': [ - dict(fieldname='is_inter_state', label='Is Inter State', - fieldtype='Check', insert_after='disabled', print_hide=1), - dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check', - insert_after='is_inter_state', print_hide=1), - dict(fieldname='tax_category_column_break', fieldtype='Column Break', - insert_after='is_reverse_charge'), - dict(fieldname='gst_state', label='Source State', fieldtype='Select', - options='\n'.join(states), insert_after='company') - ] - }, update=True) + create_custom_fields( + { + "Tax Category": [ + dict( + fieldname="is_inter_state", + label="Is Inter State", + fieldtype="Check", + insert_after="disabled", + print_hide=1, + ), + dict( + fieldname="is_reverse_charge", + label="Is Reverse Charge", + fieldtype="Check", + insert_after="is_inter_state", + print_hide=1, + ), + dict( + fieldname="tax_category_column_break", + fieldtype="Column Break", + insert_after="is_reverse_charge", + ), + dict( + fieldname="gst_state", + label="Source State", + fieldtype="Select", + options="\n".join(states), + insert_after="company", + ), + ] + }, + update=True, + ) tax_category = frappe.qb.DocType("Tax Category") - frappe.qb.update(tax_category).set( - tax_category.is_reverse_charge, 1 - ).where( - tax_category.name.isin(['Reverse Charge Out-State', 'Reverse Charge In-State']) - ).run() \ No newline at end of file + frappe.qb.update(tax_category).set(tax_category.is_reverse_charge, 1).where( + tax_category.name.isin(["Reverse Charge Out-State", "Reverse Charge In-State"]) + ).run() diff --git a/erpnext/patches/v13_0/update_tds_check_field.py b/erpnext/patches/v13_0/update_tds_check_field.py index 436d2e6a6da..0505266b3c4 100644 --- a/erpnext/patches/v13_0/update_tds_check_field.py +++ b/erpnext/patches/v13_0/update_tds_check_field.py @@ -2,9 +2,12 @@ import frappe def execute(): - if frappe.db.has_table("Tax Withholding Category") \ - and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"): - frappe.db.sql(""" + if frappe.db.has_table("Tax Withholding Category") and frappe.db.has_column( + "Tax Withholding Category", "round_off_tax_amount" + ): + frappe.db.sql( + """ UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0 WHERE round_off_tax_amount IS NULL - """) + """ + ) diff --git a/erpnext/patches/v13_0/update_timesheet_changes.py b/erpnext/patches/v13_0/update_timesheet_changes.py index cc38a0c9a16..02654c11d30 100644 --- a/erpnext/patches/v13_0/update_timesheet_changes.py +++ b/erpnext/patches/v13_0/update_timesheet_changes.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -10,17 +9,23 @@ def execute(): if frappe.db.has_column("Timesheet Detail", "billable"): rename_field("Timesheet Detail", "billable", "is_billable") - base_currency = frappe.defaults.get_global_default('currency') + base_currency = frappe.defaults.get_global_default("currency") - frappe.db.sql("""UPDATE `tabTimesheet Detail` + frappe.db.sql( + """UPDATE `tabTimesheet Detail` SET base_billing_rate = billing_rate, base_billing_amount = billing_amount, base_costing_rate = costing_rate, - base_costing_amount = costing_amount""") + base_costing_amount = costing_amount""" + ) - frappe.db.sql("""UPDATE `tabTimesheet` + frappe.db.sql( + """UPDATE `tabTimesheet` SET currency = '{0}', exchange_rate = 1.0, base_total_billable_amount = total_billable_amount, base_total_billed_amount = total_billed_amount, - base_total_costing_amount = total_costing_amount""".format(base_currency)) + base_total_costing_amount = total_costing_amount""".format( + base_currency + ) + ) diff --git a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py index 902707b4b66..326fc579f4c 100644 --- a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py +++ b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py @@ -2,10 +2,10 @@ import frappe def execute(): - frappe.reload_doc('custom', 'doctype', 'custom_field', force=True) - company = frappe.get_all('Company', filters = {'country': 'India'}) + frappe.reload_doc("custom", "doctype", "custom_field", force=True) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - if frappe.db.exists('Custom Field', { 'fieldname': 'vehicle_no' }): - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', '') + if frappe.db.exists("Custom Field", {"fieldname": "vehicle_no"}): + frappe.db.set_value("Custom Field", {"fieldname": "vehicle_no"}, "mandatory_depends_on", "") diff --git a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py index c760a6a52f1..b395c01c1df 100644 --- a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py +++ b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py @@ -8,122 +8,116 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('Accounts', 'doctype', 'Salary Component Account') - if frappe.db.has_column('Salary Component Account', 'default_account'): + frappe.reload_doc("Accounts", "doctype", "Salary Component Account") + if frappe.db.has_column("Salary Component Account", "default_account"): rename_field("Salary Component Account", "default_account", "account") doctype_list = [ - { - 'module':'HR', - 'doctype':'Employee Advance' - }, - { - 'module':'HR', - 'doctype':'Leave Encashment' - }, - { - 'module':'Payroll', - 'doctype':'Additional Salary' - }, - { - 'module':'Payroll', - 'doctype':'Employee Benefit Application' - }, - { - 'module':'Payroll', - 'doctype':'Employee Benefit Claim' - }, - { - 'module':'Payroll', - 'doctype':'Employee Incentive' - }, - { - 'module':'Payroll', - 'doctype':'Employee Tax Exemption Declaration' - }, - { - 'module':'Payroll', - 'doctype':'Employee Tax Exemption Proof Submission' - }, - { - 'module':'Payroll', - 'doctype':'Income Tax Slab' - }, - { - 'module':'Payroll', - 'doctype':'Payroll Entry' - }, - { - 'module':'Payroll', - 'doctype':'Retention Bonus' - }, - { - 'module':'Payroll', - 'doctype':'Salary Structure' - }, - { - 'module':'Payroll', - 'doctype':'Salary Structure Assignment' - }, - { - 'module':'Payroll', - 'doctype':'Salary Slip' - }, + {"module": "HR", "doctype": "Employee Advance"}, + {"module": "HR", "doctype": "Leave Encashment"}, + {"module": "Payroll", "doctype": "Additional Salary"}, + {"module": "Payroll", "doctype": "Employee Benefit Application"}, + {"module": "Payroll", "doctype": "Employee Benefit Claim"}, + {"module": "Payroll", "doctype": "Employee Incentive"}, + {"module": "Payroll", "doctype": "Employee Tax Exemption Declaration"}, + {"module": "Payroll", "doctype": "Employee Tax Exemption Proof Submission"}, + {"module": "Payroll", "doctype": "Income Tax Slab"}, + {"module": "Payroll", "doctype": "Payroll Entry"}, + {"module": "Payroll", "doctype": "Retention Bonus"}, + {"module": "Payroll", "doctype": "Salary Structure"}, + {"module": "Payroll", "doctype": "Salary Structure Assignment"}, + {"module": "Payroll", "doctype": "Salary Slip"}, ] for item in doctype_list: - frappe.reload_doc(item['module'], 'doctype', item['doctype']) + frappe.reload_doc(item["module"], "doctype", item["doctype"]) # update company in employee advance based on employee company - for dt in ['Employee Incentive', 'Leave Encashment', 'Employee Benefit Application', 'Employee Benefit Claim']: - frappe.db.sql(""" + for dt in [ + "Employee Incentive", + "Leave Encashment", + "Employee Benefit Application", + "Employee Benefit Claim", + ]: + frappe.db.sql( + """ update `tab{doctype}` set company = (select company from tabEmployee where name=`tab{doctype}`.employee) - """.format(doctype=dt)) + """.format( + doctype=dt + ) + ) # update exchange rate for employee advance frappe.db.sql("update `tabEmployee Advance` set exchange_rate=1") # get all companies and it's currency - all_companies = frappe.db.get_all("Company", fields=["name", "default_currency", "default_payroll_payable_account"]) + all_companies = frappe.db.get_all( + "Company", fields=["name", "default_currency", "default_payroll_payable_account"] + ) for d in all_companies: company = d.name company_currency = d.default_currency default_payroll_payable_account = d.default_payroll_payable_account if not default_payroll_payable_account: - default_payroll_payable_account = frappe.db.get_value("Account", - {"account_name": _("Payroll Payable"), "company": company, "account_currency": company_currency, "is_group": 0}) + default_payroll_payable_account = frappe.db.get_value( + "Account", + { + "account_name": _("Payroll Payable"), + "company": company, + "account_currency": company_currency, + "is_group": 0, + }, + ) # update currency in following doctypes based on company currency - doctypes_for_currency = ['Employee Advance', 'Leave Encashment', 'Employee Benefit Application', - 'Employee Benefit Claim', 'Employee Incentive', 'Additional Salary', - 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission', - 'Income Tax Slab', 'Retention Bonus', 'Salary Structure'] + doctypes_for_currency = [ + "Employee Advance", + "Leave Encashment", + "Employee Benefit Application", + "Employee Benefit Claim", + "Employee Incentive", + "Additional Salary", + "Employee Tax Exemption Declaration", + "Employee Tax Exemption Proof Submission", + "Income Tax Slab", + "Retention Bonus", + "Salary Structure", + ] for dt in doctypes_for_currency: - frappe.db.sql("""update `tab{doctype}` set currency = %s where company=%s""" - .format(doctype=dt), (company_currency, company)) + frappe.db.sql( + """update `tab{doctype}` set currency = %s where company=%s""".format(doctype=dt), + (company_currency, company), + ) # update fields in payroll entry - frappe.db.sql(""" + frappe.db.sql( + """ update `tabPayroll Entry` set currency = %s, exchange_rate = 1, payroll_payable_account=%s where company=%s - """, (company_currency, default_payroll_payable_account, company)) + """, + (company_currency, default_payroll_payable_account, company), + ) # update fields in Salary Structure Assignment - frappe.db.sql(""" + frappe.db.sql( + """ update `tabSalary Structure Assignment` set currency = %s, payroll_payable_account=%s where company=%s - """, (company_currency, default_payroll_payable_account, company)) + """, + (company_currency, default_payroll_payable_account, company), + ) # update fields in Salary Slip - frappe.db.sql(""" + frappe.db.sql( + """ update `tabSalary Slip` set currency = %s, exchange_rate = 1, @@ -134,4 +128,6 @@ def execute(): base_rounded_total = rounded_total, base_total_in_words = total_in_words where company=%s - """, (company_currency, company)) + """, + (company_currency, company), + ) diff --git a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py index e43a8bad8ea..693d06dc1a0 100644 --- a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py +++ b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py @@ -11,8 +11,6 @@ def execute(): sr_item = frappe.qb.DocType(doctype) - (frappe.qb - .update(sr_item) - .set(sr_item.current_serial_no, None) - .where(sr_item.current_qty == 0) + ( + frappe.qb.update(sr_item).set(sr_item.current_serial_no, None).where(sr_item.current_qty == 0) ).run() diff --git a/erpnext/patches/v4_2/repost_reserved_qty.py b/erpnext/patches/v4_2/repost_reserved_qty.py index ed4b19d07d3..72ac524a5c8 100644 --- a/erpnext/patches/v4_2/repost_reserved_qty.py +++ b/erpnext/patches/v4_2/repost_reserved_qty.py @@ -11,7 +11,8 @@ def execute(): for doctype in ("Sales Order Item", "Bin"): frappe.reload_doctype(doctype) - repost_for = frappe.db.sql(""" + repost_for = frappe.db.sql( + """ select distinct item_code, warehouse from @@ -26,17 +27,18 @@ def execute(): ) so_item where exists(select name from tabItem where name=so_item.item_code and ifnull(is_stock_item, 0)=1) - """) + """ + ) for item_code, warehouse in repost_for: if not (item_code and warehouse): continue - update_bin_qty(item_code, warehouse, { - "reserved_qty": get_reserved_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"reserved_qty": get_reserved_qty(item_code, warehouse)}) - frappe.db.sql("""delete from tabBin + frappe.db.sql( + """delete from tabBin where exists( select name from tabItem where name=tabBin.item_code and ifnull(is_stock_item, 0) = 0 ) - """) + """ + ) diff --git a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py index dd79410ba58..8ebc649aee4 100644 --- a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py +++ b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py @@ -8,20 +8,26 @@ import frappe def execute(): from erpnext.stock.stock_balance import get_indented_qty, get_ordered_qty, update_bin_qty - count=0 - for item_code, warehouse in frappe.db.sql("""select distinct item_code, warehouse from + count = 0 + for item_code, warehouse in frappe.db.sql( + """select distinct item_code, warehouse from (select item_code, warehouse from tabBin union - select item_code, warehouse from `tabStock Ledger Entry`) a"""): - try: - if not (item_code and warehouse): - continue - count += 1 - update_bin_qty(item_code, warehouse, { + select item_code, warehouse from `tabStock Ledger Entry`) a""" + ): + try: + if not (item_code and warehouse): + continue + count += 1 + update_bin_qty( + item_code, + warehouse, + { "indented_qty": get_indented_qty(item_code, warehouse), - "ordered_qty": get_ordered_qty(item_code, warehouse) - }) - if count % 200 == 0: - frappe.db.commit() - except Exception: - frappe.db.rollback() + "ordered_qty": get_ordered_qty(item_code, warehouse), + }, + ) + if count % 200 == 0: + frappe.db.commit() + except Exception: + frappe.db.rollback() diff --git a/erpnext/patches/v5_7/update_item_description_based_on_item_master.py b/erpnext/patches/v5_7/update_item_description_based_on_item_master.py index e7ef5ff0b49..edb0eaa6b9e 100644 --- a/erpnext/patches/v5_7/update_item_description_based_on_item_master.py +++ b/erpnext/patches/v5_7/update_item_description_based_on_item_master.py @@ -1,14 +1,17 @@ - import frappe def execute(): - name = frappe.db.sql(""" select name from `tabPatch Log` \ + name = frappe.db.sql( + """ select name from `tabPatch Log` \ where \ - patch like 'execute:frappe.db.sql("update `tabProduction Order` pro set description%' """) + patch like 'execute:frappe.db.sql("update `tabProduction Order` pro set description%' """ + ) if not name: - frappe.db.sql("update `tabProduction Order` pro \ + frappe.db.sql( + "update `tabProduction Order` pro \ set \ description = (select description from tabItem where name=pro.production_item) \ where \ - ifnull(description, '') = ''") + ifnull(description, '') = ''" + ) diff --git a/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py b/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py index ed1dffe75c8..a8108745e98 100644 --- a/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py +++ b/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py @@ -6,14 +6,16 @@ import frappe def execute(): - frappe.reload_doc('core', 'doctype', 'has_role') - company = frappe.get_all('Company', filters = {'country': 'India'}) + frappe.reload_doc("core", "doctype", "has_role") + company = frappe.get_all("Company", filters={"country": "India"}) if not company: - frappe.db.sql(""" + frappe.db.sql( + """ delete from `tabHas Role` where parenttype = 'Report' and parent in('GST Sales Register', 'GST Purchase Register', 'GST Itemised Sales Register', - 'GST Itemised Purchase Register', 'Eway Bill')""") + 'GST Itemised Purchase Register', 'Eway Bill')""" + ) diff --git a/erpnext/patches/v8_1/setup_gst_india.py b/erpnext/patches/v8_1/setup_gst_india.py index 98097d00501..b03439842f2 100644 --- a/erpnext/patches/v8_1/setup_gst_india.py +++ b/erpnext/patches/v8_1/setup_gst_india.py @@ -1,37 +1,45 @@ - import frappe from frappe.email import sendmail_to_system_managers def execute(): - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "item") frappe.reload_doc("stock", "doctype", "customs_tariff_number") frappe.reload_doc("accounts", "doctype", "payment_terms_template") frappe.reload_doc("accounts", "doctype", "payment_schedule") - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('regional', 'doctype', 'gst_settings') - frappe.reload_doc('regional', 'doctype', 'gst_hsn_code') + frappe.reload_doc("regional", "doctype", "gst_settings") + frappe.reload_doc("regional", "doctype", "gst_hsn_code") - for report_name in ('GST Sales Register', 'GST Purchase Register', - 'GST Itemised Sales Register', 'GST Itemised Purchase Register'): + for report_name in ( + "GST Sales Register", + "GST Purchase Register", + "GST Itemised Sales Register", + "GST Itemised Purchase Register", + ): - frappe.reload_doc('regional', 'report', frappe.scrub(report_name)) + frappe.reload_doc("regional", "report", frappe.scrub(report_name)) from erpnext.regional.india.setup import setup + delete_custom_field_tax_id_if_exists() setup(patch=True) send_gst_update_email() + def delete_custom_field_tax_id_if_exists(): - for field in frappe.db.sql_list("""select name from `tabCustom Field` where fieldname='tax_id' - and dt in ('Sales Order', 'Sales Invoice', 'Delivery Note')"""): + for field in frappe.db.sql_list( + """select name from `tabCustom Field` where fieldname='tax_id' + and dt in ('Sales Order', 'Sales Invoice', 'Delivery Note')""" + ): frappe.delete_doc("Custom Field", field, ignore_permissions=True) frappe.db.commit() + def send_gst_update_email(): message = """Hello, @@ -46,7 +54,9 @@ Templates and update your Customer's and Supplier's GST Numbers.

Thanks,

ERPNext Team. - """.format(gst_document_link=" ERPNext GST Document ") + """.format( + gst_document_link=" ERPNext GST Document " + ) try: sendmail_to_system_managers("[Important] ERPNext GST updates", message) diff --git a/erpnext/patches/v8_7/sync_india_custom_fields.py b/erpnext/patches/v8_7/sync_india_custom_fields.py index b5d58dc2eb8..e1b9a732dea 100644 --- a/erpnext/patches/v8_7/sync_india_custom_fields.py +++ b/erpnext/patches/v8_7/sync_india_custom_fields.py @@ -1,36 +1,42 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('Payroll', 'doctype', 'payroll_period') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_declaration') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_proof_submission') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_declaration_category') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_proof_submission_detail') + frappe.reload_doc("Payroll", "doctype", "payroll_period") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_declaration") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_proof_submission") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_declaration_category") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_proof_submission_detail") - frappe.reload_doc('accounts', 'doctype', 'tax_category') + frappe.reload_doc("accounts", "doctype", "tax_category") for doctype in ["Sales Invoice", "Delivery Note", "Purchase Invoice"]: - frappe.db.sql("""delete from `tabCustom Field` where dt = %s - and fieldname in ('port_code', 'shipping_bill_number', 'shipping_bill_date')""", doctype) + frappe.db.sql( + """delete from `tabCustom Field` where dt = %s + and fieldname in ('port_code', 'shipping_bill_number', 'shipping_bill_date')""", + doctype, + ) make_custom_fields() - frappe.db.sql(""" + frappe.db.sql( + """ update `tabCustom Field` set reqd = 0, `default` = '' where fieldname = 'reason_for_issuing_document' - """) + """ + ) - frappe.db.sql(""" + frappe.db.sql( + """ update tabAddress set gst_state_number=concat("0", gst_state_number) where ifnull(gst_state_number, '') != '' and gst_state_number<10 - """) + """ + ) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index bf8bd05fcc0..f57d9d37cf1 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -30,12 +30,17 @@ class AdditionalSalary(Document): frappe.throw(_("Amount should not be less than zero")) def validate_salary_structure(self): - if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}): + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + self.employee + ) + ) def validate_recurring_additional_salary_overlap(self): if self.is_recurring: - additional_salaries = frappe.db.sql(""" + additional_salaries = frappe.db.sql( + """ SELECT name FROM `tabAdditional Salary` @@ -47,22 +52,28 @@ class AdditionalSalary(Document): AND salary_component = %s AND to_date >= %s AND from_date <= %s""", - (self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1) + (self.employee, self.name, self.salary_component, self.from_date, self.to_date), + as_dict=1, + ) additional_salaries = [salary.name for salary in additional_salaries] if additional_salaries and len(additional_salaries): - frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( - bold(comma_and(additional_salaries)), - bold(self.salary_component), - bold(formatdate(self.from_date)), - bold(formatdate(self.to_date) - ))) - + frappe.throw( + _( + "Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}" + ).format( + bold(comma_and(additional_salaries)), + bold(self.salary_component), + bold(formatdate(self.from_date)), + bold(formatdate(self.to_date)), + ) + ) def validate_dates(self): - date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) if getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("From Date can not be greater than To Date.")) @@ -81,19 +92,27 @@ class AdditionalSalary(Document): def validate_employee_referral(self): if self.ref_doctype == "Employee Referral": - referral_details = frappe.db.get_value("Employee Referral", self.ref_docname, - ["is_applicable_for_referral_bonus", "status"], as_dict=1) + referral_details = frappe.db.get_value( + "Employee Referral", + self.ref_docname, + ["is_applicable_for_referral_bonus", "status"], + as_dict=1, + ) if not referral_details.is_applicable_for_referral_bonus: - frappe.throw(_("Employee Referral {0} is not applicable for referral bonus.").format( - self.ref_docname)) + frappe.throw( + _("Employee Referral {0} is not applicable for referral bonus.").format(self.ref_docname) + ) if self.type == "Deduction": frappe.throw(_("Earning Salary Component is required for Employee Referral Bonus.")) if referral_details.status != "Accepted": - frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format( - frappe.bold("Accepted"))) + frappe.throw( + _( + "Additional Salary for referral bonus can only be created against Employee Referral with status {0}" + ).format(frappe.bold("Accepted")) + ) def update_return_amount_in_employee_advance(self): if self.ref_doctype == "Employee Advance" and self.ref_docname: @@ -123,28 +142,54 @@ class AdditionalSalary(Document): no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 return amount_per_day * no_of_days + @frappe.whitelist() def get_additional_salaries(employee, start_date, end_date, component_type): - comp_type = 'Earning' if component_type == 'earnings' else 'Deduction' + from frappe.query_builder import Criterion - additional_sal = frappe.qb.DocType('Additional Salary') - component_field = additional_sal.salary_component.as_('component') - overwrite_field = additional_sal.overwrite_salary_structure_amount.as_('overwrite') + comp_type = "Earning" if component_type == "earnings" else "Deduction" - additional_salary_list = frappe.qb.from_( - additional_sal - ).select( - additional_sal.name, component_field, additional_sal.type, - additional_sal.amount, additional_sal.is_recurring, overwrite_field, - additional_sal.deduct_full_tax_on_selected_payroll_date - ).where( - (additional_sal.employee == employee) - & (additional_sal.docstatus == 1) - & (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)) - ).run(as_dict=True) + additional_sal = frappe.qb.DocType("Additional Salary") + component_field = additional_sal.salary_component.as_("component") + overwrite_field = additional_sal.overwrite_salary_structure_amount.as_("overwrite") + + additional_salary_list = ( + frappe.qb.from_(additional_sal) + .select( + additional_sal.name, + component_field, + additional_sal.type, + additional_sal.amount, + additional_sal.is_recurring, + overwrite_field, + additional_sal.deduct_full_tax_on_selected_payroll_date, + ) + .where( + (additional_sal.employee == employee) + & (additional_sal.docstatus == 1) + & (additional_sal.type == comp_type) + ) + .where( + 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) + ) additional_salaries = [] components_to_overwrite = [] @@ -152,8 +197,12 @@ def get_additional_salaries(employee, start_date, end_date, component_type): for d in additional_salary_list: if d.overwrite: if d.component in components_to_overwrite: - frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}.").format( - frappe.bold(d.component), start_date, end_date), title=_("Error")) + frappe.throw( + _( + "Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}." + ).format(frappe.bold(d.component), start_date, end_date), + title=_("Error"), + ) components_to_overwrite.append(d.component) diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index 84de912e431..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,40 +17,87 @@ 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 emp_id = make_employee("test_additional@salary.com") frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) - salary_structure = make_salary_structure("Test Salary Structure Additional Salary", "Monthly", employee=emp_id) + salary_structure = make_salary_structure( + "Test Salary Structure Additional Salary", "Monthly", employee=emp_id + ) add_sal = get_additional_salary(emp_id) - ss = make_employee_salary_slip("test_additional@salary.com", "Monthly", salary_structure=salary_structure.name) + ss = make_employee_salary_slip( + "test_additional@salary.com", "Monthly", salary_structure=salary_structure.name + ) 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) -def get_additional_salary(emp_id): + def test_non_recurring_additional_salary(self): + amount = 0 + salary_component = None + date = nowdate() + + 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/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index eda50150ebd..0acd44711b0 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -34,19 +34,30 @@ class EmployeeBenefitApplication(Document): if self.remaining_benefit > 0: self.validate_remaining_benefit_amount() else: - frappe.throw(_("As per your assigned Salary Structure you cannot apply for benefits").format(self.employee)) + frappe.throw( + _("As per your assigned Salary Structure you cannot apply for benefits").format(self.employee) + ) def validate_prev_benefit_claim(self): if self.employee_benefits: for benefit in self.employee_benefits: if benefit.pay_against_benefit_claim == 1: payroll_period = frappe.get_doc("Payroll Period", self.payroll_period) - benefit_claimed = get_previous_claimed_amount(self.employee, payroll_period, component = benefit.earning_component) - benefit_given = get_sal_slip_total_benefit_given(self.employee, payroll_period, component = benefit.earning_component) + benefit_claimed = get_previous_claimed_amount( + self.employee, payroll_period, component=benefit.earning_component + ) + benefit_given = get_sal_slip_total_benefit_given( + self.employee, payroll_period, component=benefit.earning_component + ) benefit_claim_remining = benefit_claimed - benefit_given if benefit_claimed > 0 and benefit_claim_remining > benefit.amount: - frappe.throw(_("An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}").format( - benefit_claimed, benefit.earning_component, benefit_claim_remining)) + frappe.throw( + _( + "An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}" + ).format( + benefit_claimed, benefit.earning_component, benefit_claim_remining + ) + ) def validate_remaining_benefit_amount(self): # check salary structure earnings have flexi component (sum of max_benefit_amount) @@ -65,20 +76,34 @@ class EmployeeBenefitApplication(Document): if salary_structure.earnings: for earnings in salary_structure.earnings: if earnings.is_flexible_benefit == 1 and earnings.salary_component not in benefit_components: - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", earnings.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + earnings.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) if pay_against_benefit_claim != 1: pro_rata_amount += max_benefit_amount else: non_pro_rata_amount += max_benefit_amount - if pro_rata_amount == 0 and non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to any of the existing component").format(self.remaining_benefit)) + if pro_rata_amount == 0 and non_pro_rata_amount == 0: + frappe.throw( + _("Please add the remaining benefits {0} to any of the existing component").format( + self.remaining_benefit + ) + ) elif non_pro_rata_amount > 0 and non_pro_rata_amount < rounded(self.remaining_benefit): - frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component").format( - non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) + frappe.throw( + _( + "You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component" + ).format(non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount) + ) elif non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to the application as pro-rata component").format( - self.remaining_benefit)) + frappe.throw( + _("Please add the remaining benefits {0} to the application as pro-rata component").format( + self.remaining_benefit + ) + ) def validate_max_benefit_for_component(self): if self.employee_benefits: @@ -87,30 +112,43 @@ class EmployeeBenefitApplication(Document): self.validate_max_benefit(employee_benefit.earning_component) max_benefit_amount += employee_benefit.amount if max_benefit_amount > self.max_benefits: - frappe.throw(_("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, self.max_benefits)) + frappe.throw( + _("Maximum benefit amount of employee {0} exceeds {1}").format( + self.employee, self.max_benefits + ) + ) def validate_max_benefit(self, earning_component_name): - max_benefit_amount = frappe.db.get_value("Salary Component", earning_component_name, "max_benefit_amount") + max_benefit_amount = frappe.db.get_value( + "Salary Component", earning_component_name, "max_benefit_amount" + ) benefit_amount = 0 for employee_benefit in self.employee_benefits: if employee_benefit.earning_component == earning_component_name: benefit_amount += employee_benefit.amount - prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name) + prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given( + self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name + ) benefit_amount += prev_sal_slip_flexi_amount if rounded(benefit_amount, 2) > max_benefit_amount: - frappe.throw(_("Maximum benefit amount of component {0} exceeds {1}").format(earning_component_name, max_benefit_amount)) + frappe.throw( + _("Maximum benefit amount of component {0} exceeds {1}").format( + earning_component_name, max_benefit_amount + ) + ) def validate_duplicate_on_payroll_period(self): application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': self.employee, - 'payroll_period': self.payroll_period, - 'docstatus': 1 - } + {"employee": self.employee, "payroll_period": self.payroll_period, "docstatus": 1}, ) if application: - frappe.throw(_("Employee {0} already submited an apllication {1} for the payroll period {2}").format(self.employee, application, self.payroll_period)) + frappe.throw( + _("Employee {0} already submited an apllication {1} for the payroll period {2}").format( + self.employee, application, self.payroll_period + ) + ) + @frappe.whitelist() def get_max_benefits(employee, on_date): @@ -121,6 +159,7 @@ def get_max_benefits(employee, on_date): return max_benefits return False + @frappe.whitelist() def get_max_benefits_remaining(employee, on_date, payroll_period): max_benefits = get_max_benefits(employee, on_date) @@ -141,9 +180,14 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): sal_struct = frappe.get_doc("Salary Structure", sal_struct_name) for sal_struct_row in sal_struct.get("earnings"): salary_component = frappe.get_doc("Salary Component", sal_struct_row.salary_component) - if salary_component.depends_on_payment_days == 1 and salary_component.pay_against_benefit_claim != 1: + if ( + salary_component.depends_on_payment_days == 1 + and salary_component.pay_against_benefit_claim != 1 + ): have_depends_on_payment_days = True - benefit_amount = get_benefit_amount_based_on_pro_rata(sal_struct, salary_component.max_benefit_amount) + benefit_amount = get_benefit_amount_based_on_pro_rata( + sal_struct, salary_component.max_benefit_amount + ) amount_per_day = benefit_amount / payroll_period_days per_day_amount_total += amount_per_day @@ -159,12 +203,14 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): return max_benefits - prev_sal_slip_flexi_total return max_benefits + def calculate_lwp(employee, start_date, holidays, working_days): lwp = 0 holidays = "','".join(holidays) for d in range(working_days): dt = add_days(cstr(getdate(start_date)), d) - leave = frappe.db.sql(""" + leave = frappe.db.sql( + """ select t1.name, t1.half_day from `tabLeave Application` t1, `tabLeave Type` t2 where t2.name = t1.leave_type @@ -174,56 +220,77 @@ def calculate_lwp(employee, start_date, holidays, working_days): and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date WHEN t2.include_holiday THEN %(dt)s between from_date and to_date END - """.format(holidays), {"employee": employee, "dt": dt}) + """.format( + holidays + ), + {"employee": employee, "dt": dt}, + ) if leave: lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) return lwp -def get_benefit_component_amount(employee, start_date, end_date, salary_component, sal_struct, payroll_frequency, payroll_period): + +def get_benefit_component_amount( + employee, start_date, end_date, salary_component, sal_struct, payroll_frequency, payroll_period +): if not payroll_period: - frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}") - .format(salary_component)) + frappe.msgprint( + _("Start and end dates not in a valid Payroll Period, cannot calculate {0}").format( + salary_component + ) + ) return False # Considering there is only one application for a year - benefit_application = frappe.db.sql(""" + benefit_application = frappe.db.sql( + """ select name from `tabEmployee Benefit Application` where payroll_period=%(payroll_period)s and employee=%(employee)s and docstatus = 1 - """, { - 'employee': employee, - 'payroll_period': payroll_period.name - }) + """, + {"employee": employee, "payroll_period": payroll_period.name}, + ) current_benefit_amount = 0.0 - component_max_benefit, depends_on_payment_days = frappe.db.get_value("Salary Component", - salary_component, ["max_benefit_amount", "depends_on_payment_days"]) + component_max_benefit, depends_on_payment_days = frappe.db.get_value( + "Salary Component", salary_component, ["max_benefit_amount", "depends_on_payment_days"] + ) benefit_amount = 0 if benefit_application: - benefit_amount = frappe.db.get_value("Employee Benefit Application Detail", - {"parent": benefit_application[0][0], "earning_component": salary_component}, "amount") + benefit_amount = frappe.db.get_value( + "Employee Benefit Application Detail", + {"parent": benefit_application[0][0], "earning_component": salary_component}, + "amount", + ) elif component_max_benefit: benefit_amount = get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit) current_benefit_amount = 0 if benefit_amount: - total_sub_periods = get_period_factor(employee, - start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days)[0] + total_sub_periods = get_period_factor( + employee, start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days + )[0] current_benefit_amount = benefit_amount / total_sub_periods return current_benefit_amount + def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): max_benefits_total = 0 benefit_amount = 0 for d in sal_struct.get("earnings"): if d.is_flexible_benefit == 1: - component = frappe.db.get_value("Salary Component", d.salary_component, ["max_benefit_amount", "pay_against_benefit_claim"], as_dict=1) + component = frappe.db.get_value( + "Salary Component", + d.salary_component, + ["max_benefit_amount", "pay_against_benefit_claim"], + as_dict=1, + ) if not component.pay_against_benefit_claim: max_benefits_total += component.max_benefit_amount @@ -234,34 +301,46 @@ def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): return benefit_amount + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_earning_components(doctype, txt, searchfield, start, page_len, filters): if len(filters) < 2: return {} - salary_structure = get_assigned_salary_structure(filters['employee'], filters['date']) + salary_structure = get_assigned_salary_structure(filters["employee"], filters["date"]) if salary_structure: - return frappe.db.sql(""" + return frappe.db.sql( + """ select salary_component from `tabSalary Detail` where parent = %s and is_flexible_benefit = 1 order by name - """, salary_structure) + """, + salary_structure, + ) else: - frappe.throw(_("Salary Structure not found for employee {0} and date {1}") - .format(filters['employee'], filters['date'])) + frappe.throw( + _("Salary Structure not found for employee {0} and date {1}").format( + filters["employee"], filters["date"] + ) + ) + @frappe.whitelist() def get_earning_components_max_benefits(employee, date, earning_component): salary_structure = get_assigned_salary_structure(employee, date) - amount = frappe.db.sql(""" + amount = frappe.db.sql( + """ select amount from `tabSalary Detail` where parent = %s and is_flexible_benefit = 1 and salary_component = %s order by name - """, salary_structure, earning_component) + """, + salary_structure, + earning_component, + ) return amount if amount else 0 diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py index 801ce4ba367..31f26b25e73 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py @@ -23,9 +23,15 @@ class EmployeeBenefitClaim(Document): max_benefits = get_max_benefits(self.employee, self.claim_date) if not max_benefits or max_benefits <= 0: frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee)) - payroll_period = get_payroll_period(self.claim_date, self.claim_date, frappe.db.get_value("Employee", self.employee, "company")) + payroll_period = get_payroll_period( + self.claim_date, self.claim_date, frappe.db.get_value("Employee", self.employee, "company") + ) if not payroll_period: - frappe.throw(_("{0} is not in a valid Payroll Period").format(frappe.format(self.claim_date, dict(fieldtype='Date')))) + frappe.throw( + _("{0} is not in a valid Payroll Period").format( + frappe.format(self.claim_date, dict(fieldtype="Date")) + ) + ) self.validate_max_benefit_for_component(payroll_period) self.validate_max_benefit_for_sal_struct(max_benefits) self.validate_benefit_claim_amount(max_benefits, payroll_period) @@ -36,21 +42,31 @@ class EmployeeBenefitClaim(Document): claimed_amount = self.claimed_amount claimed_amount += get_previous_claimed_amount(self.employee, payroll_period) if max_benefits < claimed_amount: - frappe.throw(_("Maximum benefit of employee {0} exceeds {1} by the sum {2} of previous claimed\ - amount").format(self.employee, max_benefits, claimed_amount-max_benefits)) + frappe.throw( + _( + "Maximum benefit of employee {0} exceeds {1} by the sum {2} of previous claimed\ + amount" + ).format(self.employee, max_benefits, claimed_amount - max_benefits) + ) def validate_max_benefit_for_sal_struct(self, max_benefits): if self.claimed_amount > max_benefits: - frappe.throw(_("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, max_benefits)) + frappe.throw( + _("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, max_benefits) + ) def validate_max_benefit_for_component(self, payroll_period): if self.max_amount_eligible: claimed_amount = self.claimed_amount - claimed_amount += get_previous_claimed_amount(self.employee, - payroll_period, component = self.earning_component) + claimed_amount += get_previous_claimed_amount( + self.employee, payroll_period, component=self.earning_component + ) if claimed_amount > self.max_amount_eligible: - frappe.throw(_("Maximum amount eligible for the component {0} exceeds {1}") - .format(self.earning_component, self.max_amount_eligible)) + frappe.throw( + _("Maximum amount eligible for the component {0} exceeds {1}").format( + self.earning_component, self.max_amount_eligible + ) + ) def validate_non_pro_rata_benefit_claim(self, max_benefits, payroll_period): claimed_amount = self.claimed_amount @@ -64,30 +80,39 @@ class EmployeeBenefitClaim(Document): sal_struct = frappe.get_doc("Salary Structure", sal_struct_name) pro_rata_amount = get_benefit_pro_rata_ratio_amount(self.employee, self.claim_date, sal_struct) - claimed_amount += get_previous_claimed_amount(self.employee, payroll_period, non_pro_rata = True) + claimed_amount += get_previous_claimed_amount(self.employee, payroll_period, non_pro_rata=True) if max_benefits < pro_rata_amount + claimed_amount: - frappe.throw(_("Maximum benefit of employee {0} exceeds {1} by the sum {2} of benefit application pro-rata component\ - amount and previous claimed amount").format(self.employee, max_benefits, pro_rata_amount+claimed_amount-max_benefits)) + frappe.throw( + _( + "Maximum benefit of employee {0} exceeds {1} by the sum {2} of benefit application pro-rata component\ + amount and previous claimed amount" + ).format( + self.employee, max_benefits, pro_rata_amount + claimed_amount - max_benefits + ) + ) def get_pro_rata_amount_in_application(self, payroll_period): application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': self.employee, - 'payroll_period': payroll_period, - 'docstatus': 1 - } + {"employee": self.employee, "payroll_period": payroll_period, "docstatus": 1}, ) if application: - return frappe.db.get_value("Employee Benefit Application", application, "pro_rata_dispensed_amount") + return frappe.db.get_value( + "Employee Benefit Application", application, "pro_rata_dispensed_amount" + ) return False + def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): total_pro_rata_max = 0 benefit_amount_total = 0 for sal_struct_row in sal_struct.get("earnings"): try: - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", sal_struct_row.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + sal_struct_row.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) except TypeError: # show the error in tests? frappe.throw(_("Unable to find Salary Component {0}").format(sal_struct_row.salary_component)) @@ -95,7 +120,11 @@ def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): total_pro_rata_max += max_benefit_amount if total_pro_rata_max > 0: for sal_struct_row in sal_struct.get("earnings"): - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", sal_struct_row.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + sal_struct_row.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) if sal_struct_row.is_flexible_benefit == 1 and pay_against_benefit_claim != 1: component_max = max_benefit_amount @@ -105,6 +134,7 @@ def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): benefit_amount_total += benefit_amount return benefit_amount_total + def get_benefit_claim_amount(employee, start_date, end_date, salary_component=None): query = """ select sum(claimed_amount) @@ -119,41 +149,54 @@ def get_benefit_claim_amount(employee, start_date, end_date, salary_component=No if salary_component: query += " and earning_component = %(earning_component)s" - claimed_amount = flt(frappe.db.sql(query, { - 'employee': employee, - 'start_date': start_date, - 'end_date': end_date, - 'earning_component': salary_component - })[0][0]) + claimed_amount = flt( + frappe.db.sql( + query, + { + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "earning_component": salary_component, + }, + )[0][0] + ) return claimed_amount + def get_total_benefit_dispensed(employee, sal_struct, sal_slip_start_date, payroll_period): pro_rata_amount = 0 claimed_amount = 0 application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': employee, - 'payroll_period': payroll_period.name, - 'docstatus': 1 - } + {"employee": employee, "payroll_period": payroll_period.name, "docstatus": 1}, ) if application: application_obj = frappe.get_doc("Employee Benefit Application", application) - pro_rata_amount = application_obj.pro_rata_dispensed_amount + application_obj.max_benefits - application_obj.remaining_benefit + pro_rata_amount = ( + application_obj.pro_rata_dispensed_amount + + application_obj.max_benefits + - application_obj.remaining_benefit + ) else: pro_rata_amount = get_benefit_pro_rata_ratio_amount(employee, sal_slip_start_date, sal_struct) - claimed_amount += get_benefit_claim_amount(employee, payroll_period.start_date, payroll_period.end_date) + claimed_amount += get_benefit_claim_amount( + employee, payroll_period.start_date, payroll_period.end_date + ) return claimed_amount + pro_rata_amount -def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end_date, payroll_period, sal_struct): + +def get_last_payroll_period_benefits( + employee, sal_slip_start_date, sal_slip_end_date, payroll_period, sal_struct +): max_benefits = get_max_benefits(employee, payroll_period.end_date) if not max_benefits: max_benefits = 0 - remaining_benefit = max_benefits - get_total_benefit_dispensed(employee, sal_struct, sal_slip_start_date, payroll_period) + remaining_benefit = max_benefits - get_total_benefit_dispensed( + employee, sal_struct, sal_slip_start_date, payroll_period + ) if remaining_benefit > 0: have_remaining = True # Set the remaining benefits to flexi non pro-rata component in the salary structure @@ -162,7 +205,9 @@ def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end if d.is_flexible_benefit == 1: salary_component = frappe.get_doc("Salary Component", d.salary_component) if salary_component.pay_against_benefit_claim == 1: - claimed_amount = get_benefit_claim_amount(employee, payroll_period.start_date, sal_slip_end_date, d.salary_component) + claimed_amount = get_benefit_claim_amount( + employee, payroll_period.start_date, sal_slip_end_date, d.salary_component + ) amount_fit_to_component = salary_component.max_benefit_amount - claimed_amount if amount_fit_to_component > 0: if remaining_benefit > amount_fit_to_component: @@ -171,19 +216,23 @@ def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end else: amount = remaining_benefit have_remaining = False - current_claimed_amount = get_benefit_claim_amount(employee, sal_slip_start_date, sal_slip_end_date, d.salary_component) + current_claimed_amount = get_benefit_claim_amount( + employee, sal_slip_start_date, sal_slip_end_date, d.salary_component + ) amount += current_claimed_amount struct_row = {} salary_components_dict = {} - struct_row['depends_on_payment_days'] = salary_component.depends_on_payment_days - struct_row['salary_component'] = salary_component.name - struct_row['abbr'] = salary_component.salary_component_abbr - struct_row['do_not_include_in_total'] = salary_component.do_not_include_in_total - struct_row['is_tax_applicable'] = salary_component.is_tax_applicable, - struct_row['is_flexible_benefit'] = salary_component.is_flexible_benefit, - struct_row['variable_based_on_taxable_salary'] = salary_component.variable_based_on_taxable_salary - salary_components_dict['amount'] = amount - salary_components_dict['struct_row'] = struct_row + struct_row["depends_on_payment_days"] = salary_component.depends_on_payment_days + struct_row["salary_component"] = salary_component.name + struct_row["abbr"] = salary_component.salary_component_abbr + struct_row["do_not_include_in_total"] = salary_component.do_not_include_in_total + struct_row["is_tax_applicable"] = (salary_component.is_tax_applicable,) + struct_row["is_flexible_benefit"] = (salary_component.is_flexible_benefit,) + struct_row[ + "variable_based_on_taxable_salary" + ] = salary_component.variable_based_on_taxable_salary + salary_components_dict["amount"] = amount + salary_components_dict["struct_row"] = struct_row salary_components_array.append(salary_components_dict) if not have_remaining: break diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py index a37e22425f7..7686185349f 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -15,13 +15,17 @@ class EmployeeIncentive(Document): self.validate_salary_structure() def validate_salary_structure(self): - if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}): + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + self.employee + ) + ) def on_submit(self): - company = frappe.db.get_value('Employee', self.employee, 'company') + company = frappe.db.get_value("Employee", self.employee, "company") - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = self.employee additional_salary.currency = self.currency additional_salary.salary_component = self.salary_component diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index 9b5eab636f1..c0ef2eee78c 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -20,7 +20,9 @@ class EmployeeTaxExemptionDeclaration(Document): def validate(self): validate_active_employee(self.employee) validate_tax_declaration(self.declarations) - validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) + validate_duplicate_exemption_for_payroll_period( + self.doctype, self.name, self.payroll_period, self.employee + ) self.set_total_declared_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() @@ -43,17 +45,23 @@ class EmployeeTaxExemptionDeclaration(Document): self.annual_hra_exemption = hra_exemption["annual_exemption"] self.monthly_hra_exemption = hra_exemption["monthly_exemption"] + @frappe.whitelist() def make_proof_submission(source_name, target_doc=None): - doclist = get_mapped_doc("Employee Tax Exemption Declaration", source_name, { - "Employee Tax Exemption Declaration": { - "doctype": "Employee Tax Exemption Proof Submission", - "field_no_map": ["monthly_house_rent", "monthly_hra_exemption"] + doclist = get_mapped_doc( + "Employee Tax Exemption Declaration", + source_name, + { + "Employee Tax Exemption Declaration": { + "doctype": "Employee Tax Exemption Proof Submission", + "field_no_map": ["monthly_house_rent", "monthly_hra_exemption"], + }, + "Employee Tax Exemption Declaration Category": { + "doctype": "Employee Tax Exemption Proof Submission Detail", + "add_if_empty": True, + }, }, - "Employee Tax Exemption Declaration Category": { - "doctype": "Employee Tax Exemption Proof Submission Detail", - "add_if_empty": True - } - }, target_doc) + target_doc, + ) return doclist diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index fc28afdc3e5..1d90e7383fe 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -19,112 +19,147 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""") def test_duplicate_category_in_declaration(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000), - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 50000) - ] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ), + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ) self.assertRaises(frappe.ValidationError, declaration.save) def test_duplicate_entry_for_payroll_period(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000), - dict(exemption_sub_category = "_Test1 Sub Category", - exemption_category = "_Test Category", - amount = 50000), - ] - }).insert() + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ).insert() - duplicate_declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000) - ] - }) + duplicate_declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ) + ], + } + ) self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert) - duplicate_declaration.employee = frappe.get_value("Employee", {"user_id":"employee1@taxexepmtion.com"}, "name") + duplicate_declaration.employee = frappe.get_value( + "Employee", {"user_id": "employee1@taxexepmtion.com"}, "name" + ) self.assertTrue(duplicate_declaration.insert) def test_exemption_amount(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 80000), - dict(exemption_sub_category = "_Test1 Sub Category", - exemption_category = "_Test Category", - amount = 60000), - ] - }).insert() + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() self.assertEqual(declaration.total_exemption_amount, 100000) + def create_payroll_period(**args): args = frappe._dict(args) name = args.name or "_Test Payroll Period" if not frappe.db.exists("Payroll Period", name): from datetime import date - payroll_period = frappe.get_doc(dict( - doctype = 'Payroll Period', - name = name, - company = args.company or erpnext.get_default_company(), - start_date = args.start_date or date(date.today().year, 1, 1), - end_date = args.end_date or date(date.today().year, 12, 31) - )).insert() + + payroll_period = frappe.get_doc( + dict( + doctype="Payroll Period", + name=name, + company=args.company or erpnext.get_default_company(), + start_date=args.start_date or date(date.today().year, 1, 1), + end_date=args.end_date or date(date.today().year, 12, 31), + ) + ).insert() return payroll_period else: return frappe.get_doc("Payroll Period", name) + def create_exemption_category(): if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"): - category = frappe.get_doc({ - "doctype": "Employee Tax Exemption Category", - "name": "_Test Category", - "deduction_component": "Income Tax", - "max_amount": 100000 - }).insert() + category = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Category", + "name": "_Test Category", + "deduction_component": "Income Tax", + "max_amount": 100000, + } + ).insert() if not frappe.db.exists("Employee Tax Exemption Sub Category", "_Test Sub Category"): - frappe.get_doc({ - "doctype": "Employee Tax Exemption Sub Category", - "name": "_Test Sub Category", - "exemption_category": "_Test Category", - "max_amount": 100000, - "is_active": 1 - }).insert() + frappe.get_doc( + { + "doctype": "Employee Tax Exemption Sub Category", + "name": "_Test Sub Category", + "exemption_category": "_Test Category", + "max_amount": 100000, + "is_active": 1, + } + ).insert() if not frappe.db.exists("Employee Tax Exemption Sub Category", "_Test1 Sub Category"): - frappe.get_doc({ - "doctype": "Employee Tax Exemption Sub Category", - "name": "_Test1 Sub Category", - "exemption_category": "_Test Category", - "max_amount": 50000, - "is_active": 1 - }).insert() + frappe.get_doc( + { + "doctype": "Employee Tax Exemption Sub Category", + "name": "_Test1 Sub Category", + "exemption_category": "_Test Category", + "max_amount": 50000, + "is_active": 1, + } + ).insert() diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index 56e73b37dff..c52efaba592 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -21,7 +21,9 @@ class EmployeeTaxExemptionProofSubmission(Document): self.set_total_actual_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() - validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) + validate_duplicate_exemption_for_payroll_period( + self.doctype, self.name, self.payroll_period, self.employee + ) def set_total_actual_amount(self): self.total_actual_amount = flt(self.get("house_rent_payment_amount")) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py index f2aa64c2878..58b2c1af058 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py @@ -19,40 +19,59 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""") def test_exemption_amount_lesser_than_category_max(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "payroll_period": "Test Payroll Period", - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - type_of_proof = "Test Proof", - exemption_category = "_Test Category", - amount = 150000)] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "payroll_period": "Test Payroll Period", + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + type_of_proof="Test Proof", + exemption_category="_Test Category", + amount=150000, + ) + ], + } + ) self.assertRaises(frappe.ValidationError, declaration.save) - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "payroll_period": "Test Payroll Period", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - type_of_proof = "Test Proof", - exemption_category = "_Test Category", - amount = 100000)] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "payroll_period": "Test Payroll Period", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + type_of_proof="Test Proof", + exemption_category="_Test Category", + amount=100000, + ) + ], + } + ) self.assertTrue(declaration.save) self.assertTrue(declaration.submit) def test_duplicate_category_in_proof_submission(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "payroll_period": "Test Payroll Period", - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - type_of_proof = "Test Proof", - amount = 100000), - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 50000), - ] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "payroll_period": "Test Payroll Period", + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=100000, + ), + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ) self.assertRaises(frappe.ValidationError, declaration.save) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py index 4ac11f7112d..fb75d6706c3 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py @@ -10,7 +10,12 @@ from frappe.utils import flt class EmployeeTaxExemptionSubCategory(Document): def validate(self): - category_max_amount = frappe.db.get_value("Employee Tax Exemption Category", self.exemption_category, "max_amount") + category_max_amount = frappe.db.get_value( + "Employee Tax Exemption Category", self.exemption_category, "max_amount" + ) if flt(self.max_amount) > flt(category_max_amount): - frappe.throw(_("Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1}") - .format(category_max_amount, self.exemption_category)) + frappe.throw( + _( + "Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1}" + ).format(category_max_amount, self.exemption_category) + ) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 939634a9310..91740ae8c6c 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -27,7 +27,7 @@ class Gratuity(AccountsController): self.create_gl_entries() def on_cancel(self): - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] self.create_gl_entries(cancel=True) def create_gl_entries(self, cancel=False): @@ -39,28 +39,34 @@ class Gratuity(AccountsController): # payable entry if self.amount: gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "credit": self.amount, - "credit_in_account_currency": self.amount, - "against": self.expense_account, - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.payable_account, + "credit": self.amount, + "credit_in_account_currency": self.amount, + "against": self.expense_account, + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": self.doctype, + "against_voucher": self.name, + "cost_center": self.cost_center, + }, + item=self, + ) ) # expense entries gl_entry.append( - self.get_gl_dict({ - "account": self.expense_account, - "debit": self.amount, - "debit_in_account_currency": self.amount, - "against": self.payable_account, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.expense_account, + "debit": self.amount, + "debit_in_account_currency": self.amount, + "against": self.payable_account, + "cost_center": self.cost_center, + }, + item=self, + ) ) else: frappe.throw(_("Total Amount can not be zero")) @@ -69,7 +75,7 @@ class Gratuity(AccountsController): def create_additional_salary(self): if self.pay_via_salary_slip: - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = self.employee additional_salary.salary_component = self.salary_component additional_salary.overwrite_salary_structure_amount = 0 @@ -81,19 +87,22 @@ class Gratuity(AccountsController): additional_salary.submit() def set_total_advance_paid(self): - paid_amount = frappe.db.sql(""" + paid_amount = frappe.db.sql( + """ select ifnull(sum(debit_in_account_currency), 0) as paid_amount from `tabGL Entry` where against_voucher_type = 'Gratuity' and against_voucher = %s and party_type = 'Employee' and party = %s - """, (self.name, self.employee), as_dict=1)[0].paid_amount + """, + (self.name, self.employee), + as_dict=1, + )[0].paid_amount if flt(paid_amount) > self.amount: frappe.throw(_("Row {0}# Paid Amount cannot be greater than Total amount")) - self.db_set("paid_amount", paid_amount) if self.amount == self.paid_amount: self.db_set("status", "Paid") @@ -104,69 +113,97 @@ def calculate_work_experience_and_amount(employee, gratuity_rule): current_work_experience = calculate_work_experience(employee, gratuity_rule) or 0 gratuity_amount = calculate_gratuity_amount(employee, gratuity_rule, current_work_experience) or 0 - return {'current_work_experience': current_work_experience, "amount": gratuity_amount} + return {"current_work_experience": current_work_experience, "amount": gratuity_amount} + def calculate_work_experience(employee, gratuity_rule): - total_working_days_per_year, minimum_year_for_gratuity = frappe.db.get_value("Gratuity Rule", gratuity_rule, ["total_working_days_per_year", "minimum_year_for_gratuity"]) + total_working_days_per_year, minimum_year_for_gratuity = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, ["total_working_days_per_year", "minimum_year_for_gratuity"] + ) - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) if not relieving_date: - frappe.throw(_("Please set Relieving Date for employee: {0}").format(bold(get_link_to_form("Employee", employee)))) + frappe.throw( + _("Please set Relieving Date for employee: {0}").format( + bold(get_link_to_form("Employee", employee)) + ) + ) - method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function") - employee_total_workings_days = calculate_employee_total_workings_days(employee, date_of_joining, relieving_date) + method = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, "work_experience_calculation_function" + ) + employee_total_workings_days = calculate_employee_total_workings_days( + employee, date_of_joining, relieving_date + ) - current_work_experience = employee_total_workings_days/total_working_days_per_year or 1 - current_work_experience = get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee) + current_work_experience = employee_total_workings_days / total_working_days_per_year or 1 + current_work_experience = get_work_experience_using_method( + method, current_work_experience, minimum_year_for_gratuity, employee + ) return current_work_experience -def calculate_employee_total_workings_days(employee, date_of_joining, relieving_date ): + +def calculate_employee_total_workings_days(employee, date_of_joining, relieving_date): employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days payroll_based_on = frappe.db.get_value("Payroll Settings", None, "payroll_based_on") or "Leave" if payroll_based_on == "Leave": total_lwp = get_non_working_days(employee, relieving_date, "On Leave") employee_total_workings_days -= total_lwp - elif payroll_based_on == "Attendance": + elif payroll_based_on == "Attendance": total_absents = get_non_working_days(employee, relieving_date, "Absent") employee_total_workings_days -= total_absents return employee_total_workings_days -def get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee): + +def get_work_experience_using_method( + method, current_work_experience, minimum_year_for_gratuity, employee +): if method == "Round off Work Experience": current_work_experience = round(current_work_experience) else: current_work_experience = floor(current_work_experience) if current_work_experience < minimum_year_for_gratuity: - frappe.throw(_("Employee: {0} have to complete minimum {1} years for gratuity").format(bold(employee), minimum_year_for_gratuity)) + frappe.throw( + _("Employee: {0} have to complete minimum {1} years for gratuity").format( + bold(employee), minimum_year_for_gratuity + ) + ) return current_work_experience + def get_non_working_days(employee, relieving_date, status): - filters={ - "docstatus": 1, - "status": status, - "employee": employee, - "attendance_date": ("<=", get_datetime(relieving_date)) - } + filters = { + "docstatus": 1, + "status": status, + "employee": employee, + "attendance_date": ("<=", get_datetime(relieving_date)), + } if status == "On Leave": - lwp_leave_types = frappe.get_list("Leave Type", filters = {"is_lwp":1}) + lwp_leave_types = frappe.get_list("Leave Type", filters={"is_lwp": 1}) lwp_leave_types = [leave_type.name for leave_type in lwp_leave_types] - filters["leave_type"] = ("IN", lwp_leave_types) + filters["leave_type"] = ("IN", lwp_leave_types) - - record = frappe.get_all("Attendance", filters=filters, fields = ["COUNT(name) as total_lwp"]) + record = frappe.get_all("Attendance", filters=filters, fields=["COUNT(name) as total_lwp"]) return record[0].total_lwp if len(record) else 0 + def calculate_gratuity_amount(employee, gratuity_rule, experience): applicable_earnings_component = get_applicable_components(gratuity_rule) - total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule) + total_applicable_components_amount = get_total_applicable_component_amount( + employee, applicable_earnings_component, gratuity_rule + ) - calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on") + calculate_gratuity_amount_based_on = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on" + ) gratuity_amount = 0 slabs = get_gratuity_rule_slabs(gratuity_rule) slab_found = False @@ -174,49 +211,78 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): for slab in slabs: if calculate_gratuity_amount_based_on == "Current Slab": - slab_found, gratuity_amount = calculate_amount_based_on_current_slab(slab.from_year, slab.to_year, - experience, total_applicable_components_amount, slab.fraction_of_applicable_earnings) + slab_found, gratuity_amount = calculate_amount_based_on_current_slab( + slab.from_year, + slab.to_year, + experience, + total_applicable_components_amount, + slab.fraction_of_applicable_earnings, + ) if slab_found: - break + break elif calculate_gratuity_amount_based_on == "Sum of all previous slabs": if slab.to_year == 0 and slab.from_year == 0: - gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + gratuity_amount += ( + year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + ) slab_found = True break - if experience > slab.to_year and experience > slab.from_year and slab.to_year !=0: - gratuity_amount += (slab.to_year - slab.from_year) * total_applicable_components_amount * slab.fraction_of_applicable_earnings - year_left -= (slab.to_year - slab.from_year) + if experience > slab.to_year and experience > slab.from_year and slab.to_year != 0: + gratuity_amount += ( + (slab.to_year - slab.from_year) + * total_applicable_components_amount + * slab.fraction_of_applicable_earnings + ) + year_left -= slab.to_year - slab.from_year slab_found = True elif slab.from_year <= experience and (experience < slab.to_year or slab.to_year == 0): - gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + gratuity_amount += ( + year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + ) slab_found = True if not slab_found: - frappe.throw(_("No Suitable Slab found for Calculation of gratuity amount in Gratuity Rule: {0}").format(bold(gratuity_rule))) + frappe.throw( + _("No Suitable Slab found for Calculation of gratuity amount in Gratuity Rule: {0}").format( + bold(gratuity_rule) + ) + ) return gratuity_amount + def get_applicable_components(gratuity_rule): - applicable_earnings_component = frappe.get_all("Gratuity Applicable Component", filters= {'parent': gratuity_rule}, fields=["salary_component"]) + applicable_earnings_component = frappe.get_all( + "Gratuity Applicable Component", filters={"parent": gratuity_rule}, fields=["salary_component"] + ) if len(applicable_earnings_component) == 0: - frappe.throw(_("No Applicable Earnings Component found for Gratuity Rule: {0}").format(bold(get_link_to_form("Gratuity Rule",gratuity_rule)))) - applicable_earnings_component = [component.salary_component for component in applicable_earnings_component] + frappe.throw( + _("No Applicable Earnings Component found for Gratuity Rule: {0}").format( + bold(get_link_to_form("Gratuity Rule", gratuity_rule)) + ) + ) + applicable_earnings_component = [ + component.salary_component for component in applicable_earnings_component + ] return applicable_earnings_component + def get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule): - sal_slip = get_last_salary_slip(employee) + sal_slip = get_last_salary_slip(employee) if not sal_slip: frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee))) - component_and_amounts = frappe.get_all("Salary Detail", + component_and_amounts = frappe.get_all( + "Salary Detail", filters={ "docstatus": 1, - 'parent': sal_slip, + "parent": sal_slip, "parentfield": "earnings", - 'salary_component': ('in', applicable_earnings_component) + "salary_component": ("in", applicable_earnings_component), }, - fields=["amount"]) + fields=["amount"], + ) total_applicable_components_amount = 0 if not len(component_and_amounts): frappe.throw(_("No Applicable Component is present in last month salary slip")) @@ -224,30 +290,44 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen total_applicable_components_amount += data.amount return total_applicable_components_amount -def calculate_amount_based_on_current_slab(from_year, to_year, experience, total_applicable_components_amount, fraction_of_applicable_earnings): - slab_found = False; gratuity_amount = 0 + +def calculate_amount_based_on_current_slab( + from_year, + to_year, + experience, + total_applicable_components_amount, + fraction_of_applicable_earnings, +): + slab_found = False + gratuity_amount = 0 if experience >= from_year and (to_year == 0 or experience < to_year): - gratuity_amount = total_applicable_components_amount * experience * fraction_of_applicable_earnings + gratuity_amount = ( + total_applicable_components_amount * experience * fraction_of_applicable_earnings + ) if fraction_of_applicable_earnings: slab_found = True return slab_found, gratuity_amount + def get_gratuity_rule_slabs(gratuity_rule): - return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx") + return frappe.get_all( + "Gratuity Rule Slab", filters={"parent": gratuity_rule}, fields=["*"], order_by="idx" + ) + def get_salary_structure(employee): - return frappe.get_list("Salary Structure Assignment", filters = { - "employee": employee, 'docstatus': 1 - }, + return frappe.get_list( + "Salary Structure Assignment", + filters={"employee": employee, "docstatus": 1}, fields=["from_date", "salary_structure"], - order_by = "from_date desc")[0].salary_structure + order_by="from_date desc", + )[0].salary_structure + def get_last_salary_slip(employee): - salary_slips = frappe.get_list("Salary Slip", filters = { - "employee": employee, 'docstatus': 1 - }, - order_by = "start_date desc" + salary_slips = frappe.get_list( + "Salary Slip", filters={"employee": employee, "docstatus": 1}, order_by="start_date desc" ) if not salary_slips: return diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py index 6c3cdfda512..35ae1f4feaf 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py +++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py @@ -1,21 +1,14 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'reference_name', - 'non_standard_fieldnames': { - 'Additional Salary': 'ref_docname', + "fieldname": "reference_name", + "non_standard_fieldnames": { + "Additional Salary": "ref_docname", }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry'] - }, - { - 'label': _('Additional Salary'), - 'items': ['Additional Salary'] - } - ] + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry"]}, + {"label": _("Additional Salary"), "items": ["Additional Salary"]}, + ], } diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 098d71c8f80..0e39dd36710 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -17,18 +17,20 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule test_dependencies = ["Salary Component", "Salary Slip", "Account"] + + class TestGratuity(unittest.TestCase): @classmethod def setUpClass(cls): - make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) - make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) + make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) def setUp(self): frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") def test_get_last_salary_slip_should_return_none_for_new_employee(self): - new_employee = make_employee("new_employee@salary.com", company='_Test Company') + new_employee = make_employee("new_employee@salary.com", company="_Test Company") salary_slip = get_last_salary_slip(new_employee) assert salary_slip is None @@ -37,35 +39,42 @@ class TestGratuity(unittest.TestCase): rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)") - gratuity = create_gratuity(pay_via_salary_slip = 1, employee=employee, rule=rule.name) + gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name) - #work experience calculation - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) - employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days + # work experience calculation + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) + employee_total_workings_days = ( + get_datetime(relieving_date) - get_datetime(date_of_joining) + ).days - experience = employee_total_workings_days/rule.total_working_days_per_year + experience = employee_total_workings_days / rule.total_working_days_per_year gratuity.reload() from math import floor + self.assertEqual(floor(experience), gratuity.current_work_experience) - #amount Calculation - component_amount = frappe.get_all("Salary Detail", - filters={ - "docstatus": 1, - 'parent': sal_slip, - "parentfield": "earnings", - 'salary_component': "Basic Salary" - }, - fields=["amount"]) + # amount Calculation + component_amount = frappe.get_all( + "Salary Detail", + filters={ + "docstatus": 1, + "parent": sal_slip, + "parentfield": "earnings", + "salary_component": "Basic Salary", + }, + fields=["amount"], + ) - ''' 5 - 0 fraction is 1 ''' + """ 5 - 0 fraction is 1 """ gratuity_amount = component_amount[0].amount * experience gratuity.reload() self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) - #additional salary creation (Pay via salary slip) + # additional salary creation (Pay via salary slip) self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) def test_check_gratuity_amount_based_on_all_previous_slabs(self): @@ -73,13 +82,19 @@ class TestGratuity(unittest.TestCase): rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") set_mode_of_payment_account() - gratuity = create_gratuity(expense_account = 'Payment Account - _TC', mode_of_payment='Cash', employee=employee) + gratuity = create_gratuity( + expense_account="Payment Account - _TC", mode_of_payment="Cash", employee=employee + ) - #work experience calculation - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) - employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days + # work experience calculation + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) + employee_total_workings_days = ( + get_datetime(relieving_date) - get_datetime(date_of_joining) + ).days - experience = employee_total_workings_days/rule.total_working_days_per_year + experience = employee_total_workings_days / rule.total_working_days_per_year gratuity.reload() @@ -87,29 +102,32 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) - #amount Calculation - component_amount = frappe.get_all("Salary Detail", - filters={ - "docstatus": 1, - 'parent': sal_slip, - "parentfield": "earnings", - 'salary_component': "Basic Salary" - }, - fields=["amount"]) + # amount Calculation + component_amount = frappe.get_all( + "Salary Detail", + filters={ + "docstatus": 1, + "parent": sal_slip, + "parentfield": "earnings", + "salary_component": "Basic Salary", + }, + fields=["amount"], + ) - ''' range | Fraction + """ range | Fraction 0-1 | 0 1-5 | 0.7 5-0 | 1 - ''' + """ - gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount + gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount gratuity.reload() self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) self.assertEqual(gratuity.status, "Unpaid") from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + pay_entry = get_payment_entry("Gratuity", gratuity.name) pay_entry.reference_no = "123467" pay_entry.reference_date = getdate() @@ -118,26 +136,26 @@ class TestGratuity(unittest.TestCase): gratuity.reload() self.assertEqual(gratuity.status, "Paid") - self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2)) + self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2)) def tearDown(self): frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") + def get_gratuity_rule(name): rule = frappe.db.exists("Gratuity Rule", name) if not rule: create_gratuity_rule() rule = frappe.get_doc("Gratuity Rule", name) rule.applicable_earnings_component = [] - rule.append("applicable_earnings_component", { - "salary_component": "Basic Salary" - }) + rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"}) rule.save() rule.reload() return rule + def create_gratuity(**args): if args: args = frappe._dict(args) @@ -150,15 +168,16 @@ def create_gratuity(**args): gratuity.payroll_date = getdate() gratuity.salary_component = "Performance Bonus" else: - gratuity.expense_account = args.expense_account or 'Payment Account - _TC' + gratuity.expense_account = args.expense_account or "Payment Account - _TC" gratuity.payable_account = args.payable_account or get_payable_account("_Test Company") - gratuity.mode_of_payment = args.mode_of_payment or 'Cash' + gratuity.mode_of_payment = args.mode_of_payment or "Cash" gratuity.save() gratuity.submit() return gratuity + def set_mode_of_payment_account(): if not frappe.db.exists("Account", "Payment Account - _TC"): mode_of_payment = create_account() @@ -166,14 +185,15 @@ def set_mode_of_payment_account(): mode_of_payment = frappe.get_doc("Mode of Payment", "Cash") mode_of_payment.accounts = [] - mode_of_payment.append("accounts", { - "company": "_Test Company", - "default_account": "_Test Bank - _TC" - }) + mode_of_payment.append( + "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} + ) mode_of_payment.save() + def create_account(): - return frappe.get_doc({ + return frappe.get_doc( + { "doctype": "Account", "company": "_Test Company", "account_name": "Payment Account", @@ -182,13 +202,15 @@ def create_account(): "currency": "INR", "parent_account": "Bank Accounts - _TC", "account_type": "Bank", - }).insert(ignore_permissions=True) + } + ).insert(ignore_permissions=True) + def create_employee_and_get_last_salary_slip(): - employee = make_employee("test_employee@salary.com", company='_Test Company') + employee = make_employee("test_employee@salary.com", company="_Test Company") frappe.db.set_value("Employee", employee, "relieving_date", getdate()) - frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), - (6*365))) - if not frappe.db.exists("Salary Slip", {"employee":employee}): + frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365))) + if not frappe.db.exists("Salary Slip", {"employee": employee}): salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly") salary_slip.submit() salary_slip = salary_slip.name @@ -197,7 +219,10 @@ def create_employee_and_get_last_salary_slip(): if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"): from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + make_holiday_list() - frappe.db.set_value("Company", '_Test Company', "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value( + "Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List" + ) return employee, salary_slip diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py index d30cfc64848..5cde79a1627 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py @@ -8,25 +8,34 @@ from frappe.model.document import Document class GratuityRule(Document): - def validate(self): for current_slab in self.gratuity_rule_slabs: if (current_slab.from_year > current_slab.to_year) and current_slab.to_year != 0: - frappe.throw(_("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx)) + frappe.throw( + _("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx) + ) + + if ( + current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1 + ): + frappe.throw( + _("You can not define multiple slabs if you have a slab with no lower and upper limits.") + ) - if current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1: - frappe.throw(_("You can not define multiple slabs if you have a slab with no lower and upper limits.")) def get_gratuity_rule(name, slabs, **args): args = frappe._dict(args) rule = frappe.new_doc("Gratuity Rule") rule.name = name - rule.calculate_gratuity_amount_based_on = args.calculate_gratuity_amount_based_on or "Current Slab" - rule.work_experience_calculation_method = args.work_experience_calculation_method or "Take Exact Completed Years" + rule.calculate_gratuity_amount_based_on = ( + args.calculate_gratuity_amount_based_on or "Current Slab" + ) + rule.work_experience_calculation_method = ( + args.work_experience_calculation_method or "Take Exact Completed Years" + ) rule.minimum_year_for_gratuity = 1 - for slab in slabs: slab = frappe._dict(slab) rule.append("gratuity_rule_slabs", slab) diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py index 15e15d13620..fa5a9dedd35 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'gratuity_rule', - 'transactions': [ - { - 'label': _('Gratuity'), - 'items': ['Gratuity'] - } - ] + "fieldname": "gratuity_rule", + "transactions": [{"label": _("Gratuity"), "items": ["Gratuity"]}], } diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py index 040b2c89353..e62d61f4c2f 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -4,7 +4,7 @@ from frappe.model.document import Document -#import frappe +# import frappe import erpnext diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 4ef29848bc6..60d38f4ca49 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -28,11 +28,11 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee class PayrollEntry(Document): def onload(self): - if not self.docstatus==1 or self.salary_slips_submitted: + if not self.docstatus == 1 or self.salary_slips_submitted: return # check if salary slips were manually submitted - entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) + entries = frappe.db.count("Salary Slip", {"payroll_entry": self.name, "docstatus": 1}, ["name"]) if cint(entries) == len(self.employees): self.set_onload("submitted_ss", True) @@ -51,33 +51,51 @@ class PayrollEntry(Document): def validate_employee_details(self): emp_with_sal_slip = [] for employee_details in self.employees: - if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + if frappe.db.exists( + "Salary Slip", + { + "employee": employee_details.employee, + "start_date": self.start_date, + "end_date": self.end_date, + "docstatus": 1, + }, + ): emp_with_sal_slip.append(employee_details.employee) if len(emp_with_sal_slip): frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip))) def on_cancel(self): - frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` - where payroll_entry=%s """, (self.name))) + frappe.delete_doc( + "Salary Slip", + frappe.db.sql_list( + """select name from `tabSalary Slip` + where payroll_entry=%s """, + (self.name), + ), + ) self.db_set("salary_slips_created", 0) self.db_set("salary_slips_submitted", 0) def get_emp_list(self): """ - Returns list of active employees based on selected criteria - and for which salary structure exists + Returns list of active employees based on selected criteria + and for which salary structure exists """ self.check_mandatory() filters = self.make_filters() cond = get_filter_condition(filters) cond += get_joining_relieving_condition(self.start_date, self.end_date) - condition = '' + condition = "" if self.payroll_frequency: - condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency} + condition = """and payroll_frequency = '%(payroll_frequency)s'""" % { + "payroll_frequency": self.payroll_frequency + } - sal_struct = get_sal_struct(self.company, self.currency, self.salary_slip_based_on_timesheet, condition) + sal_struct = get_sal_struct( + self.company, self.currency, self.salary_slip_based_on_timesheet, condition + ) if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " @@ -88,20 +106,25 @@ class PayrollEntry(Document): def make_filters(self): filters = frappe._dict() - filters['company'] = self.company - filters['branch'] = self.branch - filters['department'] = self.department - filters['designation'] = self.designation + filters["company"] = self.company + filters["branch"] = self.branch + filters["department"] = self.department + filters["designation"] = self.designation return filters @frappe.whitelist() def fill_employee_details(self): - self.set('employees', []) + self.set("employees", []) employees = self.get_emp_list() if not employees: - error_msg = _("No employees found for the mentioned criteria:
Company: {0}
Currency: {1}
Payroll Payable Account: {2}").format( - frappe.bold(self.company), frappe.bold(self.currency), frappe.bold(self.payroll_payable_account)) + error_msg = _( + "No employees found for the mentioned criteria:
Company: {0}
Currency: {1}
Payroll Payable Account: {2}" + ).format( + frappe.bold(self.company), + frappe.bold(self.currency), + frappe.bold(self.payroll_payable_account), + ) if self.branch: error_msg += "
" + _("Branch: {0}").format(frappe.bold(self.branch)) if self.department: @@ -115,38 +138,40 @@ class PayrollEntry(Document): frappe.throw(error_msg, title=_("No employees found")) for d in employees: - self.append('employees', d) + self.append("employees", d) self.number_of_employees = len(self.employees) if self.validate_attendance: return self.validate_employee_attendance() def check_mandatory(self): - for fieldname in ['company', 'start_date', 'end_date']: + for fieldname in ["company", "start_date", "end_date"]: if not self.get(fieldname): frappe.throw(_("Please set {0}").format(self.meta.get_label(fieldname))) @frappe.whitelist() def create_salary_slips(self): """ - Creates salary slip for selected employees if already not created + Creates salary slip for selected employees if already not created """ - self.check_permission('write') + self.check_permission("write") employees = [emp.employee for emp in self.employees] if employees: - args = frappe._dict({ - "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, - "payroll_frequency": self.payroll_frequency, - "start_date": self.start_date, - "end_date": self.end_date, - "company": self.company, - "posting_date": self.posting_date, - "deduct_tax_for_unclaimed_employee_benefits": self.deduct_tax_for_unclaimed_employee_benefits, - "deduct_tax_for_unsubmitted_tax_exemption_proof": self.deduct_tax_for_unsubmitted_tax_exemption_proof, - "payroll_entry": self.name, - "exchange_rate": self.exchange_rate, - "currency": self.currency - }) + args = frappe._dict( + { + "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, + "payroll_frequency": self.payroll_frequency, + "start_date": self.start_date, + "end_date": self.end_date, + "company": self.company, + "posting_date": self.posting_date, + "deduct_tax_for_unclaimed_employee_benefits": self.deduct_tax_for_unclaimed_employee_benefits, + "deduct_tax_for_unsubmitted_tax_exemption_proof": self.deduct_tax_for_unsubmitted_tax_exemption_proof, + "payroll_entry": self.name, + "exchange_rate": self.exchange_rate, + "currency": self.currency, + } + ) if len(employees) > 30: frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args) else: @@ -156,22 +181,28 @@ class PayrollEntry(Document): def get_sal_slip_list(self, ss_status, as_dict=False): """ - Returns list of salary slips based on selected criteria + Returns list of salary slips based on selected criteria """ - ss_list = frappe.db.sql(""" + ss_list = frappe.db.sql( + """ select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1 where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s - """, (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict) + """, + (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), + as_dict=as_dict, + ) return ss_list @frappe.whitelist() def submit_salary_slips(self): - self.check_permission('write') + self.check_permission("write") ss_list = self.get_sal_slip_list(ss_status=0) if len(ss_list) > 30: - frappe.enqueue(submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list) + frappe.enqueue( + submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list + ) else: submit_salary_slips_for_employees(self, ss_list, publish_progress=False) @@ -181,44 +212,51 @@ class PayrollEntry(Document): ss.email_salary_slip() def get_salary_component_account(self, salary_component): - account = frappe.db.get_value("Salary Component Account", - {"parent": salary_component, "company": self.company}, "account") + account = frappe.db.get_value( + "Salary Component Account", {"parent": salary_component, "company": self.company}, "account" + ) if not account: - frappe.throw(_("Please set account in Salary Component {0}") - .format(salary_component)) + frappe.throw(_("Please set account in Salary Component {0}").format(salary_component)) return account def get_salary_components(self, component_type): - salary_slips = self.get_sal_slip_list(ss_status = 1, as_dict = True) + salary_slips = self.get_sal_slip_list(ss_status=1, as_dict=True) if salary_slips: - salary_components = frappe.db.sql(""" + salary_components = frappe.db.sql( + """ select ssd.salary_component, ssd.amount, ssd.parentfield, ss.payroll_cost_center from `tabSalary Slip` ss, `tabSalary Detail` ssd where ss.name = ssd.parent and ssd.parentfield = '%s' and ss.name in (%s) - """ % (component_type, ', '.join(['%s']*len(salary_slips))), - tuple([d.name for d in salary_slips]), as_dict=True) + """ + % (component_type, ", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=True, + ) return salary_components - def get_salary_component_total(self, component_type = None): + def get_salary_component_total(self, component_type=None): salary_components = self.get_salary_components(component_type) if salary_components: component_dict = {} for item in salary_components: add_component_to_accrual_jv_entry = True if component_type == "earnings": - is_flexible_benefit, only_tax_impact = frappe.db.get_value("Salary Component", item['salary_component'], ['is_flexible_benefit', 'only_tax_impact']) - if is_flexible_benefit == 1 and only_tax_impact ==1: + is_flexible_benefit, only_tax_impact = frappe.db.get_value( + "Salary Component", item["salary_component"], ["is_flexible_benefit", "only_tax_impact"] + ) + if is_flexible_benefit == 1 and only_tax_impact == 1: add_component_to_accrual_jv_entry = False if add_component_to_accrual_jv_entry: - component_dict[(item.salary_component, item.payroll_cost_center)] \ - = component_dict.get((item.salary_component, item.payroll_cost_center), 0) + flt(item.amount) - account_details = self.get_account(component_dict = component_dict) + component_dict[(item.salary_component, item.payroll_cost_center)] = component_dict.get( + (item.salary_component, item.payroll_cost_center), 0 + ) + flt(item.amount) + account_details = self.get_account(component_dict=component_dict) return account_details - def get_account(self, component_dict = None): + def get_account(self, component_dict=None): account_dict = {} for key, amount in component_dict.items(): account = self.get_salary_component_account(key[0]) @@ -227,8 +265,8 @@ class PayrollEntry(Document): def make_accrual_jv_entry(self): self.check_permission("write") - earnings = self.get_salary_component_total(component_type = "earnings") or {} - deductions = self.get_salary_component_total(component_type = "deductions") or {} + earnings = self.get_salary_component_total(component_type="earnings") or {} + deductions = self.get_salary_component_total(component_type="deductions") or {} payroll_payable_account = self.payroll_payable_account jv_name = "" precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") @@ -236,8 +274,9 @@ class PayrollEntry(Document): if earnings or deductions: journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Journal Entry" - journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\ - .format(self.start_date, self.end_date) + journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}").format( + self.start_date, self.end_date + ) journal_entry.company = self.company journal_entry.posting_date = self.posting_date accounting_dimensions = get_accounting_dimensions() or [] @@ -250,36 +289,57 @@ class PayrollEntry(Document): # Earnings for acc_cc, amount in earnings.items(): - exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) + exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry( + acc_cc[0], amount, company_currency, currencies + ) payable_amount += flt(amount, precision) - accounts.append(self.update_accounting_dimensions({ - "account": acc_cc[0], - "debit_in_account_currency": flt(amt, precision), - "exchange_rate": flt(exchange_rate), - "cost_center": acc_cc[1] or self.cost_center, - "project": self.project - }, accounting_dimensions)) + accounts.append( + self.update_accounting_dimensions( + { + "account": acc_cc[0], + "debit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), + "cost_center": acc_cc[1] or self.cost_center, + "project": self.project, + }, + accounting_dimensions, + ) + ) # Deductions for acc_cc, amount in deductions.items(): - exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) + exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry( + acc_cc[0], amount, company_currency, currencies + ) payable_amount -= flt(amount, precision) - accounts.append(self.update_accounting_dimensions({ - "account": acc_cc[0], - "credit_in_account_currency": flt(amt, precision), - "exchange_rate": flt(exchange_rate), - "cost_center": acc_cc[1] or self.cost_center, - "project": self.project - }, accounting_dimensions)) + accounts.append( + self.update_accounting_dimensions( + { + "account": acc_cc[0], + "credit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), + "cost_center": acc_cc[1] or self.cost_center, + "project": self.project, + }, + accounting_dimensions, + ) + ) # Payable amount - exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) - accounts.append(self.update_accounting_dimensions({ - "account": payroll_payable_account, - "credit_in_account_currency": flt(payable_amt, precision), - "exchange_rate": flt(exchange_rate), - "cost_center": self.cost_center - }, accounting_dimensions)) + exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry( + payroll_payable_account, payable_amount, company_currency, currencies + ) + accounts.append( + self.update_accounting_dimensions( + { + "account": payroll_payable_account, + "credit_in_account_currency": flt(payable_amt, precision), + "exchange_rate": flt(exchange_rate), + "cost_center": self.cost_center, + }, + accounting_dimensions, + ) + ) journal_entry.set("accounts", accounts) if len(currencies) > 1: @@ -291,7 +351,7 @@ class PayrollEntry(Document): try: journal_entry.submit() jv_name = journal_entry.name - self.update_salary_slip_status(jv_name = jv_name) + self.update_salary_slip_status(jv_name=jv_name) except Exception as e: if type(e) in (str, list, tuple): frappe.msgprint(e) @@ -305,10 +365,12 @@ class PayrollEntry(Document): return row - def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): + def get_amount_and_exchange_rate_for_journal_entry( + self, account, amount, company_currency, currencies + ): conversion_rate = 1 exchange_rate = self.exchange_rate - account_currency = frappe.db.get_value('Account', account, 'account_currency') + account_currency = frappe.db.get_value("Account", account, "account_currency") if account_currency not in currencies: currencies.append(account_currency) if account_currency == company_currency: @@ -319,26 +381,45 @@ class PayrollEntry(Document): @frappe.whitelist() def make_payment_entry(self): - self.check_permission('write') + self.check_permission("write") - salary_slip_name_list = frappe.db.sql(""" select t1.name from `tabSalary Slip` t1 + salary_slip_name_list = frappe.db.sql( + """ select t1.name from `tabSalary Slip` t1 where t1.docstatus = 1 and start_date >= %s and end_date <= %s and t1.payroll_entry = %s - """, (self.start_date, self.end_date, self.name), as_list = True) + """, + (self.start_date, self.end_date, self.name), + as_list=True, + ) if salary_slip_name_list and len(salary_slip_name_list) > 0: salary_slip_total = 0 for salary_slip_name in salary_slip_name_list: salary_slip = frappe.get_doc("Salary Slip", salary_slip_name[0]) for sal_detail in salary_slip.earnings: - is_flexible_benefit, only_tax_impact, creat_separate_je, statistical_component = frappe.db.get_value("Salary Component", sal_detail.salary_component, - ['is_flexible_benefit', 'only_tax_impact', 'create_separate_payment_entry_against_benefit_claim', 'statistical_component']) + ( + is_flexible_benefit, + only_tax_impact, + creat_separate_je, + statistical_component, + ) = frappe.db.get_value( + "Salary Component", + sal_detail.salary_component, + [ + "is_flexible_benefit", + "only_tax_impact", + "create_separate_payment_entry_against_benefit_claim", + "statistical_component", + ], + ) if only_tax_impact != 1 and statistical_component != 1: if is_flexible_benefit == 1 and creat_separate_je == 1: self.create_journal_entry(sal_detail.amount, sal_detail.salary_component) else: salary_slip_total += sal_detail.amount for sal_detail in salary_slip.deductions: - statistical_component = frappe.db.get_value("Salary Component", sal_detail.salary_component, 'statistical_component') + statistical_component = frappe.db.get_value( + "Salary Component", sal_detail.salary_component, "statistical_component" + ) if statistical_component != 1: salary_slip_total -= sal_detail.amount if salary_slip_total > 0: @@ -354,91 +435,114 @@ class PayrollEntry(Document): company_currency = erpnext.get_company_currency(self.company) accounting_dimensions = get_accounting_dimensions() or [] - exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies) - accounts.append(self.update_accounting_dimensions({ - "account": self.payment_account, - "bank_account": self.bank_account, - "credit_in_account_currency": flt(amount, precision), - "exchange_rate": flt(exchange_rate), - }, accounting_dimensions)) + exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry( + self.payment_account, je_payment_amount, company_currency, currencies + ) + accounts.append( + self.update_accounting_dimensions( + { + "account": self.payment_account, + "bank_account": self.bank_account, + "credit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + }, + accounting_dimensions, + ) + ) - exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies) - accounts.append(self.update_accounting_dimensions({ - "account": payroll_payable_account, - "debit_in_account_currency": flt(amount, precision), - "exchange_rate": flt(exchange_rate), - "reference_type": self.doctype, - "reference_name": self.name - }, accounting_dimensions)) + exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry( + payroll_payable_account, je_payment_amount, company_currency, currencies + ) + accounts.append( + self.update_accounting_dimensions( + { + "account": payroll_payable_account, + "debit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + "reference_type": self.doctype, + "reference_name": self.name, + }, + accounting_dimensions, + ) + ) if len(currencies) > 1: - multi_currency = 1 + multi_currency = 1 - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Bank Entry' - journal_entry.user_remark = _('Payment of {0} from {1} to {2}')\ - .format(user_remark, self.start_date, self.end_date) + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Bank Entry" + journal_entry.user_remark = _("Payment of {0} from {1} to {2}").format( + user_remark, self.start_date, self.end_date + ) journal_entry.company = self.company journal_entry.posting_date = self.posting_date journal_entry.multi_currency = multi_currency journal_entry.set("accounts", accounts) - journal_entry.save(ignore_permissions = True) + journal_entry.save(ignore_permissions=True) - def update_salary_slip_status(self, jv_name = None): + def update_salary_slip_status(self, jv_name=None): ss_list = self.get_sal_slip_list(ss_status=1) for ss in ss_list: - ss_obj = frappe.get_doc("Salary Slip",ss[0]) + ss_obj = frappe.get_doc("Salary Slip", ss[0]) frappe.db.set_value("Salary Slip", ss_obj.name, "journal_entry", jv_name) def set_start_end_dates(self): - self.update(get_start_end_dates(self.payroll_frequency, - self.start_date or self.posting_date, self.company)) + self.update( + get_start_end_dates(self.payroll_frequency, self.start_date or self.posting_date, self.company) + ) @frappe.whitelist() def validate_employee_attendance(self): employees_to_mark_attendance = [] days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0 for employee_detail in self.employees: - employee_joining_date = frappe.db.get_value("Employee", employee_detail.employee, 'date_of_joining') + employee_joining_date = frappe.db.get_value( + "Employee", employee_detail.employee, "date_of_joining" + ) start_date = self.start_date if employee_joining_date > getdate(self.start_date): start_date = employee_joining_date days_holiday = self.get_count_holidays_of_employee(employee_detail.employee, start_date) - days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee, start_date) + days_attendance_marked = self.get_count_employee_attendance( + employee_detail.employee, start_date + ) days_in_payroll = date_diff(self.end_date, start_date) + 1 if days_in_payroll > days_holiday + days_attendance_marked: - employees_to_mark_attendance.append({ - "employee": employee_detail.employee, - "employee_name": employee_detail.employee_name - }) + employees_to_mark_attendance.append( + {"employee": employee_detail.employee, "employee_name": employee_detail.employee_name} + ) return employees_to_mark_attendance def get_count_holidays_of_employee(self, employee, start_date): holiday_list = get_holiday_list_for_employee(employee) holidays = 0 if holiday_list: - days = frappe.db.sql("""select count(*) from tabHoliday where - parent=%s and holiday_date between %s and %s""", (holiday_list, - start_date, self.end_date)) + days = frappe.db.sql( + """select count(*) from tabHoliday where + parent=%s and holiday_date between %s and %s""", + (holiday_list, start_date, self.end_date), + ) if days and days[0][0]: holidays = days[0][0] return holidays def get_count_employee_attendance(self, employee, start_date): marked_days = 0 - attendances = frappe.get_all("Attendance", - fields = ["count(*)"], - filters = { - "employee": employee, - "attendance_date": ('between', [start_date, self.end_date]) - }, as_list=1) + attendances = frappe.get_all( + "Attendance", + fields=["count(*)"], + filters={"employee": employee, "attendance_date": ("between", [start_date, self.end_date])}, + as_list=1, + ) if attendances and attendances[0][0]: marked_days = attendances[0][0] return marked_days + def get_sal_struct(company, currency, salary_slip_based_on_timesheet, condition): - return frappe.db.sql_list(""" + return frappe.db.sql_list( + """ select name from `tabSalary Structure` where @@ -447,26 +551,40 @@ def get_sal_struct(company, currency, salary_slip_based_on_timesheet, condition) and company = %(company)s and currency = %(currency)s and ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s - {condition}""".format(condition=condition), - {"company": company, "currency": currency, "salary_slip_based_on_timesheet": salary_slip_based_on_timesheet}) + {condition}""".format( + condition=condition + ), + { + "company": company, + "currency": currency, + "salary_slip_based_on_timesheet": salary_slip_based_on_timesheet, + }, + ) + def get_filter_condition(filters): - cond = '' - for f in ['company', 'branch', 'department', 'designation']: + cond = "" + for f in ["company", "branch", "department", "designation"]: if filters.get(f): cond += " and t1." + f + " = " + frappe.db.escape(filters.get(f)) return cond + def get_joining_relieving_condition(start_date, end_date): cond = """ and ifnull(t1.date_of_joining, '0000-00-00') <= '%(end_date)s' and ifnull(t1.relieving_date, '2199-12-31') >= '%(start_date)s' - """ % {"start_date": start_date, "end_date": end_date} + """ % { + "start_date": start_date, + "end_date": end_date, + } return cond + def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): - return frappe.db.sql(""" + return frappe.db.sql( + """ select distinct t1.name as employee, t1.employee_name, t1.department, t1.designation from @@ -476,19 +594,37 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): and t2.docstatus = 1 and t1.status != 'Inactive' %s order by t2.from_date desc - """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) + """ + % cond, + { + "sal_struct": tuple(sal_struct), + "from_date": end_date, + "payroll_payable_account": payroll_payable_account, + }, + as_dict=True, + ) + def remove_payrolled_employees(emp_list, start_date, end_date): new_emp_list = [] for employee_details in emp_list: - if not frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}): + if not frappe.db.exists( + "Salary Slip", + { + "employee": employee_details.employee, + "start_date": start_date, + "end_date": end_date, + "docstatus": 1, + }, + ): new_emp_list.append(employee_details) return new_emp_list + @frappe.whitelist() def get_start_end_dates(payroll_frequency, start_date=None, company=None): - '''Returns dict of start and end dates for given payroll frequency based on start_date''' + """Returns dict of start and end dates for given payroll frequency based on start_date""" if payroll_frequency == "Monthly" or payroll_frequency == "Bimonthly" or payroll_frequency == "": fiscal_year = get_fiscal_year(start_date, company=company)[0] @@ -496,14 +632,14 @@ def get_start_end_dates(payroll_frequency, start_date=None, company=None): m = get_month_details(fiscal_year, month) if payroll_frequency == "Bimonthly": if getdate(start_date).day <= 15: - start_date = m['month_start_date'] - end_date = m['month_mid_end_date'] + start_date = m["month_start_date"] + end_date = m["month_mid_end_date"] else: - start_date = m['month_mid_start_date'] - end_date = m['month_end_date'] + start_date = m["month_mid_start_date"] + end_date = m["month_end_date"] else: - start_date = m['month_start_date'] - end_date = m['month_end_date'] + start_date = m["month_start_date"] + end_date = m["month_end_date"] if payroll_frequency == "Weekly": end_date = add_days(start_date, 6) @@ -514,16 +650,15 @@ def get_start_end_dates(payroll_frequency, start_date=None, company=None): if payroll_frequency == "Daily": end_date = start_date - return frappe._dict({ - 'start_date': start_date, 'end_date': end_date - }) + return frappe._dict({"start_date": start_date, "end_date": end_date}) + def get_frequency_kwargs(frequency_name): frequency_dict = { - 'monthly': {'months': 1}, - 'fortnightly': {'days': 14}, - 'weekly': {'days': 7}, - 'daily': {'days': 1} + "monthly": {"months": 1}, + "fortnightly": {"days": 14}, + "weekly": {"days": 7}, + "daily": {"days": 1}, } return frequency_dict.get(frequency_name) @@ -531,16 +666,18 @@ def get_frequency_kwargs(frequency_name): @frappe.whitelist() def get_end_date(start_date, frequency): start_date = getdate(start_date) - frequency = frequency.lower() if frequency else 'monthly' - kwargs = get_frequency_kwargs(frequency) if frequency != 'bimonthly' else get_frequency_kwargs('monthly') + frequency = frequency.lower() if frequency else "monthly" + kwargs = ( + get_frequency_kwargs(frequency) if frequency != "bimonthly" else get_frequency_kwargs("monthly") + ) # weekly, fortnightly and daily intervals have fixed days so no problems end_date = add_to_date(start_date, **kwargs) - relativedelta(days=1) - if frequency != 'bimonthly': + if frequency != "bimonthly": return dict(end_date=end_date.strftime(DATE_FORMAT)) else: - return dict(end_date='') + return dict(end_date="") def get_month_details(year, month): @@ -548,32 +685,36 @@ def get_month_details(year, month): if ysd: import calendar import datetime - diff_mnt = cint(month)-cint(ysd.month) - if diff_mnt<0: - diff_mnt = 12-int(ysd.month)+cint(month) - msd = ysd + relativedelta(months=diff_mnt) # month start date - month_days = cint(calendar.monthrange(cint(msd.year) ,cint(month))[1]) # days in month - mid_start = datetime.date(msd.year, cint(month), 16) # month mid start date - mid_end = datetime.date(msd.year, cint(month), 15) # month mid end date - med = datetime.date(msd.year, cint(month), month_days) # month end date - return frappe._dict({ - 'year': msd.year, - 'month_start_date': msd, - 'month_end_date': med, - 'month_mid_start_date': mid_start, - 'month_mid_end_date': mid_end, - 'month_days': month_days - }) + + diff_mnt = cint(month) - cint(ysd.month) + if diff_mnt < 0: + diff_mnt = 12 - int(ysd.month) + cint(month) + msd = ysd + relativedelta(months=diff_mnt) # month start date + month_days = cint(calendar.monthrange(cint(msd.year), cint(month))[1]) # days in month + mid_start = datetime.date(msd.year, cint(month), 16) # month mid start date + mid_end = datetime.date(msd.year, cint(month), 15) # month mid end date + med = datetime.date(msd.year, cint(month), month_days) # month end date + return frappe._dict( + { + "year": msd.year, + "month_start_date": msd, + "month_end_date": med, + "month_mid_start_date": mid_start, + "month_mid_end_date": mid_end, + "month_days": month_days, + } + ) else: frappe.throw(_("Fiscal Year {0} not found").format(year)) + def get_payroll_entry_bank_entries(payroll_entry_name): journal_entries = frappe.db.sql( - 'select name from `tabJournal Entry Account` ' + "select name from `tabJournal Entry Account` " 'where reference_type="Payroll Entry" ' - 'and reference_name=%s and docstatus=1', + "and reference_name=%s and docstatus=1", payroll_entry_name, - as_dict=1 + as_dict=1, ) return journal_entries @@ -583,26 +724,26 @@ def get_payroll_entry_bank_entries(payroll_entry_name): def payroll_entry_has_bank_entries(name): response = {} bank_entries = get_payroll_entry_bank_entries(name) - response['submitted'] = 1 if bank_entries else 0 + response["submitted"] = 1 if bank_entries else 0 return response + def create_salary_slips_for_employees(employees, args, publish_progress=True): salary_slips_exists_for = get_existing_salary_slips(employees, args) - count=0 + count = 0 salary_slips_not_created = [] for emp in employees: if emp not in salary_slips_exists_for: - args.update({ - "doctype": "Salary Slip", - "employee": emp - }) + args.update({"doctype": "Salary Slip", "employee": emp}) ss = frappe.get_doc(args) ss.insert() - count+=1 + count += 1 if publish_progress: - frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)), - title = _("Creating Salary Slips...")) + frappe.publish_progress( + count * 100 / len(set(employees) - set(salary_slips_exists_for)), + title=_("Creating Salary Slips..."), + ) else: salary_slips_not_created.append(emp) @@ -612,17 +753,27 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): payroll_entry.notify_update() if salary_slips_not_created: - frappe.msgprint(_("Salary Slips already exists for employees {}, and will not be processed by this payroll.") - .format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))) , title=_("Message"), indicator="orange") + frappe.msgprint( + _( + "Salary Slips already exists for employees {}, and will not be processed by this payroll." + ).format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))), + title=_("Message"), + indicator="orange", + ) + def get_existing_salary_slips(employees, args): - return frappe.db.sql_list(""" + return frappe.db.sql_list( + """ select distinct employee from `tabSalary Slip` where docstatus!= 2 and company = %s and payroll_entry = %s and start_date >= %s and end_date <= %s and employee in (%s) - """ % ('%s', '%s', '%s', '%s', ', '.join(['%s']*len(employees))), - [args.company, args.payroll_entry, args.start_date, args.end_date] + employees) + """ + % ("%s", "%s", "%s", "%s", ", ".join(["%s"] * len(employees))), + [args.company, args.payroll_entry, args.start_date, args.end_date] + employees, + ) + def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True): submitted_ss = [] @@ -631,8 +782,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr count = 0 for ss in salary_slips: - ss_obj = frappe.get_doc("Salary Slip",ss[0]) - if ss_obj.net_pay<0: + ss_obj = frappe.get_doc("Salary Slip", ss[0]) + if ss_obj.net_pay < 0: not_submitted_ss.append(ss[0]) else: try: @@ -643,11 +794,12 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr count += 1 if publish_progress: - frappe.publish_progress(count*100/len(salary_slips), title = _("Submitting Salary Slips...")) + frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips...")) if submitted_ss: payroll_entry.make_accrual_jv_entry() - frappe.msgprint(_("Salary Slip submitted for period from {0} to {1}") - .format(ss_obj.start_date, ss_obj.end_date)) + frappe.msgprint( + _("Salary Slip submitted for period from {0} to {1}").format(ss_obj.start_date, ss_obj.end_date) + ) payroll_entry.email_salary_slip(submitted_ss) @@ -655,31 +807,44 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr payroll_entry.notify_update() if not submitted_ss and not not_submitted_ss: - frappe.msgprint(_("No salary slip found to submit for the above selected criteria OR salary slip already submitted")) + frappe.msgprint( + _( + "No salary slip found to submit for the above selected criteria OR salary slip already submitted" + ) + ) if not_submitted_ss: frappe.msgprint(_("Could not submit some Salary Slips")) + frappe.flags.via_payroll_entry = False + + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ select name from `tabPayroll Entry` where `{key}` LIKE %(txt)s and name not in (select reference_name from `tabJournal Entry Account` where reference_type="Payroll Entry") - order by name limit %(start)s, %(page_len)s""" - .format(key=searchfield), { - 'txt': "%%%s%%" % frappe.db.escape(txt), - 'start': start, 'page_len': page_len - }) + order by name limit %(start)s, %(page_len)s""".format( + key=searchfield + ), + {"txt": "%%%s%%" % txt, "start": start, "page_len": page_len}, + ) + def get_employee_list(filters): cond = get_filter_condition(filters) cond += get_joining_relieving_condition(filters.start_date, filters.end_date) - condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": filters.payroll_frequency} - sal_struct = get_sal_struct(filters.company, filters.currency, filters.salary_slip_based_on_timesheet, condition) + condition = """and payroll_frequency = '%(payroll_frequency)s'""" % { + "payroll_frequency": filters.payroll_frequency + } + sal_struct = get_sal_struct( + filters.company, filters.currency, filters.salary_slip_based_on_timesheet, condition + ) if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " @@ -690,34 +855,38 @@ def get_employee_list(filters): return [] + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def employee_query(doctype, txt, searchfield, start, page_len, filters): filters = frappe._dict(filters) conditions = [] include_employees = [] - emp_cond = '' + emp_cond = "" if not filters.payroll_frequency: - frappe.throw(_('Select Payroll Frequency.')) + frappe.throw(_("Select Payroll Frequency.")) if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) - emp = filters.get('employees') or [] - include_employees = [employee.employee for employee in employee_list if employee.employee not in emp] - filters.pop('start_date') - filters.pop('end_date') - filters.pop('salary_slip_based_on_timesheet') - filters.pop('payroll_frequency') - filters.pop('payroll_payable_account') - filters.pop('currency') + emp = filters.get("employees") or [] + include_employees = [ + employee.employee for employee in employee_list if employee.employee not in emp + ] + filters.pop("start_date") + filters.pop("end_date") + filters.pop("salary_slip_based_on_timesheet") + filters.pop("payroll_frequency") + filters.pop("payroll_payable_account") + filters.pop("currency") if filters.employees is not None: - filters.pop('employees') + filters.pop("employees") if include_employees: - emp_cond += 'and employee in %(include_employees)s' + emp_cond += "and employee in %(include_employees)s" - return frappe.db.sql("""select name, employee_name from `tabEmployee` + return frappe.db.sql( + """select name, employee_name from `tabEmployee` where status = 'Active' and docstatus < 2 and ({key} like %(txt)s @@ -729,14 +898,19 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), idx desc, name, employee_name - limit %(start)s, %(page_len)s""".format(**{ - 'key': searchfield, - 'fcond': get_filters_cond(doctype, filters, conditions), - 'mcond': get_match_cond(doctype), - 'emp_cond': emp_cond - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len, - 'include_employees': include_employees}) + limit %(start)s, %(page_len)s""".format( + **{ + "key": searchfield, + "fcond": get_filters_cond(doctype, filters, conditions), + "mcond": get_match_cond(doctype), + "emp_cond": emp_cond, + } + ), + { + "txt": "%%%s%%" % txt, + "_txt": txt.replace("%", ""), + "start": start, + "page_len": page_len, + "include_employees": include_employees, + }, + ) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py index 138fed68f4c..eb93d688f92 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py @@ -1,15 +1,9 @@ - - def get_data(): return { - 'fieldname': 'payroll_entry', - 'non_standard_fieldnames': { - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', + "fieldname": "payroll_entry", + "non_standard_fieldnames": { + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", }, - 'transactions': [ - { - 'items': ['Salary Slip', 'Journal Entry'] - } - ] + "transactions": [{"items": ["Salary Slip", "Journal Entry"]}], } diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 5eab1424811..c0932c951bb 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -32,139 +32,224 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( make_salary_structure, ) -test_dependencies = ['Holiday List'] +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') + 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"]: - frappe.db.sql("delete from `tab%s`" % dt) + for dt in [ + "Salary Slip", + "Salary Component", + "Salary Component Account", + "Payroll Entry", + "Salary Structure", + "Salary Structure Assignment", + "Payroll Employee Detail", + "Additional Salary", + ]: + frappe.db.sql("delete from `tab%s`" % dt) make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"]) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) - def test_payroll_entry(self): # pylint: disable=no-self-use + def test_payroll_entry(self): # pylint: disable=no-self-use company = erpnext.get_default_company() - for data in frappe.get_all('Salary Component', fields = ["name"]): - if not frappe.db.get_value('Salary Component Account', - {'parent': data.name, 'company': company}, 'name'): + for data in frappe.get_all("Salary Component", fields=["name"]): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data.name, "company": company}, "name" + ): get_salary_component_account(data.name) - employee = frappe.db.get_value("Employee", {'company': company}) - company_doc = frappe.get_doc('Company', company) - make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company, currency=company_doc.default_currency) - dates = get_start_end_dates('Monthly', nowdate()) - if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, - currency=company_doc.default_currency) + employee = frappe.db.get_value("Employee", {"company": company}) + company_doc = frappe.get_doc("Company", company) + make_salary_structure( + "_Test Salary Structure", + "Monthly", + employee, + company=company, + currency=company_doc.default_currency, + ) + dates = get_start_end_dates("Monthly", nowdate()) + if not frappe.db.get_value( + "Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date} + ): + make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, + ) - def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use + def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use company = erpnext.get_default_company() employee = make_employee("test_muti_currency_employee@payroll.com", company=company) - for data in frappe.get_all('Salary Component', fields = ["name"]): - if not frappe.db.get_value('Salary Component Account', - {'parent': data.name, 'company': company}, 'name'): + for data in frappe.get_all("Salary Component", fields=["name"]): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data.name, "company": company}, "name" + ): get_salary_component_account(data.name) - company_doc = frappe.get_doc('Company', company) - salary_structure = make_salary_structure("_Test Multi Currency Salary Structure", "Monthly", company=company, currency='USD') - create_salary_structure_assignment(employee, salary_structure.name, company=company, currency='USD') - frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""",(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"}))) - salary_slip = get_salary_slip("test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure") - dates = get_start_end_dates('Monthly', nowdate()) - payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, - payable_account=company_doc.default_payroll_payable_account, currency='USD', exchange_rate=70) + company_doc = frappe.get_doc("Company", company) + salary_structure = make_salary_structure( + "_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD" + ) + create_salary_structure_assignment( + employee, salary_structure.name, company=company, currency="USD" + ) + frappe.db.sql( + """delete from `tabSalary Slip` where employee=%s""", + (frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})), + ) + salary_slip = get_salary_slip( + "test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure" + ) + dates = get_start_end_dates("Monthly", nowdate()) + payroll_entry = make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, + currency="USD", + exchange_rate=70, + ) payroll_entry.make_payment_entry() salary_slip.load_from_db() payroll_je = salary_slip.journal_entry if payroll_je: - payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je) + payroll_je_doc = frappe.get_doc("Journal Entry", payroll_je) self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit) self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit) - payment_entry = frappe.db.sql(''' + payment_entry = frappe.db.sql( + """ Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea Where je.name = jea.parent And jea.reference_name = %s - ''', (payroll_entry.name), as_dict=1) + """, + (payroll_entry.name), + as_dict=1, + ) self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit) self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit) - def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use - for data in frappe.get_all('Salary Component', fields = ["name"]): - if not frappe.db.get_value('Salary Component Account', - {'parent': data.name, 'company': "_Test Company"}, 'name'): + def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use + for data in frappe.get_all("Salary Component", fields=["name"]): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name" + ): get_salary_component_account(data.name) - if not frappe.db.exists('Department', "cc - _TC"): - frappe.get_doc({ - 'doctype': 'Department', - 'department_name': "cc", - "company": "_Test Company" - }).insert() + if not frappe.db.exists("Department", "cc - _TC"): + frappe.get_doc( + {"doctype": "Department", "department_name": "cc", "company": "_Test Company"} + ).insert() frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """) frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """) frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """) frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """) - employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC", - department="cc - _TC", company="_Test Company") - employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC", - department="cc - _TC", company="_Test Company") + employee1 = make_employee( + "test_employee1@example.com", + payroll_cost_center="_Test Cost Center - _TC", + department="cc - _TC", + company="_Test Company", + ) + employee2 = make_employee( + "test_employee2@example.com", + payroll_cost_center="_Test Cost Center 2 - _TC", + department="cc - _TC", + company="_Test Company", + ) if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): - create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC", account_type="Payable") + create_account( + account_name="_Test Payroll Payable", + company="_Test Company", + parent_account="Current Liabilities - _TC", + account_type="Payable", + ) - if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ - frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": - frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", - "_Test Payroll Payable - _TC") - currency=frappe.db.get_value("Company", "_Test Company", "default_currency") - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) + if ( + not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") + or frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") + != "_Test Payroll Payable - _TC" + ): + frappe.db.set_value( + "Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC" + ) + currency = frappe.db.get_value("Company", "_Test Company", "default_currency") + make_salary_structure( + "_Test Salary Structure 1", + "Monthly", + employee1, + company="_Test Company", + currency=currency, + test_tax=False, + ) + make_salary_structure( + "_Test Salary Structure 2", + "Monthly", + employee2, + company="_Test Company", + currency=currency, + test_tax=False, + ) - dates = get_start_end_dates('Monthly', nowdate()) - if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account="_Test Payroll Payable - _TC", - currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") + dates = get_start_end_dates("Monthly", nowdate()) + if not frappe.db.get_value( + "Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date} + ): + pe = make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account="_Test Payroll Payable - _TC", + currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), + department="cc - _TC", + company="_Test Company", + payment_account="Cash - _TC", + cost_center="Main - _TC", + ) je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry") - je_entries = frappe.db.sql(""" + je_entries = frappe.db.sql( + """ select account, cost_center, debit, credit from `tabJournal Entry Account` where parent=%s order by account, cost_center - """, je) + """, + je, + ) expected_je = ( - ('_Test Payroll Payable - _TC', 'Main - _TC', 0.0, 155600.0), - ('Salary - _TC', '_Test Cost Center - _TC', 78000.0, 0.0), - ('Salary - _TC', '_Test Cost Center 2 - _TC', 78000.0, 0.0), - ('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 200.0), - ('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 200.0) + ("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0), + ("Salary - _TC", "_Test Cost Center - _TC", 78000.0, 0.0), + ("Salary - _TC", "_Test Cost Center 2 - _TC", 78000.0, 0.0), + ("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 200.0), + ("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 200.0), ) self.assertEqual(je_entries, expected_je) def test_get_end_date(self): - self.assertEqual(get_end_date('2017-01-01', 'monthly'), {'end_date': '2017-01-31'}) - self.assertEqual(get_end_date('2017-02-01', 'monthly'), {'end_date': '2017-02-28'}) - self.assertEqual(get_end_date('2017-02-01', 'fortnightly'), {'end_date': '2017-02-14'}) - self.assertEqual(get_end_date('2017-02-01', 'bimonthly'), {'end_date': ''}) - self.assertEqual(get_end_date('2017-01-01', 'bimonthly'), {'end_date': ''}) - self.assertEqual(get_end_date('2020-02-15', 'bimonthly'), {'end_date': ''}) - self.assertEqual(get_end_date('2017-02-15', 'monthly'), {'end_date': '2017-03-14'}) - self.assertEqual(get_end_date('2017-02-15', 'daily'), {'end_date': '2017-02-15'}) + self.assertEqual(get_end_date("2017-01-01", "monthly"), {"end_date": "2017-01-31"}) + self.assertEqual(get_end_date("2017-02-01", "monthly"), {"end_date": "2017-02-28"}) + self.assertEqual(get_end_date("2017-02-01", "fortnightly"), {"end_date": "2017-02-14"}) + self.assertEqual(get_end_date("2017-02-01", "bimonthly"), {"end_date": ""}) + self.assertEqual(get_end_date("2017-01-01", "bimonthly"), {"end_date": ""}) + self.assertEqual(get_end_date("2020-02-15", "bimonthly"), {"end_date": ""}) + self.assertEqual(get_end_date("2017-02-15", "monthly"), {"end_date": "2017-03-14"}) + self.assertEqual(get_end_date("2017-02-15", "daily"), {"end_date": "2017-02-15"}) def test_loan(self): branch = "Test Employee Branch" @@ -172,63 +257,88 @@ class TestPayrollEntry(unittest.TestCase): company = "_Test Company" holiday_list = make_holiday("test holiday for loan") - company_doc = frappe.get_doc('Company', company) + company_doc = frappe.get_doc("Company", company) if not company_doc.default_payroll_payable_account: - company_doc.default_payroll_payable_account = frappe.db.get_value('Account', - {'company': company, 'root_type': 'Liability', 'account_type': ''}, 'name') + company_doc.default_payroll_payable_account = frappe.db.get_value( + "Account", {"company": company, "root_type": "Liability", "account_type": ""}, "name" + ) company_doc.save() - if not frappe.db.exists('Branch', branch): - frappe.get_doc({ - 'doctype': 'Branch', - 'branch': branch - }).insert() + if not frappe.db.exists("Branch", branch): + frappe.get_doc({"doctype": "Branch", "branch": branch}).insert() - employee_doc = frappe.get_doc('Employee', applicant) + employee_doc = frappe.get_doc("Employee", applicant) employee_doc.branch = branch employee_doc.holiday_list = holiday_list employee_doc.save() salary_structure = "Test Salary Structure for Loan" - make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company", currency=company_doc.default_currency) + make_salary_structure( + salary_structure, + "Monthly", + employee=employee_doc.name, + company="_Test Company", + currency=company_doc.default_currency, + ) if not frappe.db.exists("Loan Type", "Car Loan"): create_loan_accounts() - create_loan_type("Car Loan", 500000, 8.4, + create_loan_type( + "Car Loan", + 500000, + 8.4, is_term_loan=1, - mode_of_payment='Cash', - disbursement_account='Disbursement Account - _TC', - payment_account='Payment Account - _TC', - loan_account='Loan Account - _TC', - interest_income_account='Interest Income Account - _TC', - penalty_income_account='Penalty Income Account - _TC') + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + ) - loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) + loan = create_loan( + applicant, + "Car Loan", + 280000, + "Repay Over Number of Periods", + 20, + posting_date=add_months(nowdate(), -1), + ) loan.repay_from_salary = 1 loan.submit() - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + make_loan_disbursement_entry( + loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1) + ) process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - dates = get_start_end_dates('Monthly', nowdate()) - make_payroll_entry(company="_Test Company", start_date=dates.start_date, payable_account=company_doc.default_payroll_payable_account, - currency=company_doc.default_currency, end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") + dates = get_start_end_dates("Monthly", nowdate()) + make_payroll_entry( + company="_Test Company", + start_date=dates.start_date, + payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, + end_date=dates.end_date, + branch=branch, + cost_center="Main - _TC", + payment_account="Cash - _TC", + ) - name = frappe.db.get_value('Salary Slip', - {'posting_date': nowdate(), 'employee': applicant}, 'name') + name = frappe.db.get_value( + "Salary Slip", {"posting_date": nowdate(), "employee": applicant}, "name" + ) - salary_slip = frappe.get_doc('Salary Slip', name) + salary_slip = frappe.get_doc("Salary Slip", name) for row in salary_slip.loans: if row.loan == loan.name: - interest_amount = (280000 * 8.4)/(12*100) + interest_amount = (280000 * 8.4) / (12 * 100) principal_amount = loan.monthly_repayment_amount - interest_amount self.assertEqual(row.interest_amount, interest_amount) self.assertEqual(row.principal_amount, principal_amount) - self.assertEqual(row.total_payment, - interest_amount + principal_amount) + self.assertEqual(row.total_payment, interest_amount + principal_amount) if salary_slip.docstatus == 0: - frappe.delete_doc('Salary Slip', name) + frappe.delete_doc("Salary Slip", name) def make_payroll_entry(**args): @@ -257,17 +367,22 @@ def make_payroll_entry(**args): payroll_entry.save() payroll_entry.create_salary_slips() payroll_entry.submit_salary_slips() - if payroll_entry.get_sal_slip_list(ss_status = 1): + if payroll_entry.get_sal_slip_list(ss_status=1): payroll_entry.make_payment_entry() return payroll_entry + def get_payment_account(): - return frappe.get_value('Account', - {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name") + return frappe.get_value( + "Account", + {"account_type": "Cash", "company": erpnext.get_default_company(), "is_group": 0}, + "name", + ) + def make_holiday(holiday_list_name): - if not frappe.db.exists('Holiday List', holiday_list_name): + if not frappe.db.exists("Holiday List", holiday_list_name): current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True) dt = getdate(nowdate()) @@ -275,25 +390,23 @@ def make_holiday(holiday_list_name): republic_day = dt + relativedelta(month=1, day=26, year=dt.year) test_holiday = dt + relativedelta(month=2, day=2, year=dt.year) - frappe.get_doc({ - 'doctype': 'Holiday List', - 'from_date': current_fiscal_year.year_start_date, - 'to_date': current_fiscal_year.year_end_date, - 'holiday_list_name': holiday_list_name, - 'holidays': [{ - 'holiday_date': new_year, - 'description': 'New Year' - }, { - 'holiday_date': republic_day, - 'description': 'Republic Day' - }, { - 'holiday_date': test_holiday, - 'description': 'Test Holiday' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Holiday List", + "from_date": current_fiscal_year.year_start_date, + "to_date": current_fiscal_year.year_end_date, + "holiday_list_name": holiday_list_name, + "holidays": [ + {"holiday_date": new_year, "description": "New Year"}, + {"holiday_date": republic_day, "description": "Republic Day"}, + {"holiday_date": test_holiday, "description": "Test Holiday"}, + ], + } + ).insert() return holiday_list_name + def get_salary_slip(user, period, salary_structure): salary_slip = make_employee_salary_slip(user, period, salary_structure) salary_slip.exchange_rate = 70 diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py index 659ec6de7b6..e1f1cabbc7e 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py @@ -30,71 +30,92 @@ class PayrollPeriod(Document): """ if not self.name: # hack! if name is null, it could cause problems with != - self.name = "New "+self.doctype + self.name = "New " + self.doctype - overlap_doc = frappe.db.sql(query.format(self.doctype),{ + overlap_doc = frappe.db.sql( + query.format(self.doctype), + { "start_date": self.start_date, "end_date": self.end_date, "name": self.name, - "company": self.company - }, as_dict = 1) + "company": self.company, + }, + as_dict=1, + ) if overlap_doc: - msg = _("A {0} exists between {1} and {2} (").format(self.doctype, - formatdate(self.start_date), formatdate(self.end_date)) \ - + """ {1}""".format(self.doctype, overlap_doc[0].name) \ + msg = ( + _("A {0} exists between {1} and {2} (").format( + self.doctype, formatdate(self.start_date), formatdate(self.end_date) + ) + + """ {1}""".format(self.doctype, overlap_doc[0].name) + _(") for {0}").format(self.company) + ) frappe.throw(msg) + def get_payroll_period_days(start_date, end_date, employee, company=None): if not company: company = frappe.db.get_value("Employee", employee, "company") - payroll_period = frappe.db.sql(""" + payroll_period = frappe.db.sql( + """ select name, start_date, end_date from `tabPayroll Period` where company=%(company)s and %(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date - """, { - 'company': company, - 'start_date': start_date, - 'end_date': end_date - }) + """, + {"company": company, "start_date": start_date, "end_date": end_date}, + ) if len(payroll_period) > 0: actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1 working_days = actual_no_of_days - if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")): - holidays = get_holiday_dates_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2])) + if not cint( + frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days") + ): + holidays = get_holiday_dates_for_employee( + employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2]) + ) working_days -= len(holidays) return payroll_period[0][0], working_days, actual_no_of_days return False, False, False + def get_payroll_period(from_date, to_date, company): - payroll_period = frappe.db.sql(""" + payroll_period = frappe.db.sql( + """ select name, start_date, end_date from `tabPayroll Period` where start_date<=%s and end_date>= %s and company=%s - """, (from_date, to_date, company), as_dict=1) + """, + (from_date, to_date, company), + as_dict=1, + ) return payroll_period[0] if payroll_period else None -def get_period_factor(employee, start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days=0): + +def get_period_factor( + employee, start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days=0 +): # TODO if both deduct checked update the factor to make tax consistent period_start, period_end = payroll_period.start_date, payroll_period.end_date - joining_date, relieving_date = frappe.db.get_value("Employee", employee, ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) if getdate(joining_date) > getdate(period_start): period_start = joining_date if relieving_date and getdate(relieving_date) < getdate(period_end): period_end = relieving_date if month_diff(period_end, start_date) > 1: - start_date = add_months(start_date, - (month_diff(period_end, start_date)+1)) + start_date = add_months(start_date, -(month_diff(period_end, start_date) + 1)) total_sub_periods, remaining_sub_periods = 0.0, 0.0 - if payroll_frequency == "Monthly" and not depends_on_payment_days: + if payroll_frequency == "Monthly" and not depends_on_payment_days: total_sub_periods = month_diff(payroll_period.end_date, payroll_period.start_date) remaining_sub_periods = month_diff(period_end, start_date) else: diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py b/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py index eaa67732af4..96632c50085 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'payroll_period', - 'transactions': [ - { - 'items': ['Employee Tax Exemption Proof Submission', 'Employee Tax Exemption Declaration'] - }, - ], - } + return { + "fieldname": "payroll_period", + "transactions": [ + {"items": ["Employee Tax Exemption Proof Submission", "Employee Tax Exemption Declaration"]}, + ], + } diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.py b/erpnext/payroll/doctype/payroll_settings/payroll_settings.py index 6fd30946f55..33614e9d30a 100644 --- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.py +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.py @@ -21,12 +21,25 @@ class PayrollSettings(Document): if not self.password_policy: frappe.throw(_("Password policy for Salary Slips is not set")) - def on_update(self): self.toggle_rounded_total() frappe.clear_cache() def toggle_rounded_total(self): self.disable_rounded_total = cint(self.disable_rounded_total) - make_property_setter("Salary Slip", "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) - make_property_setter("Salary Slip", "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) + make_property_setter( + "Salary Slip", + "rounded_total", + "hidden", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Salary Slip", + "rounded_total", + "print_hide", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py index 10e8381007b..cdcd9a90259 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -14,14 +14,14 @@ class RetentionBonus(Document): def validate(self): validate_active_employee(self.employee) if getdate(self.bonus_payment_date) < getdate(): - frappe.throw(_('Bonus Payment Date cannot be a past date')) + frappe.throw(_("Bonus Payment Date cannot be a past date")) def on_submit(self): - company = frappe.db.get_value('Employee', self.employee, 'company') + company = frappe.db.get_value("Employee", self.employee, "company") additional_salary = self.get_additional_salary() if not additional_salary: - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = self.employee additional_salary.salary_component = self.salary_component additional_salary.amount = self.bonus_amount @@ -34,29 +34,36 @@ class RetentionBonus(Document): # self.db_set('additional_salary', additional_salary.name) else: - bonus_added = frappe.db.get_value('Additional Salary', additional_salary, 'amount') + self.bonus_amount - frappe.db.set_value('Additional Salary', additional_salary, 'amount', bonus_added) - self.db_set('additional_salary', additional_salary) + bonus_added = ( + frappe.db.get_value("Additional Salary", additional_salary, "amount") + self.bonus_amount + ) + frappe.db.set_value("Additional Salary", additional_salary, "amount", bonus_added) + self.db_set("additional_salary", additional_salary) def on_cancel(self): additional_salary = self.get_additional_salary() if self.additional_salary: - bonus_removed = frappe.db.get_value('Additional Salary', self.additional_salary, 'amount') - self.bonus_amount + bonus_removed = ( + frappe.db.get_value("Additional Salary", self.additional_salary, "amount") - self.bonus_amount + ) if bonus_removed == 0: - frappe.get_doc('Additional Salary', self.additional_salary).cancel() + frappe.get_doc("Additional Salary", self.additional_salary).cancel() else: - frappe.db.set_value('Additional Salary', self.additional_salary, 'amount', bonus_removed) + frappe.db.set_value("Additional Salary", self.additional_salary, "amount", bonus_removed) # self.db_set('additional_salary', '') def get_additional_salary(self): - return frappe.db.exists('Additional Salary', { - 'employee': self.employee, - 'salary_component': self.salary_component, - 'payroll_date': self.bonus_payment_date, - 'company': self.company, - 'docstatus': 1, - 'ref_doctype': self.doctype, - 'ref_docname': self.name - }) + return frappe.db.exists( + "Additional Salary", + { + "employee": self.employee, + "salary_component": self.salary_component, + "payroll_date": self.bonus_payment_date, + "company": self.company, + "docstatus": 1, + "ref_doctype": self.doctype, + "ref_docname": self.name, + }, + ) diff --git a/erpnext/payroll/doctype/salary_component/salary_component.py b/erpnext/payroll/doctype/salary_component/salary_component.py index b8def58643a..409c4a1769e 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.py +++ b/erpnext/payroll/doctype/salary_component/salary_component.py @@ -12,9 +12,13 @@ class SalaryComponent(Document): def validate_abbr(self): if not self.salary_component_abbr: - self.salary_component_abbr = ''.join([c[0] for c in - self.salary_component.split()]).upper() + self.salary_component_abbr = "".join([c[0] for c in self.salary_component.split()]).upper() self.salary_component_abbr = self.salary_component_abbr.strip() - self.salary_component_abbr = append_number_if_name_exists('Salary Component', self.salary_component_abbr, - 'salary_component_abbr', separator='_', filters={"name": ["!=", self.name]}) + self.salary_component_abbr = append_number_if_name_exists( + "Salary Component", + self.salary_component_abbr, + "salary_component_abbr", + separator="_", + filters={"name": ["!=", self.name]}, + ) diff --git a/erpnext/payroll/doctype/salary_component/test_salary_component.py b/erpnext/payroll/doctype/salary_component/test_salary_component.py index 6e00971a230..cd729e82400 100644 --- a/erpnext/payroll/doctype/salary_component/test_salary_component.py +++ b/erpnext/payroll/doctype/salary_component/test_salary_component.py @@ -7,15 +7,18 @@ import frappe # test_records = frappe.get_test_records('Salary Component') + class TestSalaryComponent(unittest.TestCase): pass def create_salary_component(component_name, **args): if not frappe.db.exists("Salary Component", component_name): - frappe.get_doc({ + frappe.get_doc( + { "doctype": "Salary Component", "salary_component": component_name, "type": args.get("type") or "Earning", - "is_tax_applicable": args.get("is_tax_applicable") or 1 - }).insert() + "is_tax_applicable": args.get("is_tax_applicable") or 1, + } + ).insert() diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index e70c5116bed..b3e4ad54aa5 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -49,14 +49,14 @@ from erpnext.utilities.transaction_base import TransactionBase class SalarySlip(TransactionBase): def __init__(self, *args, **kwargs): super(SalarySlip, self).__init__(*args, **kwargs) - self.series = 'Sal Slip/{0}/.#####'.format(self.employee) + self.series = "Sal Slip/{0}/.#####".format(self.employee) self.whitelisted_globals = { "int": int, "float": float, "long": int, "round": round, "date": datetime.date, - "getdate": getdate + "getdate": getdate, } def autoname(self): @@ -74,7 +74,7 @@ class SalarySlip(TransactionBase): # get details from salary structure self.get_emp_and_working_day_details() else: - self.get_working_days_details(lwp = self.leave_without_pay) + self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() self.compute_year_to_date() @@ -83,10 +83,16 @@ class SalarySlip(TransactionBase): self.add_leave_balances() if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): - max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") + max_working_hours = frappe.db.get_single_value( + "Payroll Settings", "max_working_hours_against_timesheet" + ) if self.salary_slip_based_on_timesheet and (self.total_working_hours > int(max_working_hours)): - frappe.msgprint(_("Total working hours should not be greater than max working hours {0}"). - format(max_working_hours), alert=True) + frappe.msgprint( + _("Total working hours should not be greater than max working hours {0}").format( + max_working_hours + ), + alert=True, + ) def set_net_total_in_words(self): doc_currency = self.currency @@ -103,19 +109,25 @@ class SalarySlip(TransactionBase): self.set_status() self.update_status(self.name) self.make_loan_repayment_entry() - if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: + if ( + frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee") + ) and not frappe.flags.via_payroll_entry: self.email_salary_slip() self.update_payment_status_for_gratuity() def update_payment_status_for_gratuity(self): - add_salary = frappe.db.get_all("Additional Salary", - filters = { + add_salary = frappe.db.get_all( + "Additional Salary", + filters={ "payroll_date": ("BETWEEN", [self.start_date, self.end_date]), "employee": self.employee, "ref_doctype": "Gratuity", "docstatus": 1, - }, fields = ["ref_docname", "name"], limit=1) + }, + fields=["ref_docname", "name"], + limit=1, + ) if len(add_salary): status = "Paid" if self.docstatus == 1 else "Unpaid" @@ -130,6 +142,7 @@ class SalarySlip(TransactionBase): def on_trash(self): from frappe.model.naming import revert_series_if_last + revert_series_if_last(self.series, self.name) def get_status(self): @@ -147,9 +160,7 @@ class SalarySlip(TransactionBase): if not joining_date: joining_date, relieving_date = frappe.get_cached_value( - "Employee", - self.employee, - ("date_of_joining", "relieving_date") + "Employee", self.employee, ("date_of_joining", "relieving_date") ) if date_diff(self.end_date, joining_date) < 0: @@ -166,16 +177,26 @@ class SalarySlip(TransactionBase): cond = "" if self.payroll_entry: cond += "and payroll_entry = '{0}'".format(self.payroll_entry) - ret_exist = frappe.db.sql("""select name from `tabSalary Slip` + ret_exist = frappe.db.sql( + """select name from `tabSalary Slip` where start_date = %s and end_date = %s and docstatus != 2 - and employee = %s and name != %s {0}""".format(cond), - (self.start_date, self.end_date, self.employee, self.name)) + and employee = %s and name != %s {0}""".format( + cond + ), + (self.start_date, self.end_date, self.employee, self.name), + ) if ret_exist: - frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee)) + frappe.throw( + _("Salary Slip of employee {0} already created for this period").format(self.employee) + ) else: for data in self.timesheets: - if frappe.db.get_value('Timesheet', data.time_sheet, 'status') == 'Payrolled': - frappe.throw(_("Salary Slip of employee {0} already created for time sheet {1}").format(self.employee, data.time_sheet)) + if frappe.db.get_value("Timesheet", data.time_sheet, "status") == "Payrolled": + frappe.throw( + _("Salary Slip of employee {0} already created for time sheet {1}").format( + self.employee, data.time_sheet + ) + ) def get_date_details(self): if not self.end_date: @@ -185,7 +206,7 @@ class SalarySlip(TransactionBase): @frappe.whitelist() def get_emp_and_working_day_details(self): - '''First time, load all the components from salary structure''' + """First time, load all the components from salary structure""" if self.employee: self.set("earnings", []) self.set("deductions", []) @@ -194,52 +215,65 @@ class SalarySlip(TransactionBase): self.get_date_details() joining_date, relieving_date = frappe.get_cached_value( - "Employee", - self.employee, - ("date_of_joining", "relieving_date") + "Employee", self.employee, ("date_of_joining", "relieving_date") ) self.validate_dates(joining_date, relieving_date) - #getin leave details + # getin leave details self.get_working_days_details(joining_date, relieving_date) struct = self.check_sal_struct(joining_date, relieving_date) if struct: - self._salary_structure_doc = frappe.get_doc('Salary Structure', struct) - self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 + self._salary_structure_doc = frappe.get_doc("Salary Structure", struct) + self.salary_slip_based_on_timesheet = ( + self._salary_structure_doc.salary_slip_based_on_timesheet or 0 + ) self.set_time_sheet() self.pull_sal_struct() - ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1) + ps = frappe.db.get_value( + "Payroll Settings", None, ["payroll_based_on", "consider_unmarked_attendance_as"], as_dict=1 + ) return [ps.payroll_based_on, ps.consider_unmarked_attendance_as] def set_time_sheet(self): if self.salary_slip_based_on_timesheet: self.set("timesheets", []) - timesheets = frappe.db.sql(""" select * from `tabTimesheet` where employee = %(employee)s and start_date BETWEEN %(start_date)s AND %(end_date)s and (status = 'Submitted' or - status = 'Billed')""", {'employee': self.employee, 'start_date': self.start_date, 'end_date': self.end_date}, as_dict=1) + timesheets = frappe.db.sql( + """ select * from `tabTimesheet` where employee = %(employee)s and start_date BETWEEN %(start_date)s AND %(end_date)s and (status = 'Submitted' or + status = 'Billed')""", + {"employee": self.employee, "start_date": self.start_date, "end_date": self.end_date}, + as_dict=1, + ) for data in timesheets: - self.append('timesheets', { - 'time_sheet': data.name, - 'working_hours': data.total_hours - }) + self.append("timesheets", {"time_sheet": data.name, "working_hours": data.total_hours}) def check_sal_struct(self, joining_date, relieving_date): cond = """and sa.employee=%(employee)s and (sa.from_date <= %(start_date)s or sa.from_date <= %(end_date)s or sa.from_date <= %(joining_date)s)""" if self.payroll_frequency: - cond += """and ss.payroll_frequency = '%(payroll_frequency)s'""" % {"payroll_frequency": self.payroll_frequency} + cond += """and ss.payroll_frequency = '%(payroll_frequency)s'""" % { + "payroll_frequency": self.payroll_frequency + } - st_name = frappe.db.sql(""" + st_name = frappe.db.sql( + """ select sa.salary_structure from `tabSalary Structure Assignment` sa join `tabSalary Structure` ss where sa.salary_structure=ss.name and sa.docstatus = 1 and ss.docstatus = 1 and ss.is_active ='Yes' %s order by sa.from_date desc limit 1 - """ %cond, {'employee': self.employee, 'start_date': self.start_date, - 'end_date': self.end_date, 'joining_date': joining_date}) + """ + % cond, + { + "employee": self.employee, + "start_date": self.start_date, + "end_date": self.end_date, + "joining_date": joining_date, + }, + ) if st_name: self.salary_structure = st_name[0][0] @@ -247,8 +281,12 @@ class SalarySlip(TransactionBase): else: self.salary_structure = None - frappe.msgprint(_("No active or default Salary Structure found for employee {0} for the given dates") - .format(self.employee), title=_('Salary Structure Missing')) + frappe.msgprint( + _("No active or default Salary Structure found for employee {0} for the given dates").format( + self.employee + ), + title=_("Salary Structure Missing"), + ) def pull_sal_struct(self): from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip @@ -260,13 +298,19 @@ class SalarySlip(TransactionBase): self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0 wages_amount = self.hour_rate * self.total_working_hours - self.add_earning_for_hourly_wages(self, self._salary_structure_doc.salary_component, wages_amount) + self.add_earning_for_hourly_wages( + self, self._salary_structure_doc.salary_component, wages_amount + ) make_salary_slip(self._salary_structure_doc.name, self) - def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0): + def get_working_days_details( + self, joining_date=None, relieving_date=None, lwp=None, for_preview=0 + ): payroll_based_on = frappe.db.get_value("Payroll Settings", None, "payroll_based_on") - include_holidays_in_total_working_days = frappe.db.get_single_value("Payroll Settings", "include_holidays_in_total_working_days") + include_holidays_in_total_working_days = frappe.db.get_single_value( + "Payroll Settings", "include_holidays_in_total_working_days" + ) working_days = date_diff(self.end_date, self.start_date) + 1 if for_preview: @@ -293,14 +337,16 @@ class SalarySlip(TransactionBase): if not lwp: lwp = actual_lwp elif lwp != actual_lwp: - frappe.msgprint(_("Leave Without Pay does not match with approved {} records") - .format(payroll_based_on)) + frappe.msgprint( + _("Leave Without Pay does not match with approved {} records").format(payroll_based_on) + ) self.leave_without_pay = lwp self.total_working_days = working_days - payment_days = self.get_payment_days(joining_date, - relieving_date, include_holidays_in_total_working_days) + payment_days = self.get_payment_days( + joining_date, relieving_date, include_holidays_in_total_working_days + ) if flt(payment_days) > flt(lwp): self.payment_days = flt(payment_days) - flt(lwp) @@ -308,33 +354,81 @@ class SalarySlip(TransactionBase): if payroll_based_on == "Attendance": self.payment_days -= flt(absent) - unmarked_days = self.get_unmarked_days() - consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" + consider_unmarked_attendance_as = ( + frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" + ) - if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent": - self.absent_days += unmarked_days #will be treated as absent + if payroll_based_on == "Attendance" and consider_unmarked_attendance_as == "Absent": + unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days) + self.absent_days += unmarked_days # will be treated as absent self.payment_days -= unmarked_days - if include_holidays_in_total_working_days: - for holiday in holidays: - if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }): - self.payment_days += 1 else: self.payment_days = 0 - def get_unmarked_days(self): - marked_days = frappe.get_all("Attendance", filters = { - "attendance_date": ["between", [self.start_date, self.end_date]], - "employee": self.employee, - "docstatus": 1 - }, fields = ["COUNT(*) as marked_days"])[0].marked_days + def get_unmarked_days(self, include_holidays_in_total_working_days): + unmarked_days = self.total_working_days + joining_date, relieving_date = frappe.get_cached_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) + start_date = self.start_date + end_date = self.end_date - return self.total_working_days - marked_days + if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)): + start_date = joining_date + unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving( + unmarked_days, + include_holidays_in_total_working_days, + self.start_date, + add_days(joining_date, -1), + ) + if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)): + end_date = relieving_date + unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving( + unmarked_days, + include_holidays_in_total_working_days, + add_days(relieving_date, 1), + self.end_date, + ) + + # exclude days for which attendance has been marked + unmarked_days -= frappe.get_all( + "Attendance", + filters={ + "attendance_date": ["between", [start_date, end_date]], + "employee": self.employee, + "docstatus": 1, + }, + fields=["COUNT(*) as marked_days"], + )[0].marked_days + + return unmarked_days + + def get_unmarked_days_based_on_doj_or_relieving( + self, unmarked_days, include_holidays_in_total_working_days, start_date, end_date + ): + """ + Exclude days before DOJ or after + Relieving Date from unmarked days + """ + from erpnext.hr.doctype.employee.employee import is_holiday + + if include_holidays_in_total_working_days: + unmarked_days -= date_diff(end_date, start_date) + 1 + else: + # exclude only if not holidays + for days in range(date_diff(end_date, start_date) + 1): + date = add_days(end_date, -days) + if not is_holiday(self.employee, date): + unmarked_days -= 1 + + return unmarked_days def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days): if not joining_date: - 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"] + ) start_date = getdate(self.start_date) if joining_date: @@ -348,8 +442,7 @@ class SalarySlip(TransactionBase): if getdate(self.start_date) <= relieving_date <= getdate(self.end_date): end_date = relieving_date elif relieving_date < getdate(self.start_date): - frappe.throw(_("Employee relieved on {0} must be set as 'Left'") - .format(relieving_date)) + frappe.throw(_("Employee relieved on {0} must be set as 'Left'").format(relieving_date)) payment_days = date_diff(end_date, start_date) + 1 @@ -365,12 +458,14 @@ class SalarySlip(TransactionBase): def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): lwp = 0 holidays = "','".join(holidays) - daily_wages_fraction_for_half_day = \ + daily_wages_fraction_for_half_day = ( flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + ) for d in range(working_days): dt = add_days(cstr(getdate(self.start_date)), d) - leave = frappe.db.sql(""" + leave = frappe.db.sql( + """ SELECT t1.name, CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) THEN t1.half_day else 0 END, @@ -388,7 +483,11 @@ class SalarySlip(TransactionBase): WHEN t2.include_holiday THEN %(dt)s between from_date and to_date END - """.format(holidays), {"employee": self.employee, "dt": dt}) + """.format( + holidays + ), + {"employee": self.employee, "dt": dt}, + ) if leave: equivalent_lwp_count = 0 @@ -396,10 +495,12 @@ class SalarySlip(TransactionBase): is_partially_paid_leave = cint(leave[0][2]) fraction_of_daily_salary_per_leave = flt(leave[0][3]) - equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 if is_partially_paid_leave: - equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + equivalent_lwp_count *= ( + fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + ) lwp += equivalent_lwp_count @@ -409,18 +510,22 @@ class SalarySlip(TransactionBase): lwp = 0 absent = 0 - daily_wages_fraction_for_half_day = \ + daily_wages_fraction_for_half_day = ( flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + ) - leave_types = frappe.get_all("Leave Type", + leave_types = frappe.get_all( + "Leave Type", or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]], - fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"]) + fields=["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"], + ) leave_type_map = {} for leave_type in leave_types: leave_type_map[leave_type.name] = leave_type - attendances = frappe.db.sql(''' + attendances = frappe.db.sql( + """ SELECT attendance_date, status, leave_type FROM `tabAttendance` WHERE @@ -428,30 +533,46 @@ class SalarySlip(TransactionBase): AND employee = %s AND docstatus = 1 AND attendance_date between %s and %s - ''', values=(self.employee, self.start_date, self.end_date), as_dict=1) + """, + values=(self.employee, self.start_date, self.end_date), + as_dict=1, + ) for d in attendances: - if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys(): + if ( + d.status in ("Half Day", "On Leave") + and d.leave_type + and d.leave_type not in leave_type_map.keys() + ): continue if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays: - if d.status == "Absent" or \ - (d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']): - continue + if d.status == "Absent" or ( + d.leave_type + and d.leave_type in leave_type_map.keys() + and not leave_type_map[d.leave_type]["include_holiday"] + ): + continue if d.leave_type: - fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"] + fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type][ + "fraction_of_daily_salary_per_leave" + ] if d.status == "Half Day": - equivalent_lwp = (1 - daily_wages_fraction_for_half_day) + equivalent_lwp = 1 - daily_wages_fraction_for_half_day if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]: - equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + equivalent_lwp *= ( + fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + ) lwp += equivalent_lwp elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys(): equivalent_lwp = 1 if leave_type_map[d.leave_type]["is_ppl"]: - equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + equivalent_lwp *= ( + fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + ) lwp += equivalent_lwp elif d.status == "Absent": absent += 1 @@ -471,15 +592,17 @@ class SalarySlip(TransactionBase): "abbr": frappe.db.get_value("Salary Component", salary_component, "salary_component_abbr"), "amount": self.hour_rate * self.total_working_hours, "default_amount": 0.0, - "additional_amount": 0.0 + "additional_amount": 0.0, } - doc.append('earnings', wages_row) + doc.append("earnings", wages_row) def calculate_net_pay(self): if self.salary_structure: self.calculate_component_amounts("earnings") self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1) - self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) + self.base_gross_pay = flt( + flt(self.gross_pay) * flt(self.exchange_rate), self.precision("base_gross_pay") + ) if self.salary_structure: self.calculate_component_amounts("deductions") @@ -490,18 +613,24 @@ class SalarySlip(TransactionBase): def set_net_pay(self): self.total_deduction = self.get_component_totals("deductions") - self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) + self.base_total_deduction = flt( + flt(self.total_deduction) * flt(self.exchange_rate), self.precision("base_total_deduction") + ) self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.rounded_total = rounded(self.net_pay) - self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) - self.base_rounded_total = flt(rounded(self.base_net_pay), self.precision('base_net_pay')) + self.base_net_pay = flt( + flt(self.net_pay) * flt(self.exchange_rate), self.precision("base_net_pay") + ) + self.base_rounded_total = flt(rounded(self.base_net_pay), self.precision("base_net_pay")) if self.hour_rate: - self.base_hour_rate = flt(flt(self.hour_rate) * flt(self.exchange_rate), self.precision('base_hour_rate')) + self.base_hour_rate = flt( + flt(self.hour_rate) * flt(self.exchange_rate), self.precision("base_hour_rate") + ) self.set_net_total_in_words() def calculate_component_amounts(self, component_type): - if not getattr(self, '_salary_structure_doc', None): - self._salary_structure_doc = frappe.get_doc('Salary Structure', self.salary_structure) + if not getattr(self, "_salary_structure_doc", None): + self._salary_structure_doc = frappe.get_doc("Salary Structure", self.salary_structure) payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) @@ -520,15 +649,13 @@ class SalarySlip(TransactionBase): self.update_component_row(struct_row, amount, component_type) def get_data_for_eval(self): - '''Returns data for evaluating formula''' + """Returns data for evaluating formula""" data = frappe._dict() employee = frappe.get_doc("Employee", self.employee).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 + employee.date_of_joining if employee.date_of_joining > start_date else start_date ) salary_structure_assignment = frappe.get_value( @@ -546,8 +673,9 @@ class SalarySlip(TransactionBase): if not salary_structure_assignment: frappe.throw( - _("Please assign a Salary Structure for Employee {0} " - "applicable from or before {1} first").format( + _( + "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)), ) @@ -562,7 +690,7 @@ class SalarySlip(TransactionBase): for sc in salary_components: data.setdefault(sc.salary_component_abbr, 0) - for key in ('earnings', 'deductions'): + for key in ("earnings", "deductions"): for d in self.get(key): data[d.abbr] = d.amount @@ -585,8 +713,10 @@ class SalarySlip(TransactionBase): return amount except NameError as err: - frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err), - title=_("Name error")) + frappe.throw( + _("{0}
This error can be due to missing or deleted field.").format(err), + title=_("Name error"), + ) except SyntaxError as err: frappe.throw(_("Syntax error in formula or condition: {0}").format(err)) except Exception as e: @@ -596,13 +726,27 @@ class SalarySlip(TransactionBase): def add_employee_benefits(self, payroll_period): for struct_row in self._salary_structure_doc.get("earnings"): if struct_row.is_flexible_benefit == 1: - if frappe.db.get_value("Salary Component", struct_row.salary_component, "pay_against_benefit_claim") != 1: - benefit_component_amount = get_benefit_component_amount(self.employee, self.start_date, self.end_date, - struct_row.salary_component, self._salary_structure_doc, self.payroll_frequency, payroll_period) + if ( + frappe.db.get_value( + "Salary Component", struct_row.salary_component, "pay_against_benefit_claim" + ) + != 1 + ): + benefit_component_amount = get_benefit_component_amount( + self.employee, + self.start_date, + self.end_date, + struct_row.salary_component, + self._salary_structure_doc, + self.payroll_frequency, + payroll_period, + ) if benefit_component_amount: self.update_component_row(struct_row, benefit_component_amount, "earnings") else: - benefit_claim_amount = get_benefit_claim_amount(self.employee, self.start_date, self.end_date, struct_row.salary_component) + benefit_claim_amount = get_benefit_claim_amount( + self.employee, self.start_date, self.end_date, struct_row.salary_component + ) if benefit_claim_amount: self.update_component_row(struct_row, benefit_claim_amount, "earnings") @@ -610,9 +754,10 @@ class SalarySlip(TransactionBase): def adjust_benefits_in_last_payroll_period(self, payroll_period): if payroll_period: - if (getdate(payroll_period.end_date) <= getdate(self.end_date)): - last_benefits = get_last_payroll_period_benefits(self.employee, self.start_date, self.end_date, - payroll_period, self._salary_structure_doc) + if getdate(payroll_period.end_date) <= getdate(self.end_date): + last_benefits = get_last_payroll_period_benefits( + self.employee, self.start_date, self.end_date, payroll_period, self._salary_structure_doc + ) if last_benefits: for last_benefit in last_benefits: last_benefit = frappe._dict(last_benefit) @@ -620,8 +765,9 @@ class SalarySlip(TransactionBase): self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings") def add_additional_salary_components(self, component_type): - additional_salaries = get_additional_salaries(self.employee, - self.start_date, self.end_date, component_type) + additional_salaries = get_additional_salaries( + self.employee, self.start_date, self.end_date, component_type + ) for additional_salary in additional_salaries: self.update_component_row( @@ -629,7 +775,7 @@ class SalarySlip(TransactionBase): additional_salary.amount, component_type, additional_salary, - is_recurring = additional_salary.is_recurring + is_recurring=additional_salary.is_recurring, ) def add_tax_components(self, payroll_period): @@ -642,40 +788,43 @@ class SalarySlip(TransactionBase): other_deduction_components.append(d.salary_component) if not tax_components: - tax_components = [d.name for d in frappe.get_all("Salary Component", filters={"variable_based_on_taxable_salary": 1}) - if d.name not in other_deduction_components] + tax_components = [ + d.name + for d in frappe.get_all("Salary Component", filters={"variable_based_on_taxable_salary": 1}) + if d.name not in other_deduction_components + ] for d in tax_components: tax_amount = self.calculate_variable_based_on_taxable_salary(d, payroll_period) tax_row = get_salary_component_data(d) self.update_component_row(tax_row, tax_amount, "deductions") - def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0): + def update_component_row( + self, component_data, amount, component_type, additional_salary=None, is_recurring=0 + ): component_row = None for d in self.get(component_type): if d.salary_component != component_data.salary_component: continue - if ( - ( - not d.additional_salary - and (not additional_salary or additional_salary.overwrite) - ) or ( - additional_salary - and additional_salary.name == d.additional_salary - ) + if (not d.additional_salary and (not additional_salary or additional_salary.overwrite)) or ( + additional_salary and additional_salary.name == d.additional_salary ): component_row = d break if additional_salary and additional_salary.overwrite: # Additional Salary with overwrite checked, remove default rows of same component - self.set(component_type, [ - d for d in self.get(component_type) - if d.salary_component != component_data.salary_component - or (d.additional_salary and additional_salary.name != d.additional_salary) - or d == component_row - ]) + self.set( + component_type, + [ + d + for d in self.get(component_type) + if d.salary_component != component_data.salary_component + or (d.additional_salary and additional_salary.name != d.additional_salary) + or d == component_row + ], + ) if not component_row: if not amount: @@ -683,33 +832,40 @@ class SalarySlip(TransactionBase): component_row = self.append(component_type) for attr in ( - 'depends_on_payment_days', 'salary_component', - 'do_not_include_in_total', 'is_tax_applicable', - 'is_flexible_benefit', 'variable_based_on_taxable_salary', - 'exempted_from_income_tax' + "depends_on_payment_days", + "salary_component", + "do_not_include_in_total", + "is_tax_applicable", + "is_flexible_benefit", + "variable_based_on_taxable_salary", + "exempted_from_income_tax", ): component_row.set(attr, component_data.get(attr)) - abbr = component_data.get('abbr') or component_data.get('salary_component_abbr') - component_row.set('abbr', abbr) + abbr = component_data.get("abbr") or component_data.get("salary_component_abbr") + component_row.set("abbr", abbr) if additional_salary: if additional_salary.overwrite: - component_row.additional_amount = flt(flt(amount) - flt(component_row.get("default_amount", 0)), - component_row.precision("additional_amount")) + component_row.additional_amount = flt( + flt(amount) - flt(component_row.get("default_amount", 0)), + component_row.precision("additional_amount"), + ) else: component_row.default_amount = 0 component_row.additional_amount = amount component_row.is_recurring_additional_salary = is_recurring component_row.additional_salary = additional_salary.name - component_row.deduct_full_tax_on_selected_payroll_date = \ + component_row.deduct_full_tax_on_selected_payroll_date = ( additional_salary.deduct_full_tax_on_selected_payroll_date + ) else: component_row.default_amount = amount component_row.additional_amount = 0 - component_row.deduct_full_tax_on_selected_payroll_date = \ + component_row.deduct_full_tax_on_selected_payroll_date = ( component_data.deduct_full_tax_on_selected_payroll_date + ) component_row.amount = amount @@ -717,7 +873,9 @@ class SalarySlip(TransactionBase): def update_component_amount_based_on_payment_days(self, component_row): joining_date, relieving_date = self.get_joining_and_relieving_dates() - component_row.amount = self.get_amount_based_on_payment_days(component_row, joining_date, relieving_date)[0] + component_row.amount = self.get_amount_based_on_payment_days( + component_row, joining_date, relieving_date + )[0] def set_precision_for_component_amounts(self): for component_type in ("earnings", "deductions"): @@ -726,8 +884,11 @@ class SalarySlip(TransactionBase): def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period): if not payroll_period: - frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}.") - .format(tax_component)) + frappe.msgprint( + _("Start and end dates not in a valid Payroll Period, cannot calculate {0}.").format( + tax_component + ) + ) return # Deduct taxes forcefully for unsubmitted tax exemption proof and unclaimed benefits in the last period @@ -742,22 +903,34 @@ class SalarySlip(TransactionBase): tax_slab = self.get_income_tax_slabs(payroll_period) # get remaining numbers of sub-period (period for which one salary is processed) - remaining_sub_periods = get_period_factor(self.employee, - self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] + remaining_sub_periods = get_period_factor( + self.employee, self.start_date, self.end_date, self.payroll_frequency, payroll_period + )[1] # get taxable_earnings, paid_taxes for previous period - previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, - self.start_date, tax_slab.allow_tax_exemption) - previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component) + previous_taxable_earnings = self.get_taxable_earnings_for_prev_period( + payroll_period.start_date, self.start_date, tax_slab.allow_tax_exemption + ) + previous_total_paid_taxes = self.get_tax_paid_in_period( + payroll_period.start_date, self.start_date, tax_component + ) # get taxable_earnings for current period (all days) - current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption) - future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1) + current_taxable_earnings = self.get_taxable_earnings( + tax_slab.allow_tax_exemption, payroll_period=payroll_period + ) + future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * ( + math.ceil(remaining_sub_periods) - 1 + ) # get taxable_earnings, addition_earnings for current actual payment days - current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1) + current_taxable_earnings_for_payment_days = self.get_taxable_earnings( + tax_slab.allow_tax_exemption, based_on_payment_days=1, payroll_period=payroll_period + ) current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income - current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax + current_additional_earnings_with_full_tax = ( + current_taxable_earnings_for_payment_days.additional_income_with_full_tax + ) # Get taxable unclaimed benefits unclaimed_taxable_benefits = 0 @@ -768,20 +941,32 @@ class SalarySlip(TransactionBase): # Total exemption amount based on tax exemption declaration total_exemption_amount = self.get_total_exemption_amount(payroll_period, tax_slab) - #Employee Other Incomes + # Employee Other Incomes other_incomes = self.get_income_form_other_sources(payroll_period) or 0.0 # Total taxable earnings including additional and other incomes - total_taxable_earnings = previous_taxable_earnings + current_structured_taxable_earnings + future_structured_taxable_earnings \ - + current_additional_earnings + other_incomes + unclaimed_taxable_benefits - total_exemption_amount + total_taxable_earnings = ( + previous_taxable_earnings + + current_structured_taxable_earnings + + future_structured_taxable_earnings + + current_additional_earnings + + other_incomes + + unclaimed_taxable_benefits + - total_exemption_amount + ) # Total taxable earnings without additional earnings with full tax - total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax + total_taxable_earnings_without_full_tax_addl_components = ( + total_taxable_earnings - current_additional_earnings_with_full_tax + ) # Structured tax amount total_structured_tax_amount = self.calculate_tax_by_tax_slab( - total_taxable_earnings_without_full_tax_addl_components, tax_slab) - current_structured_tax_amount = (total_structured_tax_amount - previous_total_paid_taxes) / remaining_sub_periods + total_taxable_earnings_without_full_tax_addl_components, tax_slab + ) + current_structured_tax_amount = ( + total_structured_tax_amount - previous_total_paid_taxes + ) / remaining_sub_periods # Total taxable earnings with additional earnings with full tax full_tax_on_additional_earnings = 0.0 @@ -796,25 +981,33 @@ class SalarySlip(TransactionBase): return current_tax_amount def get_income_tax_slabs(self, payroll_period): - income_tax_slab, ss_assignment_name = frappe.db.get_value("Salary Structure Assignment", - {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, ["income_tax_slab", 'name']) + income_tax_slab, ss_assignment_name = frappe.db.get_value( + "Salary Structure Assignment", + {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, + ["income_tax_slab", "name"], + ) if not income_tax_slab: - frappe.throw(_("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name)) + frappe.throw( + _("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name) + ) income_tax_slab_doc = frappe.get_doc("Income Tax Slab", income_tax_slab) if income_tax_slab_doc.disabled: frappe.throw(_("Income Tax Slab: {0} is disabled").format(income_tax_slab)) if getdate(income_tax_slab_doc.effective_from) > getdate(payroll_period.start_date): - frappe.throw(_("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}") - .format(payroll_period.start_date)) + frappe.throw( + _("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}").format( + payroll_period.start_date + ) + ) return income_tax_slab_doc - def get_taxable_earnings_for_prev_period(self, start_date, end_date, allow_tax_exemption=False): - taxable_earnings = frappe.db.sql(""" + taxable_earnings = frappe.db.sql( + """ select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name @@ -826,16 +1019,15 @@ class SalarySlip(TransactionBase): and ss.employee=%(employee)s and ss.start_date between %(from_date)s and %(to_date)s and ss.end_date between %(from_date)s and %(to_date)s - """, { - "employee": self.employee, - "from_date": start_date, - "to_date": end_date - }) + """, + {"employee": self.employee, "from_date": start_date, "to_date": end_date}, + ) taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0 exempted_amount = 0 if allow_tax_exemption: - exempted_amount = frappe.db.sql(""" + exempted_amount = frappe.db.sql( + """ select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name @@ -847,18 +1039,18 @@ class SalarySlip(TransactionBase): and ss.employee=%(employee)s and ss.start_date between %(from_date)s and %(to_date)s and ss.end_date between %(from_date)s and %(to_date)s - """, { - "employee": self.employee, - "from_date": start_date, - "to_date": end_date - }) + """, + {"employee": self.employee, "from_date": start_date, "to_date": end_date}, + ) exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 return taxable_earnings - exempted_amount def get_tax_paid_in_period(self, start_date, end_date, tax_component): # find total_tax_paid, tax paid for benefit, additional_salary - total_tax_paid = flt(frappe.db.sql(""" + total_tax_paid = flt( + frappe.db.sql( + """ select sum(sd.amount) from @@ -871,16 +1063,21 @@ class SalarySlip(TransactionBase): and ss.employee=%(employee)s and ss.start_date between %(from_date)s and %(to_date)s and ss.end_date between %(from_date)s and %(to_date)s - """, { - "salary_component": tax_component, - "employee": self.employee, - "from_date": start_date, - "to_date": end_date - })[0][0]) + """, + { + "salary_component": tax_component, + "employee": self.employee, + "from_date": start_date, + "to_date": end_date, + }, + )[0][0] + ) return total_tax_paid - def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0): + def get_taxable_earnings( + self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None + ): joining_date, relieving_date = self.get_joining_and_relieving_dates() taxable_earnings = 0 @@ -890,7 +1087,9 @@ class SalarySlip(TransactionBase): for earning in self.earnings: if based_on_payment_days: - amount, additional_amount = self.get_amount_based_on_payment_days(earning, joining_date, relieving_date) + amount, additional_amount = self.get_amount_based_on_payment_days( + earning, joining_date, relieving_date + ) else: if earning.additional_amount: amount, additional_amount = earning.amount, earning.additional_amount @@ -901,13 +1100,14 @@ class SalarySlip(TransactionBase): if earning.is_flexible_benefit: flexi_benefits += amount else: - taxable_earnings += (amount - additional_amount) + taxable_earnings += amount - additional_amount additional_income += additional_amount # Get additional amount based on future recurring additional salary if additional_amount and earning.is_recurring_additional_salary: - additional_income += self.get_future_recurring_additional_amount(earning.additional_salary, - earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month + additional_income += self.get_future_recurring_additional_amount( + earning.additional_salary, earning.additional_amount, payroll_period + ) # Used earning.additional_amount to consider the amount for the full month if earning.deduct_full_tax_on_selected_payroll_date: additional_income_with_full_tax += additional_amount @@ -917,64 +1117,104 @@ class SalarySlip(TransactionBase): if ded.exempted_from_income_tax: amount, additional_amount = ded.amount, ded.additional_amount if based_on_payment_days: - amount, additional_amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date) + amount, additional_amount = self.get_amount_based_on_payment_days( + ded, joining_date, relieving_date + ) taxable_earnings -= flt(amount - additional_amount) additional_income -= additional_amount if additional_amount and ded.is_recurring_additional_salary: - additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary, - ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month + additional_income -= self.get_future_recurring_additional_amount( + ded.additional_salary, ded.additional_amount, payroll_period + ) # Used ded.additional_amount to consider the amount for the full month - return frappe._dict({ - "taxable_earnings": taxable_earnings, - "additional_income": additional_income, - "additional_income_with_full_tax": additional_income_with_full_tax, - "flexi_benefits": flexi_benefits - }) + return frappe._dict( + { + "taxable_earnings": taxable_earnings, + "additional_income": additional_income, + "additional_income_with_full_tax": additional_income_with_full_tax, + "flexi_benefits": flexi_benefits, + } + ) - def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount): + def get_future_recurring_additional_amount( + self, additional_salary, monthly_additional_amount, payroll_period + ): future_recurring_additional_amount = 0 - to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date') + to_date = frappe.db.get_value("Additional Salary", additional_salary, "to_date") # future month count excluding current from_date, to_date = getdate(self.start_date), getdate(to_date) - future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month) + + # If recurring period end date is beyond the payroll period, + # last day of payroll period should be considered for recurring period calculation + if getdate(to_date) > getdate(payroll_period.end_date): + to_date = getdate(payroll_period.end_date) + + future_recurring_period = ((to_date.year - from_date.year) * 12) + ( + to_date.month - from_date.month + ) if future_recurring_period > 0: - future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month + future_recurring_additional_amount = ( + monthly_additional_amount * future_recurring_period + ) # Used earning.additional_amount to consider the amount for the full month return future_recurring_additional_amount def get_amount_based_on_payment_days(self, row, joining_date, relieving_date): amount, additional_amount = row.amount, row.additional_amount - timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component") + timesheet_component = frappe.db.get_value( + "Salary Structure", self.salary_structure, "salary_component" + ) - if (self.salary_structure and - cint(row.depends_on_payment_days) and cint(self.total_working_days) - and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary - and (row.salary_component != timesheet_component or - getdate(self.start_date) < joining_date or - (relieving_date and getdate(self.end_date) > relieving_date) - )): - additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days) - / cint(self.total_working_days)), row.precision("additional_amount")) - amount = flt((flt(row.default_amount) * flt(self.payment_days) - / cint(self.total_working_days)), row.precision("amount")) + additional_amount + if ( + self.salary_structure + and cint(row.depends_on_payment_days) + and cint(self.total_working_days) + and not ( + row.additional_salary and row.default_amount + ) # to identify overwritten additional salary + and ( + row.salary_component != timesheet_component + or getdate(self.start_date) < joining_date + or (relieving_date and getdate(self.end_date) > relieving_date) + ) + ): + additional_amount = flt( + (flt(row.additional_amount) * flt(self.payment_days) / cint(self.total_working_days)), + row.precision("additional_amount"), + ) + amount = ( + flt( + (flt(row.default_amount) * flt(self.payment_days) / cint(self.total_working_days)), + row.precision("amount"), + ) + + additional_amount + ) - elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days): + elif ( + not self.payment_days + and row.salary_component != timesheet_component + and cint(row.depends_on_payment_days) + ): amount, additional_amount = 0, 0 elif not row.amount: amount = flt(row.default_amount) + flt(row.additional_amount) # apply rounding - if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"): - amount, additional_amount = rounded(amount), rounded(additional_amount) + if frappe.get_cached_value( + "Salary Component", row.salary_component, "round_to_the_nearest_integer" + ): + amount, additional_amount = rounded(amount or 0), rounded(additional_amount or 0) return amount, additional_amount def calculate_unclaimed_taxable_benefits(self, payroll_period): # get total sum of benefits paid - total_benefits_paid = flt(frappe.db.sql(""" + total_benefits_paid = flt( + frappe.db.sql( + """ select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name where @@ -985,21 +1225,29 @@ class SalarySlip(TransactionBase): and ss.employee=%(employee)s and ss.start_date between %(start_date)s and %(end_date)s and ss.end_date between %(start_date)s and %(end_date)s - """, { - "employee": self.employee, - "start_date": payroll_period.start_date, - "end_date": self.start_date - })[0][0]) + """, + { + "employee": self.employee, + "start_date": payroll_period.start_date, + "end_date": self.start_date, + }, + )[0][0] + ) # get total benefits claimed - total_benefits_claimed = flt(frappe.db.sql(""" + total_benefits_claimed = flt( + frappe.db.sql( + """ select sum(claimed_amount) from `tabEmployee Benefit Claim` where docstatus=1 and employee=%s and claim_date between %s and %s - """, (self.employee, payroll_period.start_date, self.end_date))[0][0]) + """, + (self.employee, payroll_period.start_date, self.end_date), + )[0][0] + ) return total_benefits_paid - total_benefits_claimed @@ -1007,15 +1255,19 @@ class SalarySlip(TransactionBase): total_exemption_amount = 0 if tax_slab.allow_tax_exemption: if self.deduct_tax_for_unsubmitted_tax_exemption_proof: - exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission", + exemption_proof = frappe.db.get_value( + "Employee Tax Exemption Proof Submission", {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["exemption_amount"]) + ["exemption_amount"], + ) if exemption_proof: total_exemption_amount = exemption_proof else: - declaration = frappe.db.get_value("Employee Tax Exemption Declaration", + declaration = frappe.db.get_value( + "Employee Tax Exemption Declaration", {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["total_exemption_amount"]) + ["total_exemption_amount"], + ) if declaration: total_exemption_amount = declaration @@ -1024,14 +1276,15 @@ class SalarySlip(TransactionBase): return total_exemption_amount def get_income_form_other_sources(self, payroll_period): - return frappe.get_all("Employee Other Income", + return frappe.get_all( + "Employee Other Income", filters={ "employee": self.employee, "payroll_period": payroll_period.name, "company": self.company, - "docstatus": 1 + "docstatus": 1, }, - fields="SUM(amount) as total_amount" + fields="SUM(amount) as total_amount", )[0].total_amount def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab): @@ -1043,12 +1296,12 @@ class SalarySlip(TransactionBase): if cond and not self.eval_tax_slab_condition(cond, data): continue if not slab.to_amount and annual_taxable_earning >= slab.from_amount: - tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 continue if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount: - tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount: - tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * .01 + tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * 0.01 # other taxes and charges on income tax for d in tax_slab.other_taxes_and_charges: @@ -1068,8 +1321,10 @@ class SalarySlip(TransactionBase): if condition: return frappe.safe_eval(condition, self.whitelisted_globals, data) except NameError as err: - frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err), - title=_("Name error")) + frappe.throw( + _("{0}
This error can be due to missing or deleted field.").format(err), + title=_("Name error"), + ) except SyntaxError as err: frappe.throw(_("Syntax error in condition: {0}").format(err)) except Exception as e: @@ -1077,8 +1332,9 @@ class SalarySlip(TransactionBase): raise def get_component_totals(self, component_type, depends_on_payment_days=0): - 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"] + ) total = 0.0 for d in self.get(component_type): @@ -1091,14 +1347,17 @@ class SalarySlip(TransactionBase): return total def get_joining_and_relieving_dates(self): - 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"] + ) if not relieving_date: relieving_date = getdate(self.end_date) if not joining_date: - frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) + frappe.throw( + _("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)) + ) return joining_date, relieving_date @@ -1107,28 +1366,38 @@ class SalarySlip(TransactionBase): self.total_interest_amount = 0 self.total_principal_amount = 0 - if not self.get('loans'): + if not self.get("loans"): for loan in self.get_loan_details(): amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment") - if amounts['interest_amount'] or amounts['payable_principal_amount']: - self.append('loans', { - 'loan': loan.name, - 'total_payment': amounts['interest_amount'] + amounts['payable_principal_amount'], - 'interest_amount': amounts['interest_amount'], - 'principal_amount': amounts['payable_principal_amount'], - 'loan_account': loan.loan_account, - 'interest_income_account': loan.interest_income_account - }) + if amounts["interest_amount"] or amounts["payable_principal_amount"]: + self.append( + "loans", + { + "loan": loan.name, + "total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"], + "interest_amount": amounts["interest_amount"], + "principal_amount": amounts["payable_principal_amount"], + "loan_account": loan.loan_account, + "interest_income_account": loan.interest_income_account, + }, + ) - for payment in self.get('loans'): + for payment in self.get("loans"): amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment") - total_amount = amounts['interest_amount'] + amounts['payable_principal_amount'] + total_amount = amounts["interest_amount"] + amounts["payable_principal_amount"] if payment.total_payment > total_amount: - frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2} against loan {3}""") - .format(payment.idx, frappe.bold(payment.total_payment), - frappe.bold(total_amount), frappe.bold(payment.loan))) + frappe.throw( + _( + """Row {0}: Paid amount {1} is greater than pending accrued amount {2} against loan {3}""" + ).format( + payment.idx, + frappe.bold(payment.total_payment), + frappe.bold(total_amount), + frappe.bold(payment.loan), + ) + ) self.total_interest_amount += payment.interest_amount self.total_principal_amount += payment.principal_amount @@ -1136,27 +1405,40 @@ class SalarySlip(TransactionBase): self.total_loan_repayment += payment.total_payment def get_loan_details(self): - return frappe.get_all("Loan", + return frappe.get_all( + "Loan", fields=["name", "interest_income_account", "loan_account", "loan_type"], - filters = { + filters={ "applicant": self.employee, "docstatus": 1, "repay_from_salary": 1, - "company": self.company - }) + "company": self.company, + }, + ) def make_loan_repayment_entry(self): payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry) for loan in self.loans: if loan.total_payment: - repayment_entry = create_repayment_entry(loan.loan, self.employee, - self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, - loan.principal_amount, loan.total_payment, payroll_payable_account=payroll_payable_account) + repayment_entry = create_repayment_entry( + loan.loan, + self.employee, + self.company, + self.posting_date, + loan.loan_type, + "Regular Payment", + loan.interest_amount, + loan.principal_amount, + loan.total_payment, + payroll_payable_account=payroll_payable_account, + ) repayment_entry.save() repayment_entry.submit() - frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name) + frappe.db.set_value( + "Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name + ) def cancel_loan_repayment_entry(self): for loan in self.loans: @@ -1172,19 +1454,23 @@ class SalarySlip(TransactionBase): if payroll_settings.encrypt_salary_slips_in_emails: password = generate_password_for_pdf(payroll_settings.password_policy, self.employee) message += """
Note: Your salary slip is password protected, - the password to unlock the PDF is of the format {0}. """.format(payroll_settings.password_policy) + the password to unlock the PDF is of the format {0}. """.format( + payroll_settings.password_policy + ) if receiver: email_args = { "recipients": [receiver], "message": _(message), - "subject": 'Salary Slip - from {0} to {1}'.format(self.start_date, self.end_date), - "attachments": [frappe.attach_print(self.doctype, self.name, file_name=self.name, password=password)], + "subject": "Salary Slip - from {0} to {1}".format(self.start_date, self.end_date), + "attachments": [ + frappe.attach_print(self.doctype, self.name, file_name=self.name, password=password) + ], "reference_doctype": self.doctype, - "reference_name": self.name - } + "reference_name": self.name, + } if not frappe.flags.in_test: - enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args) else: frappe.sendmail(**email_args) else: @@ -1193,21 +1479,20 @@ class SalarySlip(TransactionBase): def update_status(self, salary_slip=None): for data in self.timesheets: if data.time_sheet: - timesheet = frappe.get_doc('Timesheet', data.time_sheet) + timesheet = frappe.get_doc("Timesheet", data.time_sheet) timesheet.salary_slip = salary_slip timesheet.flags.ignore_validate_update_after_submit = True timesheet.set_status() timesheet.save() def set_status(self, status=None): - '''Get and update status''' + """Get and update status""" if not status: status = self.get_status() self.db_set("status", status) - def process_salary_structure(self, for_preview=0): - '''Calculate salary after salary structure details have been updated''' + """Calculate salary after salary structure details have been updated""" if not self.salary_slip_based_on_timesheet: self.get_date_details() self.pull_emp_details() @@ -1215,7 +1500,9 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() def pull_emp_details(self): - emp = frappe.db.get_value("Employee", self.employee, ["bank_name", "bank_ac_no", "salary_mode"], as_dict=1) + emp = frappe.db.get_value( + "Employee", self.employee, ["bank_name", "bank_ac_no", "salary_mode"], as_dict=1 + ) if emp: self.mode_of_payment = emp.salary_mode self.bank_name = emp.bank_name @@ -1245,12 +1532,12 @@ class SalarySlip(TransactionBase): def set_base_totals(self): self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate) self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate) - self.rounded_total = rounded(self.net_pay) + self.rounded_total = rounded(self.net_pay or 0) self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate) - self.base_rounded_total = rounded(self.base_net_pay) + self.base_rounded_total = rounded(self.base_net_pay or 0) self.set_net_total_in_words() - #calculate total working hours, earnings based on hourly wages and totals + # calculate total working hours, earnings based on hourly wages and totals def calculate_total_for_salary_slip_based_on_timesheet(self): if self.timesheets: self.total_working_hours = 0 @@ -1260,7 +1547,9 @@ class SalarySlip(TransactionBase): wages_amount = self.total_working_hours * self.hour_rate self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate) - salary_component = frappe.db.get_value('Salary Structure', {'name': self.salary_structure}, 'salary_component') + salary_component = frappe.db.get_value( + "Salary Structure", {"name": self.salary_structure}, "salary_component" + ) if self.earnings: for i, earning in enumerate(self.earnings): if earning.salary_component == salary_component: @@ -1272,14 +1561,17 @@ class SalarySlip(TransactionBase): year_to_date = 0 period_start_date, period_end_date = self.get_year_to_date_period() - salary_slip_sum = frappe.get_list('Salary Slip', - fields = ['sum(net_pay) as net_sum', 'sum(gross_pay) as gross_sum'], - filters = {'employee' : self.employee, - 'start_date' : ['>=', period_start_date], - 'end_date' : ['<', period_end_date], - 'name': ['!=', self.name], - 'docstatus': 1 - }) + salary_slip_sum = frappe.get_list( + "Salary Slip", + fields=["sum(net_pay) as net_sum", "sum(gross_pay) as gross_sum"], + filters={ + "employee": self.employee, + "start_date": [">=", period_start_date], + "end_date": ["<", period_end_date], + "name": ["!=", self.name], + "docstatus": 1, + }, + ) year_to_date = flt(salary_slip_sum[0].net_sum) if salary_slip_sum else 0.0 gross_year_to_date = flt(salary_slip_sum[0].gross_sum) if salary_slip_sum else 0.0 @@ -1292,14 +1584,17 @@ class SalarySlip(TransactionBase): def compute_month_to_date(self): month_to_date = 0 first_day_of_the_month = get_first_day(self.start_date) - salary_slip_sum = frappe.get_list('Salary Slip', - fields = ['sum(net_pay) as sum'], - filters = {'employee' : self.employee, - 'start_date' : ['>=', first_day_of_the_month], - 'end_date' : ['<', self.start_date], - 'name': ['!=', self.name], - 'docstatus': 1 - }) + salary_slip_sum = frappe.get_list( + "Salary Slip", + fields=["sum(net_pay) as sum"], + filters={ + "employee": self.employee, + "start_date": [">=", first_day_of_the_month], + "end_date": ["<", self.start_date], + "name": ["!=", self.name], + "docstatus": 1, + }, + ) month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 @@ -1309,10 +1604,11 @@ class SalarySlip(TransactionBase): def compute_component_wise_year_to_date(self): period_start_date, period_end_date = self.get_year_to_date_period() - for key in ('earnings', 'deductions'): + for key in ("earnings", "deductions"): for component in self.get(key): year_to_date = 0 - component_sum = frappe.db.sql(""" + component_sum = frappe.db.sql( + """ SELECT sum(detail.amount) as sum FROM `tabSalary Detail` as detail INNER JOIN `tabSalary Slip` as salary_slip @@ -1324,8 +1620,13 @@ class SalarySlip(TransactionBase): AND salary_slip.end_date < %(period_end_date)s AND salary_slip.name != %(docname)s AND salary_slip.docstatus = 1""", - {'employee': self.employee, 'component': component.salary_component, 'period_start_date': period_start_date, - 'period_end_date': period_end_date, 'docname': self.name} + { + "employee": self.employee, + "component": component.salary_component, + "period_start_date": period_start_date, + "period_end_date": period_end_date, + "docname": self.name, + }, ) year_to_date = flt(component_sum[0][0]) if component_sum else 0.0 @@ -1347,34 +1648,44 @@ class SalarySlip(TransactionBase): return period_start_date, period_end_date def add_leave_balances(self): - self.set('leave_details', []) + self.set("leave_details", []) - if frappe.db.get_single_value('Payroll Settings', 'show_leave_balances_in_salary_slip'): + if frappe.db.get_single_value("Payroll Settings", "show_leave_balances_in_salary_slip"): from erpnext.hr.doctype.leave_application.leave_application import get_leave_details + leave_details = get_leave_details(self.employee, self.end_date) - for leave_type, leave_values in iteritems(leave_details['leave_allocation']): - self.append('leave_details', { - 'leave_type': leave_type, - 'total_allocated_leaves': flt(leave_values.get('total_leaves')), - 'expired_leaves': flt(leave_values.get('expired_leaves')), - 'used_leaves': flt(leave_values.get('leaves_taken')), - 'pending_leaves': flt(leave_values.get('pending_leaves')), - 'available_leaves': flt(leave_values.get('remaining_leaves')) - }) + for leave_type, leave_values in iteritems(leave_details["leave_allocation"]): + self.append( + "leave_details", + { + "leave_type": leave_type, + "total_allocated_leaves": flt(leave_values.get("total_leaves")), + "expired_leaves": flt(leave_values.get("expired_leaves")), + "used_leaves": flt(leave_values.get("leaves_taken")), + "pending_leaves": flt(leave_values.get("leaves_pending_approval")), + "available_leaves": flt(leave_values.get("remaining_leaves")), + }, + ) + def unlink_ref_doc_from_salary_slip(ref_no): - linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` - where journal_entry=%s and docstatus < 2""", (ref_no)) + linked_ss = frappe.db.sql_list( + """select name from `tabSalary Slip` + where journal_entry=%s and docstatus < 2""", + (ref_no), + ) if linked_ss: for ss in linked_ss: ss_doc = frappe.get_doc("Salary Slip", ss) frappe.db.set_value("Salary Slip", ss_doc.name, "journal_entry", "") + def generate_password_for_pdf(policy_template, employee): employee = frappe.get_doc("Employee", employee) return policy_template.format(**employee.as_dict()) + def get_salary_component_data(component): return frappe.get_value( "Salary Component", @@ -1391,10 +1702,15 @@ def get_salary_component_data(component): as_dict=1, ) + def get_payroll_payable_account(company, payroll_entry): if payroll_entry: - payroll_payable_account = frappe.db.get_value('Payroll Entry', payroll_entry, 'payroll_payable_account') + payroll_payable_account = frappe.db.get_value( + "Payroll Entry", payroll_entry, "payroll_payable_account" + ) else: - payroll_payable_account = frappe.db.get_value('Company', company, 'default_payroll_payable_account') + payroll_payable_account = frappe.db.get_value( + "Company", company, "default_payroll_payable_account" + ) - return payroll_payable_account \ No newline at end of file + return payroll_payable_account diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 4249fa76c71..0abf58b062c 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -7,10 +7,12 @@ import unittest import frappe from frappe.model.document import Document +from frappe.tests.utils import change_settings from frappe.utils import ( add_days, add_months, cstr, + date_diff, flt, get_first_day, get_last_day, @@ -21,6 +23,7 @@ from frappe.utils.make_random import get_random import erpnext from erpnext.accounts.utils import get_fiscal_year +from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type @@ -35,19 +38,19 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar class TestSalarySlip(unittest.TestCase): def setUp(self): setup_test() + frappe.flags.pop("via_payroll_entry", None) def tearDown(self): + frappe.db.rollback() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") + @change_settings( + "Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75} + ) def test_payment_days_based_on_attendance(self): - from erpnext.hr.doctype.attendance.attendance import mark_attendance no_of_days = self.get_no_of_days() - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") - frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75) - emp_id = make_employee("test_payment_days_based_on_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -56,21 +59,50 @@ class TestSalarySlip(unittest.TestCase): month_start_date = get_first_day(nowdate()) month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = 'Salary Slip Test Holiday List' and holiday_date between %s and %s order by holiday_date - """, (month_start_date, month_end_date))[0][0] + """, + (month_start_date, month_end_date), + )[0][0] - mark_attendance(emp_id, first_sunday, 'Absent', ignore_validate=True) # invalid lwp - mark_attendance(emp_id, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent - mark_attendance(emp_id, add_days(first_sunday, 2), 'Half Day', leave_type='Leave Without Pay', ignore_validate=True) # valid 0.75 lwp - mark_attendance(emp_id, add_days(first_sunday, 3), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # valid lwp - mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp - mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp + mark_attendance(emp_id, first_sunday, "Absent", ignore_validate=True) # invalid lwp + mark_attendance( + emp_id, add_days(first_sunday, 1), "Absent", ignore_validate=True + ) # counted as absent + mark_attendance( + emp_id, + add_days(first_sunday, 2), + "Half Day", + leave_type="Leave Without Pay", + ignore_validate=True, + ) # valid 0.75 lwp + mark_attendance( + emp_id, + add_days(first_sunday, 3), + "On Leave", + leave_type="Leave Without Pay", + ignore_validate=True, + ) # valid lwp + mark_attendance( + emp_id, add_days(first_sunday, 4), "On Leave", leave_type="Casual Leave", ignore_validate=True + ) # invalid lwp + mark_attendance( + emp_id, + add_days(first_sunday, 7), + "On Leave", + leave_type="Leave Without Pay", + ignore_validate=True, + ) # invalid lwp - ss = make_employee_salary_slip("test_payment_days_based_on_attendance@salary.com", "Monthly", "Test Payment Based On Attendence") + ss = make_employee_salary_slip( + "test_payment_days_based_on_attendance@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) self.assertEqual(ss.leave_without_pay, 1.25) self.assertEqual(ss.absent_days, 1) @@ -80,19 +112,163 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 2.25) - #Gross pay calculation based on attendances - gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay + ss.absent_days)) + # Gross pay calculation based on attendances + gross_pay = 78000 - ( + (78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay + ss.absent_days) + ) self.assertEqual(ss.gross_pay, gross_pay) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + @change_settings( + "Payroll Settings", + { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": True, + }, + ) + def test_payment_days_for_mid_joinee_including_holidays(self): + no_of_days = self.get_no_of_days() + month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) + new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") + joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) + + for days in range(date_diff(month_end_date, month_start_date) + 1): + date = add_days(month_start_date, days) + mark_attendance(new_emp_id, date, "Present", ignore_validate=True) + + # Case 1: relieving in mid month + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": month_start_date, "relieving_date": relieving_date, "status": "Active"}, + ) + + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + self.assertEqual(new_ss.payment_days, no_of_days[0] - 5) + + # Case 2: joining in mid month + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": month_end_date, "status": "Active"}, + ) + + frappe.delete_doc("Salary Slip", new_ss.name, force=True) + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + self.assertEqual(new_ss.payment_days, no_of_days[0] - 3) + + # Case 3: joining and relieving in mid-month + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"}, + ) + + frappe.delete_doc("Salary Slip", new_ss.name, force=True) + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + + self.assertEqual(new_ss.total_working_days, no_of_days[0]) + self.assertEqual(new_ss.payment_days, no_of_days[0] - 8) + + @change_settings( + "Payroll Settings", + { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": True, + }, + ) + def test_payment_days_for_mid_joinee_including_holidays_and_unmarked_days(self): + # tests mid month joining and relieving along with unmarked days + from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday + + no_of_days = self.get_no_of_days() + month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) + + new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") + joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) + holidays = 0 + + for days in range(date_diff(relieving_date, joining_date) + 1): + date = add_days(joining_date, days) + if not is_holiday("Salary Slip Test Holiday List", date): + mark_attendance(new_emp_id, date, "Present", ignore_validate=True) + else: + holidays += 1 + + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"}, + ) + + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + + self.assertEqual(new_ss.total_working_days, no_of_days[0]) + self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8) + + @change_settings( + "Payroll Settings", + { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": False, + }, + ) + def test_payment_days_for_mid_joinee_excluding_holidays(self): + from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday + + no_of_days = self.get_no_of_days() + month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) + + new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") + joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"}, + ) + + holidays = 0 + + for days in range(date_diff(relieving_date, joining_date) + 1): + date = add_days(joining_date, days) + if not is_holiday("Salary Slip Test Holiday List", date): + mark_attendance(new_emp_id, date, "Present", ignore_validate=True) + else: + holidays += 1 + + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + + self.assertEqual(new_ss.total_working_days, no_of_days[0] - no_of_days[1]) + self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8) + + @change_settings("Payroll Settings", {"payroll_based_on": "Leave"}) def test_payment_days_based_on_leave_application(self): no_of_days = self.get_no_of_days() - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -101,30 +277,41 @@ class TestSalarySlip(unittest.TestCase): month_start_date = get_first_day(nowdate()) month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = 'Salary Slip Test Holiday List' and holiday_date between %s and %s order by holiday_date - """, (month_start_date, month_end_date))[0][0] + """, + (month_start_date, month_end_date), + )[0][0] make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay") - leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl = 1) + leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl=1) leave_type_ppl.save() alloc = create_leave_allocation( - employee = emp_id, from_date = add_days(first_sunday, 4), - to_date = add_days(first_sunday, 10), new_leaves_allocated = 3, - leave_type = "Test Partially Paid Leave") + employee=emp_id, + from_date=add_days(first_sunday, 4), + to_date=add_days(first_sunday, 10), + new_leaves_allocated=3, + leave_type="Test Partially Paid Leave", + ) alloc.save() alloc.submit() - #two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp - make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave") - - ss = make_employee_salary_slip("test_payment_days_based_on_leave_application@salary.com", "Monthly", "Test Payment Based On Leave Application") + # two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp + make_leave_application( + emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave" + ) + ss = make_employee_salary_slip( + "test_payment_days_based_on_leave_application@salary.com", + "Monthly", + "Test Payment Based On Leave Application", + ) self.assertEqual(ss.leave_without_pay, 4) @@ -133,8 +320,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - + @change_settings("Payroll Settings", {"payroll_based_on": "Attendance"}) def test_payment_days_in_salary_slip_based_on_timesheet(self): from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.projects.doctype.timesheet.test_timesheet import ( @@ -145,24 +331,30 @@ class TestSalarySlip(unittest.TestCase): make_salary_slip as make_salary_slip_for_timesheet, ) - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") - - emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List") + emp = make_employee( + "test_employee_timesheet@salary.com", + company="_Test Company", + holiday_list="Salary Slip Test Holiday List", + ) frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"}) # mark attendance month_start_date = get_first_day(nowdate()) month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = 'Salary Slip Test Holiday List' and holiday_date between %s and %s order by holiday_date - """, (month_start_date, month_end_date))[0][0] + """, + (month_start_date, month_end_date), + )[0][0] - mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent + mark_attendance( + emp, add_days(first_sunday, 1), "Absent", ignore_validate=True + ) # counted as absent # salary structure based on timesheet make_salary_structure_for_timesheet(emp) @@ -181,12 +373,14 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1) # gross pay calculation based on attendance (payment days) - gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days)) + gross_pay = 78100 - ( + (78000 / (days_in_month - no_of_holidays)) + * flt(salary_slip.leave_without_pay + salary_slip.absent_days) + ) self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2)) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - + @change_settings("Payroll Settings", {"payroll_based_on": "Attendance"}) def test_component_amount_dependent_on_another_payment_days_based_component(self): from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( @@ -194,30 +388,36 @@ class TestSalarySlip(unittest.TestCase): ) no_of_days = self.get_no_of_days() - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") - salary_structure = make_salary_structure_for_payment_days_based_component_dependency() employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company") # base = 50000 - create_salary_structure_assignment(employee, salary_structure.name, company="_Test Company", currency="INR") + create_salary_structure_assignment( + employee, salary_structure.name, company="_Test Company", currency="INR" + ) # mark employee absent for a day since this case works fine if payment days are equal to working days month_start_date = get_first_day(nowdate()) month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = 'Salary Slip Test Holiday List' and holiday_date between %s and %s order by holiday_date - """, (month_start_date, month_end_date))[0][0] + """, + (month_start_date, month_end_date), + )[0][0] - mark_attendance(employee, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent + mark_attendance( + employee, add_days(first_sunday, 1), "Absent", ignore_validate=True + ) # counted as absent # make salary slip and assert payment days - ss = make_salary_slip_for_payment_days_dependency_test("test_payment_days_based_component@salary.com", salary_structure.name) + ss = make_salary_slip_for_payment_days_dependency_test( + "test_payment_days_based_component@salary.com", salary_structure.name + ) self.assertEqual(ss.absent_days, 1) days_in_month = no_of_days[0] @@ -242,17 +442,32 @@ class TestSalarySlip(unittest.TestCase): expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision) self.assertEqual(actual_amount, expected_amount) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1}) def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() - frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) make_employee("test_salary_slip_with_holidays_included@salary.com") - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_salary_slip_with_holidays_included@salary.com", "Monthly", "Test Salary Slip With Holidays Included") + frappe.db.set_value( + "Employee", + frappe.get_value( + "Employee", {"employee_name": "test_salary_slip_with_holidays_included@salary.com"}, "name" + ), + "relieving_date", + None, + ) + frappe.db.set_value( + "Employee", + frappe.get_value( + "Employee", {"employee_name": "test_salary_slip_with_holidays_included@salary.com"}, "name" + ), + "status", + "Active", + ) + ss = make_employee_salary_slip( + "test_salary_slip_with_holidays_included@salary.com", + "Monthly", + "Test Salary Slip With Holidays Included", + ) self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, no_of_days[0]) @@ -260,15 +475,31 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[1].amount, 3000) self.assertEqual(ss.gross_pay, 78000) + @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0}) def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() - frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) make_employee("test_salary_slip_with_holidays_excluded@salary.com") - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_salary_slip_with_holidays_excluded@salary.com", "Monthly", "Test Salary Slip With Holidays Excluded") + frappe.db.set_value( + "Employee", + frappe.get_value( + "Employee", {"employee_name": "test_salary_slip_with_holidays_excluded@salary.com"}, "name" + ), + "relieving_date", + None, + ) + frappe.db.set_value( + "Employee", + frappe.get_value( + "Employee", {"employee_name": "test_salary_slip_with_holidays_excluded@salary.com"}, "name" + ), + "status", + "Active", + ) + ss = make_employee_salary_slip( + "test_salary_slip_with_holidays_excluded@salary.com", + "Monthly", + "Test Salary Slip With Holidays Excluded", + ) self.assertEqual(ss.total_working_days, no_of_days[0] - no_of_days[1]) self.assertEqual(ss.payment_days, no_of_days[0] - no_of_days[1]) @@ -277,35 +508,34 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[1].amount, 3000) self.assertEqual(ss.gross_pay, 78000) + @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1}) 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 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)) + relieving_date = getdate(add_days(nowdate(), -10)) + date_of_joining = getdate(add_days(nowdate(), -10)) elif getdate(nowdate()).day < 15 and getdate(nowdate()).day >= 5: - date_of_joining = getdate(add_days(nowdate(),-3)) - relieving_date = getdate(add_days(nowdate(),-3)) + date_of_joining = getdate(add_days(nowdate(), -3)) + relieving_date = getdate(add_days(nowdate(), -3)) elif getdate(nowdate()).day < 5 and not getdate(nowdate()).day == 1: - date_of_joining = getdate(add_days(nowdate(),-1)) - relieving_date = getdate(add_days(nowdate(),-1)) + date_of_joining = getdate(add_days(nowdate(), -1)) + relieving_date = getdate(add_days(nowdate(), -1)) elif getdate(nowdate()).day == 1: date_of_joining = getdate(nowdate()) relieving_date = getdate(nowdate()) - frappe.db.set_value("Employee", employee, { - "date_of_joining": date_of_joining, - "relieving_date": None, - "status": "Active" - }) + frappe.db.set_value( + "Employee", + employee, + {"date_of_joining": date_of_joining, "relieving_date": None, "status": "Active"}, + ) salary_structure = "Test Payment Days" ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", salary_structure) @@ -314,11 +544,15 @@ class TestSalarySlip(unittest.TestCase): 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", employee, { - "date_of_joining": add_days(nowdate(),-60), - "relieving_date": relieving_date, - "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) @@ -330,30 +564,43 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, getdate(relieving_date).day) - 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", + 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", + ) def test_employee_salary_slip_read_permission(self): make_employee("test_employee_salary_slip_read_permission@salary.com") - salary_slip_test_employee = make_employee_salary_slip("test_employee_salary_slip_read_permission@salary.com", "Monthly", "Test Employee Salary Slip Read Permission") + salary_slip_test_employee = make_employee_salary_slip( + "test_employee_salary_slip_read_permission@salary.com", + "Monthly", + "Test Employee Salary Slip Read Permission", + ) frappe.set_user("test_employee_salary_slip_read_permission@salary.com") self.assertTrue(salary_slip_test_employee.has_permission("read")) + @change_settings("Payroll Settings", {"email_salary_slip_to_employee": 1}) def test_email_salary_slip(self): - frappe.db.sql("delete from `tabEmail Queue`") + frappe.db.delete("Email Queue") - frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1) + user_id = "test_email_salary_slip@salary.com" - make_employee("test_email_salary_slip@salary.com") - ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email") + make_employee(user_id, company="_Test Company") + ss = make_employee_salary_slip(user_id, "Monthly", "Test Salary Slip Email") ss.company = "_Test Company" ss.save() ss.submit() - email_queue = frappe.db.sql("""select name from `tabEmail Queue`""") + email_queue = frappe.db.a_row_exists("Email Queue") self.assertTrue(email_queue) def test_loan_repayment_salary_slip(self): @@ -372,34 +619,58 @@ class TestSalarySlip(unittest.TestCase): create_loan_accounts() - create_loan_type("Car Loan", 500000, 8.4, + create_loan_type( + "Car Loan", + 500000, + 8.4, is_term_loan=1, - mode_of_payment='Cash', - disbursement_account='Disbursement Account - _TC', - payment_account='Payment Account - _TC', - loan_account='Loan Account - _TC', - interest_income_account='Interest Income Account - _TC', - penalty_income_account='Penalty Income Account - _TC') + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + ) payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") - make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', - payroll_period=payroll_period) + make_salary_structure( + "Test Loan Repayment Salary Structure", + "Monthly", + employee=applicant, + currency="INR", + payroll_period=payroll_period, + ) - frappe.db.sql("delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'") - loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) + frappe.db.sql( + "delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'" + ) + loan = create_loan( + applicant, + "Car Loan", + 11000, + "Repay Over Number of Periods", + 20, + posting_date=add_months(nowdate(), -1), + ) loan.repay_from_salary = 1 loan.submit() - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + make_loan_disbursement_entry( + loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1) + ) process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - ss = make_employee_salary_slip("test_loan_repayment_salary_slip@salary.com", "Monthly", "Test Loan Repayment Salary Structure") + ss = make_employee_salary_slip( + "test_loan_repayment_salary_slip@salary.com", "Monthly", "Test Loan Repayment Salary Structure" + ) ss.submit() self.assertEqual(ss.total_loan_repayment, 592) - self.assertEqual(ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment)))) + self.assertEqual( + ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment))) + ) def test_payroll_frequency(self): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())[0] @@ -408,18 +679,22 @@ class TestSalarySlip(unittest.TestCase): for payroll_frequency in ["Monthly", "Bimonthly", "Fortnightly", "Weekly", "Daily"]: make_employee(payroll_frequency + "_test_employee@salary.com") - ss = make_employee_salary_slip(payroll_frequency + "_test_employee@salary.com", payroll_frequency, payroll_frequency + "_Test Payroll Frequency") + ss = make_employee_salary_slip( + payroll_frequency + "_test_employee@salary.com", + payroll_frequency, + payroll_frequency + "_Test Payroll Frequency", + ) if payroll_frequency == "Monthly": - self.assertEqual(ss.end_date, m['month_end_date']) + self.assertEqual(ss.end_date, m["month_end_date"]) elif payroll_frequency == "Bimonthly": if getdate(ss.start_date).day <= 15: - self.assertEqual(ss.end_date, m['month_mid_end_date']) + self.assertEqual(ss.end_date, m["month_mid_end_date"]) else: - self.assertEqual(ss.end_date, m['month_end_date']) + self.assertEqual(ss.end_date, m["month_end_date"]) elif payroll_frequency == "Fortnightly": - self.assertEqual(ss.end_date, add_days(nowdate(),13)) + self.assertEqual(ss.end_date, add_days(nowdate(), 13)) elif payroll_frequency == "Weekly": - self.assertEqual(ss.end_date, add_days(nowdate(),6)) + self.assertEqual(ss.end_date, add_days(nowdate(), 6)) elif payroll_frequency == "Daily": self.assertEqual(ss.end_date, nowdate()) @@ -427,14 +702,22 @@ class TestSalarySlip(unittest.TestCase): 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') - salary_slip = make_salary_slip(salary_structure.name, employee = applicant) + 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", + ) + salary_slip = make_salary_slip(salary_structure.name, employee=applicant) salary_slip.exchange_rate = 70 salary_slip.calculate_net_pay() self.assertEqual(salary_slip.gross_pay, 78000) - self.assertEqual(salary_slip.base_gross_pay, 78000*70) + self.assertEqual(salary_slip.base_gross_pay, 78000 * 70) def test_year_to_date_computation(self): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure @@ -443,20 +726,36 @@ class TestSalarySlip(unittest.TestCase): payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") - create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"), - company="_Test Company") + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) - salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD", - "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) + salary_structure = make_salary_structure( + "Monthly Salary Structure Test for Salary Slip YTD", + "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'") - create_salary_slips_for_payroll_period(applicant, salary_structure.name, - payroll_period, deduct_random=False, num=6) + create_salary_slips_for_payroll_period( + applicant, salary_structure.name, payroll_period, deduct_random=False, num=6 + ) - salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name': - 'test_ytd@salary.com'}, order_by = 'posting_date') + salary_slips = frappe.get_all( + "Salary Slip", + fields=["year_to_date", "net_pay"], + filters={"employee_name": "test_ytd@salary.com"}, + order_by="posting_date", + ) year_to_date = 0 for slip in salary_slips: @@ -471,20 +770,36 @@ class TestSalarySlip(unittest.TestCase): payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") - create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"), - company="_Test Company") + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) - salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD", - "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) + salary_structure = make_salary_structure( + "Monthly Salary Structure Test for Salary Slip YTD", + "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 = '%s'" % employee_name) - create_salary_slips_for_payroll_period(applicant, salary_structure.name, - payroll_period, deduct_random=False, num=3) + 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": - employee_name}, order_by="posting_date") + salary_slips = frappe.get_all( + "Salary Slip", + fields=["name"], + filters={"employee_name": employee_name}, + order_by="posting_date", + ) year_to_date = dict() for slip in salary_slips: @@ -515,21 +830,27 @@ class TestSalarySlip(unittest.TestCase): "Employee Tax Exemption Declaration", "Employee Tax Exemption Proof Submission", "Employee Benefit Claim", - "Salary Structure Assignment" + "Salary Structure Assignment", ] for doc in delete_docs: frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee)) from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - salary_structure = make_salary_structure("Stucture to test tax", "Monthly", - other_details={"max_benefits": 100000}, test_tax=True, - employee=employee, payroll_period=payroll_period) + salary_structure = make_salary_structure( + "Stucture to test tax", + "Monthly", + 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 - create_salary_slips_for_payroll_period(employee, salary_structure.name, - payroll_period, deduct_random=False) + create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period, deduct_random=False + ) tax_paid = get_tax_paid_in_period(employee) annual_tax = 113589.0 @@ -544,8 +865,9 @@ class TestSalarySlip(unittest.TestCase): create_exemption_declaration(employee, payroll_period.name) # create for payroll deducting in random months - data["deducted_dates"] = create_salary_slips_for_payroll_period(employee, - salary_structure.name, payroll_period) + data["deducted_dates"] = create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period + ) tax_paid = get_tax_paid_in_period(employee) # No proof, benefit claim sumitted, total tax paid, should not change @@ -560,12 +882,14 @@ class TestSalarySlip(unittest.TestCase): # Submit benefit claim for total 50000 data["benefit-1"] = create_benefit_claim(employee, payroll_period, 15000, "Medical Allowance") - data["benefit-2"] = create_benefit_claim(employee, payroll_period, 35000, "Leave Travel Allowance") - + data["benefit-2"] = create_benefit_claim( + employee, payroll_period, 35000, "Leave Travel Allowance" + ) frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) - data["deducted_dates"] = create_salary_slips_for_payroll_period(employee, - salary_structure.name, payroll_period) + data["deducted_dates"] = create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period + ) tax_paid = get_tax_paid_in_period(employee) # total taxable income 416000, 166000 @ 5% ie. 8300 @@ -578,8 +902,9 @@ class TestSalarySlip(unittest.TestCase): # create additional salary of 150000 frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) data["additional-1"] = create_additional_salary(employee, payroll_period, 150000) - data["deducted_dates"] = create_salary_slips_for_payroll_period(employee, - salary_structure.name, payroll_period) + data["deducted_dates"] = create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period + ) # total taxable income 566000, 250000 @ 5%, 66000 @ 20%, 12500 + 13200 tax_paid = get_tax_paid_in_period(employee) @@ -608,20 +933,25 @@ class TestSalarySlip(unittest.TestCase): "Employee Tax Exemption Declaration", "Employee Tax Exemption Proof Submission", "Employee Benefit Claim", - "Salary Structure Assignment" + "Salary Structure Assignment", ] for doc in delete_docs: frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee)) from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - salary_structure = make_salary_structure("Stucture to test tax", "Monthly", - other_details={"max_benefits": 100000}, test_tax=True, - employee=employee, payroll_period=payroll_period) + salary_structure = make_salary_structure( + "Stucture to test tax", + "Monthly", + other_details={"max_benefits": 100000}, + test_tax=True, + employee=employee, + payroll_period=payroll_period, + ) - - create_salary_slips_for_payroll_period(employee, salary_structure.name, - payroll_period, deduct_random=False, num=3) + create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period, deduct_random=False, num=3 + ) tax_paid = get_tax_paid_in_period(employee) @@ -630,7 +960,7 @@ class TestSalarySlip(unittest.TestCase): frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) - #------------------------------------ + # ------------------------------------ # Recurring additional salary start_date = add_months(payroll_period.start_date, 3) end_date = add_months(payroll_period.start_date, 5) @@ -638,8 +968,9 @@ class TestSalarySlip(unittest.TestCase): frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) - create_salary_slips_for_payroll_period(employee, salary_structure.name, - payroll_period, deduct_random=False, num=4) + create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period, deduct_random=False, num=4 + ) tax_paid = get_tax_paid_in_period(employee) @@ -656,44 +987,50 @@ class TestSalarySlip(unittest.TestCase): activity_type.save() def get_no_of_days(self): - no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, - getdate(nowdate()).month) - no_of_holidays_in_month = len([1 for i in calendar.monthcalendar(getdate(nowdate()).year, - getdate(nowdate()).month) if i[6] != 0]) + no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month) + no_of_holidays_in_month = len( + [ + 1 + for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month) + if i[6] != 0 + ] + ) return [no_of_days_in_month[1], no_of_holidays_in_month] + 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" + employee = frappe.db.get_value( + "Employee", {"user_id": user}, ["name", "company", "employee_name"], as_dict=True + ) - employee = frappe.db.get_value("Employee", - { - "user_id": user - }, - ["name", "company", "employee_name"], - as_dict=True) - - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company) - salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) + salary_structure_doc = make_salary_structure( + salary_structure, payroll_frequency, employee=employee.name, company=employee.company + ) + salary_slip_name = frappe.db.get_value( + "Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})} + ) if not salary_slip_name: - salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name) + salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name) salary_slip.employee_name = employee.employee_name salary_slip.payroll_frequency = payroll_frequency salary_slip.posting_date = nowdate() salary_slip.insert() else: - salary_slip = frappe.get_doc('Salary Slip', salary_slip_name) + salary_slip = frappe.get_doc("Salary Slip", salary_slip_name) return salary_slip + def make_salary_component(salary_components, test_tax, company_list=None): for salary_component in salary_components: - if frappe.db.exists('Salary Component', salary_component["salary_component"]): + if frappe.db.exists("Salary Component", salary_component["salary_component"]): continue if test_tax: @@ -713,6 +1050,7 @@ def make_salary_component(salary_components, test_tax, company_list=None): get_salary_component_account(doc, company_list) + def get_salary_component_account(sal_comp, company_list=None): company = erpnext.get_default_company() @@ -724,7 +1062,7 @@ def get_salary_component_account(sal_comp, company_list=None): if not sal_comp.get("accounts"): for d in company_list: - company_abbr = frappe.get_cached_value('Company', d, 'abbr') + company_abbr = frappe.get_cached_value("Company", d, "abbr") if sal_comp.type == "Earning": account_name = "Salary" @@ -733,177 +1071,202 @@ def get_salary_component_account(sal_comp, company_list=None): account_name = "Salary Deductions" parent_account = "Current Liabilities - " + company_abbr - sal_comp.append("accounts", { - "company": d, - "account": create_account(account_name, d, parent_account) - }) + sal_comp.append( + "accounts", {"company": d, "account": create_account(account_name, d, parent_account)} + ) sal_comp.save() + def create_account(account_name, company, parent_account, account_type=None): - company_abbr = frappe.get_cached_value('Company', company, 'abbr') + company_abbr = frappe.get_cached_value("Company", company, "abbr") account = frappe.db.get_value("Account", account_name + " - " + company_abbr) if not account: - frappe.get_doc({ - "doctype": "Account", - "account_name": account_name, - "parent_account": parent_account, - "company": company - }).insert() + frappe.get_doc( + { + "doctype": "Account", + "account_name": account_name, + "parent_account": parent_account, + "company": company, + } + ).insert() return account + def make_earning_salary_component(setup=False, test_tax=False, company_list=None): data = [ { - "salary_component": 'Basic Salary', - "abbr":'BS', - "condition": 'base > 10000', - "formula": 'base', + "salary_component": "Basic Salary", + "abbr": "BS", + "condition": "base > 10000", + "formula": "base", "type": "Earning", - "amount_based_on_formula": 1 + "amount_based_on_formula": 1, }, + {"salary_component": "HRA", "abbr": "H", "amount": 3000, "type": "Earning"}, { - "salary_component": 'HRA', - "abbr":'H', - "amount": 3000, - "type": "Earning" - }, - { - "salary_component": 'Special Allowance', - "abbr":'SA', - "condition": 'H < 10000', - "formula": 'BS*.5', + "salary_component": "Special Allowance", + "abbr": "SA", + "condition": "H < 10000", + "formula": "BS*.5", "type": "Earning", - "amount_based_on_formula": 1 + "amount_based_on_formula": 1, }, - { - "salary_component": "Leave Encashment", - "abbr": 'LE', - "type": "Earning" - } + {"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"}, ] if test_tax: - data.extend([ - { - "salary_component": "Leave Travel Allowance", - "abbr": 'B', - "is_flexible_benefit": 1, - "type": "Earning", - "pay_against_benefit_claim": 1, - "max_benefit_amount": 100000, - "depends_on_payment_days": 0 - }, - { - "salary_component": "Medical Allowance", - "abbr": 'B', - "is_flexible_benefit": 1, - "pay_against_benefit_claim": 0, - "type": "Earning", - "max_benefit_amount": 15000 - }, - { - "salary_component": "Performance Bonus", - "abbr": 'B', - "type": "Earning" - } - ]) + data.extend( + [ + { + "salary_component": "Leave Travel Allowance", + "abbr": "B", + "is_flexible_benefit": 1, + "type": "Earning", + "pay_against_benefit_claim": 1, + "max_benefit_amount": 100000, + "depends_on_payment_days": 0, + }, + { + "salary_component": "Medical Allowance", + "abbr": "B", + "is_flexible_benefit": 1, + "pay_against_benefit_claim": 0, + "type": "Earning", + "max_benefit_amount": 15000, + }, + {"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"}, + ] + ) if setup or test_tax: make_salary_component(data, test_tax, company_list) - data.append({ - "salary_component": 'Basic Salary', - "abbr":'BS', - "condition": 'base < 10000', - "formula": 'base*.2', - "type": "Earning", - "amount_based_on_formula": 1 - }) + data.append( + { + "salary_component": "Basic Salary", + "abbr": "BS", + "condition": "base < 10000", + "formula": "base*.2", + "type": "Earning", + "amount_based_on_formula": 1, + } + ) return data + def make_deduction_salary_component(setup=False, test_tax=False, company_list=None): - data = [ + data = [ { - "salary_component": 'Professional Tax', - "abbr":'PT', + "salary_component": "Professional Tax", + "abbr": "PT", "type": "Deduction", "amount": 200, - "exempted_from_income_tax": 1 - + "exempted_from_income_tax": 1, } ] if not test_tax: - data.append({ - "salary_component": 'TDS', - "abbr":'T', - "condition": 'employment_type=="Intern"', - "type": "Deduction", - "round_to_the_nearest_integer": 1 - }) + data.append( + { + "salary_component": "TDS", + "abbr": "T", + "condition": 'employment_type=="Intern"', + "type": "Deduction", + "round_to_the_nearest_integer": 1, + } + ) else: - data.append({ - "salary_component": 'TDS', - "abbr":'T', - "type": "Deduction", - "depends_on_payment_days": 0, - "variable_based_on_taxable_salary": 1, - "round_to_the_nearest_integer": 1 - }) + data.append( + { + "salary_component": "TDS", + "abbr": "T", + "type": "Deduction", + "depends_on_payment_days": 0, + "variable_based_on_taxable_salary": 1, + "round_to_the_nearest_integer": 1, + } + ) if setup or test_tax: make_salary_component(data, test_tax, company_list) return data + def get_tax_paid_in_period(employee): - tax_paid_amount = frappe.db.sql("""select sum(sd.amount) from `tabSalary Detail` + tax_paid_amount = frappe.db.sql( + """select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss where ss.name=sd.parent and ss.employee=%s - and ss.docstatus=1 and sd.salary_component='TDS'""", (employee)) + and ss.docstatus=1 and sd.salary_component='TDS'""", + (employee), + ) return tax_paid_amount[0][0] + def create_exemption_declaration(employee, payroll_period): create_exemption_category() - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": employee, - "payroll_period": payroll_period, - "company": erpnext.get_default_company(), - "currency": erpnext.get_default_currency() - }) - declaration.append("declarations", { - "exemption_sub_category": "_Test Sub Category", - "exemption_category": "_Test Category", - "amount": 100000 - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "payroll_period": payroll_period, + "company": erpnext.get_default_company(), + "currency": erpnext.get_default_currency(), + } + ) + declaration.append( + "declarations", + { + "exemption_sub_category": "_Test Sub Category", + "exemption_category": "_Test Category", + "amount": 100000, + }, + ) declaration.submit() + def create_proof_submission(employee, payroll_period, amount): submission_date = add_months(payroll_period.start_date, random.randint(0, 11)) - proof_submission = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "employee": employee, - "payroll_period": payroll_period.name, - "submission_date": submission_date, - "currency": erpnext.get_default_currency() - }) - proof_submission.append("tax_exemption_proofs", { - "exemption_sub_category": "_Test Sub Category", - "exemption_category": "_Test Category", - "type_of_proof": "Test", "amount": amount - }) + proof_submission = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": employee, + "payroll_period": payroll_period.name, + "submission_date": submission_date, + "currency": erpnext.get_default_currency(), + } + ) + proof_submission.append( + "tax_exemption_proofs", + { + "exemption_sub_category": "_Test Sub Category", + "exemption_category": "_Test Category", + "type_of_proof": "Test", + "amount": amount, + }, + ) proof_submission.submit() return submission_date + def create_benefit_claim(employee, payroll_period, amount, component): claim_date = add_months(payroll_period.start_date, random.randint(0, 11)) - frappe.get_doc({ - "doctype": "Employee Benefit Claim", - "employee": employee, - "claimed_amount": amount, - "claim_date": claim_date, - "earning_component": component, - "currency": erpnext.get_default_currency() - }).submit() + frappe.get_doc( + { + "doctype": "Employee Benefit Claim", + "employee": employee, + "claimed_amount": amount, + "claim_date": claim_date, + "earning_component": component, + "currency": erpnext.get_default_currency(), + } + ).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None, - company=None): + +def create_tax_slab( + payroll_period, + effective_date=None, + allow_tax_exemption=False, + dont_submit=False, + currency=None, + company=None, +): if not currency: currency = erpnext.get_default_currency() @@ -915,17 +1278,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = "from_amount": 250000, "to_amount": 500000, "percent_deduction": 5, - "condition": "annual_taxable_earning > 500000" + "condition": "annual_taxable_earning > 500000", }, - { - "from_amount": 500001, - "to_amount": 1000000, - "percent_deduction": 20 - }, - { - "from_amount": 1000001, - "percent_deduction": 30 - } + {"from_amount": 500001, "to_amount": 1000000, "percent_deduction": 20}, + {"from_amount": 1000001, "percent_deduction": 30}, ] income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency}) @@ -933,7 +1289,7 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency) income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) - income_tax_slab.company = company or '' + income_tax_slab.company = company or "" income_tax_slab.currency = currency if allow_tax_exemption: @@ -943,10 +1299,7 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = for item in slabs: income_tax_slab.append("slabs", item) - income_tax_slab.append("other_taxes_and_charges", { - "description": "cess", - "percent": 4 - }) + income_tax_slab.append("other_taxes_and_charges", {"description": "cess", "percent": 4}) income_tax_slab.save() if not dont_submit: @@ -956,12 +1309,21 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = else: return income_tax_slab_name -def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True, num=12): + +def create_salary_slips_for_payroll_period( + employee, salary_structure, payroll_period, deduct_random=True, num=12 +): deducted_dates = [] i = 0 while i < num: - slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee, - "salary_structure": salary_structure, "frequency": "Monthly"}) + slip = frappe.get_doc( + { + "doctype": "Salary Slip", + "employee": employee, + "salary_structure": salary_structure, + "frequency": "Monthly", + } + ) if i == 0: posting_date = add_days(payroll_period.start_date, 25) else: @@ -980,67 +1342,100 @@ def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_p i += 1 return deducted_dates + def create_additional_salary(employee, payroll_period, amount): salary_date = add_months(payroll_period.start_date, random.randint(0, 11)) - frappe.get_doc({ - "doctype": "Additional Salary", - "employee": employee, - "company": erpnext.get_default_company(), - "salary_component": "Performance Bonus", - "payroll_date": salary_date, - "amount": amount, - "type": "Earning", - "currency": erpnext.get_default_currency() - }).submit() + frappe.get_doc( + { + "doctype": "Additional Salary", + "employee": employee, + "company": erpnext.get_default_company(), + "salary_component": "Performance Bonus", + "payroll_date": salary_date, + "amount": amount, + "type": "Earning", + "currency": erpnext.get_default_currency(), + } + ).submit() return salary_date -def make_leave_application(employee, from_date, to_date, leave_type, company=None): - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee, - leave_type = leave_type, - from_date = from_date, - to_date = to_date, - company = company or erpnext.get_default_company() or "_Test Company", - docstatus = 1, - status = "Approved", - leave_approver = 'test@example.com' - )) - leave_application.submit() + +def make_leave_application( + employee, + from_date, + to_date, + leave_type, + company=None, + half_day=False, + half_day_date=None, + submit=True, +): + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee, + leave_type=leave_type, + from_date=from_date, + to_date=to_date, + half_day=half_day, + half_day_date=half_day_date, + company=company or erpnext.get_default_company() or "_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ).insert() + + if submit: + leave_application.submit() return leave_application + def setup_test(): make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance", "Additional Salary"]: + for dt in [ + "Leave Application", + "Leave Allocation", + "Salary Slip", + "Attendance", + "Additional Salary", + ]: frappe.db.sql("delete from `tab%s`" % dt) make_holiday_list() - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value( + "Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List" + ) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) - frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) - frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) + frappe.db.set_value("HR Settings", None, "leave_status_notification_template", None) + frappe.db.set_value("HR Settings", None, "leave_approval_notification_template", None) -def make_holiday_list(): + +def make_holiday_list(list_name=None, from_date=None, to_date=None): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) - holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List") - if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): - holiday_list = frappe.get_doc({ + name = list_name or "Salary Slip Test Holiday List" + + frappe.delete_doc_if_exists("Holiday List", name, force=True) + + holiday_list = frappe.get_doc( + { "doctype": "Holiday List", - "holiday_list_name": "Salary Slip Test Holiday List", - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "weekly_off": "Sunday" - }).insert() - holiday_list.get_weekly_off_dates() - holiday_list.save() - holiday_list = holiday_list.name + "holiday_list_name": name, + "from_date": from_date or fiscal_year[1], + "to_date": to_date or fiscal_year[2], + "weekly_off": "Sunday", + } + ).insert() + holiday_list.get_weekly_off_dates() + holiday_list.save() + holiday_list = holiday_list.name return holiday_list + def make_salary_structure_for_payment_days_based_component_dependency(): earnings = [ { @@ -1048,7 +1443,7 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "abbr": "P_BS", "type": "Earning", "formula": "base", - "amount_based_on_formula": 1 + "amount_based_on_formula": 1, }, { "salary_component": "HRA - Payment Days", @@ -1056,8 +1451,8 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "type": "Earning", "depends_on_payment_days": 1, "amount_based_on_formula": 1, - "formula": "base * 0.20" - } + "formula": "base * 0.20", + }, ] make_salary_component(earnings, False, company_list=["_Test Company"]) @@ -1068,7 +1463,7 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "abbr": "P_PT", "type": "Deduction", "depends_on_payment_days": 1, - "amount": 200.00 + "amount": 200.00, }, { "salary_component": "P - Employee Provident Fund", @@ -1077,8 +1472,8 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "exempted_from_income_tax": 1, "amount_based_on_formula": 1, "depends_on_payment_days": 0, - "formula": "(gross_pay - P_HRA) * 0.12" - } + "formula": "(gross_pay - P_HRA) * 0.12", + }, ] make_salary_component(deductions, False, company_list=["_Test Company"]) @@ -1093,7 +1488,7 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "company": "_Test Company", "payroll_frequency": "Monthly", "payment_account": get_random("Account", filters={"account_currency": "INR"}), - "currency": "INR" + "currency": "INR", } salary_structure_doc = frappe.get_doc(details) @@ -1109,14 +1504,15 @@ def make_salary_structure_for_payment_days_based_component_dependency(): return salary_structure_doc -def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure): - employee = frappe.db.get_value("Employee", { - "user_id": employee - }, - ["name", "company", "employee_name"], - as_dict=True) - salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": employee})}) +def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure): + employee = frappe.db.get_value( + "Employee", {"user_id": employee}, ["name", "company", "employee_name"], as_dict=True + ) + + salary_slip_name = frappe.db.get_value( + "Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": employee})} + ) if not salary_slip_name: salary_slip = make_salary_slip(salary_structure, employee=employee.name) @@ -1129,16 +1525,21 @@ def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure return salary_slip -def create_recurring_additional_salary(employee, salary_component, amount, from_date, to_date, company=None): - frappe.get_doc({ - "doctype": "Additional Salary", - "employee": employee, - "company": company or erpnext.get_default_company(), - "salary_component": salary_component, - "is_recurring": 1, - "from_date": from_date, - "to_date": to_date, - "amount": amount, - "type": "Earning", - "currency": erpnext.get_default_currency() - }).submit() + +def create_recurring_additional_salary( + employee, salary_component, amount, from_date, to_date, company=None +): + frappe.get_doc( + { + "doctype": "Additional Salary", + "employee": employee, + "company": company or erpnext.get_default_company(), + "salary_component": salary_component, + "is_recurring": 1, + "from_date": from_date, + "to_date": to_date, + "amount": amount, + "type": "Earning", + "currency": erpnext.get_default_currency(), + } + ).submit() diff --git a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json index 7ac453b3c3d..60ed4539385 100644 --- a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json +++ b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json @@ -26,7 +26,7 @@ "fieldname": "total_allocated_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Total Allocated Leave", + "label": "Total Allocated Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -34,7 +34,7 @@ "fieldname": "expired_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Expired Leave", + "label": "Expired Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -42,7 +42,7 @@ "fieldname": "used_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Used Leave", + "label": "Used Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -50,7 +50,7 @@ "fieldname": "pending_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Pending Leave", + "label": "Leave(s) Pending Approval", "no_copy": 1, "read_only": 1 }, @@ -58,7 +58,7 @@ "fieldname": "available_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Available Leave", + "label": "Available Leave(s)", "no_copy": 1, "read_only": 1 } @@ -66,7 +66,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-19 10:47:48.546724", + "modified": "2022-02-28 14:01:32.327204", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip Leave", @@ -74,5 +74,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index ae83c046a5e..c72ada630b0 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -20,12 +20,21 @@ class SalaryStructure(Document): self.validate_component_based_on_tax_slab() def set_missing_values(self): - overwritten_fields = ["depends_on_payment_days", "variable_based_on_taxable_salary", "is_tax_applicable", "is_flexible_benefit"] + overwritten_fields = [ + "depends_on_payment_days", + "variable_based_on_taxable_salary", + "is_tax_applicable", + "is_flexible_benefit", + ] overwritten_fields_if_missing = ["amount_based_on_formula", "formula", "amount"] for table in ["earnings", "deductions"]: for d in self.get(table): - component_default_value = frappe.db.get_value("Salary Component", cstr(d.salary_component), - overwritten_fields + overwritten_fields_if_missing, as_dict=1) + component_default_value = frappe.db.get_value( + "Salary Component", + cstr(d.salary_component), + overwritten_fields + overwritten_fields_if_missing, + as_dict=1, + ) if component_default_value: for fieldname in overwritten_fields: value = component_default_value.get(fieldname) @@ -39,8 +48,11 @@ class SalaryStructure(Document): def validate_component_based_on_tax_slab(self): for row in self.deductions: if row.variable_based_on_taxable_salary and (row.amount or row.formula): - frappe.throw(_("Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary") - .format(row.idx, row.salary_component)) + frappe.throw( + _( + "Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary" + ).format(row.idx, row.salary_component) + ) def validate_amount(self): if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet: @@ -63,16 +75,23 @@ class SalaryStructure(Document): for earning_component in self.earnings: if earning_component.is_flexible_benefit == 1: have_a_flexi = True - max_of_component = frappe.db.get_value("Salary Component", earning_component.salary_component, "max_benefit_amount") + max_of_component = frappe.db.get_value( + "Salary Component", earning_component.salary_component, "max_benefit_amount" + ) flexi_amount += max_of_component if have_a_flexi and flt(self.max_benefits) == 0: frappe.throw(_("Max benefits should be greater than zero to dispense benefits")) if have_a_flexi and flexi_amount and flt(self.max_benefits) > flexi_amount: - frappe.throw(_("Total flexible benefit component amount {0} should not be less than max benefits {1}") - .format(flexi_amount, self.max_benefits)) + frappe.throw( + _( + "Total flexible benefit component amount {0} should not be less than max benefits {1}" + ).format(flexi_amount, self.max_benefits) + ) if not have_a_flexi and flt(self.max_benefits) > 0: - frappe.throw(_("Salary Structure should have flexible benefit component(s) to dispense benefit amount")) + frappe.throw( + _("Salary Structure should have flexible benefit component(s) to dispense benefit amount") + ) def get_employees(self, **kwargs): conditions, values = [], [] @@ -83,58 +102,117 @@ class SalaryStructure(Document): condition_str = " and " + " and ".join(conditions) if conditions else "" - employees = frappe.db.sql_list("select name from tabEmployee where status='Active' {condition}" - .format(condition=condition_str), tuple(values)) + employees = frappe.db.sql_list( + "select name from tabEmployee where status='Active' {condition}".format( + condition=condition_str + ), + tuple(values), + ) return employees @frappe.whitelist() - def assign_salary_structure(self, grade=None, department=None, designation=None, employee=None, - payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): - employees = self.get_employees(company= self.company, grade= grade,department= department,designation= designation,name=employee) + def assign_salary_structure( + self, + grade=None, + department=None, + designation=None, + employee=None, + payroll_payable_account=None, + from_date=None, + base=None, + variable=None, + income_tax_slab=None, + ): + employees = self.get_employees( + company=self.company, grade=grade, department=department, designation=designation, name=employee + ) if employees: if len(employees) > 20: - frappe.enqueue(assign_salary_structure_for_employees, timeout=600, - employees=employees, salary_structure=self, + frappe.enqueue( + assign_salary_structure_for_employees, + timeout=600, + employees=employees, + salary_structure=self, payroll_payable_account=payroll_payable_account, - from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) + from_date=from_date, + base=base, + variable=variable, + income_tax_slab=income_tax_slab, + ) else: - assign_salary_structure_for_employees(employees, self, + assign_salary_structure_for_employees( + employees, + self, payroll_payable_account=payroll_payable_account, - from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) + from_date=from_date, + base=base, + variable=variable, + income_tax_slab=income_tax_slab, + ) else: frappe.msgprint(_("No Employee Found")) - -def assign_salary_structure_for_employees(employees, salary_structure, payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): +def assign_salary_structure_for_employees( + employees, + salary_structure, + payroll_payable_account=None, + from_date=None, + base=None, + variable=None, + income_tax_slab=None, +): salary_structures_assignments = [] existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date) - count=0 + count = 0 for employee in employees: if employee in existing_assignments_for: continue - count +=1 + count += 1 - salary_structures_assignment = create_salary_structures_assignment(employee, - salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab) + salary_structures_assignment = create_salary_structures_assignment( + employee, salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab + ) salary_structures_assignments.append(salary_structures_assignment) - frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures...")) + frappe.publish_progress( + count * 100 / len(set(employees) - set(existing_assignments_for)), + title=_("Assigning Structures..."), + ) if salary_structures_assignments: frappe.msgprint(_("Structures have been assigned successfully")) -def create_salary_structures_assignment(employee, salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab=None): +def create_salary_structures_assignment( + employee, + salary_structure, + payroll_payable_account, + from_date, + base, + variable, + income_tax_slab=None, +): if not payroll_payable_account: - payroll_payable_account = frappe.db.get_value('Company', salary_structure.company, 'default_payroll_payable_account') + payroll_payable_account = frappe.db.get_value( + "Company", salary_structure.company, "default_payroll_payable_account" + ) if not payroll_payable_account: frappe.throw(_('Please set "Default Payroll Payable Account" in Company Defaults')) - payroll_payable_account_currency = frappe.db.get_value('Account', payroll_payable_account, 'account_currency') + payroll_payable_account_currency = frappe.db.get_value( + "Account", payroll_payable_account, "account_currency" + ) company_curency = erpnext.get_company_currency(salary_structure.company) - if payroll_payable_account_currency != salary_structure.currency and payroll_payable_account_currency != company_curency: - frappe.throw(_("Invalid Payroll Payable Account. The account currency must be {0} or {1}").format(salary_structure.currency, company_curency)) + if ( + payroll_payable_account_currency != salary_structure.currency + and payroll_payable_account_currency != company_curency + ): + frappe.throw( + _("Invalid Payroll Payable Account. The account currency must be {0} or {1}").format( + salary_structure.currency, company_curency + ) + ) assignment = frappe.new_doc("Salary Structure Assignment") assignment.employee = employee @@ -146,28 +224,48 @@ def create_salary_structures_assignment(employee, salary_structure, payroll_paya assignment.base = base assignment.variable = variable assignment.income_tax_slab = income_tax_slab - assignment.save(ignore_permissions = True) + assignment.save(ignore_permissions=True) assignment.submit() return assignment.name def get_existing_assignments(employees, salary_structure, from_date): - salary_structures_assignments = frappe.db.sql_list(""" + salary_structures_assignments = frappe.db.sql_list( + """ select distinct employee from `tabSalary Structure Assignment` where salary_structure=%s and employee in (%s) and from_date=%s and company= %s and docstatus=1 - """ % ('%s', ', '.join(['%s']*len(employees)),'%s', '%s'), [salary_structure.name] + employees+[from_date]+[salary_structure.company]) + """ + % ("%s", ", ".join(["%s"] * len(employees)), "%s", "%s"), + [salary_structure.name] + employees + [from_date] + [salary_structure.company], + ) if salary_structures_assignments: - frappe.msgprint(_("Skipping Salary Structure Assignment for the following employees, as Salary Structure Assignment records already exists against them. {0}") - .format("\n".join(salary_structures_assignments))) + frappe.msgprint( + _( + "Skipping Salary Structure Assignment for the following employees, as Salary Structure Assignment records already exists against them. {0}" + ).format("\n".join(salary_structures_assignments)) + ) return salary_structures_assignments + @frappe.whitelist() -def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0, ignore_permissions=False): +def make_salary_slip( + source_name, + target_doc=None, + employee=None, + as_print=False, + print_format=None, + for_preview=0, + ignore_permissions=False, +): def postprocess(source, target): if employee: - employee_details = frappe.db.get_value("Employee", employee, - ["employee_name", "branch", "designation", "department", "payroll_cost_center"], as_dict=1) + employee_details = frappe.db.get_value( + "Employee", + employee, + ["employee_name", "branch", "designation", "department", "payroll_cost_center"], + as_dict=1, + ) target.employee = employee target.employee_name = employee_details.employee_name target.branch = employee_details.branch @@ -175,35 +273,51 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = target.department = employee_details.department target.payroll_cost_center = employee_details.payroll_cost_center if not target.payroll_cost_center and target.department: - target.payroll_cost_center = frappe.db.get_value("Department", target.department, "payroll_cost_center") + target.payroll_cost_center = frappe.db.get_value( + "Department", target.department, "payroll_cost_center" + ) - target.run_method('process_salary_structure', for_preview=for_preview) + target.run_method("process_salary_structure", for_preview=for_preview) - doc = get_mapped_doc("Salary Structure", source_name, { - "Salary Structure": { - "doctype": "Salary Slip", - "field_map": { - "total_earning": "gross_pay", - "name": "salary_structure", - "currency": "currency" + doc = get_mapped_doc( + "Salary Structure", + source_name, + { + "Salary Structure": { + "doctype": "Salary Slip", + "field_map": { + "total_earning": "gross_pay", + "name": "salary_structure", + "currency": "currency", + }, } - } - }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions) + }, + target_doc, + postprocess, + ignore_child_tables=True, + ignore_permissions=ignore_permissions, + ) if cint(as_print): - doc.name = 'Preview for {0}'.format(employee) - return frappe.get_print(doc.doctype, doc.name, doc = doc, print_format = print_format) + doc.name = "Preview for {0}".format(employee) + return frappe.get_print(doc.doctype, doc.name, doc=doc, print_format=print_format) else: return doc @frappe.whitelist() def get_employees(salary_structure): - employees = frappe.get_list('Salary Structure Assignment', - filters={'salary_structure': salary_structure, 'docstatus': 1}, fields=['employee']) + employees = frappe.get_list( + "Salary Structure Assignment", + filters={"salary_structure": salary_structure, "docstatus": 1}, + fields=["employee"], + ) if not employees: - frappe.throw(_("There's no Employee with Salary Structure: {0}. Assign {1} to an Employee to preview Salary Slip").format( - salary_structure, salary_structure)) + frappe.throw( + _( + "There's no Employee with Salary Structure: {0}. Assign {1} to an Employee to preview Salary Slip" + ).format(salary_structure, salary_structure) + ) return list(set([d.employee for d in employees])) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py b/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py index 27eb5ed8b11..cf363b410df 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py @@ -1,17 +1,9 @@ - - def get_data(): return { - 'fieldname': 'salary_structure', - 'non_standard_fieldnames': { - 'Employee Grade': 'default_salary_structure' - }, - 'transactions': [ - { - 'items': ['Salary Structure Assignment', 'Salary Slip'] - }, - { - 'items': ['Employee Grade'] - }, - ] + "fieldname": "salary_structure", + "non_standard_fieldnames": {"Employee Grade": "default_salary_structure"}, + "transactions": [ + {"items": ["Salary Structure Assignment", "Salary Slip"]}, + {"items": ["Employee Grade"]}, + ], } diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e2d0d1c864c..def622bf80e 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -22,25 +22,33 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar test_dependencies = ["Fiscal Year"] + class TestSalaryStructure(unittest.TestCase): def setUp(self): for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment"]: frappe.db.sql("delete from `tab%s`" % dt) self.make_holiday_list() - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Structure Test Holiday List") + frappe.db.set_value( + "Company", + erpnext.get_default_company(), + "default_holiday_list", + "Salary Structure Test Holiday List", + ) make_employee("test_employee@salary.com") make_employee("test_employee_2@salary.com") def make_holiday_list(self): if not frappe.db.get_value("Holiday List", "Salary Structure Test Holiday List"): - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "Salary Structure Test Holiday List", - "from_date": nowdate(), - "to_date": add_years(nowdate(), 1), - "weekly_off": "Sunday" - }).insert() + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": "Salary Structure Test Holiday List", + "from_date": nowdate(), + "to_date": add_years(nowdate(), 1), + "weekly_off": "Sunday", + } + ).insert() holiday_list.get_weekly_off_dates() holiday_list.save() @@ -48,31 +56,33 @@ class TestSalaryStructure(unittest.TestCase): emp = make_employee("test_employee_3@salary.com") - sal_struct = make_salary_structure("Salary Structure 2", "Monthly", dont_submit = True) + sal_struct = make_salary_structure("Salary Structure 2", "Monthly", dont_submit=True) sal_struct.earnings = [sal_struct.earnings[0]] sal_struct.earnings[0].amount_based_on_formula = 1 - sal_struct.earnings[0].formula = "base" + sal_struct.earnings[0].formula = "base" sal_struct.deductions = [sal_struct.deductions[0]] sal_struct.deductions[0].amount_based_on_formula = 1 sal_struct.deductions[0].condition = "gross_pay > 100" - sal_struct.deductions[0].formula = "gross_pay * 0.2" + sal_struct.deductions[0].formula = "gross_pay * 0.2" sal_struct.submit() assignment = create_salary_structure_assignment(emp, "Salary Structure 2") - ss = make_salary_slip(sal_struct.name, employee = emp) + ss = make_salary_slip(sal_struct.name, employee=emp) self.assertEqual(assignment.base * 0.2, ss.deductions[0].amount) def test_amount_totals(self): frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) - sal_slip = frappe.get_value("Salary Slip", {"employee_name":"test_employee_2@salary.com"}) + sal_slip = frappe.get_value("Salary Slip", {"employee_name": "test_employee_2@salary.com"}) if not sal_slip: - sal_slip = make_employee_salary_slip("test_employee_2@salary.com", "Monthly", "Salary Structure Sample") - self.assertEqual(sal_slip.get("salary_structure"), 'Salary Structure Sample') + sal_slip = make_employee_salary_slip( + "test_employee_2@salary.com", "Monthly", "Salary Structure Sample" + ) + self.assertEqual(sal_slip.get("salary_structure"), "Salary Structure Sample") self.assertEqual(sal_slip.get("earnings")[0].amount, 50000) self.assertEqual(sal_slip.get("earnings")[1].amount, 3000) self.assertEqual(sal_slip.get("earnings")[2].amount, 25000) @@ -84,12 +94,12 @@ class TestSalaryStructure(unittest.TestCase): salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", dont_submit=True) for row in salary_structure.earnings: - row.formula = "\n%s\n\n"%row.formula - row.condition = "\n%s\n\n"%row.condition + row.formula = "\n%s\n\n" % row.formula + row.condition = "\n%s\n\n" % row.condition for row in salary_structure.deductions: - row.formula = "\n%s\n\n"%row.formula - row.condition = "\n%s\n\n"%row.condition + row.formula = "\n%s\n\n" % row.formula + row.condition = "\n%s\n\n" % row.condition salary_structure.save() @@ -101,29 +111,47 @@ class TestSalaryStructure(unittest.TestCase): def test_salary_structures_assignment(self): company_currency = erpnext.get_default_currency() - salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", currency=company_currency) + salary_structure = make_salary_structure( + "Salary Structure Sample", "Monthly", currency=company_currency + ) employee = "test_assign_stucture@salary.com" employee_doc_name = make_employee(employee) # clear the already assigned stuctures - frappe.db.sql('''delete from `tabSalary Structure Assignment` where employee=%s and salary_structure=%s ''', - ("test_assign_stucture@salary.com",salary_structure.name)) - #test structure_assignment - salary_structure.assign_salary_structure(employee=employee_doc_name,from_date='2013-01-01',base=5000,variable=200) - salary_structure_assignment = frappe.get_doc("Salary Structure Assignment",{'employee':employee_doc_name, 'from_date':'2013-01-01'}) + frappe.db.sql( + """delete from `tabSalary Structure Assignment` where employee=%s and salary_structure=%s """, + ("test_assign_stucture@salary.com", salary_structure.name), + ) + # test structure_assignment + salary_structure.assign_salary_structure( + employee=employee_doc_name, from_date="2013-01-01", base=5000, variable=200 + ) + salary_structure_assignment = frappe.get_doc( + "Salary Structure Assignment", {"employee": employee_doc_name, "from_date": "2013-01-01"} + ) self.assertEqual(salary_structure_assignment.docstatus, 1) self.assertEqual(salary_structure_assignment.base, 5000) self.assertEqual(salary_structure_assignment.variable, 200) def test_multi_currency_salary_structure(self): make_employee("test_muti_currency_employee@salary.com") - sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency='USD') - self.assertEqual(sal_struct.currency, 'USD') + 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, - from_date=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)) + frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure)) if frappe.db.exists("Salary Structure", salary_structure): frappe.db.delete("Salary Structure", salary_structure) @@ -132,11 +160,15 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, "doctype": "Salary Structure", "name": salary_structure, "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "earnings": make_earning_salary_component( + setup=True, test_tax=test_tax, company_list=["_Test Company"] + ), + "deductions": make_deduction_salary_component( + setup=True, test_tax=test_tax, company_list=["_Test Company"] + ), "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account", filters={'account_currency': currency}), - "currency": currency + "payment_account": get_random("Account", filters={"account_currency": currency}), + "currency": currency, } if other_details and isinstance(other_details, dict): details.update(other_details) @@ -145,31 +177,41 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, if not dont_submit: salary_structure_doc.submit() - filters = {'employee':employee, 'docstatus': 1} + 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 + filters["from_date"] = from_date - if employee and not frappe.db.get_value("Salary Structure Assignment", - filters) and salary_structure_doc.docstatus==1: + if ( + employee + and not frappe.db.get_value("Salary Structure Assignment", 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 + payroll_period=payroll_period, ) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(), - payroll_period=None): + +def create_salary_structure_assignment( + employee, + salary_structure, + from_date=None, + company=None, + currency=erpnext.get_default_currency(), + payroll_period=None, +): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): - frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) + frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee)) if not payroll_period: payroll_period = create_payroll_period() @@ -200,6 +242,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.submit() return salary_structure_assignment + def get_payable_account(company=None): if not company: company = erpnext.get_default_company() diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index e1ff9ca9f04..e34e48e6c05 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -8,7 +8,9 @@ from frappe.model.document import Document from frappe.utils import getdate -class DuplicateAssignment(frappe.ValidationError): pass +class DuplicateAssignment(frappe.ValidationError): + pass + class SalaryStructureAssignment(Document): def validate(self): @@ -17,56 +19,90 @@ class SalaryStructureAssignment(Document): self.set_payroll_payable_account() def validate_dates(self): - joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.db.get_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) if self.from_date: - if frappe.db.exists("Salary Structure Assignment", {"employee": self.employee, "from_date": self.from_date, "docstatus": 1}): + if frappe.db.exists( + "Salary Structure Assignment", + {"employee": self.employee, "from_date": self.from_date, "docstatus": 1}, + ): frappe.throw(_("Salary Structure Assignment for Employee already exists"), DuplicateAssignment) if joining_date and getdate(self.from_date) < joining_date: - frappe.throw(_("From Date {0} cannot be before employee's joining Date {1}") - .format(self.from_date, joining_date)) + frappe.throw( + _("From Date {0} cannot be before employee's joining Date {1}").format( + self.from_date, joining_date + ) + ) # flag - old_employee is for migrating the old employees data via patch if relieving_date and getdate(self.from_date) > relieving_date and not self.flags.old_employee: - frappe.throw(_("From Date {0} cannot be after employee's relieving Date {1}") - .format(self.from_date, relieving_date)) + frappe.throw( + _("From Date {0} cannot be after employee's relieving Date {1}").format( + self.from_date, relieving_date + ) + ) def validate_income_tax_slab(self): if not self.income_tax_slab: return - income_tax_slab_currency = frappe.db.get_value('Income Tax Slab', self.income_tax_slab, 'currency') + income_tax_slab_currency = frappe.db.get_value( + "Income Tax Slab", self.income_tax_slab, "currency" + ) if self.currency != income_tax_slab_currency: - frappe.throw(_("Currency of selected Income Tax Slab should be {0} instead of {1}").format(self.currency, income_tax_slab_currency)) + frappe.throw( + _("Currency of selected Income Tax Slab should be {0} instead of {1}").format( + self.currency, income_tax_slab_currency + ) + ) def set_payroll_payable_account(self): if not self.payroll_payable_account: - payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account') + payroll_payable_account = frappe.db.get_value( + "Company", self.company, "default_payroll_payable_account" + ) if not payroll_payable_account: payroll_payable_account = frappe.db.get_value( - "Account", { - "account_name": _("Payroll Payable"), "company": self.company, "account_currency": frappe.db.get_value( - "Company", self.company, "default_currency"), "is_group": 0}) + "Account", + { + "account_name": _("Payroll Payable"), + "company": self.company, + "account_currency": frappe.db.get_value("Company", self.company, "default_currency"), + "is_group": 0, + }, + ) self.payroll_payable_account = payroll_payable_account + def get_assigned_salary_structure(employee, on_date): if not employee or not on_date: return None - salary_structure = frappe.db.sql(""" + salary_structure = frappe.db.sql( + """ select salary_structure from `tabSalary Structure Assignment` where employee=%(employee)s and docstatus = 1 - and %(on_date)s >= from_date order by from_date desc limit 1""", { - 'employee': employee, - 'on_date': on_date, - }) + and %(on_date)s >= from_date order by from_date desc limit 1""", + { + "employee": employee, + "on_date": on_date, + }, + ) return salary_structure[0][0] if salary_structure else None + @frappe.whitelist() def get_employee_currency(employee): - employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency') + employee_currency = frappe.db.get_value( + "Salary Structure Assignment", {"employee": employee}, "currency" + ) if not employee_currency: - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(employee)) + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + employee + ) + ) return employee_currency diff --git a/erpnext/payroll/notification/retention_bonus/retention_bonus.py b/erpnext/payroll/notification/retention_bonus/retention_bonus.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/payroll/notification/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/notification/retention_bonus/retention_bonus.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py index 6c3bd37b043..9d8efff8218 100644 --- a/erpnext/payroll/report/bank_remittance/bank_remittance.py +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py @@ -13,63 +13,47 @@ def execute(filters=None): "fieldtype": "Link", "fieldname": "payroll_no", "options": "Payroll Entry", - "width": 150 + "width": 150, }, { "label": _("Debit A/C Number"), "fieldtype": "Int", "fieldname": "debit_account", "hidden": 1, - "width": 200 - }, - { - "label": _("Payment Date"), - "fieldtype": "Data", - "fieldname": "payment_date", - "width": 100 + "width": 200, }, + {"label": _("Payment Date"), "fieldtype": "Data", "fieldname": "payment_date", "width": 100}, { "label": _("Employee Name"), "fieldtype": "Link", "fieldname": "employee_name", "options": "Employee", - "width": 200 - }, - { - "label": _("Bank Name"), - "fieldtype": "Data", - "fieldname": "bank_name", - "width": 50 + "width": 200, }, + {"label": _("Bank Name"), "fieldtype": "Data", "fieldname": "bank_name", "width": 50}, { "label": _("Employee A/C Number"), "fieldtype": "Int", "fieldname": "employee_account_no", - "width": 50 - } + "width": 50, + }, ] - if frappe.db.has_column('Employee', 'ifsc_code'): - columns.append({ - "label": _("IFSC Code"), - "fieldtype": "Data", - "fieldname": "bank_code", - "width": 100 - }) + if frappe.db.has_column("Employee", "ifsc_code"): + columns.append( + {"label": _("IFSC Code"), "fieldtype": "Data", "fieldname": "bank_code", "width": 100} + ) - columns += [{ - "label": _("Currency"), - "fieldtype": "Data", - "fieldname": "currency", - "width": 50 - }, - { - "label": _("Net Salary Amount"), - "fieldtype": "Currency", - "options": "currency", - "fieldname": "amount", - "width": 100 - }] + columns += [ + {"label": _("Currency"), "fieldtype": "Data", "fieldname": "currency", "width": 50}, + { + "label": _("Net Salary Amount"), + "fieldtype": "Currency", + "options": "currency", + "fieldname": "amount", + "width": 100, + }, + ] data = [] @@ -77,41 +61,48 @@ def execute(filters=None): payroll_entries = get_payroll_entries(accounts, filters) salary_slips = get_salary_slips(payroll_entries) - if frappe.db.has_column('Employee', 'ifsc_code'): + if frappe.db.has_column("Employee", "ifsc_code"): get_emp_bank_ifsc_code(salary_slips) for salary in salary_slips: - if salary.bank_name and salary.bank_account_no and salary.debit_acc_no and salary.status in ["Submitted", "Paid"]: + if ( + salary.bank_name + and salary.bank_account_no + and salary.debit_acc_no + and salary.status in ["Submitted", "Paid"] + ): row = { "payroll_no": salary.payroll_entry, "debit_account": salary.debit_acc_no, - "payment_date": frappe.utils.formatdate(salary.modified.strftime('%Y-%m-%d')), + "payment_date": frappe.utils.formatdate(salary.modified.strftime("%Y-%m-%d")), "bank_name": salary.bank_name, "employee_account_no": salary.bank_account_no, "bank_code": salary.ifsc_code, - "employee_name": salary.employee+": " + salary.employee_name, - "currency": frappe.get_cached_value('Company', filters.company, 'default_currency'), + "employee_name": salary.employee + ": " + salary.employee_name, + "currency": frappe.get_cached_value("Company", filters.company, "default_currency"), "amount": salary.net_pay, } data.append(row) return columns, data + def get_bank_accounts(): accounts = [d.name for d in get_all("Account", filters={"account_type": "Bank"})] return accounts + def get_payroll_entries(accounts, filters): payroll_filter = [ - ('payment_account', 'IN', accounts), - ('number_of_employees', '>', 0), - ('Company', '=', filters.company) + ("payment_account", "IN", accounts), + ("number_of_employees", ">", 0), + ("Company", "=", filters.company), ] if filters.to_date: - payroll_filter.append(('posting_date', '<', filters.to_date)) + payroll_filter.append(("posting_date", "<", filters.to_date)) if filters.from_date: - payroll_filter.append(('posting_date', '>', filters.from_date)) + payroll_filter.append(("posting_date", ">", filters.from_date)) entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"]) @@ -119,10 +110,22 @@ def get_payroll_entries(accounts, filters): entries = set_company_account(payment_accounts, entries) return entries + def get_salary_slips(payroll_entries): - payroll = [d.name for d in payroll_entries] - salary_slips = get_all("Salary Slip", filters = [("payroll_entry", "IN", payroll)], - fields = ["modified", "net_pay", "bank_name", "bank_account_no", "payroll_entry", "employee", "employee_name", "status"] + payroll = [d.name for d in payroll_entries] + salary_slips = get_all( + "Salary Slip", + filters=[("payroll_entry", "IN", payroll)], + fields=[ + "modified", + "net_pay", + "bank_name", + "bank_account_no", + "payroll_entry", + "employee", + "employee_name", + "status", + ], ) payroll_entry_map = {} @@ -132,12 +135,13 @@ def get_salary_slips(payroll_entries): # appending company debit accounts for slip in salary_slips: if slip.payroll_entry: - slip["debit_acc_no"] = payroll_entry_map[slip.payroll_entry]['company_account'] + slip["debit_acc_no"] = payroll_entry_map[slip.payroll_entry]["company_account"] else: slip["debit_acc_no"] = None return salary_slips + def get_emp_bank_ifsc_code(salary_slips): emp_names = [d.employee for d in salary_slips] ifsc_codes = get_all("Employee", [("name", "IN", emp_names)], ["ifsc_code", "name"]) @@ -147,20 +151,23 @@ def get_emp_bank_ifsc_code(salary_slips): ifsc_codes_map[code.name] = code for slip in salary_slips: - slip["ifsc_code"] = ifsc_codes_map[code.name]['ifsc_code'] + slip["ifsc_code"] = ifsc_codes_map[code.name]["ifsc_code"] return salary_slips + def set_company_account(payment_accounts, payroll_entries): - company_accounts = get_all("Bank Account", [("account", "in", payment_accounts)], ["account", "bank_account_no"]) + company_accounts = get_all( + "Bank Account", [("account", "in", payment_accounts)], ["account", "bank_account_no"] + ) company_accounts_map = {} for acc in company_accounts: company_accounts_map[acc.account] = acc for entry in payroll_entries: - company_account = '' + company_account = "" if entry.payment_account in company_accounts_map: - company_account = company_accounts_map[entry.payment_account]['bank_account_no'] + company_account = company_accounts_map[entry.payment_account]["bank_account_no"] entry["company_account"] = company_account return payroll_entries diff --git a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py index 75a9f97ea58..ccf16565c1f 100644 --- a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py +++ b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py @@ -14,6 +14,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -21,65 +22,55 @@ def get_columns(filters): "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 - }] + "width": 160, + }, + ] if erpnext.get_region() == "India": - columns.append({ - "label": _("PAN Number"), - "fieldname": "pan_number", - "fieldtype": "Data", - "width": 140 - }) + columns.append( + {"label": _("PAN Number"), "fieldname": "pan_number", "fieldtype": "Data", "width": 140} + ) - columns += [{ - "label": _("Income Tax Component"), - "fieldname": "it_comp", - "fieldtype": "Data", - "width": 170 - }, + columns += [ + {"label": _("Income Tax Component"), "fieldname": "it_comp", "fieldtype": "Data", "width": 170}, { "label": _("Income Tax Amount"), "fieldname": "it_amount", "fieldtype": "Currency", "options": "currency", - "width": 140 + "width": 140, }, { "label": _("Gross Pay"), "fieldname": "gross_pay", "fieldtype": "Currency", "options": "currency", - "width": 140 + "width": 140, }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 140 - } + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 140}, ] return columns + def get_conditions(filters): conditions = [""] if filters.get("department"): - conditions.append("sal.department = '%s' " % (filters["department"]) ) + conditions.append("sal.department = '%s' " % (filters["department"])) if filters.get("branch"): - conditions.append("sal.branch = '%s' " % (filters["branch"]) ) + conditions.append("sal.branch = '%s' " % (filters["branch"])) if filters.get("company"): - conditions.append("sal.company = '%s' " % (filters["company"]) ) + conditions.append("sal.company = '%s' " % (filters["company"])) if filters.get("month"): conditions.append("month(sal.start_date) = '%s' " % (filters["month"])) @@ -95,10 +86,14 @@ def get_data(filters): data = [] if erpnext.get_region() == "India": - employee_pan_dict = frappe._dict(frappe.db.sql(""" select employee, pan_number from `tabEmployee`""")) + employee_pan_dict = frappe._dict( + frappe.db.sql(""" select employee, pan_number from `tabEmployee`""") + ) - component_types = frappe.db.sql(""" select name from `tabSalary Component` - where is_income_tax_component = 1 """) + component_types = frappe.db.sql( + """ select name from `tabSalary Component` + where is_income_tax_component = 1 """ + ) component_types = [comp_type[0] for comp_type in component_types] @@ -107,14 +102,19 @@ def get_data(filters): conditions = get_conditions(filters) - entry = frappe.db.sql(""" select sal.employee, sal.employee_name, sal.posting_date, ded.salary_component, ded.amount,sal.gross_pay + entry = frappe.db.sql( + """ select sal.employee, sal.employee_name, sal.posting_date, ded.salary_component, ded.amount,sal.gross_pay from `tabSalary Slip` sal, `tabSalary Detail` ded where sal.name = ded.parent and ded.parentfield = 'deductions' and ded.parenttype = 'Salary Slip' and sal.docstatus = 1 %s and ded.salary_component in (%s) - """ % (conditions , ", ".join(['%s']*len(component_types))), tuple(component_types), as_dict=1) + """ + % (conditions, ", ".join(["%s"] * len(component_types))), + tuple(component_types), + as_dict=1, + ) for d in entry: @@ -125,7 +125,7 @@ def get_data(filters): "posting_date": d.posting_date, # "pan_number": employee_pan_dict.get(d.employee), "it_amount": d.amount, - "gross_pay": d.gross_pay + "gross_pay": d.gross_pay, } if erpnext.get_region() == "India": diff --git a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py index fa68575e688..e5348df8864 100644 --- a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py +++ b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py @@ -19,42 +19,39 @@ def execute(filters=None): columns = get_columns(filters, mode_of_payments) data, total_rows, report_summary = get_data(filters, mode_of_payments) - chart = get_chart(mode_of_payments, total_rows) + chart = get_chart(mode_of_payments, total_rows) return columns, data, None, chart, report_summary + def get_columns(filters, mode_of_payments): - columns = [{ - "label": _("Branch"), - "options": "Branch", - "fieldname": "branch", - "fieldtype": "Link", - "width": 200 - }] + columns = [ + { + "label": _("Branch"), + "options": "Branch", + "fieldname": "branch", + "fieldtype": "Link", + "width": 200, + } + ] for mode in mode_of_payments: - columns.append({ - "label": _(mode), - "fieldname": mode, - "fieldtype": "Currency", - "width": 160 - }) + columns.append({"label": _(mode), "fieldname": mode, "fieldtype": "Currency", "width": 160}) - columns.append({ - "label": _("Total"), - "fieldname": "total", - "fieldtype": "Currency", - "width": 140 - }) + columns.append({"label": _("Total"), "fieldname": "total", "fieldtype": "Currency", "width": 140}) return columns + def get_payment_modes(): - mode_of_payments = frappe.db.sql_list(""" + mode_of_payments = frappe.db.sql_list( + """ select distinct mode_of_payment from `tabSalary Slip` where docstatus = 1 - """) + """ + ) return mode_of_payments + def prepare_data(entry): branch_wise_entries = {} gross_pay = 0 @@ -68,36 +65,42 @@ def prepare_data(entry): return branch_wise_entries, gross_pay + def get_data(filters, mode_of_payments): data = [] conditions = get_conditions(filters) - entry = frappe.db.sql(""" + entry = frappe.db.sql( + """ select branch, mode_of_payment, sum(net_pay) as net_pay, sum(gross_pay) as gross_pay from `tabSalary Slip` sal where docstatus = 1 %s group by branch, mode_of_payment - """ % (conditions), as_dict=1) + """ + % (conditions), + as_dict=1, + ) branch_wise_entries, gross_pay = prepare_data(entry) - branches = frappe.db.sql_list(""" + branches = frappe.db.sql_list( + """ select distinct branch from `tabSalary Slip` sal where docstatus = 1 %s - """ % (conditions)) + """ + % (conditions) + ) total_row = {"total": 0, "branch": "Total"} for branch in branches: total = 0 - row = { - "branch": branch - } + row = {"branch": branch} for mode in mode_of_payments: if branch_wise_entries.get(branch).get(mode): row[mode] = branch_wise_entries.get(branch).get(mode) - total += branch_wise_entries.get(branch).get(mode) + total += branch_wise_entries.get(branch).get(mode) row["total"] = total data.append(row) @@ -110,24 +113,18 @@ def get_data(filters, mode_of_payments): if data: data.append(total_row) data.append({}) - data.append({ - "branch": "Total Gross Pay", - mode_of_payments[0]:gross_pay - }) - data.append({ - "branch": "Total Deductions", - mode_of_payments[0]:total_deductions - }) - data.append({ - "branch": "Total Net Pay", - mode_of_payments[0]:total_row.get("total") - }) + data.append({"branch": "Total Gross Pay", mode_of_payments[0]: gross_pay}) + data.append({"branch": "Total Deductions", mode_of_payments[0]: total_deductions}) + data.append({"branch": "Total Net Pay", mode_of_payments[0]: total_row.get("total")}) currency = erpnext.get_company_currency(filters.company) - report_summary = get_report_summary(gross_pay, total_deductions, total_row.get("total"), currency) + report_summary = get_report_summary( + gross_pay, total_deductions, total_row.get("total"), currency + ) return data, total_row, report_summary + def get_total_based_on_mode_of_payment(data, mode_of_payments): total = 0 @@ -140,6 +137,7 @@ def get_total_based_on_mode_of_payment(data, mode_of_payments): total_row["total"] = total return total_row + def get_report_summary(gross_pay, total_deductions, net_pay, currency): return [ { @@ -147,24 +145,25 @@ def get_report_summary(gross_pay, total_deductions, net_pay, currency): "label": "Total Gross Pay", "indicator": "Green", "datatype": "Currency", - "currency": currency + "currency": currency, }, { "value": total_deductions, "label": "Total Deduction", "datatype": "Currency", "indicator": "Red", - "currency": currency + "currency": currency, }, { "value": net_pay, "label": "Total Net Pay", "datatype": "Currency", "indicator": "Blue", - "currency": currency - } + "currency": currency, + }, ] + def get_chart(mode_of_payments, data): if data: values = [] @@ -175,10 +174,7 @@ def get_chart(mode_of_payments, data): labels.append([mode]) chart = { - "data": { - "labels": labels, - "datasets": [{'name': 'Mode Of Payments', "values": values}] - } + "data": {"labels": labels, "datasets": [{"name": "Mode Of Payments", "values": values}]} } - chart['type'] = "bar" + chart["type"] = "bar" return chart diff --git a/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py index 578c8164009..4f9370b742b 100644 --- a/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py +++ b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py @@ -14,6 +14,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -21,71 +22,52 @@ def get_columns(filters): "options": "Branch", "fieldname": "branch", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 + "width": 160, }, { "label": _("Employee"), - "options":"Employee", + "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Gross Pay"), "fieldname": "gross_pay", "fieldtype": "Currency", "options": "currency", - "width": 140 - }, - { - "label": _("Bank"), - "fieldname": "bank", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("Account No"), - "fieldname": "account_no", - "fieldtype": "Data", - "width": 140 + "width": 140, }, + {"label": _("Bank"), "fieldname": "bank", "fieldtype": "Data", "width": 140}, + {"label": _("Account No"), "fieldname": "account_no", "fieldtype": "Data", "width": 140}, ] if erpnext.get_region() == "India": columns += [ - { - "label": _("IFSC"), - "fieldname": "ifsc", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("MICR"), - "fieldname": "micr", - "fieldtype": "Data", - "width": 140 - } + {"label": _("IFSC"), "fieldname": "ifsc", "fieldtype": "Data", "width": 140}, + {"label": _("MICR"), "fieldname": "micr", "fieldtype": "Data", "width": 140}, ] return columns + def get_conditions(filters): conditions = [""] if filters.get("department"): - conditions.append("department = '%s' " % (filters["department"]) ) + conditions.append("department = '%s' " % (filters["department"])) if filters.get("branch"): - conditions.append("branch = '%s' " % (filters["branch"]) ) + conditions.append("branch = '%s' " % (filters["branch"])) if filters.get("company"): - conditions.append("company = '%s' " % (filters["company"]) ) + conditions.append("company = '%s' " % (filters["company"])) if filters.get("month"): conditions.append("month(start_date) = '%s' " % (filters["month"])) @@ -95,6 +77,7 @@ def get_conditions(filters): return " and ".join(conditions) + def get_data(filters): data = [] @@ -103,36 +86,39 @@ def get_data(filters): if erpnext.get_region() == "India": fields += ["ifsc_code", "micr_code"] - - employee_details = frappe.get_list("Employee", fields = fields) + employee_details = frappe.get_list("Employee", fields=fields) employee_data_dict = {} for d in employee_details: employee_data_dict.setdefault( - d.employee,{ - "bank_ac_no" : d.bank_ac_no, - "ifsc_code" : d.ifsc_code or None, - "micr_code" : d.micr_code or None, - "branch" : d.branch, - "salary_mode" : d.salary_mode, - "bank_name": d.bank_name - } + d.employee, + { + "bank_ac_no": d.bank_ac_no, + "ifsc_code": d.ifsc_code or None, + "micr_code": d.micr_code or None, + "branch": d.branch, + "salary_mode": d.salary_mode, + "bank_name": d.bank_name, + }, ) conditions = get_conditions(filters) - entry = frappe.db.sql(""" select employee, employee_name, gross_pay + entry = frappe.db.sql( + """ select employee, employee_name, gross_pay from `tabSalary Slip` where docstatus = 1 %s """ - %(conditions), as_dict =1) + % (conditions), + as_dict=1, + ) for d in entry: employee = { - "branch" : employee_data_dict.get(d.employee).get("branch"), - "employee_name" : d.employee_name, - "employee" : d.employee, - "gross_pay" : d.gross_pay, + "branch": employee_data_dict.get(d.employee).get("branch"), + "employee_name": d.employee_name, + "employee": d.employee, + "gross_pay": d.gross_pay, } if employee_data_dict.get(d.employee).get("salary_mode") == "Bank": @@ -144,7 +130,9 @@ def get_data(filters): else: employee["account_no"] = employee_data_dict.get(d.employee).get("salary_mode") - if filters.get("type") and employee_data_dict.get(d.employee).get("salary_mode") == filters.get("type"): + if filters.get("type") and employee_data_dict.get(d.employee).get("salary_mode") == filters.get( + "type" + ): data.append(employee) elif not filters.get("type"): data.append(employee) diff --git a/erpnext/payroll/report/salary_register/salary_register.py b/erpnext/payroll/report/salary_register/salary_register.py index 78deb227783..0a62b43a8ea 100644 --- a/erpnext/payroll/report/salary_register/salary_register.py +++ b/erpnext/payroll/report/salary_register/salary_register.py @@ -10,29 +10,46 @@ import erpnext def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} currency = None - if filters.get('currency'): - currency = filters.get('currency') + if filters.get("currency"): + currency = filters.get("currency") company_currency = erpnext.get_company_currency(filters.get("company")) salary_slips = get_salary_slips(filters, company_currency) - if not salary_slips: return [], [] + if not salary_slips: + return [], [] columns, earning_types, ded_types = get_columns(salary_slips) ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) - ss_ded_map = get_ss_ded_map(salary_slips,currency, company_currency) + ss_ded_map = get_ss_ded_map(salary_slips, currency, company_currency) doj_map = get_employee_doj_map() data = [] for ss in salary_slips: - row = [ss.name, ss.employee, ss.employee_name, doj_map.get(ss.employee), ss.branch, ss.department, ss.designation, - ss.company, ss.start_date, ss.end_date, ss.leave_without_pay, ss.payment_days] - - if ss.branch is not None: columns[3] = columns[3].replace('-1','120') - if ss.department is not None: columns[4] = columns[4].replace('-1','120') - if ss.designation is not None: columns[5] = columns[5].replace('-1','120') - if ss.leave_without_pay is not None: columns[9] = columns[9].replace('-1','130') + row = [ + ss.name, + ss.employee, + ss.employee_name, + doj_map.get(ss.employee), + ss.branch, + ss.department, + ss.designation, + ss.company, + ss.start_date, + ss.end_date, + ss.leave_without_pay, + ss.payment_days, + ] + if ss.branch is not None: + columns[3] = columns[3].replace("-1", "120") + if ss.department is not None: + columns[4] = columns[4].replace("-1", "120") + if ss.designation is not None: + columns[5] = columns[5].replace("-1", "120") + if ss.leave_without_pay is not None: + columns[9] = columns[9].replace("-1", "130") for e in earning_types: row.append(ss_earning_map.get(ss.name, {}).get(e)) @@ -48,7 +65,10 @@ def execute(filters=None): row.append(ss.total_loan_repayment) if currency == company_currency: - row += [flt(ss.total_deduction) * flt(ss.exchange_rate), flt(ss.net_pay) * flt(ss.exchange_rate)] + row += [ + flt(ss.total_deduction) * flt(ss.exchange_rate), + flt(ss.net_pay) * flt(ss.exchange_rate), + ] else: row += [ss.total_deduction, ss.net_pay] row.append(currency or company_currency) @@ -56,53 +76,81 @@ def execute(filters=None): return columns, data + def get_columns(salary_slips): + """ + columns = [ + _("Salary Slip ID") + ":Link/Salary Slip:150", + _("Employee") + ":Link/Employee:120", + _("Employee Name") + "::140", + _("Date of Joining") + "::80", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Company") + ":Link/Company:120", + _("Start Date") + "::80", + _("End Date") + "::80", + _("Leave Without Pay") + ":Float:130", + _("Payment Days") + ":Float:120", + _("Currency") + ":Link/Currency:80" + ] """ columns = [ _("Salary Slip ID") + ":Link/Salary Slip:150", _("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", _("Date of Joining") + "::80", - _("Branch") + ":Link/Branch:120", - _("Department") + ":Link/Department:120", + _("Branch") + ":Link/Branch:-1", + _("Department") + ":Link/Department:-1", _("Designation") + ":Link/Designation:120", _("Company") + ":Link/Company:120", _("Start Date") + "::80", _("End Date") + "::80", - _("Leave Without Pay") + ":Float:130", + _("Leave Without Pay") + ":Float:50", _("Payment Days") + ":Float:120", - _("Currency") + ":Link/Currency:80" - ] - """ - columns = [ - _("Salary Slip ID") + ":Link/Salary Slip:150",_("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", - _("Date of Joining") + "::80", _("Branch") + ":Link/Branch:-1", _("Department") + ":Link/Department:-1", - _("Designation") + ":Link/Designation:120", _("Company") + ":Link/Company:120", _("Start Date") + "::80", - _("End Date") + "::80", _("Leave Without Pay") + ":Float:50", _("Payment Days") + ":Float:120" ] salary_components = {_("Earning"): [], _("Deduction"): []} - for component in frappe.db.sql("""select distinct sd.salary_component, sc.type + for component in frappe.db.sql( + """select distinct sd.salary_component, sc.type from `tabSalary Detail` sd, `tabSalary Component` sc - where sc.name=sd.salary_component and sd.amount != 0 and sd.parent in (%s)""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1): + where sc.name=sd.salary_component and sd.amount != 0 and sd.parent in (%s)""" + % (", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ): salary_components[_(component.type)].append(component.salary_component) - columns = columns + [(e + ":Currency:120") for e in salary_components[_("Earning")]] + \ - [_("Gross Pay") + ":Currency:120"] + [(d + ":Currency:120") for d in salary_components[_("Deduction")]] + \ - [_("Loan Repayment") + ":Currency:120", _("Total Deduction") + ":Currency:120", _("Net Pay") + ":Currency:120"] + columns = ( + columns + + [(e + ":Currency:120") for e in salary_components[_("Earning")]] + + [_("Gross Pay") + ":Currency:120"] + + [(d + ":Currency:120") for d in salary_components[_("Deduction")]] + + [ + _("Loan Repayment") + ":Currency:120", + _("Total Deduction") + ":Currency:120", + _("Net Pay") + ":Currency:120", + ] + ) return columns, salary_components[_("Earning")], salary_components[_("Deduction")] + def get_salary_slips(filters, company_currency): - filters.update({"from_date": filters.get("from_date"), "to_date":filters.get("to_date")}) + filters.update({"from_date": filters.get("from_date"), "to_date": filters.get("to_date")}) conditions, filters = get_conditions(filters, company_currency) - salary_slips = frappe.db.sql("""select * from `tabSalary Slip` where %s - order by employee""" % conditions, filters, as_dict=1) + salary_slips = frappe.db.sql( + """select * from `tabSalary Slip` where %s + order by employee""" + % conditions, + filters, + as_dict=1, + ) return salary_slips or [] + def get_conditions(filters, company_currency): conditions = "" doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} @@ -110,48 +158,71 @@ def get_conditions(filters, company_currency): if filters.get("docstatus"): conditions += "docstatus = {0}".format(doc_status[filters.get("docstatus")]) - if filters.get("from_date"): conditions += " and start_date >= %(from_date)s" - if filters.get("to_date"): conditions += " and end_date <= %(to_date)s" - if filters.get("company"): conditions += " and company = %(company)s" - if filters.get("employee"): conditions += " and employee = %(employee)s" + if filters.get("from_date"): + conditions += " and start_date >= %(from_date)s" + if filters.get("to_date"): + conditions += " and end_date <= %(to_date)s" + if filters.get("company"): + conditions += " and company = %(company)s" + if filters.get("employee"): + conditions += " and employee = %(employee)s" if filters.get("currency") and filters.get("currency") != company_currency: conditions += " and currency = %(currency)s" return conditions, filters + def get_employee_doj_map(): - return frappe._dict(frappe.db.sql(""" + return frappe._dict( + frappe.db.sql( + """ SELECT employee, date_of_joining FROM `tabEmployee` - """)) + """ + ) + ) + def get_ss_earning_map(salary_slips, currency, company_currency): - ss_earnings = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name - from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) + ss_earnings = frappe.db.sql( + """select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" + % (", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) ss_earning_map = {} for d in ss_earnings: ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0) if currency == company_currency: - ss_earning_map[d.parent][d.salary_component] += flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + ss_earning_map[d.parent][d.salary_component] += flt(d.amount) * flt( + d.exchange_rate if d.exchange_rate else 1 + ) else: ss_earning_map[d.parent][d.salary_component] += flt(d.amount) return ss_earning_map + def get_ss_ded_map(salary_slips, currency, company_currency): - ss_deductions = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name - from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) + ss_deductions = frappe.db.sql( + """select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" + % (", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) ss_ded_map = {} for d in ss_deductions: ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0) if currency == company_currency: - ss_ded_map[d.parent][d.salary_component] += flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + ss_ded_map[d.parent][d.salary_component] += flt(d.amount) * flt( + d.exchange_rate if d.exchange_rate else 1 + ) else: ss_ded_map[d.parent][d.salary_component] += flt(d.amount) diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py index 8092ba208a4..5bb05f0842d 100644 --- a/erpnext/portal/doctype/homepage/homepage.py +++ b/erpnext/portal/doctype/homepage/homepage.py @@ -11,17 +11,27 @@ class Homepage(Document): def validate(self): if not self.description: self.description = frappe._("This is an example website auto-generated from ERPNext") - delete_page_cache('home') + delete_page_cache("home") def setup_items(self): - for d in frappe.get_all('Website Item', fields=['name', 'item_name', 'description', 'image', 'route'], - filters={'published': 1}, limit=3): + for d in frappe.get_all( + "Website Item", + fields=["name", "item_name", "description", "image", "route"], + filters={"published": 1}, + limit=3, + ): - doc = frappe.get_doc('Website Item', d.name) + doc = frappe.get_doc("Website Item", d.name) if not doc.route: # set missing route doc.save() - self.append('products', dict(item_code=d.name, - item_name=d.item_name, description=d.description, - image=d.image, route=d.route)) - + self.append( + "products", + dict( + item_code=d.name, + item_name=d.item_name, + description=d.description, + image=d.image, + route=d.route, + ), + ) diff --git a/erpnext/portal/doctype/homepage/test_homepage.py b/erpnext/portal/doctype/homepage/test_homepage.py index 9eb1f015af6..c8a1a1deec2 100644 --- a/erpnext/portal/doctype/homepage/test_homepage.py +++ b/erpnext/portal/doctype/homepage/test_homepage.py @@ -10,7 +10,7 @@ from frappe.website.render import render class TestHomepage(unittest.TestCase): def test_homepage_load(self): - set_request(method='GET', path='home') + set_request(method="GET", path="home") response = render() self.assertEqual(response.status_code, 200) diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py index 4b8ba3002f0..ae1183a5a51 100644 --- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py +++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py @@ -12,65 +12,79 @@ from frappe.website.render import render class TestHomepageSection(unittest.TestCase): def test_homepage_section_card(self): try: - frappe.get_doc({ - 'doctype': 'Homepage Section', - 'name': 'Card Section', - 'section_based_on': 'Cards', - 'section_cards': [ - {'title': 'Card 1', 'subtitle': 'Subtitle 1', 'content': 'This is test card 1', 'route': '/card-1'}, - {'title': 'Card 2', 'subtitle': 'Subtitle 2', 'content': 'This is test card 2', 'image': 'test.jpg'}, - ], - 'no_of_columns': 3 - }).insert() + frappe.get_doc( + { + "doctype": "Homepage Section", + "name": "Card Section", + "section_based_on": "Cards", + "section_cards": [ + { + "title": "Card 1", + "subtitle": "Subtitle 1", + "content": "This is test card 1", + "route": "/card-1", + }, + { + "title": "Card 2", + "subtitle": "Subtitle 2", + "content": "This is test card 2", + "image": "test.jpg", + }, + ], + "no_of_columns": 3, + } + ).insert() except frappe.DuplicateEntryError: pass - set_request(method='GET', path='home') + set_request(method="GET", path="home") response = render() self.assertEqual(response.status_code, 200) html = frappe.safe_decode(response.get_data()) - soup = BeautifulSoup(html, 'html.parser') - sections = soup.find('main').find_all('section') + soup = BeautifulSoup(html, "html.parser") + sections = soup.find("main").find_all("section") self.assertEqual(len(sections), 3) homepage_section = sections[2] - self.assertEqual(homepage_section.h3.text, 'Card Section') + self.assertEqual(homepage_section.h3.text, "Card Section") cards = homepage_section.find_all(class_="card") self.assertEqual(len(cards), 2) - self.assertEqual(cards[0].h5.text, 'Card 1') - self.assertEqual(cards[0].a['href'], '/card-1') - self.assertEqual(cards[1].p.text, 'Subtitle 2') - self.assertEqual(cards[1].find(class_='website-image-lazy')['data-src'], 'test.jpg') + self.assertEqual(cards[0].h5.text, "Card 1") + self.assertEqual(cards[0].a["href"], "/card-1") + self.assertEqual(cards[1].p.text, "Subtitle 2") + self.assertEqual(cards[1].find(class_="website-image-lazy")["data-src"], "test.jpg") # cleanup frappe.db.rollback() def test_homepage_section_custom_html(self): - frappe.get_doc({ - 'doctype': 'Homepage Section', - 'name': 'Custom HTML Section', - 'section_based_on': 'Custom HTML', - 'section_html': '
My custom html
', - }).insert() + frappe.get_doc( + { + "doctype": "Homepage Section", + "name": "Custom HTML Section", + "section_based_on": "Custom HTML", + "section_html": '
My custom html
', + } + ).insert() - set_request(method='GET', path='home') + set_request(method="GET", path="home") response = render() self.assertEqual(response.status_code, 200) html = frappe.safe_decode(response.get_data()) - soup = BeautifulSoup(html, 'html.parser') - sections = soup.find('main').find_all(class_='custom-section') + soup = BeautifulSoup(html, "html.parser") + sections = soup.find("main").find_all(class_="custom-section") self.assertEqual(len(sections), 1) homepage_section = sections[0] - self.assertEqual(homepage_section.text, 'My custom html') + self.assertEqual(homepage_section.text, "My custom html") # cleanup frappe.db.rollback() diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py index 4552e1257d0..09d100708e3 100644 --- a/erpnext/portal/utils.py +++ b/erpnext/portal/utils.py @@ -1,4 +1,3 @@ - import frappe from frappe.utils.nestedset import get_root_of @@ -9,40 +8,41 @@ from erpnext.e_commerce.shopping_cart.cart import get_debtors_account def set_default_role(doc, method): - '''Set customer, supplier, student, guardian based on email''' + """Set customer, supplier, student, guardian based on email""" if frappe.flags.setting_role or frappe.flags.in_migrate: return roles = frappe.get_roles(doc.name) - contact_name = frappe.get_value('Contact', dict(email_id=doc.email)) + contact_name = frappe.get_value("Contact", dict(email_id=doc.email)) if contact_name: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) for link in contact.links: frappe.flags.setting_role = True - if link.link_doctype=='Customer' and 'Customer' not in roles: - doc.add_roles('Customer') - elif link.link_doctype=='Supplier' and 'Supplier' not in roles: - doc.add_roles('Supplier') - elif frappe.get_value('Student', dict(student_email_id=doc.email)) and 'Student' not in roles: - doc.add_roles('Student') - elif frappe.get_value('Guardian', dict(email_address=doc.email)) and 'Guardian' not in roles: - doc.add_roles('Guardian') + if link.link_doctype == "Customer" and "Customer" not in roles: + doc.add_roles("Customer") + elif link.link_doctype == "Supplier" and "Supplier" not in roles: + doc.add_roles("Supplier") + elif frappe.get_value("Student", dict(student_email_id=doc.email)) and "Student" not in roles: + doc.add_roles("Student") + elif frappe.get_value("Guardian", dict(email_address=doc.email)) and "Guardian" not in roles: + doc.add_roles("Guardian") + def create_customer_or_supplier(): - '''Based on the default Role (Customer, Supplier), create a Customer / Supplier. + """Based on the default Role (Customer, Supplier), create a Customer / Supplier. Called on_session_creation hook. - ''' + """ user = frappe.session.user - if frappe.db.get_value('User', user, 'user_type') != 'Website User': + if frappe.db.get_value("User", user, "user_type") != "Website User": return user_roles = frappe.get_roles() - portal_settings = frappe.get_single('Portal Settings') + portal_settings = frappe.get_single("Portal Settings") default_role = portal_settings.default_role - if default_role not in ['Customer', 'Supplier']: + if default_role not in ["Customer", "Supplier"]: return # create customer / supplier if the user has that role @@ -60,34 +60,33 @@ def create_customer_or_supplier(): party = frappe.new_doc(doctype) fullname = frappe.utils.get_fullname(user) - if doctype == 'Customer': + if doctype == "Customer": cart_settings = get_shopping_cart_settings() if cart_settings.enable_checkout: debtors_account = get_debtors_account(cart_settings) else: - debtors_account = '' + debtors_account = "" - party.update({ - "customer_name": fullname, - "customer_type": "Individual", - "customer_group": cart_settings.default_customer_group, - "territory": get_root_of("Territory") - }) + party.update( + { + "customer_name": fullname, + "customer_type": "Individual", + "customer_group": cart_settings.default_customer_group, + "territory": get_root_of("Territory"), + } + ) if debtors_account: - party.update({ - "accounts": [{ - "company": cart_settings.company, - "account": debtors_account - }] - }) + party.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]}) else: - party.update({ - "supplier_name": fullname, - "supplier_group": "All Supplier Groups", - "supplier_type": "Individual" - }) + party.update( + { + "supplier_name": fullname, + "supplier_group": "All Supplier Groups", + "supplier_type": "Individual", + } + ) party.flags.ignore_mandatory = True party.insert(ignore_permissions=True) @@ -96,28 +95,27 @@ def create_customer_or_supplier(): if party_exists(alternate_doctype, user): # if user is both customer and supplier, alter fullname to avoid contact name duplication - fullname += "-" + doctype + fullname += "-" + doctype create_party_contact(doctype, fullname, user, party.name) return party + def create_party_contact(doctype, fullname, user, party_name): contact = frappe.new_doc("Contact") - contact.update({ - "first_name": fullname, - "email_id": user - }) - contact.append('links', dict(link_doctype=doctype, link_name=party_name)) - contact.append('email_ids', dict(email_id=user)) + contact.update({"first_name": fullname, "email_id": user}) + contact.append("links", dict(link_doctype=doctype, link_name=party_name)) + contact.append("email_ids", dict(email_id=user)) contact.flags.ignore_mandatory = True contact.insert(ignore_permissions=True) + def party_exists(doctype, user): # check if contact exists against party and if it is linked to the doctype contact_name = frappe.db.get_value("Contact", {"email_id": user}) if contact_name: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) doctypes = [d.link_doctype for d in contact.links] return doctype in doctypes diff --git a/erpnext/projects/doctype/activity_cost/activity_cost.py b/erpnext/projects/doctype/activity_cost/activity_cost.py index bc4bb9dcba4..b99aa1e37d5 100644 --- a/erpnext/projects/doctype/activity_cost/activity_cost.py +++ b/erpnext/projects/doctype/activity_cost/activity_cost.py @@ -7,7 +7,9 @@ from frappe import _ from frappe.model.document import Document -class DuplicationError(frappe.ValidationError): pass +class DuplicationError(frappe.ValidationError): + pass + class ActivityCost(Document): def validate(self): @@ -24,12 +26,22 @@ class ActivityCost(Document): def check_unique(self): if self.employee: - if frappe.db.sql("""select name from `tabActivity Cost` where employee_name= %s and activity_type= %s and name != %s""", - (self.employee_name, self.activity_type, self.name)): - frappe.throw(_("Activity Cost exists for Employee {0} against Activity Type - {1}") - .format(self.employee, self.activity_type), DuplicationError) + if frappe.db.sql( + """select name from `tabActivity Cost` where employee_name= %s and activity_type= %s and name != %s""", + (self.employee_name, self.activity_type, self.name), + ): + frappe.throw( + _("Activity Cost exists for Employee {0} against Activity Type - {1}").format( + self.employee, self.activity_type + ), + DuplicationError, + ) else: - if frappe.db.sql("""select name from `tabActivity Cost` where ifnull(employee, '')='' and activity_type= %s and name != %s""", - (self.activity_type, self.name)): - frappe.throw(_("Default Activity Cost exists for Activity Type - {0}") - .format(self.activity_type), DuplicationError) + if frappe.db.sql( + """select name from `tabActivity Cost` where ifnull(employee, '')='' and activity_type= %s and name != %s""", + (self.activity_type, self.name), + ): + frappe.throw( + _("Default Activity Cost exists for Activity Type - {0}").format(self.activity_type), + DuplicationError, + ) diff --git a/erpnext/projects/doctype/activity_cost/test_activity_cost.py b/erpnext/projects/doctype/activity_cost/test_activity_cost.py index d53e582adc6..8da797ed4f2 100644 --- a/erpnext/projects/doctype/activity_cost/test_activity_cost.py +++ b/erpnext/projects/doctype/activity_cost/test_activity_cost.py @@ -11,15 +11,17 @@ from erpnext.projects.doctype.activity_cost.activity_cost import DuplicationErro class TestActivityCost(unittest.TestCase): def test_duplication(self): frappe.db.sql("delete from `tabActivity Cost`") - activity_cost1 = frappe.new_doc('Activity Cost') - activity_cost1.update({ - "employee": "_T-Employee-00001", - "employee_name": "_Test Employee", - "activity_type": "_Test Activity Type 1", - "billing_rate": 100, - "costing_rate": 50 - }) + activity_cost1 = frappe.new_doc("Activity Cost") + activity_cost1.update( + { + "employee": "_T-Employee-00001", + "employee_name": "_Test Employee", + "activity_type": "_Test Activity Type 1", + "billing_rate": 100, + "costing_rate": 50, + } + ) activity_cost1.insert() activity_cost2 = frappe.copy_doc(activity_cost1) - self.assertRaises(DuplicationError, activity_cost2.insert ) + self.assertRaises(DuplicationError, activity_cost2.insert) frappe.db.sql("delete from `tabActivity Cost`") diff --git a/erpnext/projects/doctype/activity_type/test_activity_type.py b/erpnext/projects/doctype/activity_type/test_activity_type.py index bb74b881f4c..d51439eb5b1 100644 --- a/erpnext/projects/doctype/activity_type/test_activity_type.py +++ b/erpnext/projects/doctype/activity_type/test_activity_type.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Activity Type') +test_records = frappe.get_test_records("Activity Type") diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 5ffae2d0fb9..6b691786a1a 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -17,20 +17,26 @@ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday class Project(Document): def get_feed(self): - return '{0}: {1}'.format(_(self.status), frappe.safe_decode(self.project_name)) + return "{0}: {1}".format(_(self.status), frappe.safe_decode(self.project_name)) def onload(self): - self.set_onload('activity_summary', frappe.db.sql('''select activity_type, + self.set_onload( + "activity_summary", + frappe.db.sql( + """select activity_type, sum(hours) as total_hours from `tabTimesheet Detail` where project=%s and docstatus < 2 group by activity_type - order by total_hours desc''', self.name, as_dict=True)) + order by total_hours desc""", + self.name, + as_dict=True, + ), + ) self.update_costing() def before_print(self, settings=None): self.onload() - def validate(self): if not self.is_new(): self.copy_from_template() @@ -39,17 +45,17 @@ class Project(Document): self.update_percent_complete() def copy_from_template(self): - ''' + """ Copy tasks from template - ''' - if self.project_template and not frappe.db.get_all('Task', dict(project = self.name), limit=1): + """ + if self.project_template and not frappe.db.get_all("Task", dict(project=self.name), limit=1): # has a template, and no loaded tasks, so lets create if not self.expected_start_date: # project starts today self.expected_start_date = today() - template = frappe.get_doc('Project Template', self.project_template) + template = frappe.get_doc("Project Template", self.project_template) if not self.project_type: self.project_type = template.project_type @@ -65,19 +71,22 @@ class Project(Document): self.dependency_mapping(tmp_task_details, project_tasks) def create_task_from_template(self, task_details): - return frappe.get_doc(dict( - doctype = 'Task', - subject = task_details.subject, - project = self.name, - status = 'Open', - exp_start_date = self.calculate_start_date(task_details), - exp_end_date = self.calculate_end_date(task_details), - description = task_details.description, - task_weight = task_details.task_weight, - type = task_details.type, - issue = task_details.issue, - is_group = task_details.is_group - )).insert() + return frappe.get_doc( + dict( + doctype="Task", + subject=task_details.subject, + project=self.name, + status="Open", + exp_start_date=self.calculate_start_date(task_details), + exp_end_date=self.calculate_end_date(task_details), + description=task_details.description, + task_weight=task_details.task_weight, + type=task_details.type, + issue=task_details.issue, + is_group=task_details.is_group, + color=task_details.color, + ) + ).insert() def calculate_start_date(self, task_details): self.start_date = add_days(self.expected_start_date, task_details.start) @@ -105,23 +114,26 @@ class Project(Document): if template_task.get("depends_on") and not project_task.get("depends_on"): for child_task in template_task.get("depends_on"): child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") - corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) + corresponding_project_task = list( + filter(lambda x: x.subject == child_task_subject, project_tasks) + ) if len(corresponding_project_task): - project_task.append("depends_on",{ - "task": corresponding_project_task[0].name - }) + project_task.append("depends_on", {"task": corresponding_project_task[0].name}) project_task.save() def check_for_parent_tasks(self, template_task, project_task, project_tasks): if template_task.get("parent_task") and not project_task.get("parent_task"): parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") - corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) + corresponding_project_task = list( + filter(lambda x: x.subject == parent_task_subject, project_tasks) + ) if len(corresponding_project_task): project_task.parent_task = corresponding_project_task[0].name project_task.save() def is_row_updated(self, row, existing_task_data, fields): - if self.get("__islocal") or not existing_task_data: return True + if self.get("__islocal") or not existing_task_data: + return True d = existing_task_data.get(row.task_id, {}) @@ -130,7 +142,7 @@ class Project(Document): return True def update_project(self): - '''Called externally by Task''' + """Called externally by Task""" self.update_percent_complete() self.update_costing() self.db_update() @@ -149,52 +161,74 @@ class Project(Document): self.percent_complete = 100 return - total = frappe.db.count('Task', dict(project=self.name)) + total = frappe.db.count("Task", dict(project=self.name)) if not total: self.percent_complete = 0 else: if (self.percent_complete_method == "Task Completion" and total > 0) or ( - not self.percent_complete_method and total > 0): - completed = frappe.db.sql("""select count(name) from tabTask where - project=%s and status in ('Cancelled', 'Completed')""", self.name)[0][0] + not self.percent_complete_method and total > 0 + ): + completed = frappe.db.sql( + """select count(name) from tabTask where + project=%s and status in ('Cancelled', 'Completed')""", + self.name, + )[0][0] self.percent_complete = flt(flt(completed) / total * 100, 2) - if (self.percent_complete_method == "Task Progress" and total > 0): - progress = frappe.db.sql("""select sum(progress) from tabTask where - project=%s""", self.name)[0][0] + if self.percent_complete_method == "Task Progress" and total > 0: + progress = frappe.db.sql( + """select sum(progress) from tabTask where + project=%s""", + self.name, + )[0][0] self.percent_complete = flt(flt(progress) / total, 2) - if (self.percent_complete_method == "Task Weight" and total > 0): - weight_sum = frappe.db.sql("""select sum(task_weight) from tabTask where - project=%s""", self.name)[0][0] - weighted_progress = frappe.db.sql("""select progress, task_weight from tabTask where - project=%s""", self.name, as_dict=1) + if self.percent_complete_method == "Task Weight" and total > 0: + weight_sum = frappe.db.sql( + """select sum(task_weight) from tabTask where + project=%s""", + self.name, + )[0][0] + weighted_progress = frappe.db.sql( + """select progress, task_weight from tabTask where + project=%s""", + self.name, + as_dict=1, + ) pct_complete = 0 for row in weighted_progress: pct_complete += row["progress"] * frappe.utils.safe_div(row["task_weight"], weight_sum) self.percent_complete = flt(flt(pct_complete), 2) # don't update status if it is cancelled - if self.status == 'Cancelled': + if self.status == "Cancelled": return if self.percent_complete == 100: self.status = "Completed" def update_costing(self): - from_time_sheet = frappe.db.sql("""select + from_time_sheet = frappe.db.sql( + """select sum(costing_amount) as costing_amount, sum(billing_amount) as billing_amount, min(from_time) as start_date, max(to_time) as end_date, sum(hours) as time - from `tabTimesheet Detail` where project = %s and docstatus = 1""", self.name, as_dict=1)[0] + from `tabTimesheet Detail` where project = %s and docstatus = 1""", + self.name, + as_dict=1, + )[0] - from_expense_claim = frappe.db.sql("""select + from_expense_claim = frappe.db.sql( + """select sum(total_sanctioned_amount) as total_sanctioned_amount from `tabExpense Claim` where project = %s - and docstatus = 1""", self.name, as_dict=1)[0] + and docstatus = 1""", + self.name, + as_dict=1, + )[0] self.actual_start_date = from_time_sheet.start_date self.actual_end_date = from_time_sheet.end_date @@ -210,41 +244,54 @@ class Project(Document): self.calculate_gross_margin() def calculate_gross_margin(self): - expense_amount = (flt(self.total_costing_amount) + flt(self.total_expense_claim) - + flt(self.total_purchase_cost) + flt(self.get('total_consumed_material_cost', 0))) + expense_amount = ( + flt(self.total_costing_amount) + + flt(self.total_expense_claim) + + flt(self.total_purchase_cost) + + flt(self.get("total_consumed_material_cost", 0)) + ) self.gross_margin = flt(self.total_billed_amount) - expense_amount if self.total_billed_amount: self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100 def update_purchase_costing(self): - total_purchase_cost = frappe.db.sql("""select sum(base_net_amount) - from `tabPurchase Invoice Item` where project = %s and docstatus=1""", self.name) + total_purchase_cost = frappe.db.sql( + """select sum(base_net_amount) + from `tabPurchase Invoice Item` where project = %s and docstatus=1""", + self.name, + ) self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0 def update_sales_amount(self): - total_sales_amount = frappe.db.sql("""select sum(base_net_total) - from `tabSales Order` where project = %s and docstatus=1""", self.name) + total_sales_amount = frappe.db.sql( + """select sum(base_net_total) + from `tabSales Order` where project = %s and docstatus=1""", + self.name, + ) self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0 def update_billed_amount(self): - total_billed_amount = frappe.db.sql("""select sum(base_net_total) - from `tabSales Invoice` where project = %s and docstatus=1""", self.name) + total_billed_amount = frappe.db.sql( + """select sum(base_net_total) + from `tabSales Invoice` where project = %s and docstatus=1""", + self.name, + ) self.total_billed_amount = total_billed_amount and total_billed_amount[0][0] or 0 def after_rename(self, old_name, new_name, merge=False): if old_name == self.copied_from: - frappe.db.set_value('Project', new_name, 'copied_from', new_name) + frappe.db.set_value("Project", new_name, "copied_from", new_name) def send_welcome_email(self): url = get_url("/project/?name={0}".format(self.name)) messages = ( _("You have been invited to collaborate on the project: {0}").format(self.name), url, - _("Join") + _("Join"), ) content = """ @@ -254,21 +301,31 @@ class Project(Document): for user in self.users: if user.welcome_email_sent == 0: - frappe.sendmail(user.user, subject=_("Project Collaboration Invitation"), - content=content.format(*messages)) + frappe.sendmail( + user.user, subject=_("Project Collaboration Invitation"), content=content.format(*messages) + ) user.welcome_email_sent = 1 + def get_timeline_data(doctype, name): - '''Return timeline for attendance''' - return dict(frappe.db.sql('''select unix_timestamp(from_time), count(*) + """Return timeline for attendance""" + return dict( + frappe.db.sql( + """select unix_timestamp(from_time), count(*) from `tabTimesheet Detail` where project=%s and from_time > date_sub(curdate(), interval 1 year) and docstatus < 2 - group by date(from_time)''', name)) + group by date(from_time)""", + name, + ) + ) -def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): - return frappe.db.sql('''select distinct project.* +def get_project_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" +): + return frappe.db.sql( + """select distinct project.* from tabProject project, `tabProject User` project_user where (project_user.user = %(user)s @@ -276,27 +333,32 @@ def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, o or project.owner = %(user)s order by project.modified desc limit {0}, {1} - '''.format(limit_start, limit_page_length), - {'user': frappe.session.user}, - as_dict=True, - update={'doctype': 'Project'}) + """.format( + limit_start, limit_page_length + ), + {"user": frappe.session.user}, + as_dict=True, + update={"doctype": "Project"}, + ) def get_list_context(context=None): return { "show_sidebar": True, "show_search": True, - 'no_breadcrumbs': True, + "no_breadcrumbs": True, "title": _("Projects"), "get_list": get_project_list, - "row_template": "templates/includes/projects/project_row.html" + "row_template": "templates/includes/projects/project_row.html", } + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): conditions = [] - return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name) + return frappe.db.sql( + """select name, concat_ws(' ', first_name, middle_name, last_name) from `tabUser` where enabled=1 and name not in ("Guest", "Administrator") @@ -308,47 +370,51 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999), idx desc, name, full_name - limit %(start)s, %(page_len)s""".format(**{ - 'key': searchfield, - 'fcond': get_filters_cond(doctype, filters, conditions), - 'mcond': get_match_cond(doctype) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) + limit %(start)s, %(page_len)s""".format( + **{ + "key": searchfield, + "fcond": get_filters_cond(doctype, filters, conditions), + "mcond": get_match_cond(doctype), + } + ), + {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + ) @frappe.whitelist() def get_cost_center_name(project): return frappe.db.get_value("Project", project, "cost_center") + def hourly_reminder(): fields = ["from_time", "to_time"] projects = get_projects_for_collect_progress("Hourly", fields) for project in projects: - if (get_time(nowtime()) >= get_time(project.from_time) or - get_time(nowtime()) <= get_time(project.to_time)): + if get_time(nowtime()) >= get_time(project.from_time) or get_time(nowtime()) <= get_time( + project.to_time + ): send_project_update_email_to_users(project.name) + def project_status_update_reminder(): daily_reminder() twice_daily_reminder() weekly_reminder() + def daily_reminder(): fields = ["daily_time_to_send"] - projects = get_projects_for_collect_progress("Daily", fields) + projects = get_projects_for_collect_progress("Daily", fields) for project in projects: if allow_to_make_project_update(project.name, project.get("daily_time_to_send"), "Daily"): send_project_update_email_to_users(project.name) + def twice_daily_reminder(): fields = ["first_email", "second_email"] - projects = get_projects_for_collect_progress("Twice Daily", fields) + projects = get_projects_for_collect_progress("Twice Daily", fields) fields.remove("name") for project in projects: @@ -356,9 +422,10 @@ def twice_daily_reminder(): if allow_to_make_project_update(project.name, project.get(d), "Twicely"): send_project_update_email_to_users(project.name) + def weekly_reminder(): fields = ["day_to_send", "weekly_time_to_send"] - projects = get_projects_for_collect_progress("Weekly", fields) + projects = get_projects_for_collect_progress("Weekly", fields) current_day = get_datetime().strftime("%A") for project in projects: @@ -368,12 +435,16 @@ def weekly_reminder(): if allow_to_make_project_update(project.name, project.get("weekly_time_to_send"), "Weekly"): send_project_update_email_to_users(project.name) + def allow_to_make_project_update(project, time, frequency): - data = frappe.db.sql(""" SELECT name from `tabProject Update` - WHERE project = %s and date = %s """, (project, today())) + data = frappe.db.sql( + """ SELECT name from `tabProject Update` + WHERE project = %s and date = %s """, + (project, today()), + ) # len(data) > 1 condition is checked for twicely frequency - if data and (frequency in ['Daily', 'Weekly'] or len(data) > 1): + if data and (frequency in ["Daily", "Weekly"] or len(data) > 1): return False if get_time(nowtime()) >= get_time(time): @@ -382,138 +453,162 @@ def allow_to_make_project_update(project, time, frequency): @frappe.whitelist() def create_duplicate_project(prev_doc, project_name): - ''' Create duplicate project based on the old project ''' + """Create duplicate project based on the old project""" import json + prev_doc = json.loads(prev_doc) - if project_name == prev_doc.get('name'): + if project_name == prev_doc.get("name"): frappe.throw(_("Use a name that is different from previous project name")) # change the copied doc name to new project name project = frappe.copy_doc(prev_doc) project.name = project_name - project.project_template = '' + project.project_template = "" project.project_name = project_name project.insert() # fetch all the task linked with the old project - task_list = frappe.get_all("Task", filters={ - 'project': prev_doc.get('name') - }, fields=['name']) + task_list = frappe.get_all("Task", filters={"project": prev_doc.get("name")}, fields=["name"]) # Create duplicate task for all the task for task in task_list: - task = frappe.get_doc('Task', task) + task = frappe.get_doc("Task", task) new_task = frappe.copy_doc(task) new_task.project = project.name new_task.insert() - project.db_set('project_template', prev_doc.get('project_template')) + project.db_set("project_template", prev_doc.get("project_template")) + def get_projects_for_collect_progress(frequency, fields): fields.extend(["name"]) - return frappe.get_all("Project", fields = fields, - filters = {'collect_progress': 1, 'frequency': frequency, 'status': 'Open'}) + return frappe.get_all( + "Project", + fields=fields, + filters={"collect_progress": 1, "frequency": frequency, "status": "Open"}, + ) + def send_project_update_email_to_users(project): - doc = frappe.get_doc('Project', project) + doc = frappe.get_doc("Project", project) - if is_holiday(doc.holiday_list) or not doc.users: return + if is_holiday(doc.holiday_list) or not doc.users: + return - project_update = frappe.get_doc({ - "doctype" : "Project Update", - "project" : project, - "sent": 0, - "date": today(), - "time": nowtime(), - "naming_series": "UPDATE-.project.-.YY.MM.DD.-", - }).insert() + project_update = frappe.get_doc( + { + "doctype": "Project Update", + "project": project, + "sent": 0, + "date": today(), + "time": nowtime(), + "naming_series": "UPDATE-.project.-.YY.MM.DD.-", + } + ).insert() subject = "For project %s, update your status" % (project) - incoming_email_account = frappe.db.get_value('Email Account', - dict(enable_incoming=1, default_incoming=1), 'email_id') + incoming_email_account = frappe.db.get_value( + "Email Account", dict(enable_incoming=1, default_incoming=1), "email_id" + ) - frappe.sendmail(recipients=get_users_email(doc), + frappe.sendmail( + recipients=get_users_email(doc), message=doc.message, subject=_(subject), reference_doctype=project_update.doctype, reference_name=project_update.name, - reply_to=incoming_email_account + reply_to=incoming_email_account, ) + def collect_project_status(): - for data in frappe.get_all("Project Update", - {'date': today(), 'sent': 0}): - replies = frappe.get_all('Communication', - fields=['content', 'text_content', 'sender'], - filters=dict(reference_doctype="Project Update", + for data in frappe.get_all("Project Update", {"date": today(), "sent": 0}): + replies = frappe.get_all( + "Communication", + fields=["content", "text_content", "sender"], + filters=dict( + reference_doctype="Project Update", reference_name=data.name, - communication_type='Communication', - sent_or_received='Received'), - order_by='creation asc') + communication_type="Communication", + sent_or_received="Received", + ), + order_by="creation asc", + ) for d in replies: doc = frappe.get_doc("Project Update", data.name) - user_data = frappe.db.get_values("User", {"email": d.sender}, - ["full_name", "user_image", "name"], as_dict=True)[0] + user_data = frappe.db.get_values( + "User", {"email": d.sender}, ["full_name", "user_image", "name"], as_dict=True + )[0] - doc.append("users", { - 'user': user_data.name, - 'full_name': user_data.full_name, - 'image': user_data.user_image, - 'project_status': frappe.utils.md_to_html( - EmailReplyParser.parse_reply(d.text_content) or d.content - ) - }) + doc.append( + "users", + { + "user": user_data.name, + "full_name": user_data.full_name, + "image": user_data.user_image, + "project_status": frappe.utils.md_to_html( + EmailReplyParser.parse_reply(d.text_content) or d.content + ), + }, + ) doc.save(ignore_permissions=True) + def send_project_status_email_to_users(): yesterday = add_days(today(), -1) - for d in frappe.get_all("Project Update", - {'date': yesterday, 'sent': 0}): + for d in frappe.get_all("Project Update", {"date": yesterday, "sent": 0}): doc = frappe.get_doc("Project Update", d.name) - project_doc = frappe.get_doc('Project', doc.project) + project_doc = frappe.get_doc("Project", doc.project) - args = { - "users": doc.users, - "title": _("Project Summary for {0}").format(yesterday) - } + args = {"users": doc.users, "title": _("Project Summary for {0}").format(yesterday)} - frappe.sendmail(recipients=get_users_email(project_doc), - template='daily_project_summary', + frappe.sendmail( + recipients=get_users_email(project_doc), + template="daily_project_summary", args=args, subject=_("Daily Project Summary for {0}").format(d.name), reference_doctype="Project Update", - reference_name=d.name) + reference_name=d.name, + ) + + doc.db_set("sent", 1) - doc.db_set('sent', 1) def update_project_sales_billing(): sales_update_frequency = frappe.db.get_single_value("Selling Settings", "sales_update_frequency") if sales_update_frequency == "Each Transaction": return - elif (sales_update_frequency == "Monthly" and frappe.utils.now_datetime().day != 1): + elif sales_update_frequency == "Monthly" and frappe.utils.now_datetime().day != 1: return - #Else simply fallback to Daily - exists_query = '(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)' + # Else simply fallback to Daily + exists_query = ( + "(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)" + ) project_map = {} - for project_details in frappe.db.sql(''' + for project_details in frappe.db.sql( + """ SELECT name, 1 as order_exists, null as invoice_exists from `tabProject` where exists {order_exists} union SELECT name, null as order_exists, 1 as invoice_exists from `tabProject` where exists {invoice_exists} - '''.format( + """.format( order_exists=exists_query.format(doctype="Sales Order"), invoice_exists=exists_query.format(doctype="Sales Invoice"), - ), as_dict=True): - project = project_map.setdefault(project_details.name, frappe.get_doc('Project', project_details.name)) + ), + as_dict=True, + ): + project = project_map.setdefault( + project_details.name, frappe.get_doc("Project", project_details.name) + ) if project_details.order_exists: project.update_sales_amount() if project_details.invoice_exists: @@ -522,29 +617,31 @@ def update_project_sales_billing(): for project in project_map.values(): project.save() + @frappe.whitelist() def create_kanban_board_if_not_exists(project): from frappe.desk.doctype.kanban_board.kanban_board import quick_kanban_board - project = frappe.get_doc('Project', project) - if not frappe.db.exists('Kanban Board', project.project_name): - quick_kanban_board('Task', project.project_name, 'status', project.name) + project = frappe.get_doc("Project", project) + if not frappe.db.exists("Kanban Board", project.project_name): + quick_kanban_board("Task", project.project_name, "status", project.name) return True + @frappe.whitelist() def set_project_status(project, status): - ''' + """ set status for project and all related tasks - ''' - if not status in ('Completed', 'Cancelled'): - frappe.throw(_('Status must be Cancelled or Completed')) + """ + if not status in ("Completed", "Cancelled"): + frappe.throw(_("Status must be Cancelled or Completed")) - project = frappe.get_doc('Project', project) - frappe.has_permission(doc = project, throw = True) + project = frappe.get_doc("Project", project) + frappe.has_permission(doc=project, throw=True) - for task in frappe.get_all('Task', dict(project = project.name)): - frappe.db.set_value('Task', task.name, 'status', status) + for task in frappe.get_all("Task", dict(project=project.name)): + frappe.db.set_value("Task", task.name, "status", status) project.status = status project.save() diff --git a/erpnext/projects/doctype/project/project_dashboard.py b/erpnext/projects/doctype/project/project_dashboard.py index df274ed9a94..b6c74f93870 100644 --- a/erpnext/projects/doctype/project/project_dashboard.py +++ b/erpnext/projects/doctype/project/project_dashboard.py @@ -1,28 +1,18 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on the Time Sheets created against this project'), - 'fieldname': 'project', - 'transactions': [ + "heatmap": True, + "heatmap_message": _("This is based on the Time Sheets created against this project"), + "fieldname": "project", + "transactions": [ { - 'label': _('Project'), - 'items': ['Task', 'Timesheet', 'Expense Claim', 'Issue' , 'Project Update'] + "label": _("Project"), + "items": ["Task", "Timesheet", "Expense Claim", "Issue", "Project Update"], }, - { - 'label': _('Material'), - 'items': ['Material Request', 'BOM', 'Stock Entry'] - }, - { - 'label': _('Sales'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] - }, - ] + {"label": _("Material"), "items": ["Material Request", "BOM", "Stock Entry"]}, + {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Purchase"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, + ], } diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index df42e82ad47..8a599cef753 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -11,7 +11,7 @@ from erpnext.projects.doctype.task.test_task import create_task from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -test_records = frappe.get_test_records('Project') +test_records = frappe.get_test_records("Project") test_ignore = ["Sales Order"] @@ -19,53 +19,83 @@ class TestProject(unittest.TestCase): def test_project_with_template_having_no_parent_and_depend_tasks(self): project_name = "Test Project with Template - No Parent and Dependend Tasks" frappe.db.sql(""" delete from tabTask where project = %s """, project_name) - frappe.delete_doc('Project', project_name) + frappe.delete_doc("Project", project_name) task1 = task_exists("Test Template Task with No Parent and Dependency") if not task1: - task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3) + task1 = create_task( + subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3 + ) - template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1]) + template = make_project_template( + "Test Project Template - No Parent and Dependend Tasks", [task1] + ) project = get_project(project_name, template) - tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc') + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks"], + dict(project=project.name), + order_by="creation asc", + ) - self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency') + self.assertEqual(tasks[0].subject, "Test Template Task with No Parent and Dependency") self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3)) self.assertEqual(len(tasks), 1) def test_project_template_having_parent_child_tasks(self): project_name = "Test Project with Template - Tasks with Parent-Child Relation" - if frappe.db.get_value('Project', {'project_name': project_name}, 'name'): - project_name = frappe.db.get_value('Project', {'project_name': project_name}, 'name') + if frappe.db.get_value("Project", {"project_name": project_name}, "name"): + project_name = frappe.db.get_value("Project", {"project_name": project_name}, "name") frappe.db.sql(""" delete from tabTask where project = %s """, project_name) - frappe.delete_doc('Project', project_name) + frappe.delete_doc("Project", project_name) task1 = task_exists("Test Template Task Parent") if not task1: - task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=10) + task1 = create_task( + subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=10 + ) task2 = task_exists("Test Template Task Child 1") if not task2: - task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) + task2 = create_task( + subject="Test Template Task Child 1", + parent_task=task1.name, + is_template=1, + begin=1, + duration=3, + ) task3 = task_exists("Test Template Task Child 2") if not task3: - task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) + task3 = create_task( + subject="Test Template Task Child 2", + parent_task=task1.name, + is_template=1, + begin=2, + duration=3, + ) - template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3]) + template = make_project_template( + "Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3] + ) project = get_project(project_name, template) - tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc') + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks", "name", "parent_task"], + dict(project=project.name), + order_by="creation asc", + ) - self.assertEqual(tasks[0].subject, 'Test Template Task Parent') + self.assertEqual(tasks[0].subject, "Test Template Task Parent") self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 10)) - self.assertEqual(tasks[1].subject, 'Test Template Task Child 1') + self.assertEqual(tasks[1].subject, "Test Template Task Child 1") self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3)) self.assertEqual(tasks[1].parent_task, tasks[0].name) - self.assertEqual(tasks[2].subject, 'Test Template Task Child 2') + self.assertEqual(tasks[2].subject, "Test Template Task Child 2") self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3)) self.assertEqual(tasks[2].parent_task, tasks[0].name) @@ -74,26 +104,39 @@ class TestProject(unittest.TestCase): def test_project_template_having_dependent_tasks(self): project_name = "Test Project with Template - Dependent Tasks" frappe.db.sql(""" delete from tabTask where project = %s """, project_name) - frappe.delete_doc('Project', project_name) + frappe.delete_doc("Project", project_name) task1 = task_exists("Test Template Task for Dependency") if not task1: - task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1) + task1 = create_task( + subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1 + ) task2 = task_exists("Test Template Task with Dependency") if not task2: - task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) + task2 = create_task( + subject="Test Template Task with Dependency", + depends_on=task1.name, + is_template=1, + begin=2, + duration=2, + ) template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2]) project = get_project(project_name, template) - tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc') + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks", "name"], + dict(project=project.name), + order_by="creation asc", + ) - self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency') + self.assertEqual(tasks[1].subject, "Test Template Task with Dependency") self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2)) - self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 ) + self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0) - self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) ) + self.assertEqual(tasks[0].subject, "Test Template Task for Dependency") + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1)) self.assertEqual(len(tasks), 2) @@ -112,32 +155,38 @@ class TestProject(unittest.TestCase): so.reload() self.assertFalse(so.project) + def get_project(name, template): - project = frappe.get_doc(dict( - doctype = 'Project', - project_name = name, - status = 'Open', - project_template = template.name, - expected_start_date = nowdate(), - company="_Test Company" - )).insert() + project = frappe.get_doc( + dict( + doctype="Project", + project_name=name, + status="Open", + project_template=template.name, + expected_start_date=nowdate(), + company="_Test Company", + ) + ).insert() return project + def make_project(args): args = frappe._dict(args) if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}): return frappe.get_doc("Project", {"project_name": args.project_name}) - project = frappe.get_doc(dict( - doctype = 'Project', - project_name = args.project_name, - status = 'Open', - expected_start_date = args.start_date, - company= args.company or '_Test Company' - )) + project = frappe.get_doc( + dict( + doctype="Project", + project_name=args.project_name, + status="Open", + expected_start_date=args.start_date, + company=args.company or "_Test Company", + ) + ) if args.project_template_name: template = make_project_template(args.project_template_name) @@ -147,12 +196,14 @@ def make_project(args): return project + def task_exists(subject): - result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"]) + result = frappe.db.get_list("Task", filters={"subject": subject}, fields=["name"]) if not len(result): return False return frappe.get_doc("Task", result[0].name) + def calculate_end_date(project, start, duration): start = add_days(project.expected_start_date, start) start = project.update_if_holiday(start) diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py index 3cc8d6855f2..89afb1bd770 100644 --- a/erpnext/projects/doctype/project_template/project_template.py +++ b/erpnext/projects/doctype/project_template/project_template.py @@ -9,7 +9,6 @@ from frappe.utils import get_link_to_form class ProjectTemplate(Document): - def validate(self): self.validate_dependencies() @@ -19,9 +18,13 @@ class ProjectTemplate(Document): if task_details.depends_on: for dependency_task in task_details.depends_on: if not self.check_dependent_task_presence(dependency_task.task): - task_details_format = get_link_to_form("Task",task_details.name) + task_details_format = get_link_to_form("Task", task_details.name) dependency_task_format = get_link_to_form("Task", dependency_task.task) - frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format))) + frappe.throw( + _("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format( + frappe.bold(task_details_format), frappe.bold(dependency_task_format) + ) + ) def check_dependent_task_presence(self, task): for task_details in self.tasks: diff --git a/erpnext/projects/doctype/project_template/project_template_dashboard.py b/erpnext/projects/doctype/project_template/project_template_dashboard.py index 65cd8d4b55a..0c567c1e599 100644 --- a/erpnext/projects/doctype/project_template/project_template_dashboard.py +++ b/erpnext/projects/doctype/project_template/project_template_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'project_template', - 'transactions': [ - { - 'items': ['Project'] - } - ] - } + return {"fieldname": "project_template", "transactions": [{"items": ["Project"]}]} diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 842483343a2..4fd24bf78a2 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -11,20 +11,16 @@ from erpnext.projects.doctype.task.test_task import create_task class TestProjectTemplate(unittest.TestCase): pass + def make_project_template(project_template_name, project_tasks=[]): - if not frappe.db.exists('Project Template', project_template_name): + if not frappe.db.exists("Project Template", project_template_name): project_tasks = project_tasks or [ - create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), - create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), - ] - doc = frappe.get_doc(dict( - doctype = 'Project Template', - name = project_template_name - )) + create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), + create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), + ] + doc = frappe.get_doc(dict(doctype="Project Template", name=project_template_name)) for task in project_tasks: - doc.append("tasks",{ - "task": task.name - }) + doc.append("tasks", {"task": task.name}) doc.insert() - return frappe.get_doc('Project Template', project_template_name) + return frappe.get_doc("Project Template", project_template_name) diff --git a/erpnext/projects/doctype/project_update/project_update.py b/erpnext/projects/doctype/project_update/project_update.py index 42ba5f6075c..5a29fb6c33e 100644 --- a/erpnext/projects/doctype/project_update/project_update.py +++ b/erpnext/projects/doctype/project_update/project_update.py @@ -7,36 +7,88 @@ from frappe.model.document import Document class ProjectUpdate(Document): - pass + pass + @frappe.whitelist() def daily_reminder(): - project = frappe.db.sql("""SELECT `tabProject`.project_name,`tabProject`.frequency,`tabProject`.expected_start_date,`tabProject`.expected_end_date,`tabProject`.percent_complete FROM `tabProject`;""") - for projects in project: - project_name = projects[0] - frequency = projects[1] - date_start = projects[2] - date_end = projects [3] - progress = projects [4] - draft = frappe.db.sql("""SELECT count(docstatus) from `tabProject Update` WHERE `tabProject Update`.project = %s AND `tabProject Update`.docstatus = 0;""",project_name) - for drafts in draft: - number_of_drafts = drafts[0] - update = frappe.db.sql("""SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURDATE(), INTERVAL -1 DAY);""",project_name) - email_sending(project_name,frequency,date_start,date_end,progress,number_of_drafts,update) + project = frappe.db.sql( + """SELECT `tabProject`.project_name,`tabProject`.frequency,`tabProject`.expected_start_date,`tabProject`.expected_end_date,`tabProject`.percent_complete FROM `tabProject`;""" + ) + for projects in project: + project_name = projects[0] + frequency = projects[1] + date_start = projects[2] + date_end = projects[3] + progress = projects[4] + draft = frappe.db.sql( + """SELECT count(docstatus) from `tabProject Update` WHERE `tabProject Update`.project = %s AND `tabProject Update`.docstatus = 0;""", + project_name, + ) + for drafts in draft: + number_of_drafts = drafts[0] + update = frappe.db.sql( + """SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURDATE(), INTERVAL -1 DAY);""", + project_name, + ) + email_sending(project_name, frequency, date_start, date_end, progress, number_of_drafts, update) -def email_sending(project_name,frequency,date_start,date_end,progress,number_of_drafts,update): - holiday = frappe.db.sql("""SELECT holiday_date FROM `tabHoliday` where holiday_date = CURDATE();""") - msg = "

Project Name: " + project_name + "

Frequency: " + " " + frequency + "

Update Reminder:" + " " + str(date_start) + "

Expected Date End:" + " " + str(date_end) + "

Percent Progress:" + " " + str(progress) + "

Number of Updates:" + " " + str(len(update)) + "

" + "

Number of drafts:" + " " + str(number_of_drafts) + "

" - msg += """

+def email_sending( + project_name, frequency, date_start, date_end, progress, number_of_drafts, update +): + + holiday = frappe.db.sql( + """SELECT holiday_date FROM `tabHoliday` where holiday_date = CURDATE();""" + ) + msg = ( + "

Project Name: " + + project_name + + "

Frequency: " + + " " + + frequency + + "

Update Reminder:" + + " " + + str(date_start) + + "

Expected Date End:" + + " " + + str(date_end) + + "

Percent Progress:" + + " " + + str(progress) + + "

Number of Updates:" + + " " + + str(len(update)) + + "

" + + "

Number of drafts:" + + " " + + str(number_of_drafts) + + "

" + ) + msg += """

""" - for updates in update: - msg += "" + "" + for updates in update: + msg += ( + "" + + "" + ) - msg += "
Project IDDate UpdatedTime UpdatedProject StatusNotes
" + str(updates[0]) + "" + str(updates[1]) + "" + str(updates[2]) + "" + str(updates[3]) + "" + str(updates[4]) + "
" + + str(updates[0]) + + "" + + str(updates[1]) + + "" + + str(updates[2]) + + "" + + str(updates[3]) + + "" + + str(updates[4]) + + "
" - if len(holiday) == 0: - email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project_name) - for emails in email: - frappe.sendmail(recipients=emails,subject=frappe._(project_name + ' ' + 'Summary'),message = msg) - else: - pass + msg += "" + if len(holiday) == 0: + email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project_name) + for emails in email: + frappe.sendmail( + recipients=emails, subject=frappe._(project_name + " " + "Summary"), message=msg + ) + else: + pass diff --git a/erpnext/projects/doctype/project_update/test_project_update.py b/erpnext/projects/doctype/project_update/test_project_update.py index f29c931ac05..8663350c8f2 100644 --- a/erpnext/projects/doctype/project_update/test_project_update.py +++ b/erpnext/projects/doctype/project_update/test_project_update.py @@ -9,5 +9,6 @@ import frappe class TestProjectUpdate(unittest.TestCase): pass -test_records = frappe.get_test_records('Project Update') + +test_records = frappe.get_test_records("Project Update") test_ignore = ["Sales Order"] diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 8fa0538f360..4575fb544c4 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -12,19 +12,24 @@ from frappe.utils import add_days, cstr, date_diff, flt, get_link_to_form, getda from frappe.utils.nestedset import NestedSet -class CircularReferenceError(frappe.ValidationError): pass -class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass +class CircularReferenceError(frappe.ValidationError): + pass + + +class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): + pass + class Task(NestedSet): - nsm_parent_field = 'parent_task' + nsm_parent_field = "parent_task" def get_feed(self): - return '{0}: {1}'.format(_(self.status), self.subject) + return "{0}: {1}".format(_(self.status), self.subject) def get_customer_details(self): cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) if cust: - ret = {'customer_name': cust and cust[0][0] or ''} + ret = {"customer_name": cust and cust[0][0] or ""} return ret def validate(self): @@ -38,19 +43,37 @@ class Task(NestedSet): self.validate_completed_on() def validate_dates(self): - if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): - frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \ - frappe.bold("Expected End Date"))) + if ( + self.exp_start_date + and self.exp_end_date + and getdate(self.exp_start_date) > getdate(self.exp_end_date) + ): + frappe.throw( + _("{0} can not be greater than {1}").format( + frappe.bold("Expected Start Date"), frappe.bold("Expected End Date") + ) + ) - if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): - frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ - frappe.bold("Actual End Date"))) + if ( + self.act_start_date + and self.act_end_date + and getdate(self.act_start_date) > getdate(self.act_end_date) + ): + frappe.throw( + _("{0} can not be greater than {1}").format( + frappe.bold("Actual Start Date"), frappe.bold("Actual End Date") + ) + ) def validate_parent_expected_end_date(self): if self.parent_task: parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date") if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date): - frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date))) + frappe.throw( + _( + "Expected End Date should be less than or equal to parent task's Expected End Date {0}." + ).format(getdate(parent_exp_end_date)) + ) def validate_parent_project_dates(self): if not self.project or frappe.flags.in_test: @@ -59,16 +82,24 @@ class Task(NestedSet): expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") if expected_end_date: - validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") - validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") + validate_project_dates( + getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected" + ) + validate_project_dates( + getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual" + ) def validate_status(self): if self.is_template and self.status != "Template": self.status = "Template" - if self.status!=self.get_db_value("status") and self.status == "Completed": + if self.status != self.get_db_value("status") and self.status == "Completed": for d in self.depends_on: if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): - frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task))) + frappe.throw( + _( + "Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled." + ).format(frappe.bold(self.name), frappe.bold(d.task)) + ) close_all_assignments(self.doctype, self.name) @@ -76,7 +107,7 @@ class Task(NestedSet): if flt(self.progress or 0) > 100: frappe.throw(_("Progress % for a task cannot be more than 100.")) - if self.status == 'Completed': + if self.status == "Completed": self.progress = 100 def validate_dependencies_for_template_task(self): @@ -126,34 +157,43 @@ class Task(NestedSet): clear(self.doctype, self.name) def update_total_expense_claim(self): - self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim` - where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0] + self.total_expense_claim = frappe.db.sql( + """select sum(total_sanctioned_amount) from `tabExpense Claim` + where project = %s and task = %s and docstatus=1""", + (self.project, self.name), + )[0][0] def update_time_and_costing(self): - tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date, + tl = frappe.db.sql( + """select min(from_time) as start_date, max(to_time) as end_date, sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, - sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""" - ,self.name, as_dict=1)[0] + sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""", + self.name, + as_dict=1, + )[0] if self.status == "Open": self.status = "Working" - self.total_costing_amount= tl.total_costing_amount - self.total_billing_amount= tl.total_billing_amount - self.actual_time= tl.time - self.act_start_date= tl.start_date - self.act_end_date= tl.end_date + self.total_costing_amount = tl.total_costing_amount + self.total_billing_amount = tl.total_billing_amount + self.actual_time = tl.time + self.act_start_date = tl.start_date + self.act_end_date = tl.end_date def update_project(self): if self.project and not self.flags.from_project: frappe.get_cached_doc("Project", self.project).update_project() def check_recursion(self): - if self.flags.ignore_recursion_check: return - check_list = [['task', 'parent'], ['parent', 'task']] + if self.flags.ignore_recursion_check: + return + check_list = [["task", "parent"], ["parent", "task"]] for d in check_list: task_list, count = [self.name], 0 - while (len(task_list) > count ): - tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " % - (d[0], d[1], '%s'), cstr(task_list[count])) + while len(task_list) > count: + tasks = frappe.db.sql( + " select %s from `tabTask Depends On` where %s = %s " % (d[0], d[1], "%s"), + cstr(task_list[count]), + ) count = count + 1 for b in tasks: if b[0] == self.name: @@ -167,15 +207,24 @@ class Task(NestedSet): def reschedule_dependent_tasks(self): end_date = self.exp_end_date or self.act_end_date if end_date: - for task_name in frappe.db.sql(""" + for task_name in frappe.db.sql( + """ select name from `tabTask` as parent where parent.project = %(project)s and parent.name in ( select parent from `tabTask Depends On` as child where child.task = %(task)s and child.project = %(project)s) - """, {'project': self.project, 'task':self.name }, as_dict=1): + """, + {"project": self.project, "task": self.name}, + as_dict=1, + ): task = frappe.get_doc("Task", task_name.name) - if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open": + if ( + task.exp_start_date + and task.exp_end_date + and task.exp_start_date < getdate(end_date) + and task.status == "Open" + ): task_duration = date_diff(task.exp_end_date, task.exp_start_date) task.exp_start_date = add_days(end_date, 1) task.exp_end_date = add_days(task.exp_start_date, task_duration) @@ -183,19 +232,19 @@ class Task(NestedSet): task.save() def has_webform_permission(self): - project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user") + project_user = frappe.db.get_value( + "Project User", {"parent": self.project, "user": frappe.session.user}, "user" + ) if project_user: return True def populate_depends_on(self): if self.parent_task: - parent = frappe.get_doc('Task', self.parent_task) + parent = frappe.get_doc("Task", self.parent_task) if self.name not in [row.task for row in parent.depends_on]: - parent.append("depends_on", { - "doctype": "Task Depends On", - "task": self.name, - "subject": self.subject - }) + parent.append( + "depends_on", {"doctype": "Task Depends On", "task": self.name, "subject": self.subject} + ) parent.save() def on_trash(self): @@ -208,12 +257,14 @@ class Task(NestedSet): self.update_project() def update_status(self): - if self.status not in ('Cancelled', 'Completed') and self.exp_end_date: + if self.status not in ("Cancelled", "Completed") and self.exp_end_date: from datetime import datetime + if self.exp_end_date < datetime.now().date(): - self.db_set('status', 'Overdue', update_modified=False) + self.db_set("status", "Overdue", update_modified=False) self.update_project() + @frappe.whitelist() def check_if_child_exists(name): child_tasks = frappe.get_all("Task", filters={"parent_task": name}) @@ -225,24 +276,29 @@ def check_if_child_exists(name): @frappe.validate_and_sanitize_search_inputs def get_project(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond + meta = frappe.get_meta(doctype) searchfields = meta.get_search_fields() - search_columns = ", " + ", ".join(searchfields) if searchfields else '' + search_columns = ", " + ", ".join(searchfields) if searchfields else "" search_cond = " or " + " or ".join(field + " like %(txt)s" for field in searchfields) - return frappe.db.sql(""" select name {search_columns} from `tabProject` + return frappe.db.sql( + """ select name {search_columns} from `tabProject` where %(key)s like %(txt)s %(mcond)s {search_condition} order by name - limit %(start)s, %(page_len)s""".format(search_columns = search_columns, - search_condition=search_cond), { - 'key': searchfield, - 'txt': '%' + txt + '%', - 'mcond':get_match_cond(doctype), - 'start': start, - 'page_len': page_len - }) + limit %(start)s, %(page_len)s""".format( + search_columns=search_columns, search_condition=search_cond + ), + { + "key": searchfield, + "txt": "%" + txt + "%", + "mcond": get_match_cond(doctype), + "start": start, + "page_len": page_len, + }, + ) @frappe.whitelist() @@ -253,8 +309,13 @@ def set_multiple_status(names, status): task.status = status task.save() + def set_tasks_as_overdue(): - tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"]) + tasks = frappe.get_all( + "Task", + filters={"status": ["not in", ["Cancelled", "Completed"]]}, + fields=["name", "status", "review_date"], + ) for task in tasks: if task.status == "Pending Review": if getdate(task.review_date) > getdate(today()): @@ -265,18 +326,24 @@ def set_tasks_as_overdue(): @frappe.whitelist() def make_timesheet(source_name, target_doc=None, ignore_permissions=False): def set_missing_values(source, target): - target.append("time_logs", { - "hours": source.actual_time, - "completed": source.status == "Completed", - "project": source.project, - "task": source.name - }) + target.append( + "time_logs", + { + "hours": source.actual_time, + "completed": source.status == "Completed", + "project": source.project, + "task": source.name, + }, + ) - doclist = get_mapped_doc("Task", source_name, { - "Task": { - "doctype": "Timesheet" - } - }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions) + doclist = get_mapped_doc( + "Task", + source_name, + {"Task": {"doctype": "Timesheet"}}, + target_doc, + postprocess=set_missing_values, + ignore_permissions=ignore_permissions, + ) return doclist @@ -284,60 +351,69 @@ def make_timesheet(source_name, target_doc=None, ignore_permissions=False): @frappe.whitelist() def get_children(doctype, parent, task=None, project=None, is_root=False): - filters = [['docstatus', '<', '2']] + filters = [["docstatus", "<", "2"]] if task: - filters.append(['parent_task', '=', task]) + filters.append(["parent_task", "=", task]) elif parent and not is_root: # via expand child - filters.append(['parent_task', '=', parent]) + filters.append(["parent_task", "=", parent]) else: - filters.append(['ifnull(`parent_task`, "")', '=', '']) + filters.append(['ifnull(`parent_task`, "")', "=", ""]) if project: - filters.append(['project', '=', project]) + filters.append(["project", "=", project]) - tasks = frappe.get_list(doctype, fields=[ - 'name as value', - 'subject as title', - 'is_group as expandable' - ], filters=filters, order_by='name') + tasks = frappe.get_list( + doctype, + fields=["name as value", "subject as title", "is_group as expandable"], + filters=filters, + order_by="name", + ) # return tasks return tasks + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = frappe.form_dict - args.update({ - "name_field": "subject" - }) + args.update({"name_field": "subject"}) args = make_tree_args(**args) - if args.parent_task == 'All Tasks' or args.parent_task == args.project: + if args.parent_task == "All Tasks" or args.parent_task == args.project: args.parent_task = None frappe.get_doc(args).insert() + @frappe.whitelist() def add_multiple_tasks(data, parent): data = json.loads(data) - new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""} - new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or "" + new_doc = {"doctype": "Task", "parent_task": parent if parent != "All Tasks" else ""} + new_doc["project"] = frappe.db.get_value("Task", {"name": parent}, "project") or "" for d in data: - if not d.get("subject"): continue - new_doc['subject'] = d.get("subject") + if not d.get("subject"): + continue + new_doc["subject"] = d.get("subject") new_task = frappe.get_doc(new_doc) new_task.insert() + def on_doctype_update(): frappe.db.add_index("Task", ["lft", "rgt"]) + def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date): if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0: - frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) + frappe.throw( + _("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date) + ) if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: - frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) + frappe.throw( + _("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date) + ) diff --git a/erpnext/projects/doctype/task/task_dashboard.py b/erpnext/projects/doctype/task/task_dashboard.py index 40d04e13ebc..07477da17a4 100644 --- a/erpnext/projects/doctype/task/task_dashboard.py +++ b/erpnext/projects/doctype/task/task_dashboard.py @@ -1,18 +1,11 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'task', - 'transactions': [ - { - 'label': _('Activity'), - 'items': ['Timesheet'] - }, - { - 'label': _('Accounting'), - 'items': ['Expense Claim'] - } - ] + "fieldname": "task", + "transactions": [ + {"label": _("Activity"), "items": ["Timesheet"]}, + {"label": _("Accounting"), "items": ["Expense Claim"]}, + ], } diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index a0ac7c14978..aa72ac3104e 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -16,9 +16,7 @@ class TestTask(unittest.TestCase): task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name) task1.reload() - task1.append("depends_on", { - "task": task3.name - }) + task1.append("depends_on", {"task": task3.name}) self.assertRaises(CircularReferenceError, task1.save) @@ -27,9 +25,7 @@ class TestTask(unittest.TestCase): task4 = create_task("_Test Task 4", nowdate(), add_days(nowdate(), 15), task1.name) - task3.append("depends_on", { - "task": task4.name - }) + task3.append("depends_on", {"task": task4.name}) def test_reschedule_dependent_task(self): project = frappe.get_value("Project", {"project_name": "_Test Project"}) @@ -44,20 +40,22 @@ class TestTask(unittest.TestCase): task3.get("depends_on")[0].project = project task3.save() - task1.update({ - "exp_end_date": add_days(nowdate(), 20) - }) + task1.update({"exp_end_date": add_days(nowdate(), 20)}) task1.save() - self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_start_date"), - getdate(add_days(nowdate(), 21))) - self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_end_date"), - getdate(add_days(nowdate(), 25))) + self.assertEqual( + frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate(add_days(nowdate(), 21)) + ) + self.assertEqual( + frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate(add_days(nowdate(), 25)) + ) - self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"), - getdate(add_days(nowdate(), 26))) - self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"), - getdate(add_days(nowdate(), 30))) + self.assertEqual( + frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate(add_days(nowdate(), 26)) + ) + self.assertEqual( + frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate(add_days(nowdate(), 30)) + ) def test_close_assignment(self): if not frappe.db.exists("Task", "Test Close Assignment"): @@ -67,18 +65,27 @@ class TestTask(unittest.TestCase): def assign(): from frappe.desk.form import assign_to - assign_to.add({ - "assign_to": ["test@example.com"], - "doctype": task.doctype, - "name": task.name, - "description": "Close this task" - }) + + assign_to.add( + { + "assign_to": ["test@example.com"], + "doctype": task.doctype, + "name": task.name, + "description": "Close this task", + } + ) def get_owner_and_status(): - return frappe.db.get_value("ToDo", - filters={"reference_type": task.doctype, "reference_name": task.name, - "description": "Close this task"}, - fieldname=("owner", "status"), as_dict=True) + return frappe.db.get_value( + "ToDo", + filters={ + "reference_type": task.doctype, + "reference_name": task.name, + "description": "Close this task", + }, + fieldname=("owner", "status"), + as_dict=True, + ) assign() todo = get_owner_and_status() @@ -97,18 +104,36 @@ class TestTask(unittest.TestCase): task = create_task("Testing Overdue", add_days(nowdate(), -10), add_days(nowdate(), -5)) from erpnext.projects.doctype.task.task import set_tasks_as_overdue + set_tasks_as_overdue() self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue") -def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True): + +def create_task( + subject, + start=None, + end=None, + depends_on=None, + project=None, + parent_task=None, + is_group=0, + is_template=0, + begin=0, + duration=0, + save=True, +): if not frappe.db.exists("Task", subject): - task = frappe.new_doc('Task') + task = frappe.new_doc("Task") task.status = "Open" task.subject = subject task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() - task.project = project or None if is_template else frappe.get_value("Project", {"project_name": "_Test Project"}) + task.project = ( + project or None + if is_template + else frappe.get_value("Project", {"project_name": "_Test Project"}) + ) task.is_template = is_template task.start = begin task.duration = duration @@ -120,9 +145,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa task = frappe.get_doc("Task", subject) if depends_on: - task.append("depends_on", { - "task": depends_on - }) + task.append("depends_on", {"task": depends_on}) if save: task.save() return task diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 8b603570217..57bfd5b6074 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -27,8 +27,8 @@ from erpnext.projects.doctype.timesheet.timesheet import ( class TestTimesheet(unittest.TestCase): @classmethod def setUpClass(cls): - make_earning_salary_component(setup=True, company_list=['_Test Company']) - make_deduction_salary_component(setup=True, company_list=['_Test Company']) + make_earning_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, company_list=["_Test Company"]) def setUp(self): for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]: @@ -62,7 +62,7 @@ class TestTimesheet(unittest.TestCase): emp = make_employee("test_employee_6@salary.com", company="_Test Company") salary_structure = make_salary_structure_for_timesheet(emp) - timesheet = make_timesheet(emp, simulate = True, is_billable=1) + timesheet = make_timesheet(emp, simulate=True, is_billable=1) salary_slip = make_salary_slip(timesheet.name) salary_slip.submit() @@ -73,27 +73,27 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(salary_slip.timesheets[0].time_sheet, timesheet.name) self.assertEqual(salary_slip.timesheets[0].working_hours, 2) - timesheet = frappe.get_doc('Timesheet', timesheet.name) - self.assertEqual(timesheet.status, 'Payslip') + timesheet = frappe.get_doc("Timesheet", timesheet.name) + self.assertEqual(timesheet.status, "Payslip") salary_slip.cancel() - timesheet = frappe.get_doc('Timesheet', timesheet.name) - self.assertEqual(timesheet.status, 'Submitted') + timesheet = frappe.get_doc("Timesheet", timesheet.name) + self.assertEqual(timesheet.status, "Submitted") def test_sales_invoice_from_timesheet(self): emp = make_employee("test_employee_6@salary.com") timesheet = make_timesheet(emp, simulate=True, is_billable=1) - sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer') + sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer") sales_invoice.due_date = nowdate() sales_invoice.submit() - timesheet = frappe.get_doc('Timesheet', timesheet.name) + timesheet = frappe.get_doc("Timesheet", timesheet.name) self.assertEqual(sales_invoice.total_billing_amount, 100) - self.assertEqual(timesheet.status, 'Billed') - self.assertEqual(sales_invoice.customer, '_Test Customer') + self.assertEqual(timesheet.status, "Billed") + self.assertEqual(sales_invoice.customer, "_Test Customer") item = sales_invoice.items[0] - self.assertEqual(item.item_code, '_Test Item') + self.assertEqual(item.item_code, "_Test Item") self.assertEqual(item.qty, 2.00) self.assertEqual(item.rate, 50.00) @@ -101,19 +101,21 @@ class TestTimesheet(unittest.TestCase): emp = make_employee("test_employee_6@salary.com") project = frappe.get_value("Project", {"project_name": "_Test Project"}) - timesheet = make_timesheet(emp, simulate=True, is_billable=1, project=project, company='_Test Company') + timesheet = make_timesheet( + emp, simulate=True, is_billable=1, project=project, company="_Test Company" + ) sales_invoice = create_sales_invoice(do_not_save=True) sales_invoice.project = project sales_invoice.submit() - ts = frappe.get_doc('Timesheet', timesheet.name) + ts = frappe.get_doc("Timesheet", timesheet.name) self.assertEqual(ts.per_billed, 100) self.assertEqual(ts.time_logs[0].sales_invoice, sales_invoice.name) def test_timesheet_time_overlap(self): emp = make_employee("test_employee_6@salary.com") - settings = frappe.get_single('Projects Settings') + settings = frappe.get_single("Projects Settings") initial_setting = settings.ignore_employee_time_overlap settings.ignore_employee_time_overlap = 0 settings.save() @@ -122,24 +124,24 @@ class TestTimesheet(unittest.TestCase): timesheet = frappe.new_doc("Timesheet") timesheet.employee = emp timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": now_datetime(), "to_time": now_datetime() + datetime.timedelta(hours=3), - "company": "_Test Company" - } + "company": "_Test Company", + }, ) timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": now_datetime(), "to_time": now_datetime() + datetime.timedelta(hours=3), - "company": "_Test Company" - } + "company": "_Test Company", + }, ) self.assertRaises(frappe.ValidationError, timesheet.save) @@ -158,27 +160,27 @@ class TestTimesheet(unittest.TestCase): timesheet = frappe.new_doc("Timesheet") timesheet.employee = emp timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": now_datetime(), "to_time": now_datetime() + datetime.timedelta(hours=3), - "company": "_Test Company" - } + "company": "_Test Company", + }, ) timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": now_datetime() + datetime.timedelta(hours=3), "to_time": now_datetime() + datetime.timedelta(hours=4), - "company": "_Test Company" - } + "company": "_Test Company", + }, ) - timesheet.save() # should not throw an error + timesheet.save() # should not throw an error def test_to_time(self): emp = make_employee("test_employee_6@salary.com") @@ -187,14 +189,14 @@ class TestTimesheet(unittest.TestCase): timesheet = frappe.new_doc("Timesheet") timesheet.employee = emp timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": from_time, "hours": 2, - "company": "_Test Company" - } + "company": "_Test Company", + }, ) timesheet.save() @@ -207,39 +209,51 @@ def make_salary_structure_for_timesheet(employee, company=None): frequency = "Monthly" if not frappe.db.exists("Salary Component", "Timesheet Component"): - frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert() + frappe.get_doc( + {"doctype": "Salary Component", "salary_component": "Timesheet Component"} + ).insert() - salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True) + salary_structure = make_salary_structure( + salary_structure_name, frequency, company=company, dont_submit=True + ) salary_structure.salary_component = "Timesheet Component" salary_structure.salary_slip_based_on_timesheet = 1 salary_structure.hour_rate = 50.0 salary_structure.save() salary_structure.submit() - if not frappe.db.get_value("Salary Structure Assignment", - {'employee':employee, 'docstatus': 1}): - frappe.db.set_value('Employee', employee, 'date_of_joining', - add_months(nowdate(), -5)) - create_salary_structure_assignment(employee, salary_structure.name) + if not frappe.db.get_value("Salary Structure Assignment", {"employee": employee, "docstatus": 1}): + frappe.db.set_value("Employee", employee, "date_of_joining", add_months(nowdate(), -5)) + create_salary_structure_assignment(employee, salary_structure.name) return salary_structure -def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None): +def make_timesheet( + employee, + simulate=False, + is_billable=0, + activity_type="_Test Activity Type", + project=None, + task=None, + company=None, +): update_activity_type(activity_type) timesheet = frappe.new_doc("Timesheet") timesheet.employee = employee - timesheet.company = company or '_Test Company' - timesheet_detail = timesheet.append('time_logs', {}) + timesheet.company = company or "_Test Company" + timesheet_detail = timesheet.append("time_logs", {}) timesheet_detail.is_billable = is_billable timesheet_detail.activity_type = activity_type timesheet_detail.from_time = now_datetime() timesheet_detail.hours = 2 - timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(hours= timesheet_detail.hours) + timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta( + hours=timesheet_detail.hours + ) timesheet_detail.project = project timesheet_detail.task = task - for data in timesheet.get('time_logs'): + for data in timesheet.get("time_logs"): if simulate: while True: try: @@ -247,7 +261,7 @@ def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Te break except OverlapError: data.from_time = data.from_time + datetime.timedelta(minutes=10) - data.to_time = data.from_time + datetime.timedelta(hours= data.hours) + data.to_time = data.from_time + datetime.timedelta(hours=data.hours) else: timesheet.save(ignore_permissions=True) @@ -255,7 +269,8 @@ def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Te return timesheet + def update_activity_type(activity_type): - activity_type = frappe.get_doc('Activity Type',activity_type) + activity_type = frappe.get_doc("Activity Type", activity_type) activity_type.billing_rate = 50.0 activity_type.save(ignore_permissions=True) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index b44d5017431..2ef966b3192 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -14,8 +14,13 @@ from erpnext.hr.utils import validate_active_employee from erpnext.setup.utils import get_exchange_rate -class OverlapError(frappe.ValidationError): pass -class OverWorkLoggedError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + + +class OverWorkLoggedError(frappe.ValidationError): + pass + class Timesheet(Document): def validate(self): @@ -32,7 +37,7 @@ class Timesheet(Document): def set_employee_name(self): if self.employee and not self.employee_name: - self.employee_name = frappe.db.get_value('Employee', self.employee, 'employee_name') + self.employee_name = frappe.db.get_value("Employee", self.employee, "employee_name") def calculate_total_amounts(self): self.total_hours = 0.0 @@ -70,11 +75,7 @@ class Timesheet(Document): args.billing_hours = 0 def set_status(self): - self.status = { - "0": "Draft", - "1": "Submitted", - "2": "Cancelled" - }[str(self.docstatus or 0)] + self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)] if self.per_billed == 100: self.status = "Billed" @@ -135,7 +136,7 @@ class Timesheet(Document): frappe.throw(_("To date cannot be before from date")) def validate_time_logs(self): - for data in self.get('time_logs'): + for data in self.get("time_logs"): self.set_to_time(data) self.validate_overlap(data) self.set_project(data) @@ -150,7 +151,7 @@ class Timesheet(Document): data.to_time = _to_time def validate_overlap(self, data): - settings = frappe.get_single('Projects Settings') + settings = frappe.get_single("Projects Settings") self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap) self.validate_overlap_for("employee", data, self.employee, settings.ignore_employee_time_overlap) @@ -159,7 +160,11 @@ class Timesheet(Document): def validate_project(self, data): if self.parent_project and self.parent_project != data.project: - frappe.throw(_("Row {0}: Project must be same as the one set in the Timesheet: {1}.").format(data.idx, self.parent_project)) + frappe.throw( + _("Row {0}: Project must be same as the one set in the Timesheet: {1}.").format( + data.idx, self.parent_project + ) + ) def validate_overlap_for(self, fieldname, args, value, ignore_validation=False): if not value or ignore_validation: @@ -167,8 +172,12 @@ class Timesheet(Document): existing = self.get_overlap_for(fieldname, args, value) if existing: - frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}") - .format(args.idx, self.name, existing.name), OverlapError) + frappe.throw( + _("Row {0}: From Time and To Time of {1} is overlapping with {2}").format( + args.idx, self.name, existing.name + ), + OverlapError, + ) def get_overlap_for(self, fieldname, args, value): timesheet = frappe.qb.DocType("Timesheet") @@ -179,20 +188,22 @@ class Timesheet(Document): existing = ( frappe.qb.from_(timesheet) - .join(timelog) - .on(timelog.parent == timesheet.name) - .select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time')) - .where( - (timelog.name != (args.name or "No Name")) - & (timesheet.name != (args.parent or "No Name")) - & (timesheet.docstatus < 2) - & (timesheet[fieldname] == value) - & ( - ((from_time > timelog.from_time) & (from_time < timelog.to_time)) - | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) - | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) - ) + .join(timelog) + .on(timelog.parent == timesheet.name) + .select( + timesheet.name.as_("name"), timelog.from_time.as_("from_time"), timelog.to_time.as_("to_time") + ) + .where( + (timelog.name != (args.name or "No Name")) + & (timesheet.name != (args.parent or "No Name")) + & (timesheet.docstatus < 2) + & (timesheet[fieldname] == value) + & ( + ((from_time > timelog.from_time) & (from_time < timelog.to_time)) + | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) + | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) ) + ) ).run(as_dict=True) if self.check_internal_overlap(fieldname, args): @@ -202,8 +213,7 @@ class Timesheet(Document): def check_internal_overlap(self, fieldname, args): for time_log in self.time_logs: - if not (time_log.from_time and time_log.to_time - and args.from_time and args.to_time): + if not (time_log.from_time and time_log.to_time and args.from_time and args.to_time): continue from_time = get_datetime(time_log.from_time) @@ -211,10 +221,14 @@ class Timesheet(Document): args_from_time = get_datetime(args.from_time) args_to_time = get_datetime(args.to_time) - if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and ( - (args_from_time > from_time and args_from_time < to_time) - or (args_to_time > from_time and args_to_time < to_time) - or (args_from_time <= from_time and args_to_time >= to_time) + if ( + (args.get(fieldname) == time_log.get(fieldname)) + and (args.idx != time_log.idx) + and ( + (args_from_time > from_time and args_from_time < to_time) + or (args_to_time > from_time and args_to_time < to_time) + or (args_from_time <= from_time and args_to_time >= to_time) + ) ): return True return False @@ -226,8 +240,12 @@ class Timesheet(Document): hours = data.billing_hours or 0 costing_hours = data.billing_hours or data.hours or 0 if rate: - data.billing_rate = flt(rate.get('billing_rate')) if flt(data.billing_rate) == 0 else data.billing_rate - data.costing_rate = flt(rate.get('costing_rate')) if flt(data.costing_rate) == 0 else data.costing_rate + data.billing_rate = ( + flt(rate.get("billing_rate")) if flt(data.billing_rate) == 0 else data.billing_rate + ) + data.costing_rate = ( + flt(rate.get("costing_rate")) if flt(data.costing_rate) == 0 else data.costing_rate + ) data.billing_amount = data.billing_rate * hours data.costing_amount = data.costing_rate * costing_hours @@ -235,6 +253,7 @@ class Timesheet(Document): if not ts_detail.is_billable: ts_detail.billing_rate = 0.0 + @frappe.whitelist() def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None): condition = "" @@ -269,22 +288,22 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to ORDER BY tsd.from_time ASC """ - filters = { - "project": project, - "parent": parent, - "from_time": from_time, - "to_time": to_time - } + filters = {"project": project, "parent": parent, "from_time": from_time, "to_time": to_time} return frappe.db.sql(query, filters, as_dict=1) @frappe.whitelist() def get_timesheet_detail_rate(timelog, currency): - timelog_detail = frappe.db.sql("""SELECT tsd.billing_amount as billing_amount, + timelog_detail = frappe.db.sql( + """SELECT tsd.billing_amount as billing_amount, ts.currency as currency FROM `tabTimesheet Detail` tsd INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent - WHERE tsd.name = '{0}'""".format(timelog), as_dict = 1)[0] + WHERE tsd.name = '{0}'""".format( + timelog + ), + as_dict=1, + )[0] if timelog_detail.currency: exchange_rate = get_exchange_rate(timelog_detail.currency, currency) @@ -292,44 +311,60 @@ def get_timesheet_detail_rate(timelog, currency): return timelog_detail.billing_amount * exchange_rate return timelog_detail.billing_amount + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_timesheet(doctype, txt, searchfield, start, page_len, filters): - if not filters: filters = {} + if not filters: + filters = {} condition = "" if filters.get("project"): condition = "and tsd.project = %(project)s" - return frappe.db.sql("""select distinct tsd.parent from `tabTimesheet Detail` tsd, + return frappe.db.sql( + """select distinct tsd.parent from `tabTimesheet Detail` tsd, `tabTimesheet` ts where ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and tsd.docstatus = 1 and ts.total_billable_amount > 0 and tsd.parent LIKE %(txt)s {condition} - order by tsd.parent limit %(start)s, %(page_len)s""" - .format(condition=condition), { - 'txt': '%' + txt + '%', - "start": start, "page_len": page_len, 'project': filters.get("project") - }) + order by tsd.parent limit %(start)s, %(page_len)s""".format( + condition=condition + ), + { + "txt": "%" + txt + "%", + "start": start, + "page_len": page_len, + "project": filters.get("project"), + }, + ) + @frappe.whitelist() def get_timesheet_data(name, project): data = None - if project and project!='': + if project and project != "": data = get_projectwise_timesheet_data(project, name) else: - data = frappe.get_all('Timesheet', - fields = ["(total_billable_amount - total_billed_amount) as billing_amt", "total_billable_hours as billing_hours"], filters = {'name': name}) + data = frappe.get_all( + "Timesheet", + fields=[ + "(total_billable_amount - total_billed_amount) as billing_amt", + "total_billable_hours as billing_hours", + ], + filters={"name": name}, + ) return { - 'billing_hours': data[0].billing_hours if data else None, - 'billing_amount': data[0].billing_amt if data else None, - 'timesheet_detail': data[0].name if data and project and project!= '' else None + "billing_hours": data[0].billing_hours if data else None, + "billing_amount": data[0].billing_amt if data else None, + "timesheet_detail": data[0].name if data and project and project != "" else None, } + @frappe.whitelist() def make_sales_invoice(source_name, item_code=None, customer=None, currency=None): target = frappe.new_doc("Sales Invoice") - timesheet = frappe.get_doc('Timesheet', source_name) + timesheet = frappe.get_doc("Timesheet", source_name) if not timesheet.total_billable_hours: frappe.throw(_("Invoice can't be made for zero billing hour")) @@ -349,28 +384,28 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None target.currency = currency if item_code: - target.append('items', { - 'item_code': item_code, - 'qty': hours, - 'rate': billing_rate - }) + target.append("items", {"item_code": item_code, "qty": hours, "rate": billing_rate}) for time_log in timesheet.time_logs: if time_log.is_billable: - target.append('timesheets', { - 'time_sheet': timesheet.name, - 'billing_hours': time_log.billing_hours, - 'billing_amount': time_log.billing_amount, - 'timesheet_detail': time_log.name, - 'activity_type': time_log.activity_type, - 'description': time_log.description - }) + target.append( + "timesheets", + { + "time_sheet": timesheet.name, + "billing_hours": time_log.billing_hours, + "billing_amount": time_log.billing_amount, + "timesheet_detail": time_log.name, + "activity_type": time_log.activity_type, + "description": time_log.description, + }, + ) target.run_method("calculate_billing_amount_for_timesheet") target.run_method("set_missing_values") return target + @frappe.whitelist() def make_salary_slip(source_name, target_doc=None): target = frappe.new_doc("Salary Slip") @@ -379,8 +414,9 @@ def make_salary_slip(source_name, target_doc=None): return target + def set_missing_values(time_sheet, target): - doc = frappe.get_doc('Timesheet', time_sheet) + doc = frappe.get_doc("Timesheet", time_sheet) target.employee = doc.employee target.employee_name = doc.employee_name target.salary_slip_based_on_timesheet = 1 @@ -388,26 +424,33 @@ def set_missing_values(time_sheet, target): target.end_date = doc.end_date target.posting_date = doc.modified target.total_working_hours = doc.total_hours - target.append('timesheets', { - 'time_sheet': doc.name, - 'working_hours': doc.total_hours - }) + target.append("timesheets", {"time_sheet": doc.name, "working_hours": doc.total_hours}) + @frappe.whitelist() def get_activity_cost(employee=None, activity_type=None, currency=None): - base_currency = frappe.defaults.get_global_default('currency') - rate = frappe.db.get_values("Activity Cost", {"employee": employee, - "activity_type": activity_type}, ["costing_rate", "billing_rate"], as_dict=True) + base_currency = frappe.defaults.get_global_default("currency") + rate = frappe.db.get_values( + "Activity Cost", + {"employee": employee, "activity_type": activity_type}, + ["costing_rate", "billing_rate"], + as_dict=True, + ) if not rate: - rate = frappe.db.get_values("Activity Type", {"activity_type": activity_type}, - ["costing_rate", "billing_rate"], as_dict=True) - if rate and currency and currency!=base_currency: + rate = frappe.db.get_values( + "Activity Type", + {"activity_type": activity_type}, + ["costing_rate", "billing_rate"], + as_dict=True, + ) + if rate and currency and currency != base_currency: exchange_rate = get_exchange_rate(base_currency, currency) rate[0]["costing_rate"] = rate[0]["costing_rate"] * exchange_rate rate[0]["billing_rate"] = rate[0]["billing_rate"] * exchange_rate return rate[0] if rate else {} + @frappe.whitelist() def get_events(start, end, filters=None): """Returns events for Gantt / Calendar view rendering. @@ -417,9 +460,11 @@ def get_events(start, end, filters=None): """ filters = json.loads(filters) from frappe.desk.calendar import get_event_conditions + conditions = get_event_conditions("Timesheet", filters) - return frappe.db.sql("""select `tabTimesheet Detail`.name as name, + return frappe.db.sql( + """select `tabTimesheet Detail`.name as name, `tabTimesheet Detail`.docstatus as status, `tabTimesheet Detail`.parent as parent, from_time as start_date, hours, activity_type, `tabTimesheet Detail`.project, to_time as end_date, @@ -428,29 +473,37 @@ def get_events(start, end, filters=None): where `tabTimesheet Detail`.parent = `tabTimesheet`.name and `tabTimesheet`.docstatus < 2 and (from_time <= %(end)s and to_time >= %(start)s) {conditions} {match_cond} - """.format(conditions=conditions, match_cond = get_match_cond('Timesheet')), - { - "start": start, - "end": end - }, as_dict=True, update={"allDay": 0}) + """.format( + conditions=conditions, match_cond=get_match_cond("Timesheet") + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) -def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): + +def get_timesheets_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" +): user = frappe.session.user # find customer name from contact. - customer = '' + customer = "" timesheets = [] - contact = frappe.db.exists('Contact', {'user': user}) + contact = frappe.db.exists("Contact", {"user": user}) if contact: # find customer - contact = frappe.get_doc('Contact', contact) - customer = contact.get_link_for('Customer') + contact = frappe.get_doc("Contact", contact) + customer = contact.get_link_for("Customer") if customer: - sales_invoices = [d.name for d in frappe.get_all('Sales Invoice', filters={'customer': customer})] or [None] - projects = [d.name for d in frappe.get_all('Project', filters={'customer': customer})] + sales_invoices = [ + d.name for d in frappe.get_all("Sales Invoice", filters={"customer": customer}) + ] or [None] + projects = [d.name for d in frappe.get_all("Project", filters={"customer": customer})] # Return timesheet related data to web portal. - timesheets = frappe.db.sql(''' + timesheets = frappe.db.sql( + """ SELECT ts.name, tsd.activity_type, ts.status, ts.total_billable_hours, COALESCE(ts.sales_invoice, tsd.sales_invoice) AS sales_invoice, tsd.project @@ -463,16 +516,22 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20 ) ORDER BY `end_date` ASC LIMIT {0}, {1} - '''.format(limit_start, limit_page_length), dict(sales_invoices=sales_invoices, projects=projects), as_dict=True) #nosec + """.format( + limit_start, limit_page_length + ), + dict(sales_invoices=sales_invoices, projects=projects), + as_dict=True, + ) # nosec return timesheets + def get_list_context(context=None): return { "show_sidebar": True, "show_search": True, - 'no_breadcrumbs': True, + "no_breadcrumbs": True, "title": _("Timesheets"), "get_list": get_timesheets_list, - "row_template": "templates/includes/timesheet/timesheet_row.html" + "row_template": "templates/includes/timesheet/timesheet_row.html", } diff --git a/erpnext/projects/doctype/timesheet/timesheet_dashboard.py b/erpnext/projects/doctype/timesheet/timesheet_dashboard.py index d9a341d4dff..6d6b57bebd5 100644 --- a/erpnext/projects/doctype/timesheet/timesheet_dashboard.py +++ b/erpnext/projects/doctype/timesheet/timesheet_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'time_sheet', - 'transactions': [ - { - 'label': _('References'), - 'items': ['Sales Invoice', 'Salary Slip'] - } - ] + "fieldname": "time_sheet", + "transactions": [{"label": _("References"), "items": ["Sales Invoice", "Salary Slip"]}], } diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py index 46479d0a19b..bc8f2afb8c9 100644 --- a/erpnext/projects/report/billing_summary.py +++ b/erpnext/projects/report/billing_summary.py @@ -2,7 +2,6 @@ # For license information, please see license.txt - import frappe from frappe import _ from frappe.utils import flt, time_diff_in_hours @@ -15,52 +14,45 @@ def get_columns(): "fieldtype": "Link", "fieldname": "employee", "options": "Employee", - "width": 300 + "width": 300, }, { "label": _("Employee Name"), "fieldtype": "data", "fieldname": "employee_name", "hidden": 1, - "width": 200 + "width": 200, }, { "label": _("Timesheet"), "fieldtype": "Link", "fieldname": "timesheet", "options": "Timesheet", - "width": 150 - }, - { - "label": _("Working Hours"), - "fieldtype": "Float", - "fieldname": "total_hours", - "width": 150 + "width": 150, }, + {"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "total_hours", "width": 150}, { "label": _("Billable Hours"), "fieldtype": "Float", "fieldname": "total_billable_hours", - "width": 150 + "width": 150, }, - { - "label": _("Billing Amount"), - "fieldtype": "Currency", - "fieldname": "amount", - "width": 150 - } + {"label": _("Billing Amount"), "fieldtype": "Currency", "fieldname": "amount", "width": 150}, ] + def get_data(filters): data = [] - if(filters.from_date > filters.to_date): + if filters.from_date > filters.to_date: frappe.msgprint(_("From Date can not be greater than To Date")) return data timesheets = get_timesheets(filters) filters.from_date = frappe.utils.get_datetime(filters.from_date) - filters.to_date = frappe.utils.add_to_date(frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1) + filters.to_date = frappe.utils.add_to_date( + frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1 + ) timesheet_details = get_timesheet_details(filters, timesheets.keys()) @@ -88,46 +80,58 @@ def get_data(filters): total_amount += billing_duration * flt(row.billing_rate) if total_hours: - data.append({ - "employee": timesheets.get(ts).employee, - "employee_name": timesheets.get(ts).employee_name, - "timesheet": ts, - "total_billable_hours": total_billing_hours, - "total_hours": total_hours, - "amount": total_amount - }) + data.append( + { + "employee": timesheets.get(ts).employee, + "employee_name": timesheets.get(ts).employee_name, + "timesheet": ts, + "total_billable_hours": total_billing_hours, + "total_hours": total_hours, + "amount": total_amount, + } + ) return data + def get_timesheets(filters): record_filters = [ - ["start_date", "<=", filters.to_date], - ["end_date", ">=", filters.from_date], - ["docstatus", "=", 1] - ] + ["start_date", "<=", filters.to_date], + ["end_date", ">=", filters.from_date], + ["docstatus", "=", 1], + ] if "employee" in filters: record_filters.append(["employee", "=", filters.employee]) - timesheets = frappe.get_all("Timesheet", filters=record_filters, fields=["employee", "employee_name", "name"]) + timesheets = frappe.get_all( + "Timesheet", filters=record_filters, fields=["employee", "employee_name", "name"] + ) timesheet_map = frappe._dict() for d in timesheets: timesheet_map.setdefault(d.name, d) return timesheet_map + def get_timesheet_details(filters, timesheet_list): - timesheet_details_filter = { - "parent": ["in", timesheet_list] - } + timesheet_details_filter = {"parent": ["in", timesheet_list]} if "project" in filters: timesheet_details_filter["project"] = filters.project timesheet_details = frappe.get_all( "Timesheet Detail", - filters = timesheet_details_filter, - fields=["from_time", "to_time", "hours", "is_billable", "billing_hours", "billing_rate", "parent"] + filters=timesheet_details_filter, + fields=[ + "from_time", + "to_time", + "hours", + "is_billable", + "billing_hours", + "billing_rate", + "parent", + ], ) timesheet_details_map = frappe._dict() @@ -136,6 +140,7 @@ def get_timesheet_details(filters, timesheet_list): return timesheet_details_map + def get_billable_and_total_duration(activity, start_time, end_time): precision = frappe.get_precision("Timesheet Detail", "hours") activity_duration = time_diff_in_hours(end_time, start_time) diff --git a/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py b/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py index f73376871aa..b31a063c6af 100644 --- a/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py +++ b/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py @@ -20,21 +20,37 @@ def execute(filters=None): return columns, data + def get_column(): - return [_("Timesheet") + ":Link/Timesheet:120", _("Employee") + "::150", _("Employee Name") + "::150", - _("From Datetime") + "::140", _("To Datetime") + "::140", _("Hours") + "::70", - _("Activity Type") + "::120", _("Task") + ":Link/Task:150", - _("Project") + ":Link/Project:120", _("Status") + "::70"] + return [ + _("Timesheet") + ":Link/Timesheet:120", + _("Employee") + "::150", + _("Employee Name") + "::150", + _("From Datetime") + "::140", + _("To Datetime") + "::140", + _("Hours") + "::70", + _("Activity Type") + "::120", + _("Task") + ":Link/Task:150", + _("Project") + ":Link/Project:120", + _("Status") + "::70", + ] + def get_data(conditions, filters): - time_sheet = frappe.db.sql(""" select `tabTimesheet`.name, `tabTimesheet`.employee, `tabTimesheet`.employee_name, + time_sheet = frappe.db.sql( + """ select `tabTimesheet`.name, `tabTimesheet`.employee, `tabTimesheet`.employee_name, `tabTimesheet Detail`.from_time, `tabTimesheet Detail`.to_time, `tabTimesheet Detail`.hours, `tabTimesheet Detail`.activity_type, `tabTimesheet Detail`.task, `tabTimesheet Detail`.project, `tabTimesheet`.status from `tabTimesheet Detail`, `tabTimesheet` where - `tabTimesheet Detail`.parent = `tabTimesheet`.name and %s order by `tabTimesheet`.name"""%(conditions), filters, as_list=1) + `tabTimesheet Detail`.parent = `tabTimesheet`.name and %s order by `tabTimesheet`.name""" + % (conditions), + filters, + as_list=1, + ) return time_sheet + def get_conditions(filters): conditions = "`tabTimesheet`.docstatus = 1" if filters.get("from_date"): diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py index 3ab2bb652b0..5c3dc2da118 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py @@ -13,14 +13,24 @@ def execute(filters=None): charts = get_chart_data(data) return columns, data, None, charts + def get_data(filters): conditions = get_conditions(filters) - tasks = frappe.get_all("Task", - filters = conditions, - fields = ["name", "subject", "exp_start_date", "exp_end_date", - "status", "priority", "completed_on", "progress"], - order_by="creation" - ) + tasks = frappe.get_all( + "Task", + filters=conditions, + fields=[ + "name", + "subject", + "exp_start_date", + "exp_end_date", + "status", + "priority", + "completed_on", + "progress", + ], + order_by="creation", + ) for task in tasks: if task.exp_end_date: if task.completed_on: @@ -39,6 +49,7 @@ def get_data(filters): tasks.sort(key=lambda x: x["delay"], reverse=True) return tasks + def get_conditions(filters): conditions = frappe._dict() keys = ["priority", "status"] @@ -51,6 +62,7 @@ def get_conditions(filters): conditions.exp_start_date = ["<=", filters.get("to_date")] return conditions + def get_chart_data(data): delay, on_track = 0, 0 for entry in data: @@ -61,74 +73,29 @@ def get_chart_data(data): charts = { "data": { "labels": ["On Track", "Delayed"], - "datasets": [ - { - "name": "Delayed", - "values": [on_track, delay] - } - ] + "datasets": [{"name": "Delayed", "values": [on_track, delay]}], }, "type": "percentage", - "colors": ["#84D5BA", "#CB4B5F"] + "colors": ["#84D5BA", "#CB4B5F"], } return charts + def get_columns(): columns = [ - { - "fieldname": "name", - "fieldtype": "Link", - "label": "Task", - "options": "Task", - "width": 150 - }, - { - "fieldname": "subject", - "fieldtype": "Data", - "label": "Subject", - "width": 200 - }, - { - "fieldname": "status", - "fieldtype": "Data", - "label": "Status", - "width": 100 - }, - { - "fieldname": "priority", - "fieldtype": "Data", - "label": "Priority", - "width": 80 - }, - { - "fieldname": "progress", - "fieldtype": "Data", - "label": "Progress (%)", - "width": 120 - }, + {"fieldname": "name", "fieldtype": "Link", "label": "Task", "options": "Task", "width": 150}, + {"fieldname": "subject", "fieldtype": "Data", "label": "Subject", "width": 200}, + {"fieldname": "status", "fieldtype": "Data", "label": "Status", "width": 100}, + {"fieldname": "priority", "fieldtype": "Data", "label": "Priority", "width": 80}, + {"fieldname": "progress", "fieldtype": "Data", "label": "Progress (%)", "width": 120}, { "fieldname": "exp_start_date", "fieldtype": "Date", "label": "Expected Start Date", - "width": 150 + "width": 150, }, - { - "fieldname": "exp_end_date", - "fieldtype": "Date", - "label": "Expected End Date", - "width": 150 - }, - { - "fieldname": "completed_on", - "fieldtype": "Date", - "label": "Actual End Date", - "width": 130 - }, - { - "fieldname": "delay", - "fieldtype": "Data", - "label": "Delay (In Days)", - "width": 120 - } + {"fieldname": "exp_end_date", "fieldtype": "Date", "label": "Expected End Date", "width": 150}, + {"fieldname": "completed_on", "fieldtype": "Date", "label": "Actual End Date", "width": 130}, + {"fieldname": "delay", "fieldtype": "Data", "label": "Delay (In Days)", "width": 120}, ] return columns diff --git a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py index 0d97ddf85ae..91a0607b17c 100644 --- a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -19,25 +18,17 @@ class TestDelayedTasksSummary(unittest.TestCase): task1.save() def test_delayed_tasks_summary(self): - filters = frappe._dict({ - "from_date": add_months(nowdate(), -1), - "to_date": nowdate(), - "priority": "Low", - "status": "Open" - }) - expected_data = [ + filters = frappe._dict( { - "subject": "_Test Task 99", + "from_date": add_months(nowdate(), -1), + "to_date": nowdate(), + "priority": "Low", "status": "Open", - "priority": "Low", - "delay": 1 - }, - { - "subject": "_Test Task 98", - "status": "Completed", - "priority": "Low", - "delay": -1 } + ) + expected_data = [ + {"subject": "_Test Task 99", "status": "Open", "priority": "Low", "delay": 1}, + {"subject": "_Test Task 98", "status": "Completed", "priority": "Low", "delay": -1}, ] report = execute(filters) data = list(filter(lambda x: x.subject == "_Test Task 99", report[1]))[0] diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 2854ea31fe0..6e42b0fe834 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -11,8 +11,10 @@ from six import iteritems def execute(filters=None): return EmployeeHoursReport(filters).run() + class EmployeeHoursReport: - '''Employee Hours Utilization Report Based On Timesheet''' + """Employee Hours Utilization Report Based On Timesheet""" + def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -26,13 +28,17 @@ class EmployeeHoursReport: self.day_span = (self.to_date - self.from_date).days if self.day_span <= 0: - frappe.throw(_('From Date must come before To Date')) + frappe.throw(_("From Date must come before To Date")) def validate_standard_working_hours(self): - self.standard_working_hours = frappe.db.get_single_value('HR Settings', 'standard_working_hours') + self.standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours") if not self.standard_working_hours: - msg = _('The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.').format( - frappe.bold('Standard Working Hours'), frappe.utils.get_link_to_form('HR Settings', 'HR Settings')) + msg = _( + "The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}." + ).format( + frappe.bold("Standard Working Hours"), + frappe.utils.get_link_to_form("HR Settings", "HR Settings"), + ) frappe.throw(msg) @@ -47,55 +53,50 @@ class EmployeeHoursReport: def generate_columns(self): self.columns = [ { - 'label': _('Employee'), - 'options': 'Employee', - 'fieldname': 'employee', - 'fieldtype': 'Link', - 'width': 230 + "label": _("Employee"), + "options": "Employee", + "fieldname": "employee", + "fieldtype": "Link", + "width": 230, }, { - 'label': _('Department'), - 'options': 'Department', - 'fieldname': 'department', - 'fieldtype': 'Link', - 'width': 120 + "label": _("Department"), + "options": "Department", + "fieldname": "department", + "fieldtype": "Link", + "width": 120, + }, + {"label": _("Total Hours (T)"), "fieldname": "total_hours", "fieldtype": "Float", "width": 120}, + { + "label": _("Billed Hours (B)"), + "fieldname": "billed_hours", + "fieldtype": "Float", + "width": 170, }, { - 'label': _('Total Hours (T)'), - 'fieldname': 'total_hours', - 'fieldtype': 'Float', - 'width': 120 + "label": _("Non-Billed Hours (NB)"), + "fieldname": "non_billed_hours", + "fieldtype": "Float", + "width": 170, }, { - 'label': _('Billed Hours (B)'), - 'fieldname': 'billed_hours', - 'fieldtype': 'Float', - 'width': 170 + "label": _("Untracked Hours (U)"), + "fieldname": "untracked_hours", + "fieldtype": "Float", + "width": 170, }, { - 'label': _('Non-Billed Hours (NB)'), - 'fieldname': 'non_billed_hours', - 'fieldtype': 'Float', - 'width': 170 + "label": _("% Utilization (B + NB) / T"), + "fieldname": "per_util", + "fieldtype": "Percentage", + "width": 200, }, { - 'label': _('Untracked Hours (U)'), - 'fieldname': 'untracked_hours', - 'fieldtype': 'Float', - 'width': 170 + "label": _("% Utilization (B / T)"), + "fieldname": "per_util_billed_only", + "fieldtype": "Percentage", + "width": 200, }, - { - 'label': _('% Utilization (B + NB) / T'), - 'fieldname': 'per_util', - 'fieldtype': 'Percentage', - 'width': 200 - }, - { - 'label': _('% Utilization (B / T)'), - 'fieldname': 'per_util_billed_only', - 'fieldtype': 'Percentage', - 'width': 200 - } ] def generate_data(self): @@ -112,35 +113,36 @@ class EmployeeHoursReport: for emp, data in iteritems(self.stats_by_employee): row = frappe._dict() - row['employee'] = emp + row["employee"] = emp row.update(data) self.data.append(row) # Sort by descending order of percentage utilization - self.data.sort(key=lambda x: x['per_util'], reverse=True) + self.data.sort(key=lambda x: x["per_util"], reverse=True) def filter_stats_by_department(self): filtered_data = frappe._dict() for emp, data in self.stats_by_employee.items(): - if data['department'] == self.filters.department: + if data["department"] == self.filters.department: filtered_data[emp] = data # Update stats self.stats_by_employee = filtered_data def generate_filtered_time_logs(self): - additional_filters = '' + additional_filters = "" - filter_fields = ['employee', 'project', 'company'] + filter_fields = ["employee", "project", "company"] for field in filter_fields: if self.filters.get(field): - if field == 'project': + if field == "project": additional_filters += f"AND ttd.{field} = '{self.filters.get(field)}'" else: additional_filters += f"AND tt.{field} = '{self.filters.get(field)}'" - self.filtered_time_logs = frappe.db.sql(''' + self.filtered_time_logs = frappe.db.sql( + """ SELECT tt.employee AS employee, ttd.hours AS hours, ttd.is_billable AS is_billable, ttd.project AS project FROM `tabTimesheet Detail` AS ttd JOIN `tabTimesheet` AS tt @@ -149,47 +151,46 @@ class EmployeeHoursReport: AND tt.start_date BETWEEN '{0}' AND '{1}' AND tt.end_date BETWEEN '{0}' AND '{1}' {2} - '''.format(self.filters.from_date, self.filters.to_date, additional_filters)) + """.format( + self.filters.from_date, self.filters.to_date, additional_filters + ) + ) def generate_stats_by_employee(self): self.stats_by_employee = frappe._dict() for emp, hours, is_billable, project in self.filtered_time_logs: - self.stats_by_employee.setdefault( - emp, frappe._dict() - ).setdefault('billed_hours', 0.0) + self.stats_by_employee.setdefault(emp, frappe._dict()).setdefault("billed_hours", 0.0) - self.stats_by_employee[emp].setdefault('non_billed_hours', 0.0) + self.stats_by_employee[emp].setdefault("non_billed_hours", 0.0) if is_billable: - self.stats_by_employee[emp]['billed_hours'] += flt(hours, 2) + self.stats_by_employee[emp]["billed_hours"] += flt(hours, 2) else: - self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2) + self.stats_by_employee[emp]["non_billed_hours"] += flt(hours, 2) def set_employee_department_and_name(self): for emp in self.stats_by_employee: - emp_name = frappe.db.get_value( - 'Employee', emp, 'employee_name' - ) - emp_dept = frappe.db.get_value( - 'Employee', emp, 'department' - ) + emp_name = frappe.db.get_value("Employee", emp, "employee_name") + emp_dept = frappe.db.get_value("Employee", emp, "department") - self.stats_by_employee[emp]['department'] = emp_dept - self.stats_by_employee[emp]['employee_name'] = emp_name + self.stats_by_employee[emp]["department"] = emp_dept + self.stats_by_employee[emp]["employee_name"] = emp_name def calculate_utilizations(self): TOTAL_HOURS = flt(self.standard_working_hours * self.day_span, 2) for emp, data in iteritems(self.stats_by_employee): - data['total_hours'] = TOTAL_HOURS - data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2) + data["total_hours"] = TOTAL_HOURS + data["untracked_hours"] = flt(TOTAL_HOURS - data["billed_hours"] - data["non_billed_hours"], 2) # To handle overtime edge-case - if data['untracked_hours'] < 0: - data['untracked_hours'] = 0.0 + if data["untracked_hours"] < 0: + data["untracked_hours"] = 0.0 - data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) - data['per_util_billed_only'] = flt((data['billed_hours'] / TOTAL_HOURS) * 100, 2) + data["per_util"] = flt( + ((data["billed_hours"] + data["non_billed_hours"]) / TOTAL_HOURS) * 100, 2 + ) + data["per_util_billed_only"] = flt((data["billed_hours"] / TOTAL_HOURS) * 100, 2) def generate_report_summary(self): self.report_summary = [] @@ -203,11 +204,11 @@ class EmployeeHoursReport: total_untracked = 0.0 for row in self.data: - avg_utilization += row['per_util'] - avg_utilization_billed_only += row['per_util_billed_only'] - total_billed += row['billed_hours'] - total_non_billed += row['non_billed_hours'] - total_untracked += row['untracked_hours'] + avg_utilization += row["per_util"] + avg_utilization_billed_only += row["per_util_billed_only"] + total_billed += row["billed_hours"] + total_non_billed += row["non_billed_hours"] + total_untracked += row["untracked_hours"] avg_utilization /= len(self.data) avg_utilization = flt(avg_utilization, 2) @@ -218,27 +219,19 @@ class EmployeeHoursReport: THRESHOLD_PERCENTAGE = 70.0 self.report_summary = [ { - 'value': f'{avg_utilization}%', - 'indicator': 'Red' if avg_utilization < THRESHOLD_PERCENTAGE else 'Green', - 'label': _('Avg Utilization'), - 'datatype': 'Percentage' + "value": f"{avg_utilization}%", + "indicator": "Red" if avg_utilization < THRESHOLD_PERCENTAGE else "Green", + "label": _("Avg Utilization"), + "datatype": "Percentage", }, { - 'value': f'{avg_utilization_billed_only}%', - 'indicator': 'Red' if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else 'Green', - 'label': _('Avg Utilization (Billed Only)'), - 'datatype': 'Percentage' + "value": f"{avg_utilization_billed_only}%", + "indicator": "Red" if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else "Green", + "label": _("Avg Utilization (Billed Only)"), + "datatype": "Percentage", }, - { - 'value': total_billed, - 'label': _('Total Billed Hours'), - 'datatype': 'Float' - }, - { - 'value': total_non_billed, - 'label': _('Total Non-Billed Hours'), - 'datatype': 'Float' - } + {"value": total_billed, "label": _("Total Billed Hours"), "datatype": "Float"}, + {"value": total_non_billed, "label": _("Total Non-Billed Hours"), "datatype": "Float"}, ] def generate_chart_data(self): @@ -249,33 +242,21 @@ class EmployeeHoursReport: non_billed_hours = [] untracked_hours = [] - for row in self.data: - labels.append(row.get('employee_name')) - billed_hours.append(row.get('billed_hours')) - non_billed_hours.append(row.get('non_billed_hours')) - untracked_hours.append(row.get('untracked_hours')) + labels.append(row.get("employee_name")) + billed_hours.append(row.get("billed_hours")) + non_billed_hours.append(row.get("non_billed_hours")) + untracked_hours.append(row.get("untracked_hours")) self.chart = { - 'data': { - 'labels': labels[:30], - 'datasets': [ - { - 'name': _('Billed Hours'), - 'values': billed_hours[:30] - }, - { - 'name': _('Non-Billed Hours'), - 'values': non_billed_hours[:30] - }, - { - 'name': _('Untracked Hours'), - 'values': untracked_hours[:30] - } - ] + "data": { + "labels": labels[:30], + "datasets": [ + {"name": _("Billed Hours"), "values": billed_hours[:30]}, + {"name": _("Non-Billed Hours"), "values": non_billed_hours[:30]}, + {"name": _("Untracked Hours"), "values": untracked_hours[:30]}, + ], }, - 'type': 'bar', - 'barOptions': { - 'stacked': True - } + "type": "bar", + "barOptions": {"stacked": True}, } diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index 99593822052..4cddc4a3495 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -12,191 +11,189 @@ from erpnext.projects.report.employee_hours_utilization_based_on_timesheet.emplo class TestEmployeeUtilization(unittest.TestCase): - @classmethod - def setUpClass(cls): - # Create test employee - cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company") - cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company") + @classmethod + def setUpClass(cls): + # Create test employee + cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company") + cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company") - # Create test project - cls.test_project = make_project({"project_name": "_Test Project"}) + # Create test project + cls.test_project = make_project({"project_name": "_Test Project"}) - # Create test timesheets - cls.create_test_timesheets() + # Create test timesheets + cls.create_test_timesheets() - frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9) + frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9) - @classmethod - def create_test_timesheets(cls): - timesheet1 = frappe.new_doc("Timesheet") - timesheet1.employee = cls.test_emp1 - timesheet1.company = '_Test Company' + @classmethod + def create_test_timesheets(cls): + timesheet1 = frappe.new_doc("Timesheet") + timesheet1.employee = cls.test_emp1 + timesheet1.company = "_Test Company" - timesheet1.append("time_logs", { - "activity_type": get_random("Activity Type"), - "hours": 5, - "is_billable": 1, - "from_time": '2021-04-01 13:30:00.000000', - "to_time": '2021-04-01 18:30:00.000000' - }) + timesheet1.append( + "time_logs", + { + "activity_type": get_random("Activity Type"), + "hours": 5, + "is_billable": 1, + "from_time": "2021-04-01 13:30:00.000000", + "to_time": "2021-04-01 18:30:00.000000", + }, + ) - timesheet1.save() - timesheet1.submit() + timesheet1.save() + timesheet1.submit() - timesheet2 = frappe.new_doc("Timesheet") - timesheet2.employee = cls.test_emp2 - timesheet2.company = '_Test Company' + timesheet2 = frappe.new_doc("Timesheet") + timesheet2.employee = cls.test_emp2 + timesheet2.company = "_Test Company" - timesheet2.append("time_logs", { - "activity_type": get_random("Activity Type"), - "hours": 10, - "is_billable": 0, - "from_time": '2021-04-01 13:30:00.000000', - "to_time": '2021-04-01 23:30:00.000000', - "project": cls.test_project.name - }) + timesheet2.append( + "time_logs", + { + "activity_type": get_random("Activity Type"), + "hours": 10, + "is_billable": 0, + "from_time": "2021-04-01 13:30:00.000000", + "to_time": "2021-04-01 23:30:00.000000", + "project": cls.test_project.name, + }, + ) - timesheet2.save() - timesheet2.submit() + timesheet2.save() + timesheet2.submit() - @classmethod - def tearDownClass(cls): - # Delete time logs - frappe.db.sql(""" + @classmethod + def tearDownClass(cls): + # Delete time logs + frappe.db.sql( + """ DELETE FROM `tabTimesheet Detail` WHERE parent IN ( SELECT name FROM `tabTimesheet` WHERE company = '_Test Company' ) - """) + """ + ) - frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'") - frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") + frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'") + frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") - def test_utilization_report_with_required_filters_only(self): - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03" - } + def test_utilization_report_with_required_filters_only(self): + filters = {"company": "_Test Company", "from_date": "2021-04-01", "to_date": "2021-04-03"} - report = execute(filters) + report = execute(filters) - expected_data = self.get_expected_data_for_test_employees() - self.assertEqual(report[1], expected_data) + expected_data = self.get_expected_data_for_test_employees() + self.assertEqual(report[1], expected_data) - def test_utilization_report_for_single_employee(self): - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03", - "employee": self.test_emp1 - } + def test_utilization_report_for_single_employee(self): + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "employee": self.test_emp1, + } - report = execute(filters) + report = execute(filters) - emp1_data = frappe.get_doc('Employee', self.test_emp1) - expected_data = [ - { - 'employee': self.test_emp1, - 'employee_name': 'test1@employeeutil.com', - 'billed_hours': 5.0, - 'non_billed_hours': 0.0, - 'department': emp1_data.department, - 'total_hours': 18.0, - 'untracked_hours': 13.0, - 'per_util': 27.78, - 'per_util_billed_only': 27.78 - } - ] + emp1_data = frappe.get_doc("Employee", self.test_emp1) + expected_data = [ + { + "employee": self.test_emp1, + "employee_name": "test1@employeeutil.com", + "billed_hours": 5.0, + "non_billed_hours": 0.0, + "department": emp1_data.department, + "total_hours": 18.0, + "untracked_hours": 13.0, + "per_util": 27.78, + "per_util_billed_only": 27.78, + } + ] - self.assertEqual(report[1], expected_data) + self.assertEqual(report[1], expected_data) - def test_utilization_report_for_project(self): - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03", - "project": self.test_project.name - } + def test_utilization_report_for_project(self): + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "project": self.test_project.name, + } - report = execute(filters) + report = execute(filters) - emp2_data = frappe.get_doc('Employee', self.test_emp2) - expected_data = [ - { - 'employee': self.test_emp2, - 'employee_name': 'test2@employeeutil.com', - 'billed_hours': 0.0, - 'non_billed_hours': 10.0, - 'department': emp2_data.department, - 'total_hours': 18.0, - 'untracked_hours': 8.0, - 'per_util': 55.56, - 'per_util_billed_only': 0.0 - } - ] + emp2_data = frappe.get_doc("Employee", self.test_emp2) + expected_data = [ + { + "employee": self.test_emp2, + "employee_name": "test2@employeeutil.com", + "billed_hours": 0.0, + "non_billed_hours": 10.0, + "department": emp2_data.department, + "total_hours": 18.0, + "untracked_hours": 8.0, + "per_util": 55.56, + "per_util_billed_only": 0.0, + } + ] - self.assertEqual(report[1], expected_data) + self.assertEqual(report[1], expected_data) - def test_utilization_report_for_department(self): - emp1_data = frappe.get_doc('Employee', self.test_emp1) - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03", - "department": emp1_data.department - } + def test_utilization_report_for_department(self): + emp1_data = frappe.get_doc("Employee", self.test_emp1) + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "department": emp1_data.department, + } - report = execute(filters) + report = execute(filters) - expected_data = self.get_expected_data_for_test_employees() - self.assertEqual(report[1], expected_data) + expected_data = self.get_expected_data_for_test_employees() + self.assertEqual(report[1], expected_data) - def test_report_summary_data(self): - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03" - } + def test_report_summary_data(self): + filters = {"company": "_Test Company", "from_date": "2021-04-01", "to_date": "2021-04-03"} - report = execute(filters) - summary = report[4] - expected_summary_values = ['41.67%', '13.89%', 5.0, 10.0] + report = execute(filters) + summary = report[4] + expected_summary_values = ["41.67%", "13.89%", 5.0, 10.0] - self.assertEqual(len(summary), 4) + self.assertEqual(len(summary), 4) - for i in range(4): - self.assertEqual( - summary[i]['value'], expected_summary_values[i] - ) + for i in range(4): + self.assertEqual(summary[i]["value"], expected_summary_values[i]) - def get_expected_data_for_test_employees(self): - emp1_data = frappe.get_doc('Employee', self.test_emp1) - emp2_data = frappe.get_doc('Employee', self.test_emp2) + def get_expected_data_for_test_employees(self): + emp1_data = frappe.get_doc("Employee", self.test_emp1) + emp2_data = frappe.get_doc("Employee", self.test_emp2) - return [ - { - 'employee': self.test_emp2, - 'employee_name': 'test2@employeeutil.com', - 'billed_hours': 0.0, - 'non_billed_hours': 10.0, - 'department': emp2_data.department, - 'total_hours': 18.0, - 'untracked_hours': 8.0, - 'per_util': 55.56, - 'per_util_billed_only': 0.0 - }, - { - 'employee': self.test_emp1, - 'employee_name': 'test1@employeeutil.com', - 'billed_hours': 5.0, - 'non_billed_hours': 0.0, - 'department': emp1_data.department, - 'total_hours': 18.0, - 'untracked_hours': 13.0, - 'per_util': 27.78, - 'per_util_billed_only': 27.78 - } - ] + return [ + { + "employee": self.test_emp2, + "employee_name": "test2@employeeutil.com", + "billed_hours": 0.0, + "non_billed_hours": 10.0, + "department": emp2_data.department, + "total_hours": 18.0, + "untracked_hours": 8.0, + "per_util": 55.56, + "per_util_billed_only": 0.0, + }, + { + "employee": self.test_emp1, + "employee_name": "test1@employeeutil.com", + "billed_hours": 5.0, + "non_billed_hours": 0.0, + "department": emp1_data.department, + "total_hours": 18.0, + "untracked_hours": 13.0, + "per_util": 27.78, + "per_util_billed_only": 27.78, + }, + ] diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 9520cd17be2..64795f7926d 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -14,17 +14,23 @@ def execute(filters=None): charts = get_chart_data(data) return columns, data, None, charts + def get_data(filters): data = get_rows(filters) data = calculate_cost_and_profit(data) return data + def get_rows(filters): conditions = get_conditions(filters) standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours") if not standard_working_hours: - msg = _("The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.").format( - frappe.bold("Standard Working Hours"), frappe.utils.get_link_to_form("HR Settings", "HR Settings")) + msg = _( + "The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}." + ).format( + frappe.bold("Standard Working Hours"), + frappe.utils.get_link_to_form("HR Settings", "HR Settings"), + ) frappe.msgprint(msg) return [] @@ -45,12 +51,17 @@ def get_rows(filters): `tabSalary Slip Timesheet` as sst join `tabTimesheet` on tabTimesheet.name = sst.time_sheet join `tabSales Invoice Timesheet` as sit on sit.time_sheet = tabTimesheet.name join `tabSales Invoice` as si on si.name = sit.parent and si.status != "Cancelled" - join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format(standard_working_hours) + join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format( + standard_working_hours + ) if conditions: sql += """ WHERE - {0}) as t""".format(conditions) - return frappe.db.sql(sql,filters, as_dict=True) + {0}) as t""".format( + conditions + ) + return frappe.db.sql(sql, filters, as_dict=True) + def calculate_cost_and_profit(data): for row in data: @@ -58,6 +69,7 @@ def calculate_cost_and_profit(data): row.profit = flt(row.base_grand_total) - flt(row.base_gross_pay) * flt(row.utilization) return data + def get_conditions(filters): conditions = [] @@ -77,11 +89,14 @@ def get_conditions(filters): conditions.append("tabTimesheet.employee={0}".format(frappe.db.escape(filters.get("employee")))) if filters.get("project"): - conditions.append("tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project")))) + conditions.append( + "tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project"))) + ) conditions = " and ".join(conditions) return conditions + def get_chart_data(data): if not data: return None @@ -94,20 +109,13 @@ def get_chart_data(data): utilization.append(entry.get("utilization")) charts = { - "data": { - "labels": labels, - "datasets": [ - { - "name": "Utilization", - "values": utilization - } - ] - }, + "data": {"labels": labels, "datasets": [{"name": "Utilization", "values": utilization}]}, "type": "bar", - "colors": ["#84BDD5"] + "colors": ["#84BDD5"], } return charts + def get_columns(): return [ { @@ -115,98 +123,78 @@ def get_columns(): "label": _("Customer"), "fieldtype": "Link", "options": "Customer", - "width": 150 + "width": 150, }, { "fieldname": "employee", "label": _("Employee"), "fieldtype": "Link", "options": "Employee", - "width": 130 - }, - { - "fieldname": "employee_name", - "label": _("Employee Name"), - "fieldtype": "Data", - "width": 120 + "width": 130, }, + {"fieldname": "employee_name", "label": _("Employee Name"), "fieldtype": "Data", "width": 120}, { "fieldname": "voucher_no", "label": _("Sales Invoice"), "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 + "width": 120, }, { "fieldname": "timesheet", "label": _("Timesheet"), "fieldtype": "Link", "options": "Timesheet", - "width": 120 + "width": 120, }, { "fieldname": "project", "label": _("Project"), "fieldtype": "Link", "options": "Project", - "width": 100 + "width": 100, }, { "fieldname": "base_grand_total", "label": _("Bill Amount"), "fieldtype": "Currency", "options": "currency", - "width": 100 + "width": 100, }, { "fieldname": "base_gross_pay", "label": _("Cost"), "fieldtype": "Currency", "options": "currency", - "width": 100 + "width": 100, }, { "fieldname": "profit", "label": _("Profit"), "fieldtype": "Currency", "options": "currency", - "width": 100 - }, - { - "fieldname": "utilization", - "label": _("Utilization"), - "fieldtype": "Percentage", - "width": 100 + "width": 100, }, + {"fieldname": "utilization", "label": _("Utilization"), "fieldtype": "Percentage", "width": 100}, { "fieldname": "fractional_cost", "label": _("Fractional Cost"), "fieldtype": "Int", - "width": 120 + "width": 120, }, { "fieldname": "total_billed_hours", "label": _("Total Billed Hours"), "fieldtype": "Int", - "width": 150 - }, - { - "fieldname": "start_date", - "label": _("Start Date"), - "fieldtype": "Date", - "width": 100 - }, - { - "fieldname": "end_date", - "label": _("End Date"), - "fieldtype": "Date", - "width": 100 + "width": 150, }, + {"fieldname": "start_date", "label": _("Start Date"), "fieldtype": "Date", "width": 100}, + {"fieldname": "end_date", "label": _("End Date"), "fieldtype": "Date", "width": 100}, { "label": _("Currency"), "fieldname": "currency", "fieldtype": "Link", "options": "Currency", - "width": 80 - } + "width": 80, + }, ] diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index 3396a2193cf..f544739b4e5 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -1,7 +1,7 @@ - import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee.test_employee import make_employee @@ -13,15 +13,17 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_ from erpnext.projects.report.project_profitability.project_profitability import execute -class TestProjectProfitability(unittest.TestCase): +class TestProjectProfitability(FrappeTestCase): def setUp(self): - frappe.db.sql('delete from `tabTimesheet`') - emp = make_employee('test_employee_9@salary.com', company='_Test Company') + frappe.db.sql("delete from `tabTimesheet`") + emp = make_employee("test_employee_9@salary.com", company="_Test Company") - if not frappe.db.exists('Salary Component', 'Timesheet Component'): - frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert() + if not frappe.db.exists("Salary Component", "Timesheet Component"): + frappe.get_doc( + {"doctype": "Salary Component", "salary_component": "Timesheet Component"} + ).insert() - make_salary_structure_for_timesheet(emp, company='_Test Company') + make_salary_structure_for_timesheet(emp, company="_Test Company") date = getdate() self.timesheet = make_timesheet(emp, is_billable=1) @@ -30,21 +32,21 @@ class TestProjectProfitability(unittest.TestCase): holidays = self.salary_slip.get_holidays_for_employee(date, date) if holidays: - frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1) + frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) self.salary_slip.submit() - self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') + self.sales_invoice = make_sales_invoice(self.timesheet.name, "_Test Item", "_Test Customer") self.sales_invoice.due_date = date self.sales_invoice.submit() - frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) - frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 0) + frappe.db.set_value("HR Settings", None, "standard_working_hours", 8) + frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) def test_project_profitability(self): filters = { - 'company': '_Test Company', - 'start_date': add_days(self.timesheet.start_date, -3), - 'end_date': self.timesheet.start_date + "company": "_Test Company", + "start_date": add_days(self.timesheet.start_date, -3), + "end_date": self.timesheet.start_date, } report = execute(filters) @@ -60,7 +62,9 @@ class TestProjectProfitability(unittest.TestCase): self.assertEqual(self.salary_slip.total_working_days, row.total_working_days) standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours") - utilization = timesheet.total_billed_hours/(self.salary_slip.total_working_days * standard_working_hours) + utilization = timesheet.total_billed_hours / ( + self.salary_slip.total_working_days * standard_working_hours + ) self.assertEqual(utilization, row.utilization) profit = self.sales_invoice.base_grand_total - self.salary_slip.base_gross_pay * utilization @@ -68,6 +72,3 @@ class TestProjectProfitability(unittest.TestCase): fractional_cost = self.salary_slip.base_gross_pay * utilization self.assertEqual(fractional_cost, row.fractional_cost) - - def tearDown(self): - frappe.db.rollback() diff --git a/erpnext/projects/report/project_summary/project_summary.py b/erpnext/projects/report/project_summary/project_summary.py index ce1b70160c8..606c0c2d81d 100644 --- a/erpnext/projects/report/project_summary/project_summary.py +++ b/erpnext/projects/report/project_summary/project_summary.py @@ -10,18 +10,35 @@ def execute(filters=None): columns = get_columns() data = [] - data = frappe.db.get_all("Project", filters=filters, fields=["name", 'status', "percent_complete", "expected_start_date", "expected_end_date", "project_type"], order_by="expected_end_date") + data = frappe.db.get_all( + "Project", + filters=filters, + fields=[ + "name", + "status", + "percent_complete", + "expected_start_date", + "expected_end_date", + "project_type", + ], + order_by="expected_end_date", + ) for project in data: project["total_tasks"] = frappe.db.count("Task", filters={"project": project.name}) - project["completed_tasks"] = frappe.db.count("Task", filters={"project": project.name, "status": "Completed"}) - project["overdue_tasks"] = frappe.db.count("Task", filters={"project": project.name, "status": "Overdue"}) + project["completed_tasks"] = frappe.db.count( + "Task", filters={"project": project.name, "status": "Completed"} + ) + project["overdue_tasks"] = frappe.db.count( + "Task", filters={"project": project.name, "status": "Overdue"} + ) chart = get_chart_data(data) report_summary = get_report_summary(data) return columns, data, None, chart, report_summary + def get_columns(): return [ { @@ -29,59 +46,35 @@ def get_columns(): "label": _("Project"), "fieldtype": "Link", "options": "Project", - "width": 200 + "width": 200, }, { "fieldname": "project_type", "label": _("Type"), "fieldtype": "Link", "options": "Project Type", - "width": 120 - }, - { - "fieldname": "status", - "label": _("Status"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "total_tasks", - "label": _("Total Tasks"), - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "status", "label": _("Status"), "fieldtype": "Data", "width": 120}, + {"fieldname": "total_tasks", "label": _("Total Tasks"), "fieldtype": "Data", "width": 120}, { "fieldname": "completed_tasks", "label": _("Tasks Completed"), "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "overdue_tasks", - "label": _("Tasks Overdue"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "percent_complete", - "label": _("Completion"), - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "overdue_tasks", "label": _("Tasks Overdue"), "fieldtype": "Data", "width": 120}, + {"fieldname": "percent_complete", "label": _("Completion"), "fieldtype": "Data", "width": 120}, { "fieldname": "expected_start_date", "label": _("Start Date"), "fieldtype": "Date", - "width": 120 - }, - { - "fieldname": "expected_end_date", - "label": _("End Date"), - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"fieldname": "expected_end_date", "label": _("End Date"), "fieldtype": "Date", "width": 120}, ] + def get_chart_data(data): labels = [] total = [] @@ -96,29 +89,19 @@ def get_chart_data(data): return { "data": { - 'labels': labels[:30], - 'datasets': [ - { - "name": "Overdue", - "values": overdue[:30] - }, - { - "name": "Completed", - "values": completed[:30] - }, - { - "name": "Total Tasks", - "values": total[:30] - }, - ] + "labels": labels[:30], + "datasets": [ + {"name": "Overdue", "values": overdue[:30]}, + {"name": "Completed", "values": completed[:30]}, + {"name": "Total Tasks", "values": total[:30]}, + ], }, "type": "bar", "colors": ["#fc4f51", "#78d6ff", "#7575ff"], - "barOptions": { - "stacked": True - } + "barOptions": {"stacked": True}, } + def get_report_summary(data): if not data: return None @@ -152,5 +135,5 @@ def get_report_summary(data): "indicator": "Green" if total_overdue == 0 else "Red", "label": _("Overdue Tasks"), "datatype": "Int", - } + }, ] diff --git a/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py b/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py index 31bcc3b2ca3..da609ca769d 100644 --- a/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py +++ b/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py @@ -14,29 +14,56 @@ def execute(filters=None): data = [] for project in proj_details: - data.append([project.name, pr_item_map.get(project.name, 0), - se_item_map.get(project.name, 0), dn_item_map.get(project.name, 0), - project.project_name, project.status, project.company, - project.customer, project.estimated_costing, project.expected_start_date, - project.expected_end_date]) + data.append( + [ + project.name, + pr_item_map.get(project.name, 0), + se_item_map.get(project.name, 0), + dn_item_map.get(project.name, 0), + project.project_name, + project.status, + project.company, + project.customer, + project.estimated_costing, + project.expected_start_date, + project.expected_end_date, + ] + ) return columns, data + def get_columns(): - return [_("Project Id") + ":Link/Project:140", _("Cost of Purchased Items") + ":Currency:160", - _("Cost of Issued Items") + ":Currency:160", _("Cost of Delivered Items") + ":Currency:160", - _("Project Name") + "::120", _("Project Status") + "::120", _("Company") + ":Link/Company:100", - _("Customer") + ":Link/Customer:140", _("Project Value") + ":Currency:120", - _("Project Start Date") + ":Date:120", _("Completion Date") + ":Date:120"] + return [ + _("Project Id") + ":Link/Project:140", + _("Cost of Purchased Items") + ":Currency:160", + _("Cost of Issued Items") + ":Currency:160", + _("Cost of Delivered Items") + ":Currency:160", + _("Project Name") + "::120", + _("Project Status") + "::120", + _("Company") + ":Link/Company:100", + _("Customer") + ":Link/Customer:140", + _("Project Value") + ":Currency:120", + _("Project Start Date") + ":Date:120", + _("Completion Date") + ":Date:120", + ] + def get_project_details(): - return frappe.db.sql(""" select name, project_name, status, company, customer, estimated_costing, - expected_start_date, expected_end_date from tabProject where docstatus < 2""", as_dict=1) + return frappe.db.sql( + """ select name, project_name, status, company, customer, estimated_costing, + expected_start_date, expected_end_date from tabProject where docstatus < 2""", + as_dict=1, + ) + def get_purchased_items_cost(): - pr_items = frappe.db.sql("""select project, sum(base_net_amount) as amount + pr_items = frappe.db.sql( + """select project, sum(base_net_amount) as amount from `tabPurchase Receipt Item` where ifnull(project, '') != '' - and docstatus = 1 group by project""", as_dict=1) + and docstatus = 1 group by project""", + as_dict=1, + ) pr_item_map = {} for item in pr_items: @@ -44,11 +71,15 @@ def get_purchased_items_cost(): return pr_item_map + def get_issued_items_cost(): - se_items = frappe.db.sql("""select se.project, sum(se_item.amount) as amount + se_items = frappe.db.sql( + """select se.project, sum(se_item.amount) as amount from `tabStock Entry` se, `tabStock Entry Detail` se_item where se.name = se_item.parent and se.docstatus = 1 and ifnull(se_item.t_warehouse, '') = '' - and ifnull(se.project, '') != '' group by se.project""", as_dict=1) + and ifnull(se.project, '') != '' group by se.project""", + as_dict=1, + ) se_item_map = {} for item in se_items: @@ -56,18 +87,24 @@ def get_issued_items_cost(): return se_item_map + def get_delivered_items_cost(): - dn_items = frappe.db.sql("""select dn.project, sum(dn_item.base_net_amount) as amount + dn_items = frappe.db.sql( + """select dn.project, sum(dn_item.base_net_amount) as amount from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item where dn.name = dn_item.parent and dn.docstatus = 1 and ifnull(dn.project, '') != '' - group by dn.project""", as_dict=1) + group by dn.project""", + as_dict=1, + ) - si_items = frappe.db.sql("""select si.project, sum(si_item.base_net_amount) as amount + si_items = frappe.db.sql( + """select si.project, sum(si_item.base_net_amount) as amount from `tabSales Invoice` si, `tabSales Invoice Item` si_item where si.name = si_item.parent and si.docstatus = 1 and si.update_stock = 1 and si.is_pos = 1 and ifnull(si.project, '') != '' - group by si.project""", as_dict=1) - + group by si.project""", + as_dict=1, + ) dn_item_map = {} for item in dn_items: diff --git a/erpnext/projects/utils.py b/erpnext/projects/utils.py index 5d7455039af..000ea662756 100644 --- a/erpnext/projects/utils.py +++ b/erpnext/projects/utils.py @@ -17,14 +17,15 @@ def query_task(doctype, txt, searchfield, start, page_len, filters): match_conditions = build_match_conditions("Task") match_conditions = ("and" + match_conditions) if match_conditions else "" - return frappe.db.sql("""select name, subject from `tabTask` + return frappe.db.sql( + """select name, subject from `tabTask` where (`%s` like %s or `subject` like %s) %s order by case when `subject` like %s then 0 else 1 end, case when `%s` like %s then 0 else 1 end, `%s`, subject - limit %s, %s""" % - (searchfield, "%s", "%s", match_conditions, "%s", - searchfield, "%s", searchfield, "%s", "%s"), - (search_string, search_string, order_by_string, order_by_string, start, page_len)) + limit %s, %s""" + % (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"), + (search_string, search_string, order_by_string, order_by_string, start, page_len), + ) diff --git a/erpnext/projects/web_form/tasks/tasks.py b/erpnext/projects/web_form/tasks/tasks.py index 67cad05a482..b42297314a9 100644 --- a/erpnext/projects/web_form/tasks/tasks.py +++ b/erpnext/projects/web_form/tasks/tasks.py @@ -1,12 +1,15 @@ - import frappe def get_context(context): if frappe.form_dict.project: - context.parents = [{'title': frappe.form_dict.project, 'route': '/projects?project='+ frappe.form_dict.project}] + context.parents = [ + {"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project} + ] context.success_url = "/projects?project=" + frappe.form_dict.project - elif context.doc and context.doc.get('project'): - context.parents = [{'title': context.doc.project, 'route': '/projects?project='+ context.doc.project}] + elif context.doc and context.doc.get("project"): + context.parents = [ + {"title": context.doc.project, "route": "/projects?project=" + context.doc.project} + ] context.success_url = "/projects?project=" + context.doc.project diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 75ed332f4b6..bb799af36ea 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -181,6 +181,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "journal_entry", onchange: () => this.update_options(), }, + { + fieldtype: "Check", + label: "Loan Repayment", + fieldname: "loan_repayment", + onchange: () => this.update_options(), + }, { fieldname: "column_break_5", fieldtype: "Column Break", @@ -191,13 +197,18 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "sales_invoice", onchange: () => this.update_options(), }, - { fieldtype: "Check", label: "Purchase Invoice", fieldname: "purchase_invoice", onchange: () => this.update_options(), }, + { + fieldtype: "Check", + label: "Show Only Exact Amount", + fieldname: "exact_match", + onchange: () => this.update_options(), + }, { fieldname: "column_break_5", fieldtype: "Column Break", @@ -210,8 +221,8 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { }, { fieldtype: "Check", - label: "Show Only Exact Amount", - fieldname: "exact_match", + label: "Loan Disbursement", + fieldname: "loan_disbursement", onchange: () => this.update_options(), }, { diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2b80efd6e33..2b1b0e3576b 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -34,11 +34,13 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.set_value(item.doctype, item.name, "rate", item_rate); }, - calculate_taxes_and_totals: function(update_paid_amount) { + calculate_taxes_and_totals: async function(update_paid_amount) { this.discount_amount_applied = false; this._calculate_taxes_and_totals(); this.calculate_discount_amount(); + await this.calculate_shipping_charges(); + // Advance calculation applicable to Sales /Purchase Invoice if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype) && this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) { @@ -81,7 +83,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.initialize_taxes(); this.determine_exclusive_rate(); this.calculate_net_total(); - this.calculate_shipping_charges(); this.calculate_taxes(); this.manipulate_grand_total_for_inclusive_tax(); this.calculate_totals(); @@ -270,29 +271,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, calculate_shipping_charges: function() { + // Do not apply shipping rule for POS + if (this.frm.doc.is_pos) { + return; + } + frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { - this.shipping_rule(); - } - }, - - add_taxes_from_item_tax_template: function(item_tax_map) { - let me = this; - - if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) { - if (typeof (item_tax_map) == "string") { - item_tax_map = JSON.parse(item_tax_map); - } - - $.each(item_tax_map, function(tax, rate) { - let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax); - if (!found) { - let child = frappe.model.add_child(me.frm.doc, "taxes"); - child.charge_type = "On Net Total"; - child.account_head = tax; - child.rate = 0; - } - }); + return this.shipping_rule(); } }, @@ -685,7 +671,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ })); this.frm.doc.total_advance = flt(total_allocated_amount, precision("total_advance")); + if (this.frm.doc.write_off_outstanding_amount_automatically) { + this.frm.doc.write_off_amount = 0; + } + this.calculate_outstanding_amount(update_paid_amount); + this.calculate_write_off_amount(); }, is_internal_invoice: function() { @@ -810,7 +801,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.frm.set_value('base_paid_amount', flt(base_paid_amount, precision("base_paid_amount"))); }, - calculate_change_amount: function(){ + calculate_change_amount: function() { this.frm.doc.change_amount = 0.0; this.frm.doc.base_change_amount = 0.0; if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) @@ -821,26 +812,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; var base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; - this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total + - this.frm.doc.write_off_amount, precision("change_amount")); + this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total, + precision("change_amount")); this.frm.doc.base_change_amount = flt(this.frm.doc.base_paid_amount - - base_grand_total + this.frm.doc.base_write_off_amount, - precision("base_change_amount")); + base_grand_total, precision("base_change_amount")); } } }, - calculate_write_off_amount: function(){ - if(this.frm.doc.paid_amount > this.frm.doc.grand_total){ - this.frm.doc.write_off_amount = flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - + this.frm.doc.change_amount, precision("write_off_amount")); - + calculate_write_off_amount: function() { + if (this.frm.doc.write_off_outstanding_amount_automatically) { + this.frm.doc.write_off_amount = flt(this.frm.doc.outstanding_amount, precision("write_off_amount")); this.frm.doc.base_write_off_amount = flt(this.frm.doc.write_off_amount * this.frm.doc.conversion_rate, precision("base_write_off_amount")); - }else{ - this.frm.doc.paid_amount = 0.0; + + this.calculate_outstanding_amount(false); } - this.calculate_outstanding_amount(false); + } }); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a89776250f2..fdc129b9116 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -736,6 +736,26 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }); }, + add_taxes_from_item_tax_template: function(item_tax_map) { + let me = this; + + if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) { + if (typeof (item_tax_map) == "string") { + item_tax_map = JSON.parse(item_tax_map); + } + + $.each(item_tax_map, function(tax, rate) { + let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax); + if (!found) { + let child = frappe.model.add_child(me.frm.doc, "taxes"); + child.charge_type = "On Net Total"; + child.account_head = tax; + child.rate = 0; + } + }); + } + }, + serial_no: function(doc, cdt, cdn) { var me = this; var item = frappe.get_doc(cdt, cdn); @@ -1021,9 +1041,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ var me = this; this.set_dynamic_labels(); var company_currency = this.get_company_currency(); - // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc + // Added `ignore_price_list` to determine if document is loading after mapping from another doc if(this.frm.doc.currency && this.frm.doc.currency !== company_currency - && !this.frm.doc.ignore_pricing_rule) { + && !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) { this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency, function(exchange_rate) { @@ -1049,7 +1069,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } if(flt(this.frm.doc.conversion_rate)>0.0) { - if(this.frm.doc.ignore_pricing_rule) { + if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) { this.calculate_taxes_and_totals(); } else if (!this.in_apply_price_list){ this.apply_price_list(); @@ -1066,6 +1086,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ return this.frm.call({ doc: this.frm.doc, method: "apply_shipping_rule", + callback: function(r) { + me._calculate_taxes_and_totals(); + } }).fail(() => this.frm.set_value('shipping_rule', '')); } }, @@ -1123,8 +1146,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.set_dynamic_labels(); var company_currency = this.get_company_currency(); - // Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc - if(this.frm.doc.price_list_currency !== company_currency && !this.frm.doc.ignore_pricing_rule) { + // Added `ignore_price_list` to determine if document is loading after mapping from another doc + if(this.frm.doc.price_list_currency !== company_currency && + !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) { this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency, function(exchange_rate) { me.frm.set_value("plc_conversion_rate", exchange_rate); @@ -1476,6 +1500,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ return; } + // Target doc created from a mapped doc + if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) { + return; + } + return this.frm.call({ method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.apply_pricing_rule", args: { args: args, doc: me.frm.doc }, @@ -1592,7 +1621,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ me.remove_pricing_rule(frappe.get_doc(d.doctype, d.name)); } - if (d.free_item_data) { + if (d.free_item_data.length > 0) { me.apply_product_discount(d); } @@ -1863,6 +1892,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ callback: function(r) { if(!r.exc) { item.item_tax_rate = r.message; + me.add_taxes_from_item_tax_template(item.item_tax_rate); me.calculate_taxes_and_totals(); } } diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 16e3fa0abd1..ecfc37ee556 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ fieldtype:'Float', read_only: me.has_batch && !me.has_serial_no, label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'), - default: flt(me.item.stock_qty), + default: flt(me.item.stock_qty) || flt(me.item.transfer_qty), }, ...get_pending_qty_fields(me), { @@ -94,14 +94,16 @@ erpnext.SerialNoBatchSelector = Class.extend({ description: __('Fetch Serial Numbers based on FIFO'), click: () => { let qty = this.dialog.fields_dict.qty.get_value(); + let already_selected_serial_nos = get_selected_serial_nos(me); let numbers = frappe.call({ method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number", args: { qty: qty, item_code: me.item_code, warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', - batch_no: me.item.batch_no || null, - posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date + batch_nos: me.item.batch_no || null, + posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date, + exclude_sr_nos: already_selected_serial_nos } }); @@ -575,21 +577,40 @@ function get_pending_qty_fields(me) { return pending_qty_fields; } -function calc_total_selected_qty(me) { +// get all items with same item code except row for which selector is open. +function get_rows_with_same_item_code(me) { const { frm: { doc: { items }}, item: { name, item_code }} = me; - const totalSelectedQty = items - .filter( item => ( item.name !== name ) && ( item.item_code === item_code ) ) - .map( item => flt(item.qty) ) - .reduce( (i, j) => i + j, 0); + return items.filter(item => (item.name !== name) && (item.item_code === item_code)) +} + +function calc_total_selected_qty(me) { + const totalSelectedQty = get_rows_with_same_item_code(me) + .map(item => flt(item.qty)) + .reduce((i, j) => i + j, 0); return totalSelectedQty; } +function get_selected_serial_nos(me) { + const selected_serial_nos = get_rows_with_same_item_code(me) + .map(item => item.serial_no) + .filter(serial => serial) + .map(sr_no_string => sr_no_string.split('\n')) + .reduce((acc, arr) => acc.concat(arr), []) + .filter(serial => serial); + return selected_serial_nos; +}; + function check_can_calculate_pending_qty(me) { const { frm: { doc }, item } = me; const docChecks = doc.bom_no && doc.fg_completed_qty && erpnext.stock.bom && erpnext.stock.bom.name === doc.bom_no; - const itemChecks = !!item && !item.allow_alternative_item; + const itemChecks = !!item + && !item.original_item + && erpnext.stock.bom && erpnext.stock.bom.items + && (item.item_code in erpnext.stock.bom.items); return docChecks && itemChecks; } + +//# sourceURL=serial_no_batch_selector.js diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index b743504a527..843bd86baac 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -4,6 +4,7 @@ --green-info: #38A160; --product-bg-color: white; --body-bg-color: var(--gray-50); + --text-md: 13px; // variables are in desk folder in frappe for v13, this is a temporary fix } body.product-page { @@ -264,6 +265,15 @@ body.product-page { font-size: 13px; } + .filter-lookup-input { + background-color: white; + border: 1px solid var(--gray-300); + + &:focus { + border: 1px solid var(--primary); + } + } + .filter-label { font-size: 11px; font-weight: 600; @@ -569,15 +579,12 @@ body.product-page { } .scroll-categories { - white-space: nowrap; - overflow-x: auto; - .category-pill { - margin: 0px 4px; display: inline-block; - padding: 6px 12px; - background-color: #ecf5fe; width: fit-content; + padding: 6px 12px; + margin-bottom: 8px; + background-color: #ecf5fe; font-size: 14px; border-radius: 18px; color: var(--blue-500); diff --git a/erpnext/quality_management/doctype/quality_action/quality_action.py b/erpnext/quality_management/doctype/quality_action/quality_action.py index 87245f9a3f8..f7cd94dad31 100644 --- a/erpnext/quality_management/doctype/quality_action/quality_action.py +++ b/erpnext/quality_management/doctype/quality_action/quality_action.py @@ -7,4 +7,4 @@ from frappe.model.document import Document class QualityAction(Document): def validate(self): - self.status = 'Open' if any([d.status=='Open' for d in self.resolutions]) else 'Completed' + self.status = "Open" if any([d.status == "Open" for d in self.resolutions]) else "Completed" diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py index ec5d67f4f03..cc8ce26b58f 100644 --- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py +++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py @@ -9,15 +9,12 @@ from frappe.model.document import Document class QualityFeedback(Document): @frappe.whitelist() def set_parameters(self): - if self.template and not getattr(self, 'parameters', []): - for d in frappe.get_doc('Quality Feedback Template', self.template).parameters: - self.append('parameters', dict( - parameter = d.parameter, - rating = 1 - )) + if self.template and not getattr(self, "parameters", []): + for d in frappe.get_doc("Quality Feedback Template", self.template).parameters: + self.append("parameters", dict(parameter=d.parameter, rating=1)) def validate(self): if not self.document_name: - self.document_type ='User' + self.document_type = "User" self.document_name = frappe.session.user self.set_parameters() diff --git a/erpnext/quality_management/doctype/quality_feedback/test_quality_feedback.py b/erpnext/quality_management/doctype/quality_feedback/test_quality_feedback.py index fe36cc6e5b1..58d06326a72 100644 --- a/erpnext/quality_management/doctype/quality_feedback/test_quality_feedback.py +++ b/erpnext/quality_management/doctype/quality_feedback/test_quality_feedback.py @@ -8,21 +8,22 @@ import frappe class TestQualityFeedback(unittest.TestCase): def test_quality_feedback(self): - template = frappe.get_doc(dict( - doctype = 'Quality Feedback Template', - template = 'Test Template', - parameters = [ - dict(parameter='Test Parameter 1'), - dict(parameter='Test Parameter 2') - ] - )).insert() + template = frappe.get_doc( + dict( + doctype="Quality Feedback Template", + template="Test Template", + parameters=[dict(parameter="Test Parameter 1"), dict(parameter="Test Parameter 2")], + ) + ).insert() - feedback = frappe.get_doc(dict( - doctype = 'Quality Feedback', - template = template.name, - document_type = 'User', - document_name = frappe.session.user - )).insert() + feedback = frappe.get_doc( + dict( + doctype="Quality Feedback", + template=template.name, + document_type="User", + document_name=frappe.session.user, + ) + ).insert() self.assertEqual(template.parameters[0].parameter, feedback.parameters[0].parameter) diff --git a/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py b/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py index 67fdaca6d9b..40606cdca76 100644 --- a/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py +++ b/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py @@ -13,12 +13,13 @@ class TestQualityGoal(unittest.TestCase): self.assertTrue(goal) goal.delete() + def get_quality_goal(): - return frappe.get_doc(dict( - doctype = 'Quality Goal', - goal = 'Test Quality Module', - frequency = 'Daily', - objectives = [ - dict(objective = 'Check test cases', target='100', uom='Percent') - ] - )).insert() + return frappe.get_doc( + dict( + doctype="Quality Goal", + goal="Test Quality Module", + frequency="Daily", + objectives=[dict(objective="Check test cases", target="100", uom="Percent")], + ) + ).insert() diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index 0f535ba2e11..72f9e6d6e44 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -8,7 +8,7 @@ from frappe.utils.nestedset import NestedSet class QualityProcedure(NestedSet): - nsm_parent_field = 'parent_quality_procedure' + nsm_parent_field = "parent_quality_procedure" def before_save(self): self.check_for_incorrect_child() @@ -29,14 +29,19 @@ class QualityProcedure(NestedSet): def on_trash(self): # clear from child table (sub procedures) - frappe.db.sql('''update `tabQuality Procedure Process` - set `procedure`='' where `procedure`=%s''', self.name) + frappe.db.sql( + """update `tabQuality Procedure Process` + set `procedure`='' where `procedure`=%s""", + self.name, + ) NestedSet.on_trash(self, allow_root_deletion=True) def set_parent(self): for process in self.processes: # Set parent for only those children who don't have a parent - has_parent = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") + has_parent = frappe.db.get_value( + "Quality Procedure", process.procedure, "parent_quality_procedure" + ) if not has_parent and process.procedure: frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name) @@ -45,10 +50,17 @@ class QualityProcedure(NestedSet): if process.procedure: self.is_group = 1 # Check if any child process belongs to another parent. - parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") + parent_quality_procedure = frappe.db.get_value( + "Quality Procedure", process.procedure, "parent_quality_procedure" + ) if parent_quality_procedure and parent_quality_procedure != self.name: - frappe.throw(_("{0} already has a Parent Procedure {1}.").format(frappe.bold(process.procedure), frappe.bold(parent_quality_procedure)), - title=_("Invalid Child Procedure")) + frappe.throw( + _("{0} already has a Parent Procedure {1}.").format( + frappe.bold(process.procedure), frappe.bold(parent_quality_procedure) + ), + title=_("Invalid Child Procedure"), + ) + @frappe.whitelist() def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False): @@ -56,16 +68,23 @@ def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=Fa parent = "" if parent: - parent_procedure = frappe.get_doc('Quality Procedure', parent) + parent_procedure = frappe.get_doc("Quality Procedure", parent) # return the list in order - return [dict( - value=d.procedure, - expandable=frappe.db.get_value('Quality Procedure', d.procedure, 'is_group')) - for d in parent_procedure.processes if d.procedure - ] + return [ + dict( + value=d.procedure, expandable=frappe.db.get_value("Quality Procedure", d.procedure, "is_group") + ) + for d in parent_procedure.processes + if d.procedure + ] else: - return frappe.get_all(doctype, fields=['name as value', 'is_group as expandable'], - filters = dict(parent_quality_procedure = parent), order_by='name asc') + return frappe.get_all( + doctype, + fields=["name as value", "is_group as expandable"], + filters=dict(parent_quality_procedure=parent), + order_by="name asc", + ) + @frappe.whitelist() def add_node(): @@ -74,7 +93,7 @@ def add_node(): args = frappe.form_dict args = make_tree_args(**args) - if args.parent_quality_procedure == 'All Quality Procedures': + if args.parent_quality_procedure == "All Quality Procedures": args.parent_quality_procedure = None return frappe.get_doc(args).insert() diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index 6130895e38d..daf7a694a35 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -11,16 +11,21 @@ from .quality_procedure import add_node class TestQualityProcedure(unittest.TestCase): def test_add_node(self): try: - procedure = frappe.get_doc(dict( - doctype = 'Quality Procedure', - quality_procedure_name = 'Test Procedure 1', - processes = [ - dict(process_description = 'Test Step 1') - ] - )).insert() + procedure = frappe.get_doc( + dict( + doctype="Quality Procedure", + quality_procedure_name="Test Procedure 1", + processes=[dict(process_description="Test Step 1")], + ) + ).insert() - frappe.form_dict = dict(doctype = 'Quality Procedure', quality_procedure_name = 'Test Child 1', - parent_quality_procedure = procedure.name, cmd='test', is_root='false') + frappe.form_dict = dict( + doctype="Quality Procedure", + quality_procedure_name="Test Child 1", + parent_quality_procedure=procedure.name, + cmd="test", + is_root="false", + ) node = add_node() procedure.reload() @@ -39,12 +44,13 @@ class TestQualityProcedure(unittest.TestCase): finally: procedure.delete() + def create_procedure(): - return frappe.get_doc(dict( - doctype = 'Quality Procedure', - quality_procedure_name = 'Test Procedure 1', - is_group = 1, - processes = [ - dict(process_description = 'Test Step 1') - ] - )).insert() + return frappe.get_doc( + dict( + doctype="Quality Procedure", + quality_procedure_name="Test Procedure 1", + is_group=1, + processes=[dict(process_description="Test Step 1")], + ) + ).insert() diff --git a/erpnext/quality_management/doctype/quality_review/quality_review.py b/erpnext/quality_management/doctype/quality_review/quality_review.py index b896f8dfe0c..f691005566d 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review.py +++ b/erpnext/quality_management/doctype/quality_review/quality_review.py @@ -10,55 +10,52 @@ class QualityReview(Document): def validate(self): # fetch targets from goal if not self.reviews: - for d in frappe.get_doc('Quality Goal', self.goal).objectives: - self.append('reviews', dict( - objective = d.objective, - target = d.target, - uom = d.uom - )) + for d in frappe.get_doc("Quality Goal", self.goal).objectives: + self.append("reviews", dict(objective=d.objective, target=d.target, uom=d.uom)) self.set_status() def set_status(self): # if any child item is failed, fail the parent - if not len(self.reviews or []) or any([d.status=='Open' for d in self.reviews]): - self.status = 'Open' - elif any([d.status=='Failed' for d in self.reviews]): - self.status = 'Failed' + if not len(self.reviews or []) or any([d.status == "Open" for d in self.reviews]): + self.status = "Open" + elif any([d.status == "Failed" for d in self.reviews]): + self.status = "Failed" else: - self.status = 'Passed' + self.status = "Passed" + def review(): day = frappe.utils.getdate().day weekday = frappe.utils.getdate().strftime("%A") month = frappe.utils.getdate().strftime("%B") - for goal in frappe.get_list("Quality Goal", fields=['name', 'frequency', 'date', 'weekday']): - if goal.frequency == 'Daily': + for goal in frappe.get_list("Quality Goal", fields=["name", "frequency", "date", "weekday"]): + if goal.frequency == "Daily": create_review(goal.name) - elif goal.frequency == 'Weekly' and goal.weekday == weekday: + elif goal.frequency == "Weekly" and goal.weekday == weekday: create_review(goal.name) - elif goal.frequency == 'Monthly' and goal.date == str(day): + elif goal.frequency == "Monthly" and goal.date == str(day): create_review(goal.name) - elif goal.frequency == 'Quarterly' and day==1 and get_quarter(month): + elif goal.frequency == "Quarterly" and day == 1 and get_quarter(month): create_review(goal.name) + def create_review(goal): goal = frappe.get_doc("Quality Goal", goal) - review = frappe.get_doc({ - "doctype": "Quality Review", - "goal": goal.name, - "date": frappe.utils.getdate() - }) + review = frappe.get_doc( + {"doctype": "Quality Review", "goal": goal.name, "date": frappe.utils.getdate()} + ) review.insert(ignore_permissions=True) + def get_quarter(month): - if month in ["January", "April", "July", "October"]: + if month in ["January", "April", "July", "October"]: return True else: return False diff --git a/erpnext/quality_management/doctype/quality_review/test_quality_review.py b/erpnext/quality_management/doctype/quality_review/test_quality_review.py index 8a254dba2a5..c76e7f2731a 100644 --- a/erpnext/quality_management/doctype/quality_review/test_quality_review.py +++ b/erpnext/quality_management/doctype/quality_review/test_quality_review.py @@ -15,7 +15,7 @@ class TestQualityReview(unittest.TestCase): review() # check if review exists - quality_review = frappe.get_doc('Quality Review', dict(goal = quality_goal.name)) + quality_review = frappe.get_doc("Quality Review", dict(goal=quality_goal.name)) self.assertEqual(quality_goal.objectives[0].target, quality_review.reviews[0].target) quality_review.delete() diff --git a/erpnext/regional/__init__.py b/erpnext/regional/__init__.py index c460286078d..ec2db811240 100644 --- a/erpnext/regional/__init__.py +++ b/erpnext/regional/__init__.py @@ -13,6 +13,7 @@ def check_deletion_permission(doc, method): if region in ["Nepal", "France"] and doc.docstatus != 0: frappe.throw(_("Deletion is not permitted for country {0}").format(region)) + def create_transaction_log(doc, method): """ Appends the transaction to a chain of hashed logs for legal resons. @@ -24,10 +25,11 @@ def create_transaction_log(doc, method): data = str(doc.as_dict()) - frappe.get_doc({ - "doctype": "Transaction Log", - "reference_doctype": doc.doctype, - "document_name": doc.name, - "data": data - }).insert(ignore_permissions=True) - + frappe.get_doc( + { + "doctype": "Transaction Log", + "reference_doctype": doc.doctype, + "document_name": doc.name, + "data": data, + } + ).insert(ignore_permissions=True) diff --git a/erpnext/regional/address_template/setup.py b/erpnext/regional/address_template/setup.py index 0f9a1b19f53..ff26460d767 100644 --- a/erpnext/regional/address_template/setup.py +++ b/erpnext/regional/address_template/setup.py @@ -9,12 +9,14 @@ def set_up_address_templates(default_country=None): is_default = 1 if country == default_country else 0 update_address_template(country, html, is_default) + def get_address_templates(): """ Return country and path for all HTML files in this directory. Returns a list of dicts. """ + def country(file_name): """Convert 'united_states.html' to 'United States'.""" suffix_pos = file_name.find(".html") @@ -47,9 +49,6 @@ def update_address_template(country, html, is_default=0): frappe.db.set_value("Address Template", country, "template", html) frappe.db.set_value("Address Template", country, "is_default", is_default) else: - frappe.get_doc(dict( - doctype="Address Template", - country=country, - is_default=is_default, - template=html - )).insert() + frappe.get_doc( + dict(doctype="Address Template", country=country, is_default=is_default, template=html) + ).insert() diff --git a/erpnext/regional/address_template/test_regional_address_template.py b/erpnext/regional/address_template/test_regional_address_template.py index 9ad3d470d4a..523653b5846 100644 --- a/erpnext/regional/address_template/test_regional_address_template.py +++ b/erpnext/regional/address_template/test_regional_address_template.py @@ -1,4 +1,3 @@ - from unittest import TestCase import frappe @@ -10,13 +9,11 @@ def ensure_country(country): if frappe.db.exists("Country", country): return frappe.get_doc("Country", country) else: - c = frappe.get_doc({ - "doctype": "Country", - "country_name": country - }) + c = frappe.get_doc({"doctype": "Country", "country_name": country}) c.insert() return c + class TestRegionalAddressTemplate(TestCase): def test_get_address_templates(self): """Get the countries and paths from the templates directory.""" @@ -35,11 +32,9 @@ class TestRegionalAddressTemplate(TestCase): """Update an existing Address Template.""" country = ensure_country("Germany") if not frappe.db.exists("Address Template", country.name): - template = frappe.get_doc({ - "doctype": "Address Template", - "country": country.name, - "template": "EXISTING" - }).insert() + template = frappe.get_doc( + {"doctype": "Address Template", "country": country.name, "template": "EXISTING"} + ).insert() update_address_template(country.name, "NEW") doc = frappe.get_doc("Address Template", country.name) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index b770566ecc9..897d8d86da4 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -9,4 +9,4 @@ from frappe.model.document import Document class EInvoiceSettings(Document): def validate(self): if self.enable and not self.credentials: - frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.')) + frappe.throw(_("You must add atleast one credentials to be able to use E Invoicing.")) diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py index 3b73a5c23ec..e6bdabdf2b7 100644 --- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py +++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py @@ -9,25 +9,28 @@ from frappe.model.document import Document class GSTHSNCode(Document): pass + @frappe.whitelist() def update_taxes_in_item_master(taxes, hsn_code): - items = frappe.get_list("Item", filters={ - 'gst_hsn_code': hsn_code - }) + items = frappe.get_list("Item", filters={"gst_hsn_code": hsn_code}) taxes = frappe.parse_json(taxes) frappe.enqueue(update_item_document, items=items, taxes=taxes) return 1 + def update_item_document(items, taxes): for item in items: - item_to_be_updated=frappe.get_doc("Item", item.name) + item_to_be_updated = frappe.get_doc("Item", item.name) item_to_be_updated.taxes = [] for tax in taxes: tax = frappe._dict(tax) - item_to_be_updated.append("taxes", { - 'item_tax_template': tax.item_tax_template, - 'tax_category': tax.tax_category, - 'valid_from': tax.valid_from - }) + item_to_be_updated.append( + "taxes", + { + "item_tax_template": tax.item_tax_template, + "tax_category": tax.tax_category, + "valid_from": tax.valid_from, + }, + ) item_to_be_updated.save() diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.py b/erpnext/regional/doctype/gst_settings/gst_settings.py index 13ef3e04885..ff09ed01300 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.py +++ b/erpnext/regional/doctype/gst_settings/gst_settings.py @@ -11,15 +11,21 @@ from frappe.model.document import Document from frappe.utils import date_diff, get_url, nowdate -class EmailMissing(frappe.ValidationError): pass +class EmailMissing(frappe.ValidationError): + pass + class GSTSettings(Document): def onload(self): data = frappe._dict() - data.total_addresses = frappe.db.sql('''select count(*) from tabAddress where country = "India"''') - data.total_addresses_with_gstin = frappe.db.sql('''select distinct count(*) - from tabAddress where country = "India" and ifnull(gstin, '')!='' ''') - self.set_onload('data', data) + data.total_addresses = frappe.db.sql( + '''select count(*) from tabAddress where country = "India"''' + ) + data.total_addresses_with_gstin = frappe.db.sql( + """select distinct count(*) + from tabAddress where country = "India" and ifnull(gstin, '')!='' """ + ) + self.set_onload("data", data) def validate(self): # Validate duplicate accounts @@ -27,37 +33,44 @@ class GSTSettings(Document): def validate_duplicate_accounts(self): account_list = [] - for account in self.get('gst_accounts'): - for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']: + for account in self.get("gst_accounts"): + for fieldname in ["cgst_account", "sgst_account", "igst_account", "cess_account"]: if account.get(fieldname) in account_list: - frappe.throw(_("Account {0} appears multiple times").format( - frappe.bold(account.get(fieldname)))) + frappe.throw( + _("Account {0} appears multiple times").format(frappe.bold(account.get(fieldname))) + ) if account.get(fieldname): account_list.append(account.get(fieldname)) + @frappe.whitelist() def send_reminder(): - frappe.has_permission('GST Settings', throw=True) + frappe.has_permission("GST Settings", throw=True) - last_sent = frappe.db.get_single_value('GST Settings', 'gstin_email_sent_on') + last_sent = frappe.db.get_single_value("GST Settings", "gstin_email_sent_on") if last_sent and date_diff(nowdate(), last_sent) < 3: frappe.throw(_("Please wait 3 days before resending the reminder.")) - frappe.db.set_value('GST Settings', 'GST Settings', 'gstin_email_sent_on', nowdate()) + frappe.db.set_value("GST Settings", "GST Settings", "gstin_email_sent_on", nowdate()) # enqueue if large number of customers, suppliser - frappe.enqueue('erpnext.regional.doctype.gst_settings.gst_settings.send_gstin_reminder_to_all_parties') - frappe.msgprint(_('Email Reminders will be sent to all parties with email contacts')) + frappe.enqueue( + "erpnext.regional.doctype.gst_settings.gst_settings.send_gstin_reminder_to_all_parties" + ) + frappe.msgprint(_("Email Reminders will be sent to all parties with email contacts")) + def send_gstin_reminder_to_all_parties(): parties = [] - for address_name in frappe.db.sql('''select name - from tabAddress where country = "India" and ifnull(gstin, '')='' '''): - address = frappe.get_doc('Address', address_name[0]) + for address_name in frappe.db.sql( + """select name + from tabAddress where country = "India" and ifnull(gstin, '')='' """ + ): + address = frappe.get_doc("Address", address_name[0]) for link in address.links: party = frappe.get_doc(link.link_doctype, link.link_name) - if link.link_doctype in ('Customer', 'Supplier'): + if link.link_doctype in ("Customer", "Supplier"): t = (link.link_doctype, link.link_name, address.email_id) if not t in parties: parties.append(t) @@ -74,29 +87,30 @@ def send_gstin_reminder_to_all_parties(): @frappe.whitelist() def send_gstin_reminder(party_type, party): - '''Send GSTIN reminder to one party (called from Customer, Supplier form)''' + """Send GSTIN reminder to one party (called from Customer, Supplier form)""" frappe.has_permission(party_type, throw=True) - email = _send_gstin_reminder(party_type ,party) + email = _send_gstin_reminder(party_type, party) if email: - frappe.msgprint(_('Reminder to update GSTIN Sent'), title='Reminder sent', indicator='green') + frappe.msgprint(_("Reminder to update GSTIN Sent"), title="Reminder sent", indicator="green") + def _send_gstin_reminder(party_type, party, default_email_id=None, sent_to=None): - '''Send GST Reminder email''' - email_id = frappe.db.get_value('Contact', get_default_contact(party_type, party), 'email_id') + """Send GST Reminder email""" + email_id = frappe.db.get_value("Contact", get_default_contact(party_type, party), "email_id") if not email_id: # get email from address email_id = default_email_id if not email_id: - frappe.throw(_('Email not found in default contact'), exc=EmailMissing) + frappe.throw(_("Email not found in default contact"), exc=EmailMissing) if sent_to and email_id in sent_to: return frappe.sendmail( - subject='Please update your GSTIN', + subject="Please update your GSTIN", recipients=email_id, - message=''' + message="""

Hello,

Please help us send you GST Ready Invoices.

@@ -109,7 +123,9 @@ def _send_gstin_reminder(party_type, party, default_email_id=None, sent_to=None)
ERPNext is a free and open source ERP system.

- '''.format(os.path.join(get_url(), '/regional/india/update-gstin'), party) + """.format( + os.path.join(get_url(), "/regional/india/update-gstin"), party + ), ) return email_id 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 6b31bcc05fc..8c891c886ab 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -19,7 +19,7 @@ class GSTR3BReport(Document): self.get_data() def get_data(self): - self.report_dict = json.loads(get_json('gstr_3b_report_template')) + self.report_dict = json.loads(get_json("gstr_3b_report_template")) self.gst_details = self.get_company_gst_details() self.report_dict["gstin"] = self.gst_details.get("gstin") @@ -43,40 +43,46 @@ class GSTR3BReport(Document): self.json_output = frappe.as_json(self.report_dict) def set_inward_nil_exempt(self, inward_nil_exempt): - self.report_dict["inward_sup"]["isup_details"][0]["inter"] = flt(inward_nil_exempt.get("gst").get("inter"), 2) - self.report_dict["inward_sup"]["isup_details"][0]["intra"] = flt(inward_nil_exempt.get("gst").get("intra"), 2) - self.report_dict["inward_sup"]["isup_details"][1]["inter"] = flt(inward_nil_exempt.get("non_gst").get("inter"), 2) - self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt(inward_nil_exempt.get("non_gst").get("intra"), 2) + self.report_dict["inward_sup"]["isup_details"][0]["inter"] = flt( + inward_nil_exempt.get("gst").get("inter"), 2 + ) + self.report_dict["inward_sup"]["isup_details"][0]["intra"] = flt( + inward_nil_exempt.get("gst").get("intra"), 2 + ) + self.report_dict["inward_sup"]["isup_details"][1]["inter"] = flt( + inward_nil_exempt.get("non_gst").get("inter"), 2 + ) + self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt( + inward_nil_exempt.get("non_gst").get("intra"), 2 + ) def set_itc_details(self, itc_details): itc_eligible_type_map = { - 'IMPG': 'Import Of Capital Goods', - 'IMPS': 'Import Of Service', - 'ISRC': 'ITC on Reverse Charge', - 'ISD': 'Input Service Distributor', - 'OTH': 'All Other ITC' + "IMPG": "Import Of Capital Goods", + "IMPS": "Import Of Service", + "ISRC": "ITC on Reverse Charge", + "ISD": "Input Service Distributor", + "OTH": "All Other ITC", } - itc_ineligible_map = { - 'RUL': 'Ineligible As Per Section 17(5)', - 'OTH': 'Ineligible Others' - } + itc_ineligible_map = {"RUL": "Ineligible As Per Section 17(5)", "OTH": "Ineligible Others"} net_itc = self.report_dict["itc_elg"]["itc_net"] for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_eligible_type_map.get(d["ty"]) - for key in ['iamt', 'camt', 'samt', 'csamt']: + for key in ["iamt", "camt", "samt", "csamt"]: d[key] = flt(itc_details.get(itc_type, {}).get(key)) net_itc[key] += flt(d[key], 2) for d in self.report_dict["itc_elg"]["itc_inelg"]: itc_type = itc_ineligible_map.get(d["ty"]) - for key in ['iamt', 'camt', 'samt', 'csamt']: + for key in ["iamt", "camt", "samt", "csamt"]: d[key] = flt(itc_details.get(itc_type, {}).get(key)) def get_itc_reversal_entries(self): - reversal_entries = frappe.db.sql(""" + reversal_entries = frappe.db.sql( + """ SELECT ja.account, j.reversal_type, sum(credit_in_account_currency) as amount FROM `tabJournal Entry` j, `tabJournal Entry Account` ja where j.docstatus = 1 @@ -85,24 +91,27 @@ class GSTR3BReport(Document): and j.voucher_type = 'Reversal Of ITC' and month(j.posting_date) = %s and year(j.posting_date) = %s and j.company = %s and j.company_gstin = %s - GROUP BY ja.account, j.reversal_type""", (self.month_no, self.year, self.company, - self.gst_details.get("gstin")), as_dict=1) + GROUP BY ja.account, j.reversal_type""", + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), + as_dict=1, + ) net_itc = self.report_dict["itc_elg"]["itc_net"] for entry in reversal_entries: - if entry.reversal_type == 'As per rules 42 & 43 of CGST Rules': + if entry.reversal_type == "As per rules 42 & 43 of CGST Rules": index = 0 else: index = 1 - for key in ['camt', 'samt', 'iamt', 'csamt']: + for key in ["camt", "samt", "iamt", "csamt"]: if entry.account in self.account_heads.get(key): self.report_dict["itc_elg"]["itc_rev"][index][key] += flt(entry.amount) net_itc[key] -= flt(entry.amount) def get_itc_details(self): - itc_amounts = frappe.db.sql(""" + itc_amounts = frappe.db.sql( + """ SELECT eligibility_for_itc, sum(itc_integrated_tax) as itc_integrated_tax, sum(itc_central_tax) as itc_central_tax, sum(itc_state_tax) as itc_state_tax, @@ -113,22 +122,30 @@ class GSTR3BReport(Document): and month(posting_date) = %s and year(posting_date) = %s and company = %s and company_gstin = %s GROUP BY eligibility_for_itc - """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + """, + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), + as_dict=1, + ) itc_details = {} for d in itc_amounts: - itc_details.setdefault(d.eligibility_for_itc, { - 'iamt': d.itc_integrated_tax, - 'camt': d.itc_central_tax, - 'samt': d.itc_state_tax, - 'csamt': d.itc_cess_amount - }) + itc_details.setdefault( + d.eligibility_for_itc, + { + "iamt": d.itc_integrated_tax, + "camt": d.itc_central_tax, + "samt": d.itc_state_tax, + "csamt": d.itc_cess_amount, + }, + ) return itc_details def get_inward_nil_exempt(self, state): - inward_nil_exempt = frappe.db.sql(""" - SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst + inward_nil_exempt = frappe.db.sql( + """ + SELECT p.place_of_supply, p.supplier_address, + i.base_amount, i.is_nil_exempt, i.is_non_gst FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i WHERE p.docstatus = 1 and p.name = i.parent and p.is_opening = 'No' @@ -136,32 +153,36 @@ class GSTR3BReport(Document): and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s - GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", - (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + """, + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), + as_dict=1, + ) inward_nil_exempt_details = { - "gst": { - "intra": 0.0, - "inter": 0.0 - }, - "non_gst": { - "intra": 0.0, - "inter": 0.0 - } + "gst": {"intra": 0.0, "inter": 0.0}, + "non_gst": {"intra": 0.0, "inter": 0.0}, } + address_state_map = get_address_state_map() + for d in inward_nil_exempt: - if d.place_of_supply: - if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ - and state == d.place_of_supply.split("-")[1]: - inward_nil_exempt_details["gst"]["intra"] += d.base_amount - elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ - and state != d.place_of_supply.split("-")[1]: - inward_nil_exempt_details["gst"]["inter"] += d.base_amount - elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]: - inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount - elif d.is_non_gst == 1 and state != d.place_of_supply.split("-")[1]: - inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount + if not d.place_of_supply: + d.place_of_supply = "00-" + cstr(state) + + supplier_state = address_state_map.get(d.supplier_address) or state + + if (d.is_nil_exempt == 1 or d.get("gst_category") == "Registered Composition") and cstr( + supplier_state + ) == cstr(d.place_of_supply.split("-")[1]): + inward_nil_exempt_details["gst"]["intra"] += d.base_amount + elif (d.is_nil_exempt == 1 or d.get("gst_category") == "Registered Composition") and cstr( + supplier_state + ) != cstr(d.place_of_supply.split("-")[1]): + inward_nil_exempt_details["gst"]["inter"] += d.base_amount + elif d.is_non_gst == 1 and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]): + inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount + elif d.is_non_gst == 1 and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]): + inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount return inward_nil_exempt_details @@ -173,12 +194,13 @@ class GSTR3BReport(Document): def get_outward_tax_invoices(self, doctype, reverse_charge=None): self.invoices = [] self.invoice_detail_map = {} - condition = '' + condition = "" if reverse_charge: condition += "AND reverse_charge = 'Y'" - invoice_details = frappe.db.sql(""" + invoice_details = frappe.db.sql( + """ SELECT name, gst_category, export_type, place_of_supply FROM @@ -192,8 +214,12 @@ class GSTR3BReport(Document): AND is_opening = 'No' {reverse_charge} ORDER BY name - """.format(doctype=doctype, reverse_charge=condition), (self.month_no, self.year, - self.company, self.gst_details.get("gstin")), as_dict=1) + """.format( + doctype=doctype, reverse_charge=condition + ), + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), + as_dict=1, + ) for d in invoice_details: self.invoice_detail_map.setdefault(d.name, d) @@ -204,20 +230,27 @@ class GSTR3BReport(Document): self.is_nil_exempt = [] self.is_non_gst = [] - if self.get('invoices'): - item_details = frappe.db.sql(""" + if self.get("invoices"): + item_details = frappe.db.sql( + """ SELECT item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, is_non_gst FROM `tab%s Item` WHERE parent in (%s) - """ % (doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (doctype, ", ".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in item_details: if d.item_code not in self.invoice_items.get(d.parent, {}): self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) - self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get( + "base_net_amount", 0 + ) if d.is_nil_exempt and d.item_code not in self.is_nil_exempt: self.is_nil_exempt.append(d.item_code) @@ -227,16 +260,17 @@ class GSTR3BReport(Document): def get_outward_tax_details(self, doctype): if doctype == "Sales Invoice": - tax_template = 'Sales Taxes and Charges' + tax_template = "Sales Taxes and Charges" elif doctype == "Purchase Invoice": - tax_template = 'Purchase Taxes and Charges' + tax_template = "Purchase Taxes and Charges" self.items_based_on_tax_rate = {} self.invoice_cess = frappe._dict() self.cgst_sgst_invoices = [] - if self.get('invoices'): - tax_details = frappe.db.sql(""" + if self.get("invoices"): + tax_details = frappe.db.sql( + """ SELECT parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount FROM `tab%s` @@ -244,24 +278,28 @@ class GSTR3BReport(Document): parenttype = %s and docstatus = 1 and parent in (%s) ORDER BY account_head - """ % (tax_template, '%s', ', '.join(['%s']*len(self.invoices))), - tuple([doctype] + list(self.invoices))) + """ + % (tax_template, "%s", ", ".join(["%s"] * len(self.invoices))), + tuple([doctype] + list(self.invoices)), + ) for parent, account, item_wise_tax_detail, tax_amount in tax_details: - if account in self.account_heads.get('csamt'): + if account in self.account_heads.get("csamt"): self.invoice_cess.setdefault(parent, tax_amount) else: if item_wise_tax_detail: try: item_wise_tax_detail = json.loads(item_wise_tax_detail) cgst_or_sgst = False - if account in self.account_heads.get('camt') \ - or account in self.account_heads.get('samt'): + if account in self.account_heads.get("camt") or account in self.account_heads.get("samt"): cgst_or_sgst = True for item_code, tax_amounts in item_wise_tax_detail.items(): - if not (cgst_or_sgst or account in self.account_heads.get('iamt') or - (item_code in self.is_non_gst + self.is_nil_exempt)): + if not ( + cgst_or_sgst + or account in self.account_heads.get("iamt") + or (item_code in self.is_non_gst + self.is_nil_exempt) + ): continue tax_rate = tax_amounts[0] @@ -271,66 +309,76 @@ class GSTR3BReport(Document): if parent not in self.cgst_sgst_invoices: self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( + tax_rate, [] + ) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: continue - - if self.get('invoice_items'): + if self.get("invoice_items"): # Build itemised tax for export invoices, nil and exempted where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate and self.invoice_detail_map.get(invoice, {}).get('export_type') \ - == "Without Payment of Tax" and self.invoice_detail_map.get(invoice, {}).get('gst_category') == "Overseas": + if ( + invoice not in self.items_based_on_tax_rate + and self.invoice_detail_map.get(invoice, {}).get("export_type") == "Without Payment of Tax" + and self.invoice_detail_map.get(invoice, {}).get("gst_category") == "Overseas" + ): self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) else: for item in items.keys(): - if item in self.is_nil_exempt + self.is_non_gst and \ - item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, []) - self.items_based_on_tax_rate[invoice][0].append(item) + if ( + item in self.is_nil_exempt + self.is_non_gst + and item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []) + ): + self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, []) + self.items_based_on_tax_rate[invoice][0].append(item) def set_outward_taxable_supplies(self): inter_state_supply_details = {} for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): - gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') - place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory' - export_type = self.invoice_detail_map.get(inv, {}).get('export_type') + gst_category = self.invoice_detail_map.get(inv, {}).get("gst_category") + place_of_supply = ( + self.invoice_detail_map.get(inv, {}).get("place_of_supply") or "00-Other Territory" + ) + export_type = self.invoice_detail_map.get(inv, {}).get("export_type") for rate, items in items_based_on_rate.items(): for item_code, taxable_value in self.invoice_items.get(inv).items(): if item_code in items: if item_code in self.is_nil_exempt: - self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value + self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] += taxable_value elif item_code in self.is_non_gst: - self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value - elif rate == 0 or (gst_category == 'Overseas' and export_type == 'Without Payment of Tax'): - self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value + self.report_dict["sup_details"]["osup_nongst"]["txval"] += taxable_value + elif rate == 0 or (gst_category == "Overseas" and export_type == "Without Payment of Tax"): + self.report_dict["sup_details"]["osup_zero"]["txval"] += taxable_value else: if inv in self.cgst_sgst_invoices: - tax_rate = rate/2 - self.report_dict['sup_details']['osup_det']['camt'] += (taxable_value * tax_rate /100) - self.report_dict['sup_details']['osup_det']['samt'] += (taxable_value * tax_rate /100) - self.report_dict['sup_details']['osup_det']['txval'] += taxable_value + tax_rate = rate / 2 + self.report_dict["sup_details"]["osup_det"]["camt"] += taxable_value * tax_rate / 100 + self.report_dict["sup_details"]["osup_det"]["samt"] += taxable_value * tax_rate / 100 + self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value else: - self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100) - self.report_dict['sup_details']['osup_det']['txval'] += taxable_value + self.report_dict["sup_details"]["osup_det"]["iamt"] += taxable_value * rate / 100 + self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value - if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \ - self.gst_details.get("gst_state") != place_of_supply.split("-")[1]: - inter_state_supply_details.setdefault((gst_category, place_of_supply), { - "txval": 0.0, - "pos": place_of_supply.split("-")[0], - "iamt": 0.0 - }) - inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value - inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100) + if ( + gst_category in ["Unregistered", "Registered Composition", "UIN Holders"] + and self.gst_details.get("gst_state") != place_of_supply.split("-")[1] + ): + inter_state_supply_details.setdefault( + (gst_category, place_of_supply), + {"txval": 0.0, "pos": place_of_supply.split("-")[0], "iamt": 0.0}, + ) + inter_state_supply_details[(gst_category, place_of_supply)]["txval"] += taxable_value + inter_state_supply_details[(gst_category, place_of_supply)]["iamt"] += ( + taxable_value * rate / 100 + ) if self.invoice_cess.get(inv): - self.report_dict['sup_details']['osup_det']['csamt'] += flt(self.invoice_cess.get(inv), 2) + self.report_dict["sup_details"]["osup_det"]["csamt"] += flt(self.invoice_cess.get(inv), 2) self.set_inter_state_supply(inter_state_supply_details) @@ -340,13 +388,13 @@ class GSTR3BReport(Document): for item_code, taxable_value in self.invoice_items.get(inv).items(): if item_code in items: if inv in self.cgst_sgst_invoices: - tax_rate = rate/2 - self.report_dict['sup_details']['isup_rev']['camt'] += (taxable_value * tax_rate /100) - self.report_dict['sup_details']['isup_rev']['samt'] += (taxable_value * tax_rate /100) - self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value + tax_rate = rate / 2 + self.report_dict["sup_details"]["isup_rev"]["camt"] += taxable_value * tax_rate / 100 + self.report_dict["sup_details"]["isup_rev"]["samt"] += taxable_value * tax_rate / 100 + self.report_dict["sup_details"]["isup_rev"]["txval"] += taxable_value else: - self.report_dict['sup_details']['isup_rev']['iamt'] += (taxable_value * rate /100) - self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value + self.report_dict["sup_details"]["isup_rev"]["iamt"] += taxable_value * rate / 100 + self.report_dict["sup_details"]["isup_rev"]["txval"] += taxable_value def set_inter_state_supply(self, inter_state_supply): for key, value in iteritems(inter_state_supply): @@ -360,29 +408,33 @@ class GSTR3BReport(Document): self.report_dict["inter_sup"]["uin_details"].append(value) def get_company_gst_details(self): - gst_details = frappe.get_all("Address", + gst_details = frappe.get_all( + "Address", fields=["gstin", "gst_state", "gst_state_number"], - filters={ - "name":self.company_address - }) + filters={"name": self.company_address}, + ) if gst_details: return gst_details[0] else: - frappe.throw(_("Please enter GSTIN and state for the Company Address {0}").format(self.company_address)) + frappe.throw( + _("Please enter GSTIN and state for the Company Address {0}").format(self.company_address) + ) def get_account_heads(self): account_map = { - 'sgst_account': 'samt', - 'cess_account': 'csamt', - 'cgst_account': 'camt', - 'igst_account': 'iamt' + "sgst_account": "samt", + "cess_account": "csamt", + "cgst_account": "camt", + "igst_account": "iamt", } account_heads = {} - gst_settings_accounts = frappe.get_all("GST Account", - filters={'company': self.company, 'is_reverse_charge_account': 0}, - fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) + gst_settings_accounts = frappe.get_all( + "GST Account", + filters={"company": self.company, "is_reverse_charge_account": 0}, + fields=["cgst_account", "sgst_account", "igst_account", "cess_account"], + ) if not gst_settings_accounts: frappe.throw(_("Please set GST Accounts in GST Settings")) @@ -399,37 +451,48 @@ class GSTR3BReport(Document): for doctype in ["Sales Invoice", "Purchase Invoice"]: if doctype == "Sales Invoice": - party_type = 'Customer' - party = 'customer' + party_type = "Customer" + party = "customer" else: - party_type = 'Supplier' - party = 'supplier' + party_type = "Supplier" + party = "supplier" docnames = frappe.db.sql( - """ + """ SELECT t1.name FROM `tab{doctype}` t1, `tab{party_type}` t2 WHERE t1.docstatus = 1 and t1.is_opening = 'No' and month(t1.posting_date) = %s and year(t1.posting_date) = %s and t1.company = %s and t1.place_of_supply IS NULL and t1.{party} = t2.name and t2.gst_category != 'Overseas' - """.format(doctype = doctype, party_type = party_type, - party=party) ,(self.month_no, self.year, self.company), as_dict=1) #nosec + """.format( + doctype=doctype, party_type=party_type, party=party + ), + (self.month_no, self.year, self.company), + as_dict=1, + ) # nosec for d in docnames: missing_field_invoices.append(d.name) return ",".join(missing_field_invoices) + +def get_address_state_map(): + return frappe._dict(frappe.get_all("Address", fields=["name", "gst_state"], as_list=1)) + + def get_json(template): - file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template)) - with open(file_path, 'r') as f: + file_path = os.path.join(os.path.dirname(__file__), "{template}.json".format(template=template)) + with open(file_path, "r") as f: return cstr(f.read()) + def get_state_code(state): state_code = state_numbers.get(state) return state_code + def get_period(month, year=None): month_no = { "January": 1, @@ -443,7 +506,7 @@ def get_period(month, year=None): "September": 9, "October": 10, "November": 11, - "December": 12 + "December": 12, }.get(month) if year: @@ -454,12 +517,13 @@ def get_period(month, year=None): @frappe.whitelist() def view_report(name): - json_data = frappe.get_value("GSTR 3B Report", name, 'json_output') + json_data = frappe.get_value("GSTR 3B Report", name, "json_output") return json.loads(json_data) + @frappe.whitelist() def make_json(name): - json_data = frappe.get_value("GSTR 3B Report", name, 'json_output') + json_data = frappe.get_value("GSTR 3B Report", name, "json_output") file_name = "GST3B.json" frappe.local.response.filename = file_name frappe.local.response.filecontent = json_data diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index e12e3d7b800..3862c625303 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -13,6 +13,7 @@ from erpnext.stock.doctype.item.test_item import make_item test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] + class TestGSTR3BReport(unittest.TestCase): def setUp(self): frappe.set_user("Administrator") @@ -22,7 +23,7 @@ class TestGSTR3BReport(unittest.TestCase): frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'") make_company() - make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000}) + make_item("Milk", properties={"is_nil_exempt": 1, "standard_rate": 0.000000}) set_account_heads() make_customers() make_suppliers() @@ -40,7 +41,7 @@ class TestGSTR3BReport(unittest.TestCase): 9: "September", 10: "October", 11: "November", - 12: "December" + 12: "December", } make_sales_invoice() @@ -50,13 +51,15 @@ class TestGSTR3BReport(unittest.TestCase): report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing") report.save() else: - report = frappe.get_doc({ - "doctype": "GSTR 3B Report", - "company": "_Test Company GST", - "company_address": "_Test Address GST-Billing", - "year": getdate().year, - "month": month_number_mapping.get(getdate().month) - }).insert() + report = frappe.get_doc( + { + "doctype": "GSTR 3B Report", + "company": "_Test Company GST", + "company_address": "_Test Address GST-Billing", + "year": getdate().year, + "month": month_number_mapping.get(getdate().month), + } + ).insert() output = json.loads(report.json_output) @@ -68,32 +71,36 @@ class TestGSTR3BReport(unittest.TestCase): self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50) def test_gst_rounding(self): - gst_settings = frappe.get_doc('GST Settings') + gst_settings = frappe.get_doc("GST Settings") gst_settings.round_off_gst_values = 1 gst_settings.save() current_country = frappe.flags.country - frappe.flags.country = 'India' + frappe.flags.country = "India" - si = create_sales_invoice(company="_Test Company GST", - customer = '_Test GST Customer', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', + si = create_sales_invoice( + company="_Test Company GST", + customer="_Test GST Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", rate=216, - do_not_save=1 + do_not_save=1, ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) si.save() # Check for 39 instead of 38.88 @@ -105,20 +112,313 @@ class TestGSTR3BReport(unittest.TestCase): def test_gst_category_auto_update(self): if not frappe.db.exists("Customer", "_Test GST Customer With GSTIN"): - customer = frappe.get_doc({ + customer = frappe.get_doc( + { + "customer_group": "_Test Customer Group", + "customer_name": "_Test GST Customer With GSTIN", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + } + ).insert() + + self.assertEqual(customer.gst_category, "Unregistered") + + if not frappe.db.exists("Address", "_Test GST Category-1-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test GST Category-1", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gstin": "29AZWPS7135H1ZG", + "gst_state": "Karnataka", + "gst_state_number": "29", + } + ).insert() + + address.append( + "links", {"link_doctype": "Customer", "link_name": "_Test GST Customer With GSTIN"} + ) + + address.save() + + customer.load_from_db() + self.assertEqual(customer.gst_category, "Registered Regular") + + +def make_sales_invoice(): + si = create_sales_invoice( + company="_Test Company GST", + customer="_Test GST Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, + ) + + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) + + si.submit() + + si1 = create_sales_invoice( + company="_Test Company GST", + customer="_Test GST SEZ Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, + ) + + si1.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) + + si1.submit() + + si2 = create_sales_invoice( + company="_Test Company GST", + customer="_Test Unregistered Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, + ) + + si2.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) + + si2.submit() + + si3 = create_sales_invoice( + company="_Test Company GST", + customer="_Test GST Customer", + currency="INR", + item="Milk", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, + ) + si3.submit() + + +def create_purchase_invoices(): + pi = make_purchase_invoice( + company="_Test Company GST", + supplier="_Test Registered Supplier", + currency="INR", + warehouse="Finished Goods - _GST", + cost_center="Main - _GST", + expense_account="Cost of Goods Sold - _GST", + do_not_save=1, + ) + + pi.eligibility_for_itc = "All Other ITC" + + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Input Tax CGST - _GST", + "cost_center": "Main - _GST", + "description": "CGST @ 9.0", + "rate": 9, + }, + ) + + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Input Tax SGST - _GST", + "cost_center": "Main - _GST", + "description": "SGST @ 9.0", + "rate": 9, + }, + ) + + pi.submit() + + pi1 = make_purchase_invoice( + company="_Test Company GST", + supplier="_Test Registered Supplier", + currency="INR", + warehouse="Finished Goods - _GST", + cost_center="Main - _GST", + expense_account="Cost of Goods Sold - _GST", + item="Milk", + do_not_save=1, + ) + + pi1.shipping_address = "_Test Supplier GST-1-Billing" + pi1.save() + + pi1.submit() + + pi2 = make_purchase_invoice( + company="_Test Company GST", + customer="_Test Registered Supplier", + currency="INR", + item="Milk", + warehouse="Finished Goods - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + rate=250, + qty=1, + do_not_save=1, + ) + pi2.submit() + + +def make_suppliers(): + if not frappe.db.exists("Supplier", "_Test Registered Supplier"): + frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Registered Supplier", + "gst_category": "Registered Regular", + "supplier_type": "Individual", + "doctype": "Supplier", + } + ).insert() + + if not frappe.db.exists("Supplier", "_Test Unregistered Supplier"): + frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Unregistered Supplier", + "gst_category": "Unregistered", + "supplier_type": "Individual", + "doctype": "Supplier", + } + ).insert() + + if not frappe.db.exists("Address", "_Test Supplier GST-1-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Supplier GST-1", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gstin": "29AACCV0498C1Z9", + "gst_state": "Karnataka", + } + ).insert() + + address.append("links", {"link_doctype": "Supplier", "link_name": "_Test Registered Supplier"}) + + address.is_shipping_address = 1 + address.save() + + if not frappe.db.exists("Address", "_Test Supplier GST-2-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Supplier GST-2", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gst_state": "Karnataka", + } + ).insert() + + address.append("links", {"link_doctype": "Supplier", "link_name": "_Test Unregistered Supplier"}) + + address.save() + + +def make_customers(): + if not frappe.db.exists("Customer", "_Test GST Customer"): + frappe.get_doc( + { "customer_group": "_Test Customer Group", - "customer_name": "_Test GST Customer With GSTIN", + "customer_name": "_Test GST Customer", + "gst_category": "Registered Regular", "customer_type": "Individual", "doctype": "Customer", - "territory": "_Test Territory" - }).insert() + "territory": "_Test Territory", + } + ).insert() - self.assertEqual(customer.gst_category, 'Unregistered') + if not frappe.db.exists("Customer", "_Test GST SEZ Customer"): + frappe.get_doc( + { + "customer_group": "_Test Customer Group", + "customer_name": "_Test GST SEZ Customer", + "gst_category": "SEZ", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + } + ).insert() - if not frappe.db.exists('Address', '_Test GST Category-1-Billing'): - address = frappe.get_doc({ + if not frappe.db.exists("Customer", "_Test Unregistered Customer"): + frappe.get_doc( + { + "customer_group": "_Test Customer Group", + "customer_name": "_Test Unregistered Customer", + "gst_category": "Unregistered", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + } + ).insert() + + if not frappe.db.exists("Address", "_Test GST-1-Billing"): + address = frappe.get_doc( + { "address_line1": "_Test Address Line 1", - "address_title": "_Test GST Category-1", + "address_title": "_Test GST-1", "address_type": "Billing", "city": "_Test City", "state": "Test State", @@ -128,315 +428,54 @@ class TestGSTR3BReport(unittest.TestCase): "phone": "+91 0000000000", "gstin": "29AZWPS7135H1ZG", "gst_state": "Karnataka", - "gst_state_number": "29" - }).insert() + "gst_state_number": "29", + } + ).insert() - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test GST Customer With GSTIN" - }) - - address.save() - - customer.load_from_db() - self.assertEqual(customer.gst_category, 'Registered Regular') - - -def make_sales_invoice(): - si = create_sales_invoice(company="_Test Company GST", - customer = '_Test GST Customer', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - do_not_save=1 - ) - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) - - si.submit() - - si1 = create_sales_invoice(company="_Test Company GST", - customer = '_Test GST SEZ Customer', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - do_not_save=1 - ) - - si1.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) - - si1.submit() - - si2 = create_sales_invoice(company="_Test Company GST", - customer = '_Test Unregistered Customer', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - do_not_save=1 - ) - - si2.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) - - si2.submit() - - si3 = create_sales_invoice(company="_Test Company GST", - customer = '_Test GST Customer', - currency = 'INR', - item = 'Milk', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - do_not_save=1 - ) - si3.submit() - -def create_purchase_invoices(): - pi = make_purchase_invoice( - company="_Test Company GST", - supplier = '_Test Registered Supplier', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - cost_center = 'Main - _GST', - expense_account = 'Cost of Goods Sold - _GST', - do_not_save=1, - ) - - pi.eligibility_for_itc = "All Other ITC" - - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Input Tax CGST - _GST", - "cost_center": "Main - _GST", - "description": "CGST @ 9.0", - "rate": 9 - }) - - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Input Tax SGST - _GST", - "cost_center": "Main - _GST", - "description": "SGST @ 9.0", - "rate": 9 - }) - - pi.submit() - - pi1 = make_purchase_invoice( - company="_Test Company GST", - supplier = '_Test Registered Supplier', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - cost_center = 'Main - _GST', - expense_account = 'Cost of Goods Sold - _GST', - item = "Milk", - do_not_save=1 - ) - - pi1.shipping_address = "_Test Supplier GST-1-Billing" - pi1.save() - - pi1.submit() - - pi2 = make_purchase_invoice(company="_Test Company GST", - customer = '_Test Registered Supplier', - currency = 'INR', - item = 'Milk', - warehouse = 'Finished Goods - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - rate=250, - qty=1, - do_not_save=1 - ) - pi2.submit() - -def make_suppliers(): - if not frappe.db.exists("Supplier", "_Test Registered Supplier"): - frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Registered Supplier", - "gst_category": "Registered Regular", - "supplier_type": "Individual", - "doctype": "Supplier", - }).insert() - - if not frappe.db.exists("Supplier", "_Test Unregistered Supplier"): - frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Unregistered Supplier", - "gst_category": "Unregistered", - "supplier_type": "Individual", - "doctype": "Supplier", - }).insert() - - if not frappe.db.exists('Address', '_Test Supplier GST-1-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Supplier GST-1", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "29AACCV0498C1Z9", - "gst_state": "Karnataka", - }).insert() - - address.append("links", { - "link_doctype": "Supplier", - "link_name": "_Test Registered Supplier" - }) - - address.is_shipping_address = 1 - address.save() - - if not frappe.db.exists('Address', '_Test Supplier GST-2-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Supplier GST-2", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Karnataka", - }).insert() - - address.append("links", { - "link_doctype": "Supplier", - "link_name": "_Test Unregistered Supplier" - }) + address.append("links", {"link_doctype": "Customer", "link_name": "_Test GST Customer"}) address.save() -def make_customers(): - if not frappe.db.exists("Customer", "_Test GST Customer"): - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test GST Customer", - "gst_category": "Registered Regular", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert() + if not frappe.db.exists("Address", "_Test GST-2-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test GST-2", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gst_state": "Haryana", + } + ).insert() - if not frappe.db.exists("Customer", "_Test GST SEZ Customer"): - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test GST SEZ Customer", - "gst_category": "SEZ", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert() - - if not frappe.db.exists("Customer", "_Test Unregistered Customer"): - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test Unregistered Customer", - "gst_category": "Unregistered", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert() - - if not frappe.db.exists('Address', '_Test GST-1-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test GST-1", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "29AZWPS7135H1ZG", - "gst_state": "Karnataka", - "gst_state_number": "29" - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test GST Customer" - }) + address.append("links", {"link_doctype": "Customer", "link_name": "_Test Unregistered Customer"}) address.save() - if not frappe.db.exists('Address', '_Test GST-2-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test GST-2", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Haryana", - }).insert() + if not frappe.db.exists("Address", "_Test GST-3-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test GST-3", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gst_state": "Gujarat", + } + ).insert() - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Unregistered Customer" - }) + address.append("links", {"link_doctype": "Customer", "link_name": "_Test GST SEZ Customer"}) address.save() - if not frappe.db.exists('Address', '_Test GST-3-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test GST-3", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Gujarat", - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test GST SEZ Customer" - }) - - address.save() def make_company(): if frappe.db.exists("Company", "_Test Company GST"): @@ -449,43 +488,47 @@ def make_company(): company.country = "India" company.insert() - if not frappe.db.exists('Address', '_Test Address GST-Billing'): - address = frappe.get_doc({ - "address_title": "_Test Address GST", - "address_line1": "_Test Address Line 1", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "27AAECE4835E1ZR", - "gst_state": "Maharashtra", - "gst_state_number": "27" - }).insert() + if not frappe.db.exists("Address", "_Test Address GST-Billing"): + address = frappe.get_doc( + { + "address_title": "_Test Address GST", + "address_line1": "_Test Address Line 1", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gstin": "27AAECE4835E1ZR", + "gst_state": "Maharashtra", + "gst_state_number": "27", + } + ).insert() - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company GST" - }) + address.append("links", {"link_doctype": "Company", "link_name": "_Test Company GST"}) address.save() + def set_account_heads(): gst_settings = frappe.get_doc("GST Settings") gst_account = frappe.get_all( "GST Account", fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company GST"}) + filters={"company": "_Test Company GST"}, + ) if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company GST", - "cgst_account": "Output Tax CGST - _GST", - "sgst_account": "Output Tax SGST - _GST", - "igst_account": "Output Tax IGST - _GST" - }) + gst_settings.append( + "gst_accounts", + { + "company": "_Test Company GST", + "cgst_account": "Output Tax CGST - _GST", + "sgst_account": "Output Tax SGST - _GST", + "igst_account": "Output Tax IGST - _GST", + }, + ) gst_settings.save() diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py index 97b8488c2fe..77c4d7c6ca3 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py @@ -27,11 +27,10 @@ class ImportSupplierInvoice(Document): self.name = "Import Invoice on " + format_datetime(self.creation) def import_xml_data(self): - zip_file = frappe.get_doc("File", { - "file_url": self.zip_file, - "attached_to_doctype": self.doctype, - "attached_to_name": self.name - }) + zip_file = frappe.get_doc( + "File", + {"file_url": self.zip_file, "attached_to_doctype": self.doctype, "attached_to_name": self.name}, + ) self.publish("File Import", _("Processing XML Files"), 1, 3) @@ -65,10 +64,10 @@ class ImportSupplierInvoice(Document): "bill_no": line.Numero.text, "total_discount": 0, "items": [], - "buying_price_list": self.default_buying_price_list + "buying_price_list": self.default_buying_price_list, } - if not invoices_args.get("bill_no", ''): + if not invoices_args.get("bill_no", ""): frappe.throw(_("Numero has not set in the XML file")) supp_dict = get_supplier_details(file_content) @@ -84,15 +83,23 @@ class ImportSupplierInvoice(Document): self.file_count += 1 if pi_name: self.purchase_invoices_count += 1 - file_save = save_file(file_name, encoded_content, "Purchase Invoice", - pi_name, folder=None, decode=False, is_private=0, df=None) + file_save = save_file( + file_name, + encoded_content, + "Purchase Invoice", + pi_name, + folder=None, + decode=False, + is_private=0, + df=None, + ) def prepare_items_for_invoice(self, file_content, invoices_args): qty = 1 - rate, tax_rate = [0 ,0] + rate, tax_rate = [0, 0] uom = self.default_uom - #read file for item information + # read file for item information for line in file_content.find_all("DettaglioLinee"): if line.find("PrezzoUnitario") and line.find("PrezzoTotale"): rate = flt(line.PrezzoUnitario.text) or 0 @@ -103,30 +110,34 @@ class ImportSupplierInvoice(Document): if line.find("UnitaMisura"): uom = create_uom(line.UnitaMisura.text) - if (rate < 0 and line_total < 0): + if rate < 0 and line_total < 0: qty *= -1 invoices_args["return_invoice"] = 1 if line.find("AliquotaIVA"): tax_rate = flt(line.AliquotaIVA.text) - line_str = re.sub('[^A-Za-z0-9]+', '-', line.Descrizione.text) + line_str = re.sub("[^A-Za-z0-9]+", "-", line.Descrizione.text) item_name = line_str[0:140] - invoices_args['items'].append({ - "item_code": self.item_code, - "item_name": item_name, - "description": line_str, - "qty": qty, - "uom": uom, - "rate": abs(rate), - "conversion_factor": 1.0, - "tax_rate": tax_rate - }) + invoices_args["items"].append( + { + "item_code": self.item_code, + "item_name": item_name, + "description": line_str, + "qty": qty, + "uom": uom, + "rate": abs(rate), + "conversion_factor": 1.0, + "tax_rate": tax_rate, + } + ) for disc_line in line.find_all("ScontoMaggiorazione"): if disc_line.find("Percentuale"): - invoices_args["total_discount"] += flt((flt(disc_line.Percentuale.text) / 100) * (rate * qty)) + invoices_args["total_discount"] += flt( + (flt(disc_line.Percentuale.text) / 100) * (rate * qty) + ) @frappe.whitelist() def process_file_data(self): @@ -134,10 +145,13 @@ class ImportSupplierInvoice(Document): frappe.enqueue_doc(self.doctype, self.name, "import_xml_data", queue="long", timeout=3600) def publish(self, title, message, count, total): - frappe.publish_realtime("import_invoice_update", {"title": title, "message": message, "count": count, "total": total}) + frappe.publish_realtime( + "import_invoice_update", {"title": title, "message": message, "count": count, "total": total} + ) + def get_file_content(file_name, zip_file_object): - content = '' + content = "" encoded_content = zip_file_object.read(file_name) try: @@ -150,113 +164,122 @@ def get_file_content(file_name, zip_file_object): return content + def get_supplier_details(file_content): supplier_info = {} for line in file_content.find_all("CedentePrestatore"): - supplier_info['tax_id'] = line.DatiAnagrafici.IdPaese.text + line.DatiAnagrafici.IdCodice.text + supplier_info["tax_id"] = line.DatiAnagrafici.IdPaese.text + line.DatiAnagrafici.IdCodice.text if line.find("CodiceFiscale"): - supplier_info['fiscal_code'] = line.DatiAnagrafici.CodiceFiscale.text + supplier_info["fiscal_code"] = line.DatiAnagrafici.CodiceFiscale.text if line.find("RegimeFiscale"): - supplier_info['fiscal_regime'] = line.DatiAnagrafici.RegimeFiscale.text + supplier_info["fiscal_regime"] = line.DatiAnagrafici.RegimeFiscale.text if line.find("Denominazione"): - supplier_info['supplier'] = line.DatiAnagrafici.Anagrafica.Denominazione.text + supplier_info["supplier"] = line.DatiAnagrafici.Anagrafica.Denominazione.text if line.find("Nome"): - supplier_info['supplier'] = (line.DatiAnagrafici.Anagrafica.Nome.text - + " " + line.DatiAnagrafici.Anagrafica.Cognome.text) + supplier_info["supplier"] = ( + line.DatiAnagrafici.Anagrafica.Nome.text + " " + line.DatiAnagrafici.Anagrafica.Cognome.text + ) - supplier_info['address_line1'] = line.Sede.Indirizzo.text - supplier_info['city'] = line.Sede.Comune.text + supplier_info["address_line1"] = line.Sede.Indirizzo.text + supplier_info["city"] = line.Sede.Comune.text if line.find("Provincia"): - supplier_info['province'] = line.Sede.Provincia.text + supplier_info["province"] = line.Sede.Provincia.text - supplier_info['pin_code'] = line.Sede.CAP.text - supplier_info['country'] = get_country(line.Sede.Nazione.text) + supplier_info["pin_code"] = line.Sede.CAP.text + supplier_info["country"] = get_country(line.Sede.Nazione.text) return supplier_info + def get_taxes_from_file(file_content, tax_account): taxes = [] - #read file for taxes information + # read file for taxes information for line in file_content.find_all("DatiRiepilogo"): if line.find("AliquotaIVA"): if line.find("EsigibilitaIVA"): descr = line.EsigibilitaIVA.text else: descr = "None" - taxes.append({ - "charge_type": "Actual", - "account_head": tax_account, - "tax_rate": flt(line.AliquotaIVA.text) or 0, - "description": descr, - "tax_amount": flt(line.Imposta.text) if len(line.find("Imposta"))!=0 else 0 - }) + taxes.append( + { + "charge_type": "Actual", + "account_head": tax_account, + "tax_rate": flt(line.AliquotaIVA.text) or 0, + "description": descr, + "tax_amount": flt(line.Imposta.text) if len(line.find("Imposta")) != 0 else 0, + } + ) return taxes + def get_payment_terms_from_file(file_content): terms = [] - #Get mode of payment dict from setup - mop_options = frappe.get_meta('Mode of Payment').fields[4].options - mop_str = re.sub('\n', ',', mop_options) + # Get mode of payment dict from setup + mop_options = frappe.get_meta("Mode of Payment").fields[4].options + mop_str = re.sub("\n", ",", mop_options) mop_dict = dict(item.split("-") for item in mop_str.split(",")) - #read file for payment information + # read file for payment information for line in file_content.find_all("DettaglioPagamento"): - mop_code = line.ModalitaPagamento.text + '-' + mop_dict.get(line.ModalitaPagamento.text) + mop_code = line.ModalitaPagamento.text + "-" + mop_dict.get(line.ModalitaPagamento.text) if line.find("DataScadenzaPagamento"): due_date = dateutil.parser.parse(line.DataScadenzaPagamento.text).strftime("%Y-%m-%d") else: due_date = today() - terms.append({ - "mode_of_payment_code": mop_code, - "bank_account_iban": line.IBAN.text if line.find("IBAN") else "", - "due_date": due_date, - "payment_amount": line.ImportoPagamento.text - }) + terms.append( + { + "mode_of_payment_code": mop_code, + "bank_account_iban": line.IBAN.text if line.find("IBAN") else "", + "due_date": due_date, + "payment_amount": line.ImportoPagamento.text, + } + ) return terms + def get_destination_code_from_file(file_content): - destination_code = '' + destination_code = "" for line in file_content.find_all("DatiTrasmissione"): destination_code = line.CodiceDestinatario.text return destination_code + def create_supplier(supplier_group, args): args = frappe._dict(args) - existing_supplier_name = frappe.db.get_value("Supplier", - filters={"tax_id": args.tax_id}, fieldname="name") + existing_supplier_name = frappe.db.get_value( + "Supplier", filters={"tax_id": args.tax_id}, fieldname="name" + ) if existing_supplier_name: pass else: - existing_supplier_name = frappe.db.get_value("Supplier", - filters={"name": args.supplier}, fieldname="name") + existing_supplier_name = frappe.db.get_value( + "Supplier", filters={"name": args.supplier}, fieldname="name" + ) if existing_supplier_name: filters = [ - ["Dynamic Link", "link_doctype", "=", "Supplier"], - ["Dynamic Link", "link_name", "=", args.existing_supplier_name], - ["Dynamic Link", "parenttype", "=", "Contact"] - ] + ["Dynamic Link", "link_doctype", "=", "Supplier"], + ["Dynamic Link", "link_name", "=", args.existing_supplier_name], + ["Dynamic Link", "parenttype", "=", "Contact"], + ] if not frappe.get_list("Contact", filters): new_contact = frappe.new_doc("Contact") new_contact.first_name = args.supplier[:30] - new_contact.append('links', { - "link_doctype": "Supplier", - "link_name": existing_supplier_name - }) + new_contact.append("links", {"link_doctype": "Supplier", "link_name": existing_supplier_name}) new_contact.insert(ignore_mandatory=True) return existing_supplier_name else: new_supplier = frappe.new_doc("Supplier") - new_supplier.supplier_name = re.sub('&', '&', args.supplier) + new_supplier.supplier_name = re.sub("&", "&", args.supplier) new_supplier.supplier_group = supplier_group new_supplier.tax_id = args.tax_id new_supplier.fiscal_code = args.fiscal_code @@ -265,23 +288,21 @@ def create_supplier(supplier_group, args): new_contact = frappe.new_doc("Contact") new_contact.first_name = args.supplier[:30] - new_contact.append('links', { - "link_doctype": "Supplier", - "link_name": new_supplier.name - }) + new_contact.append("links", {"link_doctype": "Supplier", "link_name": new_supplier.name}) new_contact.insert(ignore_mandatory=True) return new_supplier.name + def create_address(supplier_name, args): args = frappe._dict(args) filters = [ - ["Dynamic Link", "link_doctype", "=", "Supplier"], - ["Dynamic Link", "link_name", "=", supplier_name], - ["Dynamic Link", "parenttype", "=", "Address"] - ] + ["Dynamic Link", "link_doctype", "=", "Supplier"], + ["Dynamic Link", "link_name", "=", supplier_name], + ["Dynamic Link", "parenttype", "=", "Address"], + ] existing_address = frappe.get_list("Address", filters) @@ -300,50 +321,52 @@ def create_address(supplier_name, args): for address in existing_address: address_doc = frappe.get_doc("Address", address["name"]) - if (address_doc.address_line1 == new_address_doc.address_line1 and - address_doc.pincode == new_address_doc.pincode): + if ( + address_doc.address_line1 == new_address_doc.address_line1 + and address_doc.pincode == new_address_doc.pincode + ): return address - new_address_doc.append("links", { - "link_doctype": "Supplier", - "link_name": supplier_name - }) + new_address_doc.append("links", {"link_doctype": "Supplier", "link_name": supplier_name}) new_address_doc.address_type = "Billing" new_address_doc.insert(ignore_mandatory=True) return new_address_doc.name else: return None + def create_purchase_invoice(supplier_name, file_name, args, name): args = frappe._dict(args) - pi = frappe.get_doc({ - "doctype": "Purchase Invoice", - "company": args.company, - "currency": erpnext.get_company_currency(args.company), - "naming_series": args.naming_series, - "supplier": supplier_name, - "is_return": args.is_return, - "posting_date": today(), - "bill_no": args.bill_no, - "buying_price_list": args.buying_price_list, - "bill_date": args.bill_date, - "destination_code": args.destination_code, - "document_type": args.document_type, - "disable_rounded_total": 1, - "items": args["items"], - "taxes": args["taxes"] - }) + pi = frappe.get_doc( + { + "doctype": "Purchase Invoice", + "company": args.company, + "currency": erpnext.get_company_currency(args.company), + "naming_series": args.naming_series, + "supplier": supplier_name, + "is_return": args.is_return, + "posting_date": today(), + "bill_no": args.bill_no, + "buying_price_list": args.buying_price_list, + "bill_date": args.bill_date, + "destination_code": args.destination_code, + "document_type": args.document_type, + "disable_rounded_total": 1, + "items": args["items"], + "taxes": args["taxes"], + } + ) try: pi.set_missing_values() pi.insert(ignore_mandatory=True) - #if discount exists in file, apply any discount on grand total + # if discount exists in file, apply any discount on grand total if args.total_discount > 0: pi.apply_discount_on = "Grand Total" pi.discount_amount = args.total_discount pi.save() - #adjust payment amount to match with grand total calculated + # adjust payment amount to match with grand total calculated calc_total = 0 adj = 0 for term in args.terms: @@ -352,31 +375,37 @@ def create_purchase_invoice(supplier_name, file_name, args, name): adj = calc_total - flt(pi.grand_total) pi.payment_schedule = [] for term in args.terms: - pi.append('payment_schedule',{"mode_of_payment_code": term["mode_of_payment_code"], - "bank_account_iban": term["bank_account_iban"], - "due_date": term["due_date"], - "payment_amount": flt(term["payment_amount"]) - adj }) + pi.append( + "payment_schedule", + { + "mode_of_payment_code": term["mode_of_payment_code"], + "bank_account_iban": term["bank_account_iban"], + "due_date": term["due_date"], + "payment_amount": flt(term["payment_amount"]) - adj, + }, + ) adj = 0 pi.imported_grand_total = calc_total pi.save() return pi.name except Exception as e: frappe.db.set_value("Import Supplier Invoice", name, "status", "Error") - frappe.log_error(message=e, - title="Create Purchase Invoice: " + args.get("bill_no") + "File Name: " + file_name) + frappe.log_error( + message=e, title="Create Purchase Invoice: " + args.get("bill_no") + "File Name: " + file_name + ) return None + def get_country(code): - existing_country_name = frappe.db.get_value("Country", - filters={"code": code}, fieldname="name") + existing_country_name = frappe.db.get_value("Country", filters={"code": code}, fieldname="name") if existing_country_name: return existing_country_name else: frappe.throw(_("Country Code in File does not match with country code set up in the system")) + def create_uom(uom): - existing_uom = frappe.db.get_value("UOM", - filters={"uom_name": uom}, fieldname="uom_name") + existing_uom = frappe.db.get_value("UOM", filters={"uom_name": uom}, fieldname="uom_name") if existing_uom: return existing_uom else: diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index f14888189a0..cc223e91bc8 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -21,24 +21,34 @@ class LowerDeductionCertificate(Document): fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) - if not (fiscal_year.year_start_date <= getdate(self.valid_from) \ - <= fiscal_year.year_end_date): + if not (fiscal_year.year_start_date <= getdate(self.valid_from) <= fiscal_year.year_end_date): frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) - if not (fiscal_year.year_start_date <= getdate(self.valid_upto) \ - <= fiscal_year.year_end_date): + if not (fiscal_year.year_start_date <= getdate(self.valid_upto) <= fiscal_year.year_end_date): frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) def validate_supplier_against_tax_category(self): - duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate', - {'supplier': self.supplier, 'tax_withholding_category': self.tax_withholding_category, 'name': ("!=", self.name)}, - ['name', 'valid_from', 'valid_upto'], as_dict=True) + duplicate_certificate = frappe.db.get_value( + "Lower Deduction Certificate", + { + "supplier": self.supplier, + "tax_withholding_category": self.tax_withholding_category, + "name": ("!=", self.name), + }, + ["name", "valid_from", "valid_upto"], + as_dict=True, + ) if duplicate_certificate and self.are_dates_overlapping(duplicate_certificate): - certificate_link = get_link_to_form('Lower Deduction Certificate', duplicate_certificate.name) - frappe.throw(_("There is already a valid Lower Deduction Certificate {0} for Supplier {1} against category {2} for this time period.") - .format(certificate_link, frappe.bold(self.supplier), frappe.bold(self.tax_withholding_category))) + certificate_link = get_link_to_form("Lower Deduction Certificate", duplicate_certificate.name) + frappe.throw( + _( + "There is already a valid Lower Deduction Certificate {0} for Supplier {1} against category {2} for this time period." + ).format( + certificate_link, frappe.bold(self.supplier), frappe.bold(self.tax_withholding_category) + ) + ) - def are_dates_overlapping(self,duplicate_certificate): + def are_dates_overlapping(self, duplicate_certificate): valid_from = duplicate_certificate.valid_from valid_upto = duplicate_certificate.valid_upto if valid_from <= getdate(self.valid_from) <= valid_upto: diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js index 54cde9c0cf4..5f840daba67 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js @@ -36,7 +36,7 @@ frappe.ui.form.on('Tax Exemption 80G Certificate', { 'date_of_donation': '', 'amount': 0, 'mode_of_payment': '', - 'razorpay_payment_id': '' + 'payment_id': '' }); } }, diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json index 9eee722f420..9b182ad4969 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json @@ -38,7 +38,7 @@ "amount", "column_break_27", "mode_of_payment", - "razorpay_payment_id" + "payment_id" ], "fields": [ { @@ -201,13 +201,6 @@ "options": "Mode of Payment", "read_only": 1 }, - { - "fetch_from": "donation.razorpay_payment_id", - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "RazorPay Payment ID", - "read_only": 1 - }, { "fetch_from": "donation.date", "fieldname": "date_of_donation", @@ -266,11 +259,18 @@ "hidden": 1, "label": "Title", "print_hide": 1 + }, + { + "fetch_from": "donation.payment_id", + "fieldname": "payment_id", + "fieldtype": "Data", + "label": "Payment ID", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-22 00:03:34.215633", + "modified": "2022-03-16 17:21:39.831059", "modified_by": "Administrator", "module": "Regional", "name": "Tax Exemption 80G Certificate", diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py index 0f0897841b4..6cfa47ed67e 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py @@ -6,50 +6,48 @@ import frappe from frappe import _ from frappe.contacts.doctype.address.address import get_company_address from frappe.model.document import Document -from frappe.utils import flt, get_link_to_form, getdate +from frappe.utils import flt, get_link_to_form from erpnext.accounts.utils import get_fiscal_year class TaxExemption80GCertificate(Document): def validate(self): - self.validate_date() self.validate_duplicates() self.validate_company_details() self.set_company_address() self.calculate_total() self.set_title() - def validate_date(self): - if self.recipient == 'Member': - if getdate(self.date): - fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) - - if not (fiscal_year.year_start_date <= getdate(self.date) \ - <= fiscal_year.year_end_date): - frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year))) - def validate_duplicates(self): - if self.recipient == 'Donor': - certificate = frappe.db.exists(self.doctype, { - 'donation': self.donation, - 'name': ('!=', self.name) - }) + if self.recipient == "Donor": + certificate = frappe.db.exists( + self.doctype, {"donation": self.donation, "name": ("!=", self.name)} + ) if certificate: - frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format( - get_link_to_form(self.doctype, certificate), frappe.bold(self.donation) - ), title=_('Duplicate Certificate')) + frappe.throw( + _("An 80G Certificate {0} already exists for the donation {1}").format( + get_link_to_form(self.doctype, certificate), frappe.bold(self.donation) + ), + title=_("Duplicate Certificate"), + ) def validate_company_details(self): - fields = ['company_80g_number', 'with_effect_from', 'pan_details'] - company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True) + fields = ["company_80g_number", "with_effect_from", "pan_details"] + company_details = frappe.db.get_value("Company", self.company, fields, as_dict=True) if not company_details.company_80g_number: - frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'), - get_link_to_form('Company', self.company))) + frappe.throw( + _("Please set the {0} for company {1}").format( + frappe.bold("80G Number"), get_link_to_form("Company", self.company) + ) + ) if not company_details.pan_details: - frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'), - get_link_to_form('Company', self.company))) + frappe.throw( + _("Please set the {0} for company {1}").format( + frappe.bold("PAN Number"), get_link_to_form("Company", self.company) + ) + ) @frappe.whitelist() def set_company_address(self): @@ -58,7 +56,7 @@ class TaxExemption80GCertificate(Document): self.company_address_display = address.company_address_display def calculate_total(self): - if self.recipient == 'Donor': + if self.recipient == "Donor": return total = 0 @@ -67,7 +65,7 @@ class TaxExemption80GCertificate(Document): self.total = total def set_title(self): - if self.recipient == 'Member': + if self.recipient == "Member": self.title = self.member_name else: self.title = self.donor_name @@ -75,30 +73,38 @@ class TaxExemption80GCertificate(Document): @frappe.whitelist() def get_payments(self): if not self.member: - frappe.throw(_('Please select a Member first.')) + frappe.throw(_("Please select a Member first.")) fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) - memberships = frappe.db.get_all('Membership', { - 'member': self.member, - 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], - 'membership_status': ('!=', 'Cancelled') - }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') + memberships = frappe.db.get_all( + "Membership", + { + "member": self.member, + "from_date": ["between", (fiscal_year.year_start_date, fiscal_year.year_end_date)], + "membership_status": ("!=", "Cancelled"), + }, + ["from_date", "amount", "name", "invoice", "payment_id"], + order_by="from_date", + ) if not memberships: - frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member)) + frappe.msgprint(_("No Membership Payments found against the Member {0}").format(self.member)) total = 0 self.payments = [] for doc in memberships: - self.append('payments', { - 'date': doc.from_date, - 'amount': doc.amount, - 'invoice_id': doc.invoice, - 'razorpay_payment_id': doc.payment_id, - 'membership': doc.name - }) + self.append( + "payments", + { + "date": doc.from_date, + "amount": doc.amount, + "invoice_id": doc.invoice, + "payment_id": doc.payment_id, + "membership": doc.name, + }, + ) total += flt(doc.amount) self.total = total diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py index 6fa3b85d061..c247f3e6409 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py @@ -7,7 +7,7 @@ import frappe from frappe.utils import getdate from erpnext.accounts.utils import get_fiscal_year -from erpnext.non_profit.doctype.donation.donation import create_donation +from erpnext.non_profit.doctype.donation.donation import create_razorpay_donation from erpnext.non_profit.doctype.donation.test_donation import ( create_donor, create_donor_type, @@ -19,43 +19,37 @@ from erpnext.non_profit.doctype.membership.test_membership import make_membershi class TestTaxExemption80GCertificate(unittest.TestCase): def setUp(self): - frappe.db.sql('delete from `tabTax Exemption 80G Certificate`') - frappe.db.sql('delete from `tabMembership`') + frappe.db.sql("delete from `tabTax Exemption 80G Certificate`") + frappe.db.sql("delete from `tabMembership`") create_donor_type() - settings = frappe.get_doc('Non Profit Settings') - settings.company = '_Test Company' - settings.donation_company = '_Test Company' - settings.default_donor_type = '_Test Donor' - settings.creation_user = 'Administrator' + settings = frappe.get_doc("Non Profit Settings") + settings.company = "_Test Company" + settings.donation_company = "_Test Company" + settings.default_donor_type = "_Test Donor" + settings.creation_user = "Administrator" settings.save() - company = frappe.get_doc('Company', '_Test Company') - company.pan_details = 'BBBTI3374C' - company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087' + company = frappe.get_doc("Company", "_Test Company") + company.pan_details = "BBBTI3374C" + company.company_80g_number = "NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087" company.with_effect_from = getdate() company.save() def test_duplicate_donation_certificate(self): donor = create_donor() create_mode_of_payment() - payment = frappe._dict({ - 'amount': 100, - 'method': 'Debit Card', - 'id': 'pay_MeXAmsgeKOhq7O' - }) - donation = create_donation(donor, payment) + payment = frappe._dict( + {"amount": 100, "method": "Debit Card", "id": "pay_MeXAmsgeKOhq7O"} # rzp sends data in paise + ) + donation = create_razorpay_donation(donor, payment) - args = frappe._dict({ - 'recipient': 'Donor', - 'donor': donor.name, - 'donation': donation.name - }) + args = frappe._dict({"recipient": "Donor", "donor": donor.name, "donation": donation.name}) certificate = create_80g_certificate(args) certificate.insert() # check company details - self.assertEqual(certificate.company_pan_number, 'BBBTI3374C') - self.assertEqual(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087') + self.assertEqual(certificate.company_pan_number, "BBBTI3374C") + self.assertEqual(certificate.company_80g_number, "NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087") # check donation details self.assertEqual(certificate.amount, donation.amount) @@ -68,22 +62,24 @@ class TestTaxExemption80GCertificate(unittest.TestCase): plan = setup_membership() # make test member - member_doc = create_member(frappe._dict({ - 'fullname': "_Test_Member", - 'email': "_test_member_erpnext@example.com", - 'plan_id': plan.name - })) + member_doc = create_member( + frappe._dict( + {"fullname": "_Test_Member", "email": "_test_member_erpnext@example.com", "plan_id": plan.name} + ) + ) member_doc.make_customer_and_link() member = member_doc.name - membership = make_membership(member, { "from_date": getdate() }) + membership = make_membership(member, {"from_date": getdate()}) invoice = membership.generate_invoice(save=True) - args = frappe._dict({ - 'recipient': 'Member', - 'member': member, - 'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name') - }) + args = frappe._dict( + { + "recipient": "Member", + "member": member, + "fiscal_year": get_fiscal_year(getdate(), as_dict=True).get("name"), + } + ) certificate = create_80g_certificate(args) certificate.get_payments() certificate.insert() @@ -94,12 +90,14 @@ class TestTaxExemption80GCertificate(unittest.TestCase): def create_80g_certificate(args): - certificate = frappe.get_doc({ - 'doctype': 'Tax Exemption 80G Certificate', - 'recipient': args.recipient, - 'date': getdate(), - 'company': '_Test Company' - }) + certificate = frappe.get_doc( + { + "doctype": "Tax Exemption 80G Certificate", + "recipient": args.recipient, + "date": getdate(), + "company": "_Test Company", + } + ) certificate.update(args) diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json index dfa817dd271..c863aab3285 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json @@ -9,7 +9,7 @@ "amount", "invoice_id", "column_break_4", - "razorpay_payment_id", + "payment_id", "membership" ], "fields": [ @@ -35,26 +35,28 @@ "options": "Sales Invoice", "reqd": 1 }, - { - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "Razorpay Payment ID" - }, { "fieldname": "membership", "fieldtype": "Link", + "in_list_view": 1, "label": "Membership", "options": "Membership" }, { "fieldname": "column_break_4", "fieldtype": "Column Break" + }, + { + "fieldname": "payment_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Payment ID" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-15 16:35:10.777587", + "modified": "2022-03-17 11:55:24.621708", "modified_by": "Administrator", "module": "Regional", "name": "Tax Exemption 80G Certificate Detail", diff --git a/erpnext/regional/france/setup.py b/erpnext/regional/france/setup.py index 5b55a444bc0..da772d6b77e 100644 --- a/erpnext/regional/france/setup.py +++ b/erpnext/regional/france/setup.py @@ -10,24 +10,21 @@ def setup(company=None, patch=True): make_custom_fields() add_custom_roles_for_reports() + def make_custom_fields(): custom_fields = { - 'Company': [ - dict(fieldname='siren_number', label='SIREN Number', - fieldtype='Data', insert_after='website') + "Company": [ + dict(fieldname="siren_number", label="SIREN Number", fieldtype="Data", insert_after="website") ] } create_custom_fields(custom_fields) -def add_custom_roles_for_reports(): - report_name = 'Fichier des Ecritures Comptables [FEC]' - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts Manager') - ] - )).insert() +def add_custom_roles_for_reports(): + report_name = "Fichier des Ecritures Comptables [FEC]" + + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict(doctype="Custom Role", report=report_name, roles=[dict(role="Accounts Manager")]) + ).insert() diff --git a/erpnext/regional/france/utils.py b/erpnext/regional/france/utils.py index 841316586dc..65dfd2db916 100644 --- a/erpnext/regional/france/utils.py +++ b/erpnext/regional/france/utils.py @@ -2,8 +2,7 @@ # For license information, please see license.txt - # don't remove this function it is used in tests def test_method(): - '''test function''' - return 'overridden' + """test function""" + return "overridden" diff --git a/erpnext/regional/germany/setup.py b/erpnext/regional/germany/setup.py index 35d14135ba5..b8e66c3ece3 100644 --- a/erpnext/regional/germany/setup.py +++ b/erpnext/regional/germany/setup.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -10,9 +9,14 @@ def setup(company=None, patch=True): def make_custom_fields(): custom_fields = { - 'Party Account': [ - dict(fieldname='debtor_creditor_number', label='Debtor/Creditor Number', - fieldtype='Data', insert_after='account', translatable=0) + "Party Account": [ + dict( + fieldname="debtor_creditor_number", + label="Debtor/Creditor Number", + fieldtype="Data", + insert_after="account", + translatable=0, + ) ] } @@ -21,12 +25,11 @@ def make_custom_fields(): def add_custom_roles_for_reports(): """Add Access Control to UAE VAT 201.""" - if not frappe.db.get_value('Custom Role', dict(report='DATEV')): - frappe.get_doc(dict( - doctype='Custom Role', - report='DATEV', - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report="DATEV")): + frappe.get_doc( + dict( + doctype="Custom Role", + report="DATEV", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() diff --git a/erpnext/regional/germany/utils/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py index be3d7a3e542..6b2feb08855 100644 --- a/erpnext/regional/germany/utils/datev/datev_constants.py +++ b/erpnext/regional/germany/utils/datev/datev_constants.py @@ -186,7 +186,7 @@ TRANSACTION_COLUMNS = [ # Steuersatz für Steuerschlüssel "Steuersatz", # Beispiel: DE für Deutschland - "Land" + "Land", ] DEBTOR_CREDITOR_COLUMNS = [ @@ -447,7 +447,7 @@ DEBTOR_CREDITOR_COLUMNS = [ "Mahnfrist 1", "Mahnfrist 2", "Mahnfrist 3", - "Letzte Frist" + "Letzte Frist", ] ACCOUNT_NAME_COLUMNS = [ @@ -457,10 +457,11 @@ ACCOUNT_NAME_COLUMNS = [ "Kontenbeschriftung", # Language of the account name # "de-DE" or "en-GB" - "Sprach-ID" + "Sprach-ID", ] -class DataCategory(): + +class DataCategory: """Field of the CSV Header.""" @@ -469,7 +470,8 @@ class DataCategory(): TRANSACTIONS = "21" POSTING_TEXT_CONSTANTS = "67" -class FormatName(): + +class FormatName: """Field of the CSV Header, corresponds to DataCategory.""" @@ -478,19 +480,22 @@ class FormatName(): TRANSACTIONS = "Buchungsstapel" POSTING_TEXT_CONSTANTS = "Buchungstextkonstanten" -class Transactions(): + +class Transactions: DATA_CATEGORY = DataCategory.TRANSACTIONS FORMAT_NAME = FormatName.TRANSACTIONS FORMAT_VERSION = "9" COLUMNS = TRANSACTION_COLUMNS -class DebtorsCreditors(): + +class DebtorsCreditors: DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS FORMAT_NAME = FormatName.DEBTORS_CREDITORS FORMAT_VERSION = "5" COLUMNS = DEBTOR_CREDITOR_COLUMNS -class AccountNames(): + +class AccountNames: DATA_CATEGORY = DataCategory.ACCOUNT_NAMES FORMAT_NAME = FormatName.ACCOUNT_NAMES FORMAT_VERSION = "2" diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index d46abe91873..9d895e1afb0 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -30,133 +30,137 @@ def get_datev_csv(data, filters, csv_class): result = empty_df.append(data_df, sort=True) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: - result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) + result["Belegdatum"] = pd.to_datetime(result["Belegdatum"]) - result['Beleginfo - Inhalt 6'] = pd.to_datetime(result['Beleginfo - Inhalt 6']) - result['Beleginfo - Inhalt 6'] = result['Beleginfo - Inhalt 6'].dt.strftime('%d%m%Y') + result["Beleginfo - Inhalt 6"] = pd.to_datetime(result["Beleginfo - Inhalt 6"]) + result["Beleginfo - Inhalt 6"] = result["Beleginfo - Inhalt 6"].dt.strftime("%d%m%Y") - result['Fälligkeit'] = pd.to_datetime(result['Fälligkeit']) - result['Fälligkeit'] = result['Fälligkeit'].dt.strftime('%d%m%y') + result["Fälligkeit"] = pd.to_datetime(result["Fälligkeit"]) + result["Fälligkeit"] = result["Fälligkeit"].dt.strftime("%d%m%y") - result.sort_values(by='Belegdatum', inplace=True, kind='stable', ignore_index=True) + result.sort_values(by="Belegdatum", inplace=True, kind="stable", ignore_index=True) if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: - result['Sprach-ID'] = 'de-DE' + result["Sprach-ID"] = "de-DE" data = result.to_csv( # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 - sep=str(';'), + sep=str(";"), # European decimal seperator - decimal=',', + decimal=",", # Windows "ANSI" encoding - encoding='latin_1', + encoding="latin_1", # format date as DDMM - date_format='%d%m', + date_format="%d%m", # Windows line terminator - line_terminator='\r\n', + line_terminator="\r\n", # Do not number rows index=False, # Use all columns defined above columns=csv_class.COLUMNS, # Quote most fields, even currency values with "," separator - quoting=QUOTE_NONNUMERIC + quoting=QUOTE_NONNUMERIC, ) - data = data.encode('latin_1', errors='replace') + data = data.encode("latin_1", errors="replace") header = get_header(filters, csv_class) - header = ';'.join(header).encode('latin_1', errors='replace') + header = ";".join(header).encode("latin_1", errors="replace") # 1st Row: Header with meta data # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. # 3rd - nth Row: Data (Nutzdaten) - return header + b'\r\n' + data + return header + b"\r\n" + data def get_header(filters, csv_class): - description = filters.get('voucher_type', csv_class.FORMAT_NAME) - company = filters.get('company') - datev_settings = frappe.get_doc('DATEV Settings', {'client': company}) - default_currency = frappe.get_value('Company', company, 'default_currency') - coa = frappe.get_value('Company', company, 'chart_of_accounts') - coa_short_code = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + description = filters.get("voucher_type", csv_class.FORMAT_NAME) + company = filters.get("company") + datev_settings = frappe.get_doc("DATEV Settings", {"client": company}) + default_currency = frappe.get_value("Company", company, "default_currency") + coa = frappe.get_value("Company", company, "chart_of_accounts") + coa_short_code = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") header = [ # DATEV format - # "DTVF" = created by DATEV software, - # "EXTF" = created by other software + # "DTVF" = created by DATEV software, + # "EXTF" = created by other software '"EXTF"', # version of the DATEV format - # 141 = 1.41, - # 510 = 5.10, - # 720 = 7.20 - '700', + # 141 = 1.41, + # 510 = 5.10, + # 720 = 7.20 + "700", csv_class.DATA_CATEGORY, '"%s"' % csv_class.FORMAT_NAME, # Format version (regarding format name) csv_class.FORMAT_VERSION, # Generated on - datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '000', + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + "000", # Imported on -- stays empty - '', + "", # Origin. Any two symbols, will be replaced by "SV" on import. '"EN"', # I = Exported by '"%s"' % frappe.session.user, # J = Imported by -- stays empty - '', + "", # K = Tax consultant number (Beraternummer) - datev_settings.get('consultant_number', '0000000'), + datev_settings.get("consultant_number", "0000000"), # L = Tax client number (Mandantennummer) - datev_settings.get('client_number', '00000'), + datev_settings.get("client_number", "00000"), # M = Start of the fiscal year (Wirtschaftsjahresbeginn) - frappe.utils.formatdate(filters.get('fiscal_year_start'), 'yyyyMMdd'), + frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"), # N = Length of account numbers (Sachkontenlänge) - str(filters.get('account_number_length', 4)), + str(filters.get("account_number_length", 4)), # O = Transaction batch start date (YYYYMMDD) - frappe.utils.formatdate(filters.get('from_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + frappe.utils.formatdate(filters.get("from_date"), "yyyyMMdd") + if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS + else "", # P = Transaction batch end date (YYYYMMDD) - frappe.utils.formatdate(filters.get('to_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + frappe.utils.formatdate(filters.get("to_date"), "yyyyMMdd") + if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS + else "", # Q = Description (for example, "Sales Invoice") Max. 30 chars - '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # R = Diktatkürzel - '', + "", # S = Buchungstyp - # 1 = Transaction batch (Finanzbuchführung), - # 2 = Annual financial statement (Jahresabschluss) - '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # 1 = Transaction batch (Finanzbuchführung), + # 2 = Annual financial statement (Jahresabschluss) + "1" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # T = Rechnungslegungszweck - # 0 oder leer = vom Rechnungslegungszweck unabhängig - # 50 = Handelsrecht - # 30 = Steuerrecht - # 64 = IFRS - # 40 = Kalkulatorik - # 11 = Reserviert - # 12 = Reserviert - '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # 0 oder leer = vom Rechnungslegungszweck unabhängig + # 50 = Handelsrecht + # 30 = Steuerrecht + # 64 = IFRS + # 40 = Kalkulatorik + # 11 = Reserviert + # 12 = Reserviert + "0" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # U = Festschreibung # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1" - '0', + "0", # V = Default currency, for example, "EUR" - '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # reserviert - '', + "", # Derivatskennzeichen - '', + "", # reserviert - '', + "", # reserviert - '', + "", # SKR '"%s"' % coa_short_code, # Branchen-Lösungs-ID - '', + "", # reserviert - '', + "", # reserviert - '', + "", # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) - '' + "", ] return header @@ -171,12 +175,12 @@ def zip_and_download(zip_filename, csv_files): """ zip_buffer = BytesIO() - zip_file = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) + zip_file = zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) for csv_file in csv_files: - zip_file.writestr(csv_file.get('file_name'), csv_file.get('csv_data')) + zip_file.writestr(csv_file.get("file_name"), csv_file.get("csv_data")) zip_file.close() - frappe.response['filecontent'] = zip_buffer.getvalue() - frappe.response['filename'] = zip_filename - frappe.response['type'] = 'binary' + frappe.response["filecontent"] = zip_buffer.getvalue() + frappe.response["filename"] = zip_filename + frappe.response["type"] = "binary" diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index 2dc762f0ebd..b547d39281c 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -1,85 +1,84 @@ - from six import iteritems states = [ - '', - 'Andaman and Nicobar Islands', - 'Andhra Pradesh', - 'Arunachal Pradesh', - 'Assam', - 'Bihar', - 'Chandigarh', - 'Chhattisgarh', - 'Dadra and Nagar Haveli and Daman and Diu', - 'Delhi', - 'Goa', - 'Gujarat', - 'Haryana', - 'Himachal Pradesh', - 'Jammu and Kashmir', - 'Jharkhand', - 'Karnataka', - 'Kerala', - 'Ladakh', - 'Lakshadweep Islands', - 'Madhya Pradesh', - 'Maharashtra', - 'Manipur', - 'Meghalaya', - 'Mizoram', - 'Nagaland', - 'Odisha', - 'Other Territory', - 'Pondicherry', - 'Punjab', - 'Rajasthan', - 'Sikkim', - 'Tamil Nadu', - 'Telangana', - 'Tripura', - 'Uttar Pradesh', - 'Uttarakhand', - 'West Bengal', + "", + "Andaman and Nicobar Islands", + "Andhra Pradesh", + "Arunachal Pradesh", + "Assam", + "Bihar", + "Chandigarh", + "Chhattisgarh", + "Dadra and Nagar Haveli and Daman and Diu", + "Delhi", + "Goa", + "Gujarat", + "Haryana", + "Himachal Pradesh", + "Jammu and Kashmir", + "Jharkhand", + "Karnataka", + "Kerala", + "Ladakh", + "Lakshadweep Islands", + "Madhya Pradesh", + "Maharashtra", + "Manipur", + "Meghalaya", + "Mizoram", + "Nagaland", + "Odisha", + "Other Territory", + "Pondicherry", + "Punjab", + "Rajasthan", + "Sikkim", + "Tamil Nadu", + "Telangana", + "Tripura", + "Uttar Pradesh", + "Uttarakhand", + "West Bengal", ] state_numbers = { - "Andaman and Nicobar Islands": "35", - "Andhra Pradesh": "37", - "Arunachal Pradesh": "12", - "Assam": "18", - "Bihar": "10", - "Chandigarh": "04", - "Chhattisgarh": "22", - "Dadra and Nagar Haveli and Daman and Diu": "26", - "Delhi": "07", - "Goa": "30", - "Gujarat": "24", - "Haryana": "06", - "Himachal Pradesh": "02", - "Jammu and Kashmir": "01", - "Jharkhand": "20", - "Karnataka": "29", - "Kerala": "32", - "Ladakh": "38", - "Lakshadweep Islands": "31", - "Madhya Pradesh": "23", - "Maharashtra": "27", - "Manipur": "14", - "Meghalaya": "17", - "Mizoram": "15", - "Nagaland": "13", - "Odisha": "21", - "Other Territory": "97", - "Pondicherry": "34", - "Punjab": "03", - "Rajasthan": "08", - "Sikkim": "11", - "Tamil Nadu": "33", - "Telangana": "36", - "Tripura": "16", - "Uttar Pradesh": "09", - "Uttarakhand": "05", - "West Bengal": "19", + "Andaman and Nicobar Islands": "35", + "Andhra Pradesh": "37", + "Arunachal Pradesh": "12", + "Assam": "18", + "Bihar": "10", + "Chandigarh": "04", + "Chhattisgarh": "22", + "Dadra and Nagar Haveli and Daman and Diu": "26", + "Delhi": "07", + "Goa": "30", + "Gujarat": "24", + "Haryana": "06", + "Himachal Pradesh": "02", + "Jammu and Kashmir": "01", + "Jharkhand": "20", + "Karnataka": "29", + "Kerala": "32", + "Ladakh": "38", + "Lakshadweep Islands": "31", + "Madhya Pradesh": "23", + "Maharashtra": "27", + "Manipur": "14", + "Meghalaya": "17", + "Mizoram": "15", + "Nagaland": "13", + "Odisha": "21", + "Other Territory": "97", + "Pondicherry": "34", + "Punjab": "03", + "Rajasthan": "08", + "Sikkim": "11", + "Tamil Nadu": "33", + "Telangana": "36", + "Tripura": "16", + "Uttar Pradesh": "09", + "Uttarakhand": "05", + "West Bengal": "19", } number_state_mapping = {v: k for k, v in iteritems(state_numbers)} diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json index 78e56518dff..2c04c6dcf4d 100644 --- a/erpnext/regional/india/e_invoice/einv_item_template.json +++ b/erpnext/regional/india/e_invoice/einv_item_template.json @@ -23,9 +23,5 @@ "StateCesAmt": "{item.state_cess_amount}", "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}", "OthChrg": "{item.other_charges}", - "TotItemVal": "{item.total_value}", - "BchDtls": {{ - "Nm": "{item.batch_no}", - "ExpDt": "{item.batch_expiry_date}" - }} + "TotItemVal": "{item.total_value}" }} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 348f0c6feed..17b018c65b4 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -105,6 +105,30 @@ erpnext.setup_einvoice_actions = (doctype) => { }, primary_action_label: __('Submit') }); + d.fields_dict.transporter.df.onchange = function () { + const transporter = d.fields_dict.transporter.value; + if (transporter) { + frappe.db.get_value('Supplier', transporter, ['gst_transporter_id', 'supplier_name']) + .then(({ message }) => { + d.set_value('gst_transporter_id', message.gst_transporter_id); + d.set_value('transporter_name', message.supplier_name); + }); + } else { + d.set_value('gst_transporter_id', ''); + d.set_value('transporter_name', ''); + } + }; + d.fields_dict.driver.df.onchange = function () { + const driver = d.fields_dict.driver.value; + if (driver) { + frappe.db.get_value('Driver', driver, ['full_name']) + .then(({ message }) => { + d.set_value('driver_name', message.full_name); + }); + } else { + d.set_value('driver_name', ''); + } + }; d.show(); }; @@ -153,7 +177,6 @@ const get_ewaybill_fields = (frm) => { 'fieldname': 'gst_transporter_id', 'label': 'GST Transporter ID', 'fieldtype': 'Data', - 'fetch_from': 'transporter.gst_transporter_id', 'default': frm.doc.gst_transporter_id }, { @@ -189,9 +212,9 @@ const get_ewaybill_fields = (frm) => { 'fieldname': 'transporter_name', 'label': 'Transporter Name', 'fieldtype': 'Data', - 'fetch_from': 'transporter.name', 'read_only': 1, - 'default': frm.doc.transporter_name + 'default': frm.doc.transporter_name, + 'depends_on': 'transporter' }, { 'fieldname': 'mode_of_transport', @@ -206,7 +229,8 @@ const get_ewaybill_fields = (frm) => { 'fieldtype': 'Data', 'fetch_from': 'driver.full_name', 'read_only': 1, - 'default': frm.doc.driver_name + 'default': frm.doc.driver_name, + 'depends_on': 'driver' }, { 'fieldname': 'lr_date', diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index afb0f592435..1a60ce2ec47 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -12,6 +12,7 @@ import traceback import frappe import jwt +import requests import six from frappe import _, bold from frappe.core.page.background_jobs.background_jobs import get_info @@ -40,121 +41,167 @@ def validate_eligibility(doc): if isinstance(doc, six.string_types): doc = json.loads(doc) - invalid_doctype = doc.get('doctype') != 'Sales Invoice' + invalid_doctype = doc.get("doctype") != "Sales Invoice" if invalid_doctype: return False - einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable')) + einvoicing_enabled = cint(frappe.db.get_single_value("E Invoice Settings", "enable")) if not einvoicing_enabled: return False - einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01' - if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): + einvoicing_eligible_from = ( + frappe.db.get_single_value("E Invoice Settings", "applicable_from") or "2021-04-01" + ) + if getdate(doc.get("posting_date")) < getdate(einvoicing_eligible_from): return False - invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) - invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] - company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') + invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")}) + invalid_supply_type = doc.get("gst_category") not in [ + "Registered Regular", + "Registered Composition", + "SEZ", + "Overseas", + "Deemed Export", + ] + company_transaction = doc.get("billing_address_gstin") == doc.get("company_gstin") # if export invoice, then taxes can be empty # invoice can only be ineligible if no taxes applied and is not an export invoice - no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' - has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) + no_taxes_applied = not doc.get("taxes") and not doc.get("gst_category") == "Overseas" + has_non_gst_item = any(d for d in doc.get("items", []) if d.get("is_non_gst")) - if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item: + if ( + invalid_company + or invalid_supply_type + or company_transaction + or no_taxes_applied + or has_non_gst_item + ): return False return True + def validate_einvoice_fields(doc): invoice_eligible = validate_eligibility(doc) if not invoice_eligible: return - if doc.docstatus == 0 and doc._action == 'save': + if doc.docstatus == 0 and doc._action == "save": if doc.irn: - frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + frappe.throw(_("You cannot edit the invoice after generating IRN"), title=_("Edit Not Allowed")) if len(doc.name) > 16: raise_document_name_too_long_error() - doc.einvoice_status = 'Pending' + doc.einvoice_status = "Pending" - elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: - frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) + elif doc.docstatus == 1 and doc._action == "submit" and not doc.irn: + frappe.throw(_("You must generate IRN before submitting the document."), title=_("Missing IRN")) + + elif doc.irn and doc.docstatus == 2 and doc._action == "cancel" and not doc.irn_cancelled: + frappe.throw( + _("You must cancel IRN before cancelling the document."), title=_("Cancel Not Allowed") + ) - elif doc.irn and doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: - frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) def raise_document_name_too_long_error(): - title = _('Document ID Too Long') - msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice') - msg += ', ' - msg += _('document id {} exceed 16 letters.').format(bold(_('should not'))) - msg += '

' - msg += _('You must {} your {} in order to have document id of {} length 16.').format( - bold(_('modify')), bold(_('naming series')), bold(_('maximum')) + title = _("Document ID Too Long") + msg = _("As you have E-Invoicing enabled, to be able to generate IRN for this invoice") + msg += ", " + msg += _("document id {} exceed 16 letters.").format(bold(_("should not"))) + msg += "

" + msg += _("You must {} your {} in order to have document id of {} length 16.").format( + bold(_("modify")), bold(_("naming series")), bold(_("maximum")) ) - msg += _('Please account for ammended documents too.') + msg += _("Please account for ammended documents too.") frappe.throw(msg, title=title) + def read_json(name): - file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name)) - with open(file_path, 'r') as f: + file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name)) + with open(file_path, "r") as f: return cstr(f.read()) + def get_transaction_details(invoice): - supply_type = '' - if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' - elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' - elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' - elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' + supply_type = "" + if ( + invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition" + ): + supply_type = "B2B" + elif invoice.gst_category == "SEZ": + if invoice.export_type == "Without Payment of Tax": + supply_type = "SEZWOP" + else: + supply_type = "SEZWP" + elif invoice.gst_category == "Overseas": + if invoice.export_type == "Without Payment of Tax": + supply_type = "EXPWOP" + else: + supply_type = "EXPWP" + elif invoice.gst_category == "Deemed Export": + supply_type = "DEXP" if not supply_type: - rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export') - frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), - title=_('Invalid Supply Type')) + rr, rc, sez, overseas, export = ( + bold("Registered Regular"), + bold("Registered Composition"), + bold("SEZ"), + bold("Overseas"), + bold("Deemed Export"), + ) + frappe.throw( + _("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export), + title=_("Invalid Supply Type"), + ) + + return frappe._dict( + dict(tax_scheme="GST", supply_type=supply_type, reverse_charge=invoice.reverse_charge) + ) - return frappe._dict(dict( - tax_scheme='GST', - supply_type=supply_type, - reverse_charge=invoice.reverse_charge - )) def get_doc_details(invoice): - if getdate(invoice.posting_date) < getdate('2021-01-01'): - frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed')) + if getdate(invoice.posting_date) < getdate("2021-01-01"): + frappe.throw( + _("IRN generation is not allowed for invoices dated before 1st Jan 2021"), + title=_("Not Allowed"), + ) - invoice_type = 'CRN' if invoice.is_return else 'INV' + invoice_type = "CRN" if invoice.is_return else "INV" invoice_name = invoice.name - invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') + invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy") + + return frappe._dict( + dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date) + ) - return frappe._dict(dict( - invoice_type=invoice_type, - invoice_name=invoice_name, - invoice_date=invoice_date - )) def validate_address_fields(address, skip_gstin_validation): - if ((not address.gstin and not skip_gstin_validation) + if ( + (not address.gstin and not skip_gstin_validation) or not address.city or not address.pincode or not address.address_title or not address.address_line1 - or not address.gst_state_number): + or not address.gst_state_number + ): frappe.throw( - msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name), - title=_('Missing Address Fields') + msg=_( + "Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again." + ).format(address.name), + title=_("Missing Address Fields"), ) if address.address_line2 and len(address.address_line2) < 2: # to prevent "The field Address 2 must be a string with a minimum length of 3 and a maximum length of 100" address.address_line2 = "" + def get_party_details(address_name, skip_gstin_validation=False): - addr = frappe.get_doc('Address', address_name) + addr = frappe.get_doc("Address", address_name) validate_address_fields(addr, skip_gstin_validation) @@ -162,44 +209,53 @@ def get_party_details(address_name, skip_gstin_validation=False): # according to einvoice standard addr.pincode = 999999 - party_address_details = frappe._dict(dict( - legal_name=sanitize_for_json(addr.address_title), - location=sanitize_for_json(addr.city), - pincode=addr.pincode, gstin=addr.gstin, - state_code=addr.gst_state_number, - address_line1=sanitize_for_json(addr.address_line1), - address_line2=sanitize_for_json(addr.address_line2) - )) + party_address_details = frappe._dict( + dict( + legal_name=sanitize_for_json(addr.address_title), + location=sanitize_for_json(addr.city), + pincode=addr.pincode, + gstin=addr.gstin, + state_code=addr.gst_state_number, + address_line1=sanitize_for_json(addr.address_line1), + address_line2=sanitize_for_json(addr.address_line2), + ) + ) return party_address_details + def get_overseas_address_details(address_name): address_title, address_line1, address_line2, city = frappe.db.get_value( - 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] + "Address", address_name, ["address_title", "address_line1", "address_line2", "city"] ) if not address_title or not address_line1 or not city: frappe.throw( - msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format( - get_link_to_form('Address', address_name) - ), - title=_('Missing Address Fields') + msg=_( + "Address lines and city is mandatory for address {}. Please set them and try again." + ).format(get_link_to_form("Address", address_name)), + title=_("Missing Address Fields"), ) - return frappe._dict(dict( - gstin='URP', - legal_name=sanitize_for_json(address_title), - location=city, - address_line1=sanitize_for_json(address_line1), - address_line2=sanitize_for_json(address_line2), - pincode=999999, state_code=96, place_of_supply=96 - )) + return frappe._dict( + dict( + gstin="URP", + legal_name=sanitize_for_json(address_title), + location=city, + address_line1=sanitize_for_json(address_line1), + address_line2=sanitize_for_json(address_line2), + pincode=999999, + state_code=96, + place_of_supply=96, + ) + ) + def get_item_list(invoice): item_list = [] for d in invoice.items: - einvoice_item_schema = read_json('einv_item_template') + einvoice_item_schema = read_json("einv_item_template") item = frappe._dict({}) item.update(d.as_dict()) @@ -215,31 +271,41 @@ def get_item_list(invoice): item.taxable_value = abs(item.taxable_value) item.discount_amount = 0 - item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None - item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None - item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N' + item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N" item.serial_no = "" item = update_item_taxes(invoice, item) item.total_value = abs( - item.taxable_value + item.igst_amount + item.sgst_amount + - item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges + item.taxable_value + + item.igst_amount + + item.sgst_amount + + item.cgst_amount + + item.cess_amount + + item.cess_nadv_amount + + item.other_charges ) einv_item = einvoice_item_schema.format(item=item) item_list.append(einv_item) - return ', '.join(item_list) + return ", ".join(item_list) + def update_item_taxes(invoice, item): gst_accounts = get_gst_accounts(invoice.company) gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] for attr in [ - 'tax_rate', 'cess_rate', 'cess_nadv_amount', - 'cgst_amount', 'sgst_amount', 'igst_amount', - 'cess_amount', 'cess_nadv_amount', 'other_charges' - ]: + "tax_rate", + "cess_rate", + "cess_nadv_amount", + "cgst_amount", + "sgst_amount", + "igst_amount", + "cess_amount", + "cess_nadv_amount", + "other_charges", + ]: item[attr] = 0 for t in invoice.taxes: @@ -254,35 +320,43 @@ def update_item_taxes(invoice, item): if t.account_head in gst_accounts.cess_account: item_tax_amount_after_discount = item_tax_detail[1] - if t.charge_type == 'On Item Quantity': + if t.charge_type == "On Item Quantity": item.cess_nadv_amount += abs(item_tax_amount_after_discount) else: item.cess_rate += item_tax_rate item.cess_amount += abs(item_tax_amount_after_discount) - for tax_type in ['igst', 'cgst', 'sgst']: - if t.account_head in gst_accounts[f'{tax_type}_account']: + for tax_type in ["igst", "cgst", "sgst", "utgst"]: + if t.account_head in gst_accounts[f"{tax_type}_account"]: item.tax_rate += item_tax_rate - item[f'{tax_type}_amount'] += abs(item_tax_amount) + if tax_type == "utgst": + # utgst taxes are reported same as sgst tax + item["sgst_amount"] += abs(item_tax_amount) + else: + item[f"{tax_type}_amount"] += abs(item_tax_amount) else: # TODO: other charges per item pass return item + def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get("items")])) invoice_value_details.invoice_discount_amt = 0 invoice_value_details.round_off = invoice.base_rounding_adjustment - invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) + invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs( + invoice.base_grand_total + ) invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) return invoice_value_details + def update_invoice_taxes(invoice, invoice_value_details): gst_accounts = get_gst_accounts(invoice.company) gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] @@ -301,90 +375,121 @@ def update_invoice_taxes(invoice, invoice_value_details): # using after discount amt since item also uses after discount amt for cess calc invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) - for tax_type in ['igst', 'cgst', 'sgst']: - if t.account_head in gst_accounts[f'{tax_type}_account']: + for tax_type in ["igst", "cgst", "sgst", "utgst"]: + if t.account_head in gst_accounts[f"{tax_type}_account"]: + if tax_type == "utgst": + invoice_value_details["total_sgst_amt"] += abs(tax_amount) + else: + invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount) - invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount) update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows) + else: invoice_value_details.total_other_charges += abs(tax_amount) return invoice_value_details -def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows): + +def update_other_charges( + tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows +): prev_row_id = cint(tax_row.row_id) - 1 if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows: - if tax_row.charge_type == 'On Previous Row Amount': - amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount + if tax_row.charge_type == "On Previous Row Amount": + amount = invoice.get("taxes")[prev_row_id].tax_amount_after_discount_amount invoice_value_details.total_other_charges -= abs(amount) considered_rows.append(prev_row_id) - if tax_row.charge_type == 'On Previous Row Total': - amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total + if tax_row.charge_type == "On Previous Row Total": + amount = invoice.get("taxes")[prev_row_id].base_total - invoice.base_net_total invoice_value_details.total_other_charges -= abs(amount) considered_rows.append(prev_row_id) + def get_payment_details(invoice): payee_name = invoice.company - mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) + mode_of_payment = "" paid_amount = invoice.base_paid_amount outstanding_amount = invoice.outstanding_amount - return frappe._dict(dict( - payee_name=payee_name, mode_of_payment=mode_of_payment, - paid_amount=paid_amount, outstanding_amount=outstanding_amount - )) + return frappe._dict( + dict( + payee_name=payee_name, + mode_of_payment=mode_of_payment, + paid_amount=paid_amount, + outstanding_amount=outstanding_amount, + ) + ) + def get_return_doc_reference(invoice): - invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') - return frappe._dict(dict( - invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') - )) + invoice_date = frappe.db.get_value("Sales Invoice", invoice.return_against, "posting_date") + return frappe._dict( + dict(invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, "dd/mm/yyyy")) + ) + def get_eway_bill_details(invoice): if invoice.is_return: - frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'), - title=_('Invalid Fields')) + frappe.throw( + _( + "E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice." + ), + title=_("Invalid Fields"), + ) + mode_of_transport = {"": "", "Road": "1", "Air": "2", "Rail": "3", "Ship": "4"} + vehicle_type = {"Regular": "R", "Over Dimensional Cargo (ODC)": "O"} - mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } - vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } + return frappe._dict( + dict( + gstin=invoice.gst_transporter_id, + name=invoice.transporter_name, + mode_of_transport=mode_of_transport[invoice.mode_of_transport or ""] or None, + distance=invoice.distance or 0, + document_name=invoice.lr_no, + document_date=format_date(invoice.lr_date, "dd/mm/yyyy"), + vehicle_no=invoice.vehicle_no, + vehicle_type=vehicle_type[invoice.gst_vehicle_type], + ) + ) - return frappe._dict(dict( - gstin=invoice.gst_transporter_id, - name=invoice.transporter_name, - mode_of_transport=mode_of_transport[invoice.mode_of_transport], - distance=invoice.distance or 0, - document_name=invoice.lr_no, - document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'), - vehicle_no=invoice.vehicle_no, - vehicle_type=vehicle_type[invoice.gst_vehicle_type] - )) def validate_mandatory_fields(invoice): if not invoice.company_address: frappe.throw( - _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'), - title=_('Missing Fields') + _( + "Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again." + ), + title=_("Missing Fields"), ) if not invoice.customer_address: frappe.throw( - _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'), - title=_('Missing Fields') + _( + "Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again." + ), + title=_("Missing Fields"), ) - if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): + if not frappe.db.get_value("Address", invoice.company_address, "gstin"): frappe.throw( - _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), - title=_('Missing Fields') + _( + "GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address." + ), + title=_("Missing Fields"), ) - if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'): + if invoice.gst_category != "Overseas" and not frappe.db.get_value( + "Address", invoice.customer_address, "gstin" + ): frappe.throw( - _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'), - title=_('Missing Fields') + _( + "GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address." + ), + title=_("Missing Fields"), ) + def validate_totals(einvoice): - item_list = einvoice['ItemList'] - value_details = einvoice['ValDtls'] + item_list = einvoice["ItemList"] + value_details = einvoice["ValDtls"] total_item_ass_value = 0 total_item_cgst_value = 0 @@ -392,39 +497,89 @@ def validate_totals(einvoice): total_item_igst_value = 0 total_item_value = 0 for item in item_list: - total_item_ass_value += flt(item['AssAmt']) - total_item_cgst_value += flt(item['CgstAmt']) - total_item_sgst_value += flt(item['SgstAmt']) - total_item_igst_value += flt(item['IgstAmt']) - total_item_value += flt(item['TotItemVal']) + total_item_ass_value += flt(item["AssAmt"]) + total_item_cgst_value += flt(item["CgstAmt"]) + total_item_sgst_value += flt(item["SgstAmt"]) + total_item_igst_value += flt(item["IgstAmt"]) + total_item_value += flt(item["TotItemVal"]) - if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1: - frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx)) + if ( + abs(flt(item["AssAmt"]) * flt(item["GstRt"]) / 100) + - (flt(item["CgstAmt"]) + flt(item["SgstAmt"]) + flt(item["IgstAmt"])) + > 1 + ): + frappe.throw( + _( + "Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table." + ).format(item.idx) + ) - if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: - frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.')) + if abs(flt(value_details["AssVal"]) - total_item_ass_value) > 1: + frappe.throw( + _( + "Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction." + ) + ) - if abs(flt(value_details['CgstVal']) + flt(value_details['SgstVal']) - total_item_cgst_value - total_item_sgst_value) > 1: - frappe.throw(_('CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction.')) + if ( + abs( + flt(value_details["CgstVal"]) + + flt(value_details["SgstVal"]) + - total_item_cgst_value + - total_item_sgst_value + ) + > 1 + ): + frappe.throw( + _( + "CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction." + ) + ) - if abs(flt(value_details['IgstVal']) - total_item_igst_value) > 1: - frappe.throw(_('IGST value of all items is not equal to total IGST value. Please review taxes for any correction.')) + if abs(flt(value_details["IgstVal"]) - total_item_igst_value) > 1: + frappe.throw( + _( + "IGST value of all items is not equal to total IGST value. Please review taxes for any correction." + ) + ) - if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1: - frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.')) + if ( + abs( + flt(value_details["TotInvVal"]) + + flt(value_details["Discount"]) + - flt(value_details["OthChrg"]) + - total_item_value + ) + > 1 + ): + frappe.throw( + _( + "Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction." + ) + ) - calculated_invoice_value = \ - flt(value_details['AssVal']) + flt(value_details['CgstVal']) \ - + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \ - + flt(value_details['OthChrg']) - flt(value_details['Discount']) + calculated_invoice_value = ( + flt(value_details["AssVal"]) + + flt(value_details["CgstVal"]) + + flt(value_details["SgstVal"]) + + flt(value_details["IgstVal"]) + + flt(value_details["CesVal"]) + + flt(value_details["OthChrg"]) + - flt(value_details["Discount"]) + ) + + if abs(flt(value_details["TotInvVal"]) - calculated_invoice_value) > 1: + frappe.throw( + _( + "Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction." + ) + ) - if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1: - frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.')) def make_einvoice(invoice): validate_mandatory_fields(invoice) - schema = read_json('einv_template') + schema = read_json("einv_template") transaction_details = get_transaction_details(invoice) item_list = get_item_list(invoice) @@ -432,13 +587,13 @@ def make_einvoice(invoice): invoice_value_details = get_invoice_value_details(invoice) seller_details = get_party_details(invoice.company_address) - if invoice.gst_category == 'Overseas': + if invoice.gst_category == "Overseas": buyer_details = get_overseas_address_details(invoice.customer_address) else: buyer_details = get_party_details(invoice.customer_address) place_of_supply = get_place_of_supply(invoice, invoice.doctype) if place_of_supply: - place_of_supply = place_of_supply.split('-')[0] + place_of_supply = place_of_supply.split("-")[0] else: place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) @@ -448,7 +603,7 @@ def make_einvoice(invoice): shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: - if invoice.gst_category == 'Overseas': + if invoice.gst_category == "Overseas": shipping_details = get_overseas_address_details(invoice.shipping_address_name) else: shipping_details = get_party_details(invoice.shipping_address_name, skip_gstin_validation=True) @@ -470,11 +625,19 @@ def make_einvoice(invoice): period_details = export_details = frappe._dict({}) einvoice = schema.format( - transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details, - seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, - item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details, - period_details=period_details, prev_doc_details=prev_doc_details, - export_details=export_details, eway_bill_details=eway_bill_details + transaction_details=transaction_details, + doc_details=doc_details, + dispatch_details=dispatch_details, + seller_details=seller_details, + buyer_details=buyer_details, + shipping_details=shipping_details, + item_list=item_list, + invoice_value_details=invoice_value_details, + payment_details=payment_details, + period_details=period_details, + prev_doc_details=prev_doc_details, + export_details=export_details, + eway_bill_details=eway_bill_details, ) try: @@ -491,15 +654,18 @@ def make_einvoice(invoice): return einvoice + def show_link_to_error_log(invoice, einvoice): err_log = log_error(einvoice) - link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log') + link_to_error_log = get_link_to_form("Error Log", err_log.name, "Error Log") frappe.throw( - _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format( - invoice.name, link_to_error_log), - title=_('E Invoice Creation Failed') + _( + "An error occurred while creating e-invoice for {}. Please check {} for more information." + ).format(invoice.name, link_to_error_log), + title=_("E Invoice Creation Failed"), ) + def log_error(data=None): if isinstance(data, six.string_types): data = json.loads(data) @@ -509,16 +675,47 @@ def log_error(data=None): err_msg = str(sys.exc_info()[1]) data = json.dumps(data, indent=4) - message = "\n".join([ - "Error", err_msg, seperator, - "Data:", data, seperator, - "Exception:", err_tb - ]) - return frappe.log_error(title=_('E Invoice Request Failed'), message=message) + message = "\n".join(["Error", err_msg, seperator, "Data:", data, seperator, "Exception:", err_tb]) + return frappe.log_error(title=_("E Invoice Request Failed"), message=message) + def santize_einvoice_fields(einvoice): - int_fields = ["Pin","Distance","CrDay"] - float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",] + int_fields = ["Pin", "Distance", "CrDay"] + float_fields = [ + "Qty", + "FreeQty", + "UnitPrice", + "TotAmt", + "Discount", + "PreTaxVal", + "AssAmt", + "GstRt", + "IgstAmt", + "CgstAmt", + "SgstAmt", + "CesRt", + "CesAmt", + "CesNonAdvlAmt", + "StateCesRt", + "StateCesAmt", + "StateCesNonAdvlAmt", + "OthChrg", + "TotItemVal", + "AssVal", + "CgstVal", + "SgstVal", + "IgstVal", + "CesVal", + "StCesVal", + "Discount", + "OthChrg", + "RndOffAmt", + "TotInvVal", + "TotInvValFc", + "PaidAmt", + "PaymtDue", + "ExpDuty", + ] copy = einvoice.copy() for key, value in copy.items(): if isinstance(value, list): @@ -550,22 +747,31 @@ def santize_einvoice_fields(einvoice): return einvoice + def safe_json_load(json_string): try: return json.loads(json_string) except json.JSONDecodeError as e: # print a snippet of 40 characters around the location where error occured pos = e.pos - start, end = max(0, pos-20), min(len(json_string)-1, pos+20) + start, end = max(0, pos - 20), min(len(json_string) - 1, pos + 20) snippet = json_string[start:end] - frappe.throw(_("Error in input data. Please check for any special characters near following input:
{}").format(snippet)) + frappe.throw( + _( + "Error in input data. Please check for any special characters near following input:
{}" + ).format(snippet) + ) + class RequestFailed(Exception): pass + + class CancellationNotAllowed(Exception): pass -class GSPConnector(): + +class GSPConnector: def __init__(self, doctype=None, docname=None): self.doctype = doctype self.docname = docname @@ -574,15 +780,19 @@ class GSPConnector(): self.set_credentials() # authenticate url is same for sandbox & live - self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' - self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test' + self.authenticate_url = "https://gsp.adaequare.com/gsp/authenticate?grant_type=token" + self.base_url = ( + "https://gsp.adaequare.com" + if not self.e_invoice_settings.sandbox_mode + else "https://gsp.adaequare.com/test" + ) - self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' - self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' - self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' - self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' - self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB' - self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' + self.cancel_irn_url = self.base_url + "/enriched/ei/api/invoice/cancel" + self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn" + self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice" + self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" + self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB" + self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" def set_invoice(self): self.invoice = None @@ -590,10 +800,14 @@ class GSPConnector(): self.invoice = frappe.get_cached_doc(self.doctype, self.docname) def set_credentials(self): - self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') + self.e_invoice_settings = frappe.get_cached_doc("E Invoice Settings") if not self.e_invoice_settings.enable: - frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings"))) + frappe.throw( + _("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format( + get_link_to_form("E Invoice Settings", "E Invoice Settings") + ) + ) if self.invoice: gstin = self.get_seller_gstin() @@ -601,14 +815,22 @@ class GSPConnector(): if credentials_for_gstin: self.credentials = credentials_for_gstin[0] else: - frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings')) + frappe.throw( + _( + "Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings" + ) + ) else: - self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None + self.credentials = ( + self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None + ) def get_seller_gstin(self): - gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin') + gstin = frappe.db.get_value("Address", self.invoice.company_address, "gstin") if not gstin: - frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) + frappe.throw( + _("Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.") + ) return gstin def get_auth_token(self): @@ -618,45 +840,57 @@ class GSPConnector(): return self.e_invoice_settings.auth_token def make_request(self, request_type, url, headers=None, data=None): - if request_type == 'post': - res = make_post_request(url, headers=headers, data=data) - else: - res = make_get_request(url, headers=headers, data=data) + try: + if request_type == "post": + res = make_post_request(url, headers=headers, data=data) + else: + res = make_get_request(url, headers=headers, data=data) + + except requests.exceptions.HTTPError as e: + if e.response.status_code in [401, 403] and not hasattr(self, "token_auto_refreshed"): + self.auto_refresh_token() + headers = self.get_headers() + return self.make_request(request_type, url, headers, data) self.log_request(url, headers, data, res) return res + def auto_refresh_token(self): + self.fetch_auth_token() + self.token_auto_refreshed = True + def log_request(self, url, headers, data, res): - headers.update({ 'password': self.credentials.password }) - request_log = frappe.get_doc({ - "doctype": "E Invoice Request Log", - "user": frappe.session.user, - "reference_invoice": self.invoice.name if self.invoice else None, - "url": url, - "headers": json.dumps(headers, indent=4) if headers else None, - "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, - "response": json.dumps(res, indent=4) if res else None - }) + headers.update({"password": self.credentials.password}) + request_log = frappe.get_doc( + { + "doctype": "E Invoice Request Log", + "user": frappe.session.user, + "reference_invoice": self.invoice.name if self.invoice else None, + "url": url, + "headers": json.dumps(headers, indent=4) if headers else None, + "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, + "response": json.dumps(res, indent=4) if res else None, + } + ) request_log.save(ignore_permissions=True) frappe.db.commit() def get_client_credentials(self): if self.e_invoice_settings.client_id and self.e_invoice_settings.client_secret: - return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password('client_secret') + return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password("client_secret") return frappe.conf.einvoice_client_id, frappe.conf.einvoice_client_secret def fetch_auth_token(self): client_id, client_secret = self.get_client_credentials() - headers = { - 'gspappid': client_id, - 'gspappsecret': client_secret - } + headers = {"gspappid": client_id, "gspappsecret": client_secret} res = {} try: - res = self.make_request('post', self.authenticate_url, headers) - self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) - self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in')) + res = self.make_request("post", self.authenticate_url, headers) + self.e_invoice_settings.auth_token = "{} {}".format( + res.get("token_type"), res.get("access_token") + ) + self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get("expires_in")) self.e_invoice_settings.save(ignore_permissions=True) self.e_invoice_settings.reload() @@ -666,22 +900,22 @@ class GSPConnector(): def get_headers(self): return { - 'content-type': 'application/json', - 'user_name': self.credentials.username, - 'password': self.credentials.get_password(), - 'gstin': self.credentials.gstin, - 'authorization': self.get_auth_token(), - 'requestid': str(base64.b64encode(os.urandom(18))), + "content-type": "application/json", + "user_name": self.credentials.username, + "password": self.credentials.get_password(), + "gstin": self.credentials.gstin, + "authorization": self.get_auth_token(), + "requestid": str(base64.b64encode(os.urandom(18))), } def fetch_gstin_details(self, gstin): headers = self.get_headers() try: - params = '?gstin={gstin}'.format(gstin=gstin) - res = self.make_request('get', self.gstin_details_url + params, headers) - if res.get('success'): - return res.get('result') + params = "?gstin={gstin}".format(gstin=gstin) + res = self.make_request("get", self.gstin_details_url + params, headers) + if res.get("success"): + return res.get("result") else: log_error(res) raise RequestFailed @@ -692,10 +926,11 @@ class GSPConnector(): except Exception: log_error() self.raise_error(True) + @staticmethod def get_gstin_details(gstin): - '''fetch and cache GSTIN details''' - if not hasattr(frappe.local, 'gstin_cache'): + """fetch and cache GSTIN details""" + if not hasattr(frappe.local, "gstin_cache"): frappe.local.gstin_cache = {} key = gstin @@ -703,7 +938,7 @@ class GSPConnector(): details = gsp_connector.fetch_gstin_details(gstin) frappe.local.gstin_cache[key] = details - frappe.cache().hset('gstin_cache', key, details) + frappe.cache().hset("gstin_cache", key, details) return details def generate_irn(self): @@ -712,27 +947,29 @@ class GSPConnector(): headers = self.get_headers() einvoice = make_einvoice(self.invoice) data = json.dumps(einvoice, indent=4) - res = self.make_request('post', self.generate_irn_url, headers, data) + res = self.make_request("post", self.generate_irn_url, headers, data) - if res.get('success'): - self.set_einvoice_data(res.get('result')) + if res.get("success"): + self.set_einvoice_data(res.get("result")) - elif '2150' in res.get('message'): + elif "2150" in res.get("message"): # IRN already generated but not updated in invoice # Extract the IRN from the response description and fetch irn details - irn = res.get('result')[0].get('Desc').get('Irn') + irn = res.get("result")[0].get("Desc").get("Irn") irn_details = self.get_irn_details(irn) if irn_details: self.set_einvoice_data(irn_details) else: - raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \ - Contact ERPNext support to resolve the issue.') + raise RequestFailed( + "IRN has already been generated for the invoice but cannot fetch details for the it. \ + Contact ERPNext support to resolve the issue." + ) else: raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.set_failed_status(errors=errors) self.raise_error(errors=errors) @@ -744,7 +981,7 @@ class GSPConnector(): @staticmethod def bulk_generate_irn(invoices): gsp_connector = GSPConnector() - gsp_connector.doctype = 'Sales Invoice' + gsp_connector.doctype = "Sales Invoice" failed = [] @@ -756,10 +993,7 @@ class GSPConnector(): gsp_connector.generate_irn() except Exception as e: - failed.append({ - 'docname': invoice, - 'message': str(e) - }) + failed.append({"docname": invoice, "message": str(e)}) return failed @@ -767,15 +1001,15 @@ class GSPConnector(): headers = self.get_headers() try: - params = '?irn={irn}'.format(irn=irn) - res = self.make_request('get', self.irn_details_url + params, headers) - if res.get('success'): - return res.get('result') + params = "?irn={irn}".format(irn=irn) + res = self.make_request("get", self.irn_details_url + params, headers) + if res.get("success"): + return res.get("result") else: raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.raise_error(errors=errors) except Exception: @@ -787,26 +1021,30 @@ class GSPConnector(): try: # validate cancellation if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24: - frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed) + frappe.throw( + _("E-Invoice cannot be cancelled after 24 hours of IRN generation."), + title=_("Not Allowed"), + exc=CancellationNotAllowed, + ) if not irn: - frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed) + frappe.throw( + _("IRN not found. You must generate IRN before cancelling."), + title=_("Not Allowed"), + exc=CancellationNotAllowed, + ) headers = self.get_headers() - data = json.dumps({ - 'Irn': irn, - 'Cnlrsn': reason, - 'Cnlrem': remark - }, indent=4) + data = json.dumps({"Irn": irn, "Cnlrsn": reason, "Cnlrem": remark}, indent=4) - res = self.make_request('post', self.cancel_irn_url, headers, data) - if res.get('success') or '9999' in res.get('message'): + res = self.make_request("post", self.cancel_irn_url, headers, data) + if res.get("success") or "9999" in res.get("message"): self.invoice.irn_cancelled = 1 - self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else "" - self.invoice.einvoice_status = 'Cancelled' + self.invoice.irn_cancel_date = res.get("result")["CancelDate"] if res.get("result") else "" + self.invoice.einvoice_status = "Cancelled" self.invoice.flags.updater_reference = { - 'doctype': self.invoice.doctype, - 'docname': self.invoice.name, - 'label': _('IRN Cancelled - {}').format(remark) + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("IRN Cancelled - {}").format(remark), } self.update_invoice() @@ -814,7 +1052,7 @@ class GSPConnector(): raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.set_failed_status(errors=errors) self.raise_error(errors=errors) @@ -830,7 +1068,7 @@ class GSPConnector(): @staticmethod def bulk_cancel_irn(invoices, reason, remark): gsp_connector = GSPConnector() - gsp_connector.doctype = 'Sales Invoice' + gsp_connector.doctype = "Sales Invoice" failed = [] @@ -843,10 +1081,7 @@ class GSPConnector(): gsp_connector.cancel_irn(irn, reason, remark) except Exception as e: - failed.append({ - 'docname': invoice, - 'message': str(e) - }) + failed.append({"docname": invoice, "message": str(e)}) return failed @@ -855,29 +1090,32 @@ class GSPConnector(): headers = self.get_headers() eway_bill_details = get_eway_bill_details(args) - data = json.dumps({ - 'Irn': args.irn, - 'Distance': cint(eway_bill_details.distance), - 'TransMode': eway_bill_details.mode_of_transport, - 'TransId': eway_bill_details.gstin, - 'TransName': eway_bill_details.transporter, - 'TrnDocDt': eway_bill_details.document_date, - 'TrnDocNo': eway_bill_details.document_name, - 'VehNo': eway_bill_details.vehicle_no, - 'VehType': eway_bill_details.vehicle_type - }, indent=4) + data = json.dumps( + { + "Irn": args.irn, + "Distance": cint(eway_bill_details.distance), + "TransMode": eway_bill_details.mode_of_transport, + "TransId": eway_bill_details.gstin, + "TransName": eway_bill_details.name, + "TrnDocDt": eway_bill_details.document_date, + "TrnDocNo": eway_bill_details.document_name, + "VehNo": eway_bill_details.vehicle_no, + "VehType": eway_bill_details.vehicle_type, + }, + indent=4, + ) try: - res = self.make_request('post', self.generate_ewaybill_url, headers, data) - if res.get('success'): - self.invoice.ewaybill = res.get('result').get('EwbNo') - self.invoice.eway_bill_validity = res.get('result').get('EwbValidTill') + res = self.make_request("post", self.generate_ewaybill_url, headers, data) + if res.get("success"): + self.invoice.ewaybill = res.get("result").get("EwbNo") + self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill") self.invoice.eway_bill_cancelled = 0 self.invoice.update(args) self.invoice.flags.updater_reference = { - 'doctype': self.invoice.doctype, - 'docname': self.invoice.name, - 'label': _('E-Way Bill Generated') + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("E-Way Bill Generated"), } self.update_invoice() @@ -885,7 +1123,7 @@ class GSPConnector(): raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.raise_error(errors=errors) except Exception: @@ -894,22 +1132,18 @@ class GSPConnector(): def cancel_eway_bill(self, eway_bill, reason, remark): headers = self.get_headers() - data = json.dumps({ - 'ewbNo': eway_bill, - 'cancelRsnCode': reason, - 'cancelRmrk': remark - }, indent=4) + data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) headers["username"] = headers["user_name"] del headers["user_name"] try: - res = self.make_request('post', self.cancel_ewaybill_url, headers, data) - if res.get('success'): - self.invoice.ewaybill = '' + res = self.make_request("post", self.cancel_ewaybill_url, headers, data) + if res.get("success"): + self.invoice.ewaybill = "" self.invoice.eway_bill_cancelled = 1 self.invoice.flags.updater_reference = { - 'doctype': self.invoice.doctype, - 'docname': self.invoice.name, - 'label': _('E-Way Bill Cancelled - {}').format(remark) + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("E-Way Bill Cancelled - {}").format(remark), } self.update_invoice() @@ -917,7 +1151,7 @@ class GSPConnector(): raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.raise_error(errors=errors) except Exception: @@ -925,24 +1159,24 @@ class GSPConnector(): self.raise_error(True) def sanitize_error_message(self, message): - ''' - On validation errors, response message looks something like this: - message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, - 3095 : Supplier GSTIN is inactive' - we search for string between ':' to extract the error messages - errors = [ - ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', - ': Test' - ] - then we trim down the message by looping over errors - ''' + """ + On validation errors, response message looks something like this: + message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, + 3095 : Supplier GSTIN is inactive' + we search for string between ':' to extract the error messages + errors = [ + ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', + ': Test' + ] + then we trim down the message by looping over errors + """ if not message: return [] - errors = re.findall(': [^:]+', message) + errors = re.findall(": [^:]+", message) for idx, e in enumerate(errors): # remove colons - errors[idx] = errors[idx].replace(':', '').strip() + errors[idx] = errors[idx].replace(":", "").strip() # if not last if idx != len(errors) - 1: # remove last 7 chars eg: ', 3095 ' @@ -951,39 +1185,41 @@ class GSPConnector(): return errors def raise_error(self, raise_exception=False, errors=None): - title = _('E Invoice Request Failed') + title = _("E Invoice Request Failed") if errors: frappe.throw(errors, title=title, as_list=1) else: link_to_error_list = 'Error Log' frappe.msgprint( - _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list), + _( + "An error occurred while making e-invoicing request. Please check {} for more information." + ).format(link_to_error_list), title=title, raise_exception=raise_exception, - indicator='red' + indicator="red", ) def set_einvoice_data(self, res): - enc_signed_invoice = res.get('SignedInvoice') - dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] + enc_signed_invoice = res.get("SignedInvoice") + dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)["data"] - self.invoice.irn = res.get('Irn') - self.invoice.ewaybill = res.get('EwbNo') - self.invoice.eway_bill_validity = res.get('EwbValidTill') - self.invoice.ack_no = res.get('AckNo') - self.invoice.ack_date = res.get('AckDt') + self.invoice.irn = res.get("Irn") + self.invoice.ewaybill = res.get("EwbNo") + self.invoice.eway_bill_validity = res.get("EwbValidTill") + self.invoice.ack_no = res.get("AckNo") + self.invoice.ack_date = res.get("AckDt") self.invoice.signed_einvoice = dec_signed_invoice - self.invoice.ack_no = res.get('AckNo') - self.invoice.ack_date = res.get('AckDt') - self.invoice.signed_qr_code = res.get('SignedQRCode') - self.invoice.einvoice_status = 'Generated' + self.invoice.ack_no = res.get("AckNo") + self.invoice.ack_date = res.get("AckDt") + self.invoice.signed_qr_code = res.get("SignedQRCode") + self.invoice.einvoice_status = "Generated" self.attach_qrcode_image() self.invoice.flags.updater_reference = { - 'doctype': self.invoice.doctype, - 'docname': self.invoice.name, - 'label': _('IRN Generated') + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("IRN Generated"), } self.update_invoice() @@ -991,19 +1227,22 @@ class GSPConnector(): qrcode = self.invoice.signed_qr_code doctype = self.invoice.doctype docname = self.invoice.name - filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__") + filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__") qr_image = io.BytesIO() - url = qrcreate(qrcode, error='L') + url = qrcreate(qrcode, error="L") url.png(qr_image, scale=2, quiet_zone=1) - _file = frappe.get_doc({ - "doctype": "File", - "file_name": filename, - "attached_to_doctype": doctype, - "attached_to_name": docname, - "attached_to_field": "qrcode_image", - "is_private": 0, - "content": qr_image.getvalue()}) + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": filename, + "attached_to_doctype": doctype, + "attached_to_name": docname, + "attached_to_field": "qrcode_image", + "is_private": 0, + "content": qr_image.getvalue(), + } + ) _file.save() frappe.db.commit() self.invoice.qrcode_image = _file.file_url @@ -1015,50 +1254,57 @@ class GSPConnector(): def set_failed_status(self, errors=None): frappe.db.rollback() - self.invoice.einvoice_status = 'Failed' + self.invoice.einvoice_status = "Failed" self.invoice.failure_description = self.get_failure_message(errors) if errors else "" self.update_invoice() frappe.db.commit() def get_failure_message(self, errors): if isinstance(errors, list): - errors = ', '.join(errors) + errors = ", ".join(errors) return errors + def sanitize_for_json(string): """Escape JSON specific characters from a string.""" # json.dumps adds double-quotes to the string. Indexing to remove them. return json.dumps(string)[1:-1] + @frappe.whitelist() def get_einvoice(doctype, docname): invoice = frappe.get_doc(doctype, docname) return make_einvoice(invoice) + @frappe.whitelist() def generate_irn(doctype, docname): gsp_connector = GSPConnector(doctype, docname) gsp_connector.generate_irn() + @frappe.whitelist() def cancel_irn(doctype, docname, irn, reason, remark): gsp_connector = GSPConnector(doctype, docname) gsp_connector.cancel_irn(irn, reason, remark) + @frappe.whitelist() def generate_eway_bill(doctype, docname, **kwargs): gsp_connector = GSPConnector(doctype, docname) gsp_connector.generate_eway_bill(**kwargs) + @frappe.whitelist() def cancel_eway_bill(doctype, docname): # TODO: uncomment when eway_bill api from Adequare is enabled # gsp_connector = GSPConnector(doctype, docname) # gsp_connector.cancel_eway_bill(eway_bill, reason, remark) - frappe.db.set_value(doctype, docname, 'ewaybill', '') - frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) + frappe.db.set_value(doctype, docname, "ewaybill", "") + frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1) + @frappe.whitelist() def generate_einvoices(docnames): @@ -1073,40 +1319,40 @@ def generate_einvoices(docnames): success = len(docnames) - len(failures) frappe.msgprint( - _('{} e-invoices generated successfully').format(success), - title=_('Bulk E-Invoice Generation Complete') + _("{} e-invoices generated successfully").format(success), + title=_("Bulk E-Invoice Generation Complete"), ) else: enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames) + def schedule_bulk_generate_irn(docnames): failures = GSPConnector.bulk_generate_irn(docnames) frappe.local.message_log = [] - frappe.publish_realtime("bulk_einvoice_generation_complete", { - "user": frappe.session.user, - "failures": failures, - "invoices": docnames - }) + frappe.publish_realtime( + "bulk_einvoice_generation_complete", + {"user": frappe.session.user, "failures": failures, "invoices": docnames}, + ) + def show_bulk_action_failure_message(failures): for doc in failures: - docname = '{0}'.format(doc.get('docname')) - message = doc.get('message').replace("'", '"') - if message[0] == '[': + docname = '{0}'.format(doc.get("docname")) + message = doc.get("message").replace("'", '"') + if message[0] == "[": errors = json.loads(message) - error_list = ''.join(['
  • {}
  • '.format(err) for err in errors]) - message = '''{} has following errors:
    -
      {}
    '''.format(docname, error_list) + error_list = "".join(["
  • {}
  • ".format(err) for err in errors]) + message = """{} has following errors:
    +
      {}
    """.format( + docname, error_list + ) else: - message = '{} - {}'.format(docname, message) + message = "{} - {}".format(docname, message) + + frappe.msgprint(message, title=_("Bulk E-Invoice Generation Complete"), indicator="red") - frappe.msgprint( - message, - title=_('Bulk E-Invoice Generation Complete'), - indicator='red' - ) @frappe.whitelist() def cancel_irns(docnames, reason, remark): @@ -1121,21 +1367,22 @@ def cancel_irns(docnames, reason, remark): success = len(docnames) - len(failures) frappe.msgprint( - _('{} e-invoices cancelled successfully').format(success), - title=_('Bulk E-Invoice Cancellation Complete') + _("{} e-invoices cancelled successfully").format(success), + title=_("Bulk E-Invoice Cancellation Complete"), ) else: enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark) + def schedule_bulk_cancel_irn(docnames, reason, remark): failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark) frappe.local.message_log = [] - frappe.publish_realtime("bulk_einvoice_cancellation_complete", { - "user": frappe.session.user, - "failures": failures, - "invoices": docnames - }) + frappe.publish_realtime( + "bulk_einvoice_cancellation_complete", + {"user": frappe.session.user, "failures": failures, "invoices": docnames}, + ) + def enqueue_bulk_action(job, **kwargs): check_scheduler_status() @@ -1150,16 +1397,18 @@ def enqueue_bulk_action(job, **kwargs): ) if job == schedule_bulk_generate_irn: - msg = _('E-Invoices will be generated in a background process.') + msg = _("E-Invoices will be generated in a background process.") else: - msg = _('E-Invoices will be cancelled in a background process.') + msg = _("E-Invoices will be cancelled in a background process.") frappe.msgprint(msg, alert=1) + def check_scheduler_status(): if is_scheduler_inactive() and not frappe.flags.in_test: frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) + def job_already_enqueued(job_name): enqueued_jobs = [d.get("job_name") for d in get_info()] if job_name in enqueued_jobs: diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index f84b0e7bd29..82d734d845c 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -17,44 +17,47 @@ from erpnext.regional.india import states def setup(company=None, patch=True): # Company independent fixtures should be called only once at the first company setup - if patch or frappe.db.count('Company', {'country': 'India'}) <=1: + if patch or frappe.db.count("Company", {"country": "India"}) <= 1: setup_company_independent_fixtures(patch=patch) if not patch: make_fixtures(company) + # TODO: for all countries def setup_company_independent_fixtures(patch=False): make_custom_fields() make_property_setters(patch=patch) add_permissions() add_custom_roles_for_reports() - frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) + frappe.enqueue("erpnext.regional.india.setup.add_hsn_sac_codes", now=frappe.flags.in_test) create_gratuity_rule() add_print_formats() update_accounts_settings_for_taxes() + def add_hsn_sac_codes(): if frappe.flags.in_test and frappe.flags.created_hsn_codes: return # HSN codes - with open(os.path.join(os.path.dirname(__file__), 'hsn_code_data.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "hsn_code_data.json"), "r") as f: hsn_codes = json.loads(f.read()) create_hsn_codes(hsn_codes, code_field="hsn_code") # SAC Codes - with open(os.path.join(os.path.dirname(__file__), 'sac_code_data.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "sac_code_data.json"), "r") as f: sac_codes = json.loads(f.read()) create_hsn_codes(sac_codes, code_field="sac_code") if frappe.flags.in_test: frappe.flags.created_hsn_codes = True + def create_hsn_codes(data, code_field): for d in data: - hsn_code = frappe.new_doc('GST HSN Code') + hsn_code = frappe.new_doc("GST HSN Code") hsn_code.description = d["description"] hsn_code.hsn_code = d[code_field] hsn_code.name = d[code_field] @@ -63,59 +66,69 @@ def create_hsn_codes(data, code_field): except frappe.DuplicateEntryError: pass + def add_custom_roles_for_reports(): - for report_name in ('GST Sales Register', 'GST Purchase Register', - 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'): + for report_name in ( + "GST Sales Register", + "GST Purchase Register", + "GST Itemised Sales Register", + "GST Itemised Purchase Register", + "Eway Bill", + "E-Invoice Summary", + ): - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() - for report_name in ('Professional Tax Deductions', 'Provident Fund Deductions'): + for report_name in ("Professional Tax Deductions", "Provident Fund Deductions"): - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='HR User'), - dict(role='HR Manager'), - dict(role='Employee') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="HR User"), dict(role="HR Manager"), dict(role="Employee")], + ) + ).insert() - for report_name in ('HSN-wise-summary of outward supplies', 'GSTR-1', 'GSTR-2'): + for report_name in ("HSN-wise-summary of outward supplies", "GSTR-1", "GSTR-2"): + + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'): - add_permission(doctype, 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + for doctype in ( + "GST HSN Code", + "GST Settings", + "GSTR 3B Report", + "Lower Deduction Certificate", + "E Invoice Settings", + ): + add_permission(doctype, "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - if doctype == 'GST HSN Code': - for role in ('Item Manager', 'Stock Manager'): + if doctype == "GST HSN Code": + for role in ("Item Manager", "Stock Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "gst_tax_invoice") @@ -126,613 +139,1117 @@ def add_print_formats(): frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) + def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters - journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] - sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") - purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") + journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + [ + "Reversal Of ITC" + ] + sales_invoice_series = ["SINV-.YY.-", "SRET-.YY.-", ""] + frappe.get_meta( + "Sales Invoice" + ).get_options("naming_series").split("\n") + purchase_invoice_series = ["PINV-.YY.-", "PRET-.YY.-", ""] + frappe.get_meta( + "Purchase Invoice" + ).get_options("naming_series").split("\n") if not patch: - make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '') - make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '') - make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '') + make_property_setter( + "Sales Invoice", "naming_series", "options", "\n".join(sales_invoice_series), "" + ) + make_property_setter( + "Purchase Invoice", "naming_series", "options", "\n".join(purchase_invoice_series), "" + ) + make_property_setter( + "Journal Entry", "voucher_type", "options", "\n".join(journal_entry_types), "" + ) + def make_custom_fields(update=True): custom_fields = get_custom_fields() create_custom_fields(custom_fields, update=update) + def get_custom_fields(): - hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description', - allow_on_submit=1, print_hide=1, fetch_if_empty=1) - nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code', - print_hide=1) - is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', - fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt', - print_hide=1) - taxable_value = dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) + hsn_sac_field = dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Data", + fetch_from="item_code.gst_hsn_code", + insert_after="description", + allow_on_submit=1, + print_hide=1, + fetch_if_empty=1, + ) + nil_rated_exempt = dict( + fieldname="is_nil_exempt", + label="Is Nil Rated or Exempted", + fieldtype="Check", + fetch_from="item_code.is_nil_exempt", + insert_after="gst_hsn_code", + print_hide=1, + ) + is_non_gst = dict( + fieldname="is_non_gst", + label="Is Non GST", + fieldtype="Check", + fetch_from="item_code.is_non_gst", + insert_after="is_nil_exempt", + print_hide=1, + ) + taxable_value = dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) purchase_invoice_gst_category = [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', - insert_after='language', print_hide=1, collapsible=1), - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_section', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - fetch_from='supplier.gst_category', fetch_if_empty=1), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='gst_category', print_hide=1, + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="language", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_section", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders", + fetch_from="supplier.gst_category", + fetch_if_empty=1, + ), + dict( + fieldname="export_type", + label="Export Type", + fieldtype="Select", + insert_after="gst_category", + print_hide=1, depends_on='eval:in_list(["SEZ", "Overseas"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='supplier.export_type', - fetch_if_empty=1), + options="\nWith Payment of Tax\nWithout Payment of Tax", + fetch_from="supplier.export_type", + fetch_if_empty=1, + ), ] sales_invoice_gst_category = [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', - insert_after='language', print_hide=1, collapsible=1), - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_section', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1, length=25), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='gst_category', print_hide=1, + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="language", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_section", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + fetch_from="customer.gst_category", + fetch_if_empty=1, + length=25, + ), + dict( + fieldname="export_type", + label="Export Type", + fieldtype="Select", + insert_after="gst_category", + print_hide=1, depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='customer.export_type', - fetch_if_empty=1, length=25), + options="\nWith Payment of Tax\nWithout Payment of Tax", + fetch_from="customer.export_type", + fetch_if_empty=1, + length=25, + ), ] delivery_note_gst_category = [ - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_vehicle_type", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + fetch_from="customer.gst_category", + fetch_if_empty=1, + ), ] invoice_gst_fields = [ - dict(fieldname='invoice_copy', label='Invoice Copy', length=30, - fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, - options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'), - dict(fieldname='reverse_charge', label='Reverse Charge', length=2, - fieldtype='Select', insert_after='invoice_copy', print_hide=1, - options='Y\nN', default='N'), - dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', length=15, - fieldtype='Data', insert_after='export_type', print_hide=1), - dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='ecommerce_gstin'), - dict(fieldname='reason_for_issuing_document', label='Reason For Issuing document', - fieldtype='Select', insert_after='gst_col_break', print_hide=1, - depends_on='eval:doc.is_return==1', length=45, - options='\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others') + dict( + fieldname="invoice_copy", + label="Invoice Copy", + length=30, + fieldtype="Select", + insert_after="export_type", + print_hide=1, + allow_on_submit=1, + options="Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier", + ), + dict( + fieldname="reverse_charge", + label="Reverse Charge", + length=2, + fieldtype="Select", + insert_after="invoice_copy", + print_hide=1, + options="Y\nN", + default="N", + ), + dict( + fieldname="ecommerce_gstin", + label="E-commerce GSTIN", + length=15, + fieldtype="Data", + insert_after="export_type", + print_hide=1, + ), + dict(fieldname="gst_col_break", fieldtype="Column Break", insert_after="ecommerce_gstin"), + dict( + fieldname="reason_for_issuing_document", + label="Reason For Issuing document", + fieldtype="Select", + insert_after="gst_col_break", + print_hide=1, + depends_on="eval:doc.is_return==1", + length=45, + options="\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others", + ), ] purchase_invoice_gst_fields = [ - dict(fieldname='supplier_gstin', label='Supplier GSTIN', - fieldtype='Data', insert_after='supplier_address', - fetch_from='supplier_address.gstin', print_hide=1, read_only=1), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='shipping_address_display', - fetch_from='shipping_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='shipping_address', - print_hide=1, read_only=1), - ] + dict( + fieldname="supplier_gstin", + label="Supplier GSTIN", + fieldtype="Data", + insert_after="supplier_address", + fetch_from="supplier_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="shipping_address_display", + fetch_from="shipping_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="shipping_address", + print_hide=1, + read_only=1, + ), + ] purchase_invoice_itc_fields = [ - dict(fieldname='eligibility_for_itc', label='Eligibility For ITC', - fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1, - options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC', - default="All Other ITC"), - dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax', - fieldtype='Currency', insert_after='eligibility_for_itc', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_central_tax', label='Availed ITC Central Tax', - fieldtype='Currency', insert_after='itc_integrated_tax', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax', - fieldtype='Currency', insert_after='itc_central_tax', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_cess_amount', label='Availed ITC Cess', - fieldtype='Currency', insert_after='itc_state_tax', - options='Company:company:default_currency', print_hide=1), - ] + dict( + fieldname="eligibility_for_itc", + label="Eligibility For ITC", + fieldtype="Select", + insert_after="reason_for_issuing_document", + print_hide=1, + options="Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC", + default="All Other ITC", + ), + dict( + fieldname="itc_integrated_tax", + label="Availed ITC Integrated Tax", + fieldtype="Currency", + insert_after="eligibility_for_itc", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_central_tax", + label="Availed ITC Central Tax", + fieldtype="Currency", + insert_after="itc_integrated_tax", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_state_tax", + label="Availed ITC State/UT Tax", + fieldtype="Currency", + insert_after="itc_central_tax", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_cess_amount", + label="Availed ITC Cess", + fieldtype="Currency", + insert_after="itc_state_tax", + options="Company:company:default_currency", + print_hide=1, + ), + ] sales_invoice_gst_fields = [ - dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', read_only=1, - fetch_from='customer_address.gstin', print_hide=1, length=15), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='shipping_address_name', - fetch_from='shipping_address_name.gstin', print_hide=1, length=15), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=1, length=50), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1, length=15), - ] + dict( + fieldname="billing_address_gstin", + label="Billing Address GSTIN", + fieldtype="Data", + insert_after="customer_address", + read_only=1, + fetch_from="customer_address.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="shipping_address_name", + fetch_from="shipping_address_name.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="customer_gstin", + print_hide=1, + read_only=1, + length=50, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + length=15, + ), + ] sales_invoice_shipping_fields = [ - dict(fieldname='port_code', label='Port Code', - fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' ", length=15), - dict(fieldname='shipping_bill_number', label=' Shipping Bill Number', - fieldtype='Data', insert_after='port_code', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' ", length=50), - dict(fieldname='shipping_bill_date', label='Shipping Bill Date', - fieldtype='Date', insert_after='shipping_bill_number', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' "), - ] + dict( + fieldname="port_code", + label="Port Code", + fieldtype="Data", + insert_after="reason_for_issuing_document", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + length=15, + ), + dict( + fieldname="shipping_bill_number", + label=" Shipping Bill Number", + fieldtype="Data", + insert_after="port_code", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + length=50, + ), + dict( + fieldname="shipping_bill_date", + label="Shipping Bill Date", + fieldtype="Date", + insert_after="shipping_bill_number", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + ), + ] journal_entry_fields = [ - dict(fieldname='reversal_type', label='Reversal Type', - fieldtype='Select', insert_after='voucher_type', print_hide=1, + dict( + fieldname="reversal_type", + label="Reversal Type", + fieldtype="Select", + insert_after="voucher_type", + print_hide=1, options="As per rules 42 & 43 of CGST Rules\nOthers", depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_address', label='Company Address', - fieldtype='Link', options='Address', insert_after='reversal_type', - print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1, - fetch_from='company_address.gstin', + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_address", + label="Company Address", + fieldtype="Link", + options="Address", + insert_after="reversal_type", + print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'") + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + read_only=1, + insert_after="company_address", + print_hide=1, + fetch_from="company_address.gstin", + depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), ] inter_state_gst_field = [ - dict(fieldname='is_inter_state', label='Is Inter State', - fieldtype='Check', insert_after='disabled', print_hide=1), - dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check', - insert_after='is_inter_state', print_hide=1), - dict(fieldname='tax_category_column_break', fieldtype='Column Break', - insert_after='is_reverse_charge'), - dict(fieldname='gst_state', label='Source State', fieldtype='Select', - options='\n'.join(states), insert_after='company') + dict( + fieldname="is_inter_state", + label="Is Inter State", + fieldtype="Check", + insert_after="disabled", + print_hide=1, + ), + dict( + fieldname="is_reverse_charge", + label="Is Reverse Charge", + fieldtype="Check", + insert_after="is_inter_state", + print_hide=1, + ), + dict( + fieldname="tax_category_column_break", + fieldtype="Column Break", + insert_after="is_reverse_charge", + ), + dict( + fieldname="gst_state", + label="Source State", + fieldtype="Select", + options="\n".join(states), + insert_after="company", + ), ] ewaybill_fields = [ { - 'fieldname': 'distance', - 'label': 'Distance (in km)', - 'fieldtype': 'Float', - 'insert_after': 'vehicle_no', - 'print_hide': 1 + "fieldname": "distance", + "label": "Distance (in km)", + "fieldtype": "Float", + "insert_after": "vehicle_no", + "print_hide": 1, }, { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'transporter', - 'fetch_from': 'transporter.gst_transporter_id', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "transporter", + "fetch_from": "transporter.gst_transporter_id", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'mode_of_transport', - 'label': 'Mode of Transport', - 'fieldtype': 'Select', - 'options': '\nRoad\nAir\nRail\nShip', - 'default': 'Road', - 'insert_after': 'transporter_name', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "mode_of_transport", + "label": "Mode of Transport", + "fieldtype": "Select", + "options": "\nRoad\nAir\nRail\nShip", + "default": "Road", + "insert_after": "transporter_name", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'gst_vehicle_type', - 'label': 'GST Vehicle Type', - 'fieldtype': 'Select', - 'options': 'Regular\nOver Dimensional Cargo (ODC)', - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', - 'default': 'Regular', - 'insert_after': 'lr_date', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "gst_vehicle_type", + "label": "GST Vehicle Type", + "fieldtype": "Select", + "options": "Regular\nOver Dimensional Cargo (ODC)", + "depends_on": 'eval:(doc.mode_of_transport === "Road")', + "default": "Regular", + "insert_after": "lr_date", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'ewaybill', - 'label': 'E-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', - 'allow_on_submit': 1, - 'insert_after': 'customer_name_in_arabic', - 'translatable': 0, - } + "fieldname": "ewaybill", + "label": "E-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:(doc.docstatus === 1)", + "allow_on_submit": 1, + "insert_after": "customer_name_in_arabic", + "translatable": 0, + }, ] si_ewaybill_fields = [ { - 'fieldname': 'transporter_info', - 'label': 'Transporter Info', - 'fieldtype': 'Section Break', - 'insert_after': 'terms', - 'collapsible': 1, - 'collapsible_depends_on': 'transporter', - 'print_hide': 1 + "fieldname": "transporter_info", + "label": "Transporter Info", + "fieldtype": "Section Break", + "insert_after": "terms", + "collapsible": 1, + "collapsible_depends_on": "transporter", + "print_hide": 1, }, { - 'fieldname': 'transporter', - 'label': 'Transporter', - 'fieldtype': 'Link', - 'insert_after': 'transporter_info', - 'options': 'Supplier', - 'print_hide': 1 + "fieldname": "transporter", + "label": "Transporter", + "fieldtype": "Link", + "insert_after": "transporter_info", + "options": "Supplier", + "print_hide": 1, }, { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'transporter', - 'fetch_from': 'transporter.gst_transporter_id', - 'print_hide': 1, - 'translatable': 0, - 'length': 20 + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "transporter", + "fetch_from": "transporter.gst_transporter_id", + "print_hide": 1, + "translatable": 0, + "length": 20, }, { - 'fieldname': 'driver', - 'label': 'Driver', - 'fieldtype': 'Link', - 'insert_after': 'gst_transporter_id', - 'options': 'Driver', - 'print_hide': 1 + "fieldname": "driver", + "label": "Driver", + "fieldtype": "Link", + "insert_after": "gst_transporter_id", + "options": "Driver", + "print_hide": 1, }, { - 'fieldname': 'lr_no', - 'label': 'Transport Receipt No', - 'fieldtype': 'Data', - 'insert_after': 'driver', - 'print_hide': 1, - 'translatable': 0, - 'length': 30 + "fieldname": "lr_no", + "label": "Transport Receipt No", + "fieldtype": "Data", + "insert_after": "driver", + "print_hide": 1, + "translatable": 0, + "length": 30, }, { - 'fieldname': 'vehicle_no', - 'label': 'Vehicle No', - 'fieldtype': 'Data', - 'insert_after': 'lr_no', - 'print_hide': 1, - 'translatable': 0, - 'length': 10 + "fieldname": "vehicle_no", + "label": "Vehicle No", + "fieldtype": "Data", + "insert_after": "lr_no", + "print_hide": 1, + "translatable": 0, + "length": 10, }, { - 'fieldname': 'distance', - 'label': 'Distance (in km)', - 'fieldtype': 'Float', - 'insert_after': 'vehicle_no', - 'print_hide': 1 + "fieldname": "distance", + "label": "Distance (in km)", + "fieldtype": "Float", + "insert_after": "vehicle_no", + "print_hide": 1, + }, + {"fieldname": "transporter_col_break", "fieldtype": "Column Break", "insert_after": "distance"}, + { + "fieldname": "transporter_name", + "label": "Transporter Name", + "fieldtype": "Small Text", + "insert_after": "transporter_col_break", + "fetch_from": "transporter.name", + "read_only": 1, + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'transporter_col_break', - 'fieldtype': 'Column Break', - 'insert_after': 'distance' + "fieldname": "mode_of_transport", + "label": "Mode of Transport", + "fieldtype": "Select", + "options": "\nRoad\nAir\nRail\nShip", + "insert_after": "transporter_name", + "print_hide": 1, + "translatable": 0, + "length": 5, }, { - 'fieldname': 'transporter_name', - 'label': 'Transporter Name', - 'fieldtype': 'Small Text', - 'insert_after': 'transporter_col_break', - 'fetch_from': 'transporter.name', - 'read_only': 1, - 'print_hide': 1, - 'translatable': 0 + "fieldname": "driver_name", + "label": "Driver Name", + "fieldtype": "Small Text", + "insert_after": "mode_of_transport", + "fetch_from": "driver.full_name", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'mode_of_transport', - 'label': 'Mode of Transport', - 'fieldtype': 'Select', - 'options': '\nRoad\nAir\nRail\nShip', - 'insert_after': 'transporter_name', - 'print_hide': 1, - 'translatable': 0, - 'length': 5 + "fieldname": "lr_date", + "label": "Transport Receipt Date", + "fieldtype": "Date", + "insert_after": "driver_name", + "default": "Today", + "print_hide": 1, }, { - 'fieldname': 'driver_name', - 'label': 'Driver Name', - 'fieldtype': 'Small Text', - 'insert_after': 'mode_of_transport', - 'fetch_from': 'driver.full_name', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "gst_vehicle_type", + "label": "GST Vehicle Type", + "fieldtype": "Select", + "options": "Regular\nOver Dimensional Cargo (ODC)", + "depends_on": 'eval:(doc.mode_of_transport === "Road")', + "default": "Regular", + "insert_after": "lr_date", + "print_hide": 1, + "translatable": 0, + "length": 30, }, { - 'fieldname': 'lr_date', - 'label': 'Transport Receipt Date', - 'fieldtype': 'Date', - 'insert_after': 'driver_name', - 'default': 'Today', - 'print_hide': 1 + "fieldname": "ewaybill", + "label": "E-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)", + "allow_on_submit": 1, + "insert_after": "tax_id", + "translatable": 0, + "length": 20, }, - { - 'fieldname': 'gst_vehicle_type', - 'label': 'GST Vehicle Type', - 'fieldtype': 'Select', - 'options': 'Regular\nOver Dimensional Cargo (ODC)', - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', - 'default': 'Regular', - 'insert_after': 'lr_date', - 'print_hide': 1, - 'translatable': 0, - 'length': 30 - }, - { - 'fieldname': 'ewaybill', - 'label': 'E-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)', - 'allow_on_submit': 1, - 'insert_after': 'tax_id', - 'translatable': 0, - 'length': 20 - } ] si_einvoice_fields = [ - dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, - depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - - dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval: doc.irn', allow_on_submit=1, insert_after='customer'), - - dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, - depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill'), - - dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), - - dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', - print_hide=1, hidden=1), - - dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', - no_copy=1, print_hide=1), - - dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), - - dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', - no_copy=1, print_hide=1), - - dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', - options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', - hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) + dict( + fieldname="irn", + label="IRN", + fieldtype="Data", + read_only=1, + insert_after="customer", + no_copy=1, + print_hide=1, + depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0', + ), + dict( + fieldname="irn_cancelled", + label="IRN Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval: doc.irn", + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="eway_bill_validity", + label="E-Way Bill Validity", + fieldtype="Data", + no_copy=1, + print_hide=1, + depends_on="ewaybill", + read_only=1, + allow_on_submit=1, + insert_after="ewaybill", + ), + dict( + fieldname="eway_bill_cancelled", + label="E-Way Bill Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval:(doc.eway_bill_cancelled === 1)", + read_only=1, + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="einvoice_section", + label="E-Invoice Fields", + fieldtype="Section Break", + insert_after="gst_vehicle_type", + print_hide=1, + hidden=1, + ), + dict( + fieldname="ack_no", + label="Ack. No.", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="einvoice_section", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="ack_date", + label="Ack. Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_no", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="irn_cancel_date", + label="Cancel Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_date", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="signed_einvoice", + label="Signed E-Invoice", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="irn_cancel_date", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="signed_qr_code", + label="Signed QRCode", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="signed_einvoice", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="qrcode_image", + label="QRCode", + fieldtype="Attach Image", + hidden=1, + insert_after="signed_qr_code", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="einvoice_status", + label="E-Invoice Status", + fieldtype="Select", + insert_after="qrcode_image", + options="\nPending\nGenerated\nCancelled\nFailed", + default=None, + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="failure_description", + label="E-Invoice Failure Description", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="einvoice_status", + no_copy=1, + print_hide=1, + read_only=1, + ), ] payment_entry_fields = [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', - print_hide=1, collapsible=1), - dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', - print_hide=1, options='Address'), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='company_gstin', - print_hide=1, read_only=1), - dict(fieldname='gst_column_break', fieldtype='Column Break', - insert_after='place_of_supply'), - dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='gst_column_break', - print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='customer_address', - fetch_from='customer_address.gstin', print_hide=1, read_only=1) + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="deductions", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="company_address", + label="Company Address", + fieldtype="Link", + insert_after="gst_section", + print_hide=1, + options="Address", + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="company_gstin", + print_hide=1, + read_only=1, + ), + dict(fieldname="gst_column_break", fieldtype="Column Break", insert_after="place_of_supply"), + dict( + fieldname="customer_address", + label="Customer Address", + fieldtype="Link", + insert_after="gst_column_break", + print_hide=1, + options="Address", + depends_on='eval:doc.party_type == "Customer"', + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="customer_address", + fetch_from="customer_address.gstin", + print_hide=1, + read_only=1, + ), ] custom_fields = { - 'Address': [ - dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', - insert_after='fax'), - dict(fieldname='gst_state', label='GST State', fieldtype='Select', - options='\n'.join(states), insert_after='gstin'), - dict(fieldname='gst_state_number', label='GST State Number', - fieldtype='Data', insert_after='gst_state', read_only=1), + "Address": [ + dict(fieldname="gstin", label="Party GSTIN", fieldtype="Data", insert_after="fax"), + dict( + fieldname="gst_state", + label="GST State", + fieldtype="Select", + options="\n".join(states), + insert_after="gstin", + ), + dict( + fieldname="gst_state_number", + label="GST State Number", + fieldtype="Data", + insert_after="gst_state", + read_only=1, + ), ], - 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, - 'Purchase Order': purchase_invoice_gst_fields, - 'Purchase Receipt': purchase_invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, - 'POS Invoice': sales_invoice_gst_fields, - 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category, - 'Payment Entry': payment_entry_fields, - 'Journal Entry': journal_entry_fields, - 'Sales Order': sales_invoice_gst_fields, - 'Tax Category': inter_state_gst_field, - 'Item': [ - dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Link', options='GST HSN Code', insert_after='item_group'), - dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', insert_after='gst_hsn_code'), - dict(fieldname='is_non_gst', label='Is Non GST ', - fieldtype='Check', insert_after='is_nil_exempt') + "Purchase Invoice": purchase_invoice_gst_category + + invoice_gst_fields + + purchase_invoice_itc_fields + + purchase_invoice_gst_fields, + "Purchase Order": purchase_invoice_gst_fields, + "Purchase Receipt": purchase_invoice_gst_fields, + "Sales Invoice": sales_invoice_gst_category + + invoice_gst_fields + + sales_invoice_shipping_fields + + sales_invoice_gst_fields + + si_ewaybill_fields + + si_einvoice_fields, + "POS Invoice": sales_invoice_gst_fields, + "Delivery Note": sales_invoice_gst_fields + + ewaybill_fields + + sales_invoice_shipping_fields + + delivery_note_gst_category, + "Payment Entry": payment_entry_fields, + "Journal Entry": journal_entry_fields, + "Sales Order": sales_invoice_gst_fields, + "Tax Category": inter_state_gst_field, + "Quotation": sales_invoice_gst_fields, + "Item": [ + dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Link", + options="GST HSN Code", + insert_after="item_group", + ), + dict( + fieldname="is_nil_exempt", + label="Is Nil Rated or Exempted", + fieldtype="Check", + insert_after="gst_hsn_code", + ), + dict( + fieldname="is_non_gst", label="Is Non GST ", fieldtype="Check", insert_after="is_nil_exempt" + ), ], - 'Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'Material Request Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Salary Component': [ - dict(fieldname= 'component_type', - label= 'Component Type', - fieldtype= 'Select', - insert_after= 'description', - options= "\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax", - depends_on = 'eval:doc.type == "Deduction"' + "Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Supplier Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Sales Order Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Delivery Note Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Sales Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "POS Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "Purchase Order Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Purchase Receipt Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Purchase Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "Material Request Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Salary Component": [ + dict( + fieldname="component_type", + label="Component Type", + fieldtype="Select", + insert_after="description", + options="\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax", + depends_on='eval:doc.type == "Deduction"', ) ], - 'Employee': [ - dict(fieldname='ifsc_code', - label='IFSC Code', - fieldtype='Data', - insert_after='bank_ac_no', + "Employee": [ + dict( + fieldname="ifsc_code", + label="IFSC Code", + fieldtype="Data", + insert_after="bank_ac_no", print_hide=1, - depends_on='eval:doc.salary_mode == "Bank"' - ), - dict( - fieldname = 'pan_number', - label = 'PAN Number', - fieldtype = 'Data', - insert_after = 'payroll_cost_center', - print_hide = 1 + depends_on='eval:doc.salary_mode == "Bank"', ), dict( - fieldname = 'micr_code', - label = 'MICR Code', - fieldtype = 'Data', - insert_after = 'ifsc_code', - print_hide = 1, - depends_on='eval:doc.salary_mode == "Bank"' + fieldname="pan_number", + label="PAN Number", + fieldtype="Data", + insert_after="payroll_cost_center", + print_hide=1, ), dict( - fieldname = 'provident_fund_account', - label = 'Provident Fund Account', - fieldtype = 'Data', - insert_after = 'pan_number' - ) - + fieldname="micr_code", + label="MICR Code", + fieldtype="Data", + insert_after="ifsc_code", + print_hide=1, + depends_on='eval:doc.salary_mode == "Bank"', + ), + dict( + fieldname="provident_fund_account", + label="Provident Fund Account", + fieldtype="Data", + insert_after="pan_number", + ), ], - 'Company': [ - dict(fieldname='hra_section', label='HRA Settings', - fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), - dict(fieldname='basic_component', label='Basic Component', - fieldtype='Link', options='Salary Component', insert_after='hra_section'), - dict(fieldname='hra_component', label='HRA Component', - fieldtype='Link', options='Salary Component', insert_after='basic_component'), - dict(fieldname='arrear_component', label='Arrear Component', - fieldtype='Link', options='Salary Component', insert_after='hra_component'), - dict(fieldname='non_profit_section', label='Non Profit Settings', - fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), - dict(fieldname='company_80g_number', label='80G Number', - fieldtype='Data', insert_after='non_profit_section'), - dict(fieldname='with_effect_from', label='80G With Effect From', - fieldtype='Date', insert_after='company_80g_number'), - dict(fieldname='pan_details', label='PAN Number', - fieldtype='Data', insert_after='with_effect_from') + "Company": [ + dict( + fieldname="hra_section", + label="HRA Settings", + fieldtype="Section Break", + insert_after="asset_received_but_not_billed", + collapsible=1, + ), + dict( + fieldname="basic_component", + label="Basic Component", + fieldtype="Link", + options="Salary Component", + insert_after="hra_section", + ), + dict( + fieldname="hra_component", + label="HRA Component", + fieldtype="Link", + options="Salary Component", + insert_after="basic_component", + ), + dict( + fieldname="arrear_component", + label="Arrear Component", + fieldtype="Link", + options="Salary Component", + insert_after="hra_component", + ), + dict( + fieldname="non_profit_section", + label="Non Profit Settings", + fieldtype="Section Break", + insert_after="asset_received_but_not_billed", + collapsible=1, + ), + dict( + fieldname="company_80g_number", + label="80G Number", + fieldtype="Data", + insert_after="non_profit_section", + ), + dict( + fieldname="with_effect_from", + label="80G With Effect From", + fieldtype="Date", + insert_after="company_80g_number", + ), + dict( + fieldname="pan_details", label="PAN Number", fieldtype="Data", insert_after="with_effect_from" + ), ], - 'Employee Tax Exemption Declaration':[ - dict(fieldname='hra_section', label='HRA Exemption', - fieldtype='Section Break', insert_after='declarations'), - dict(fieldname='monthly_house_rent', label='Monthly House Rent', - fieldtype='Currency', insert_after='hra_section'), - dict(fieldname='rented_in_metro_city', label='Rented in Metro City', - fieldtype='Check', insert_after='monthly_house_rent', depends_on='monthly_house_rent'), - dict(fieldname='salary_structure_hra', label='HRA as per Salary Structure', - fieldtype='Currency', insert_after='rented_in_metro_city', read_only=1, depends_on='monthly_house_rent'), - dict(fieldname='hra_column_break', fieldtype='Column Break', - insert_after='salary_structure_hra', depends_on='monthly_house_rent'), - dict(fieldname='annual_hra_exemption', label='Annual HRA Exemption', - fieldtype='Currency', insert_after='hra_column_break', read_only=1, depends_on='monthly_house_rent'), - dict(fieldname='monthly_hra_exemption', label='Monthly HRA Exemption', - fieldtype='Currency', insert_after='annual_hra_exemption', read_only=1, depends_on='monthly_house_rent') + "Employee Tax Exemption Declaration": [ + dict( + fieldname="hra_section", + label="HRA Exemption", + fieldtype="Section Break", + insert_after="declarations", + ), + dict( + fieldname="monthly_house_rent", + label="Monthly House Rent", + fieldtype="Currency", + insert_after="hra_section", + ), + dict( + fieldname="rented_in_metro_city", + label="Rented in Metro City", + fieldtype="Check", + insert_after="monthly_house_rent", + depends_on="monthly_house_rent", + ), + dict( + fieldname="salary_structure_hra", + label="HRA as per Salary Structure", + fieldtype="Currency", + insert_after="rented_in_metro_city", + read_only=1, + depends_on="monthly_house_rent", + ), + dict( + fieldname="hra_column_break", + fieldtype="Column Break", + insert_after="salary_structure_hra", + depends_on="monthly_house_rent", + ), + dict( + fieldname="annual_hra_exemption", + label="Annual HRA Exemption", + fieldtype="Currency", + insert_after="hra_column_break", + read_only=1, + depends_on="monthly_house_rent", + ), + dict( + fieldname="monthly_hra_exemption", + label="Monthly HRA Exemption", + fieldtype="Currency", + insert_after="annual_hra_exemption", + read_only=1, + depends_on="monthly_house_rent", + ), ], - 'Employee Tax Exemption Proof Submission': [ - dict(fieldname='hra_section', label='HRA Exemption', - fieldtype='Section Break', insert_after='tax_exemption_proofs'), - dict(fieldname='house_rent_payment_amount', label='House Rent Payment Amount', - fieldtype='Currency', insert_after='hra_section'), - dict(fieldname='rented_in_metro_city', label='Rented in Metro City', - fieldtype='Check', insert_after='house_rent_payment_amount', depends_on='house_rent_payment_amount'), - dict(fieldname='rented_from_date', label='Rented From Date', - fieldtype='Date', insert_after='rented_in_metro_city', depends_on='house_rent_payment_amount'), - dict(fieldname='rented_to_date', label='Rented To Date', - fieldtype='Date', insert_after='rented_from_date', depends_on='house_rent_payment_amount'), - dict(fieldname='hra_column_break', fieldtype='Column Break', - insert_after='rented_to_date', depends_on='house_rent_payment_amount'), - dict(fieldname='monthly_house_rent', label='Monthly House Rent', - fieldtype='Currency', insert_after='hra_column_break', read_only=1, depends_on='house_rent_payment_amount'), - dict(fieldname='monthly_hra_exemption', label='Monthly Eligible Amount', - fieldtype='Currency', insert_after='monthly_house_rent', read_only=1, depends_on='house_rent_payment_amount'), - dict(fieldname='total_eligible_hra_exemption', label='Total Eligible HRA Exemption', - fieldtype='Currency', insert_after='monthly_hra_exemption', read_only=1, depends_on='house_rent_payment_amount') + "Employee Tax Exemption Proof Submission": [ + dict( + fieldname="hra_section", + label="HRA Exemption", + fieldtype="Section Break", + insert_after="tax_exemption_proofs", + ), + dict( + fieldname="house_rent_payment_amount", + label="House Rent Payment Amount", + fieldtype="Currency", + insert_after="hra_section", + ), + dict( + fieldname="rented_in_metro_city", + label="Rented in Metro City", + fieldtype="Check", + insert_after="house_rent_payment_amount", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="rented_from_date", + label="Rented From Date", + fieldtype="Date", + insert_after="rented_in_metro_city", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="rented_to_date", + label="Rented To Date", + fieldtype="Date", + insert_after="rented_from_date", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="hra_column_break", + fieldtype="Column Break", + insert_after="rented_to_date", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="monthly_house_rent", + label="Monthly House Rent", + fieldtype="Currency", + insert_after="hra_column_break", + read_only=1, + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="monthly_hra_exemption", + label="Monthly Eligible Amount", + fieldtype="Currency", + insert_after="monthly_house_rent", + read_only=1, + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="total_eligible_hra_exemption", + label="Total Eligible HRA Exemption", + fieldtype="Currency", + insert_after="monthly_hra_exemption", + read_only=1, + depends_on="house_rent_payment_amount", + ), ], - 'Supplier': [ + "Supplier": [ { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'supplier_type', - 'depends_on': 'eval:doc.is_transporter' + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "supplier_type", + "depends_on": "eval:doc.is_transporter", }, { - 'fieldname': 'gst_category', - 'label': 'GST Category', - 'fieldtype': 'Select', - 'insert_after': 'gst_transporter_id', - 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - 'default': 'Unregistered' + "fieldname": "gst_category", + "label": "GST Category", + "fieldtype": "Select", + "insert_after": "gst_transporter_id", + "options": "Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders", + "default": "Unregistered", }, { - 'fieldname': 'export_type', - 'label': 'Export Type', - 'fieldtype': 'Select', - 'insert_after': 'gst_category', - 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' - } + "fieldname": "export_type", + "label": "Export Type", + "fieldtype": "Select", + "insert_after": "gst_category", + "depends_on": 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + "options": "\nWith Payment of Tax\nWithout Payment of Tax", + "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + }, ], - 'Customer': [ + "Customer": [ { - 'fieldname': 'gst_category', - 'label': 'GST Category', - 'fieldtype': 'Select', - 'insert_after': 'customer_type', - 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - 'default': 'Unregistered' + "fieldname": "gst_category", + "label": "GST Category", + "fieldtype": "Select", + "insert_after": "customer_type", + "options": "Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + "default": "Unregistered", }, { - 'fieldname': 'export_type', - 'label': 'Export Type', - 'fieldtype': 'Select', - 'insert_after': 'gst_category', - 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + "fieldname": "export_type", + "label": "Export Type", + "fieldtype": "Select", + "insert_after": "gst_category", + "depends_on": 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + "options": "\nWith Payment of Tax\nWithout Payment of Tax", + "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + }, + ], + "Member": [ + { + "fieldname": "pan_number", + "label": "PAN Details", + "fieldtype": "Data", + "insert_after": "email_id", } ], - 'Member': [ + "Donor": [ { - 'fieldname': 'pan_number', - 'label': 'PAN Details', - 'fieldtype': 'Data', - 'insert_after': 'email_id' + "fieldname": "pan_number", + "label": "PAN Details", + "fieldtype": "Data", + "insert_after": "email", } ], - 'Donor': [ + "Finance Book": [ { - 'fieldname': 'pan_number', - 'label': 'PAN Details', - 'fieldtype': 'Data', - 'insert_after': 'email' + "fieldname": "for_income_tax", + "label": "For Income Tax", + "fieldtype": "Check", + "insert_after": "finance_book_name", + "description": "If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.", } ], - 'Finance Book': [ - { - 'fieldname': 'for_income_tax', - 'label': 'For Income Tax', - 'fieldtype': 'Check', - 'insert_after': 'finance_book_name', - 'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.' - } - ] } return custom_fields + def make_fixtures(company=None): docs = [] company = company or frappe.db.get_value("Global Defaults", None, "default_company") @@ -753,33 +1270,47 @@ def make_fixtures(company=None): # create records for Tax Withholding Category set_tax_withholding_category(company) + def update_regional_tax_settings(country, company): # Will only add default GST accounts if present - input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] - output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] - rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM'] - gst_settings = frappe.get_single('GST Settings') + input_account_names = ["Input Tax CGST", "Input Tax SGST", "Input Tax IGST"] + output_account_names = ["Output Tax CGST", "Output Tax SGST", "Output Tax IGST"] + rcm_accounts = ["Input Tax CGST RCM", "Input Tax SGST RCM", "Input Tax IGST RCM"] + gst_settings = frappe.get_single("GST Settings") existing_account_list = [] - for account in gst_settings.get('gst_accounts'): - for key in ['cgst_account', 'sgst_account', 'igst_account']: + for account in gst_settings.get("gst_accounts"): + for key in ["cgst_account", "sgst_account", "igst_account"]: existing_account_list.append(account.get(key)) - gst_accounts = frappe._dict(frappe.get_all("Account", - {'company': company, 'account_name': ('in', input_account_names + - output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1)) + gst_accounts = frappe._dict( + frappe.get_all( + "Account", + { + "company": company, + "account_name": ("in", input_account_names + output_account_names + rcm_accounts), + }, + ["account_name", "name"], + as_list=1, + ) + ) - add_accounts_in_gst_settings(company, input_account_names, gst_accounts, - existing_account_list, gst_settings) - add_accounts_in_gst_settings(company, output_account_names, gst_accounts, - existing_account_list, gst_settings) - add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts, - existing_account_list, gst_settings, is_reverse_charge=1) + add_accounts_in_gst_settings( + company, input_account_names, gst_accounts, existing_account_list, gst_settings + ) + add_accounts_in_gst_settings( + company, output_account_names, gst_accounts, existing_account_list, gst_settings + ) + add_accounts_in_gst_settings( + company, rcm_accounts, gst_accounts, existing_account_list, gst_settings, is_reverse_charge=1 + ) gst_settings.save() -def add_accounts_in_gst_settings(company, account_names, gst_accounts, - existing_account_list, gst_settings, is_reverse_charge=0): + +def add_accounts_in_gst_settings( + company, account_names, gst_accounts, existing_account_list, gst_settings, is_reverse_charge=0 +): accounts_not_added = 1 for account in account_names: @@ -792,35 +1323,72 @@ def add_accounts_in_gst_settings(company, account_names, gst_accounts, accounts_not_added = 0 if accounts_not_added: - gst_settings.append('gst_accounts', { - 'company': company, - 'cgst_account': gst_accounts.get(account_names[0]), - 'sgst_account': gst_accounts.get(account_names[1]), - 'igst_account': gst_accounts.get(account_names[2]), - 'is_reverse_charge_account': is_reverse_charge - }) + gst_settings.append( + "gst_accounts", + { + "company": company, + "cgst_account": gst_accounts.get(account_names[0]), + "sgst_account": gst_accounts.get(account_names[1]), + "igst_account": gst_accounts.get(account_names[2]), + "is_reverse_charge_account": is_reverse_charge, + }, + ) + def set_salary_components(docs): - docs.extend([ - {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', - 'description': 'Professional Tax', 'type': 'Deduction', 'exempted_from_income_tax': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', - 'description': 'Provident fund', 'type': 'Deduction', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', - 'description': 'House Rent Allowance', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Basic', - 'description': 'Basic', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Arrear', - 'description': 'Arrear', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', - 'description': 'Leave Encashment', 'type': 'Earning', 'is_tax_applicable': 1} - ]) + docs.extend( + [ + { + "doctype": "Salary Component", + "salary_component": "Professional Tax", + "description": "Professional Tax", + "type": "Deduction", + "exempted_from_income_tax": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Provident Fund", + "description": "Provident fund", + "type": "Deduction", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "House Rent Allowance", + "description": "House Rent Allowance", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Basic", + "description": "Basic", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Arrear", + "description": "Arrear", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Leave Encashment", + "description": "Leave Encashment", + "type": "Earning", + "is_tax_applicable": 1, + }, + ] + ) + def set_tax_withholding_category(company): accounts = [] fiscal_year_details = None abbr = frappe.get_value("Company", company, "abbr") - tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') + tds_account = frappe.get_value("Account", "TDS Payable - {0}".format(abbr), "name") if company and tds_account: accounts = [dict(company=company, account=tds_account)] @@ -847,10 +1415,13 @@ def set_tax_withholding_category(company): if fiscal_year_details: # if fiscal year don't match with any of the already entered data, append rate row - fy_exist = [k for k in doc.get('rates') if k.get('from_date') <= fiscal_year_details[1] \ - and k.get('to_date') >= fiscal_year_details[2]] + fy_exist = [ + k + for k in doc.get("rates") + if k.get("from_date") <= fiscal_year_details[1] and k.get("to_date") >= fiscal_year_details[2] + ] if not fy_exist: - doc.append("rates", d.get('rates')[0]) + doc.append("rates", d.get("rates")[0]) doc.flags.ignore_permissions = True doc.flags.ignore_validate = True @@ -858,164 +1429,451 @@ def set_tax_withholding_category(company): doc.flags.ignore_links = True doc.save() + def set_tds_account(docs, company): - parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company}) + parent_account = frappe.db.get_value( + "Account", filters={"account_name": "Duties and Taxes", "company": company} + ) if parent_account: - docs.extend([ - { - "doctype": "Account", - "account_name": "TDS Payable", - "account_type": "Tax", - "parent_account": parent_account, - "company": company - } - ]) + docs.extend( + [ + { + "doctype": "Account", + "account_name": "TDS Payable", + "account_type": "Tax", + "parent_account": parent_account, + "company": company, + } + ] + ) + def get_tds_details(accounts, fiscal_year_details): # bootstrap default tax withholding sections return [ - dict(name="TDS - 194C - Company", + dict( + name="TDS - 194C - Company", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194C - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194C - Individual", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194C - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194C - No PAN / Invalid PAN", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194D - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194D - Company", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - Company Assessee", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - Company Assessee", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - Individual", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - No PAN / Invalid PAN", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - Company", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - Individual", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - No PAN / Invalid PAN", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - Company", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - Individual", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - No PAN / Invalid PAN", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - Company", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - Individual", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - No PAN / Invalid PAN", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - Company", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - Individual", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - Company", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - Individual", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - No PAN / Invalid PAN", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - Company", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - Individual", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - No PAN / Invalid PAN", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - Company", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - Individual", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - No PAN / Invalid PAN", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 2500, "cumulative_threshold": 0}]) + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), ] + def create_gratuity_rule(): # Standard Indain Gratuity Rule if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"): @@ -1025,16 +1883,16 @@ def create_gratuity_rule(): rule.work_experience_calculation_method = "Round Off Work Experience" rule.minimum_year_for_gratuity = 5 - fraction = 15/26 - rule.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":0, - "fraction_of_applicable_earnings": fraction - }) + fraction = 15 / 26 + rule.append( + "gratuity_rule_slabs", + {"from_year": 0, "to_year": 0, "fraction_of_applicable_earnings": fraction}, + ) rule.flags.ignore_mandatory = True rule.save() + def update_accounts_settings_for_taxes(): - if frappe.db.count('Company') == 1: - frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) + if frappe.db.count("Company") == 1: + frappe.db.set_value("Accounts Settings", None, "add_taxes_from_item_tax_template", 0) diff --git a/erpnext/regional/india/test_utils.py b/erpnext/regional/india/test_utils.py index 61a0e97fe3e..5c248307ec0 100644 --- a/erpnext/regional/india/test_utils.py +++ b/erpnext/regional/india/test_utils.py @@ -1,4 +1,3 @@ - import unittest from unittest.mock import patch @@ -13,14 +12,12 @@ class TestIndiaUtils(unittest.TestCase): mock_get_cached.return_value = "India" # mock country posting_date = "2021-05-01" - invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05", - "SI.2020.0001", "PI2021 - 001"] + invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05", "SI.2020.0001", "PI2021 - 001"] for name in invalid_names: doc = frappe._dict(name=name, posting_date=posting_date) self.assertRaises(frappe.ValidationError, validate_document_name, doc) - valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001", - "2020-PI-0001", "PI2020-0001"] + valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001", "2020-PI-0001", "PI2020-0001"] for name in valid_names: doc = frappe._dict(name=name, posting_date=posting_date) try: diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 2287714a008..45104b09681 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,4 +1,3 @@ - import json import re @@ -14,93 +13,120 @@ from erpnext.hr.utils import get_salary_assignment from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.india import number_state_mapping, state_numbers, states -GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") #alphanumeric and - / -GSTIN_FORMAT = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$") +GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") # alphanumeric and - / +GSTIN_FORMAT = re.compile( + "^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$" +) GSTIN_UIN_FORMAT = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}") PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}") def validate_gstin_for_india(doc, method): - if hasattr(doc, 'gst_state') and doc.gst_state: - doc.gst_state_number = state_numbers[doc.gst_state] - if not hasattr(doc, 'gstin') or not doc.gstin: + if hasattr(doc, "gst_state"): + set_gst_state_and_state_number(doc) + + if not hasattr(doc, "gstin") or not doc.gstin: return gst_category = [] - if hasattr(doc, 'gst_category'): + if hasattr(doc, "gst_category"): if len(doc.links): link_doctype = doc.links[0].get("link_doctype") link_name = doc.links[0].get("link_name") if link_doctype in ["Customer", "Supplier"]: - gst_category = frappe.db.get_value(link_doctype, {'name': link_name}, ['gst_category']) + gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"]) doc.gstin = doc.gstin.upper().strip() - if not doc.gstin or doc.gstin == 'NA': + if not doc.gstin or doc.gstin == "NA": return if len(doc.gstin) != 15: frappe.throw(_("A GSTIN must have 15 characters."), title=_("Invalid GSTIN")) - if gst_category and gst_category == 'UIN Holders': + if gst_category and gst_category == "UIN Holders": if not GSTIN_UIN_FORMAT.match(doc.gstin): - frappe.throw(_("The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"), - title=_("Invalid GSTIN")) + frappe.throw( + _( + "The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers" + ), + title=_("Invalid GSTIN"), + ) else: if not GSTIN_FORMAT.match(doc.gstin): - frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN")) + frappe.throw( + _("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN") + ) validate_gstin_check_digit(doc.gstin) - set_gst_state_and_state_number(doc) if not doc.gst_state: frappe.throw(_("Please enter GST state"), title=_("Invalid State")) if doc.gst_state_number != doc.gstin[:2]: - frappe.throw(_("First 2 digits of GSTIN should match with State number {0}.") - .format(doc.gst_state_number), title=_("Invalid GSTIN")) + frappe.throw( + _("First 2 digits of GSTIN should match with State number {0}.").format(doc.gst_state_number), + title=_("Invalid GSTIN"), + ) + def validate_pan_for_india(doc, method): - if doc.get('country') != 'India' or not doc.pan: + if doc.get("country") != "India" or not doc.pan: return if not PAN_NUMBER_FORMAT.match(doc.pan): frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN.")) + def validate_tax_category(doc, method): - if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state, - 'is_reverse_charge': doc.is_reverse_charge}): + if doc.get("gst_state") and frappe.db.get_value( + "Tax Category", + { + "gst_state": doc.gst_state, + "is_inter_state": doc.is_inter_state, + "is_reverse_charge": doc.is_reverse_charge, + }, + ): if doc.is_inter_state: - frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state)) + frappe.throw( + _("Inter State tax category for GST State {0} already exists").format(doc.gst_state) + ) else: - frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state)) + frappe.throw( + _("Intra State tax category for GST State {0} already exists").format(doc.gst_state) + ) + def update_gst_category(doc, method): for link in doc.links: - if link.link_doctype in ['Customer', 'Supplier']: + if link.link_doctype in ["Customer", "Supplier"]: meta = frappe.get_meta(link.link_doctype) - if doc.get('gstin') and meta.has_field('gst_category'): - frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular') + if doc.get("gstin") and meta.has_field("gst_category"): + frappe.db.set_value( + link.link_doctype, + {"name": link.link_name, "gst_category": "Unregistered"}, + "gst_category", + "Registered Regular", + ) + def set_gst_state_and_state_number(doc): - if not doc.gst_state: - if not doc.state: - return + if not doc.gst_state and doc.state: state = doc.state.lower() - states_lowercase = {s.lower():s for s in states} + states_lowercase = {s.lower(): s for s in states} if state in states_lowercase: doc.gst_state = states_lowercase[state] else: return + doc.gst_state_number = state_numbers.get(doc.gst_state) - doc.gst_state_number = state_numbers[doc.gst_state] -def validate_gstin_check_digit(gstin, label='GSTIN'): - ''' Function to validate the check digit of the GSTIN.''' +def validate_gstin_check_digit(gstin, label="GSTIN"): + """Function to validate the check digit of the GSTIN.""" factor = 1 total = 0 - code_point_chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + code_point_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" mod = len(code_point_chars) input_chars = gstin[:-1] for char in input_chars: @@ -109,24 +135,30 @@ def validate_gstin_check_digit(gstin, label='GSTIN'): total += digit factor = 2 if factor == 1 else 1 if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: - frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label)) + frappe.throw( + _( + """Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""" + ).format(label) + ) + def get_itemised_tax_breakup_header(item_doctype, tax_accounts): - hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup') - if frappe.get_meta(item_doctype).has_field('gst_hsn_code') and hsn_wise_in_gst_settings: + hsn_wise_in_gst_settings = frappe.db.get_single_value("GST Settings", "hsn_wise_tax_breakup") + if frappe.get_meta(item_doctype).has_field("gst_hsn_code") and hsn_wise_in_gst_settings: return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts else: return [_("Item"), _("Taxable Amount")] + tax_accounts + def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): itemised_tax = get_itemised_tax(doc.taxes, with_tax_account=account_wise) itemised_taxable_amount = get_itemised_taxable_amount(doc.items) - if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'): + if not frappe.get_meta(doc.doctype + " Item").has_field("gst_hsn_code"): return itemised_tax, itemised_taxable_amount - hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup') + hsn_wise_in_gst_settings = frappe.db.get_single_value("GST Settings", "hsn_wise_tax_breakup") tax_breakup_hsn_wise = hsn_wise or hsn_wise_in_gst_settings if tax_breakup_hsn_wise: @@ -141,7 +173,7 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): for tax_desc, tax_detail in taxes.items(): key = tax_desc if account_wise: - key = tax_detail.get('tax_account') + key = tax_detail.get("tax_account") hsn_tax[item_or_hsn].setdefault(key, {"tax_rate": 0, "tax_amount": 0}) hsn_tax[item_or_hsn][key]["tax_rate"] = tax_detail.get("tax_rate") hsn_tax[item_or_hsn][key]["tax_amount"] += tax_detail.get("tax_amount") @@ -155,9 +187,11 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): return hsn_tax, hsn_taxable_amount + def set_place_of_supply(doc, method=None): doc.place_of_supply = get_place_of_supply(doc, doc.doctype) + def validate_document_name(doc, method=None): """Validate GST invoice number requirements.""" @@ -168,30 +202,44 @@ def validate_document_name(doc, method=None): return if len(doc.name) > 16: - frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series.")) + frappe.throw( + _( + "Maximum length of document number should be 16 characters as per GST rules. Please change the naming series." + ) + ) if not GST_INVOICE_NUMBER_FORMAT.match(doc.name): - frappe.throw(_("Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series.")) + frappe.throw( + _( + "Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series." + ) + ) + # don't remove this function it is used in tests def test_method(): - '''test function''' - return 'overridden' + """test function""" + return "overridden" + def get_place_of_supply(party_details, doctype): - if not frappe.get_meta('Address').has_field('gst_state'): return + if not frappe.get_meta("Address").has_field("gst_state"): + return - if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): address_name = party_details.customer_address or party_details.shipping_address_name elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): address_name = party_details.shipping_address or party_details.supplier_address if address_name: - address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number", "gstin"], as_dict=1) + address = frappe.db.get_value( + "Address", address_name, ["gst_state", "gst_state_number", "gstin"], as_dict=1 + ) if address and address.gst_state and address.gst_state_number: party_details.gstin = address.gstin return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) + @frappe.whitelist() def get_regional_address_details(party_details, doctype, company): if isinstance(party_details, string_types): @@ -203,28 +251,41 @@ def get_regional_address_details(party_details, doctype, company): party_details.place_of_supply = get_place_of_supply(party_details, doctype) if is_internal_transfer(party_details, doctype): - party_details.taxes_and_charges = '' + party_details.taxes_and_charges = "" party_details.taxes = [] return party_details - if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): master_doctype = "Sales Taxes and Charges Template" - tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category( + master_doctype, company, party_details + ) elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): master_doctype = "Purchase Taxes and Charges Template" - tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category( + master_doctype, company, party_details + ) if tax_template_by_category: - party_details['taxes_and_charges'] = tax_template_by_category + party_details["taxes_and_charges"] = tax_template_by_category + party_details["taxes"] = get_taxes_and_charges(master_doctype, tax_template_by_category) return party_details - if not party_details.place_of_supply: return party_details - if not party_details.company_gstin: return party_details + if not party_details.place_of_supply: + return party_details + if not party_details.company_gstin: + return party_details - if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin - and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", - "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): + if ( + doctype in ("Sales Invoice", "Delivery Note", "Sales Order") + and party_details.company_gstin + and party_details.company_gstin[:2] != party_details.place_of_supply[:2] + ) or ( + doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt") + and party_details.supplier_gstin + and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2] + ): default_tax = get_tax_template(master_doctype, company, 1, party_details.company_gstin[:2]) else: default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) @@ -233,17 +294,25 @@ def get_regional_address_details(party_details, doctype, company): return party_details party_details["taxes_and_charges"] = default_tax - party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + party_details["taxes"] = get_taxes_and_charges(master_doctype, default_tax) return party_details + def update_party_details(party_details, doctype): - for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']: + for address_field in [ + "shipping_address", + "company_address", + "supplier_address", + "shipping_address_name", + "customer_address", + ]: if party_details.get(address_field): party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field))) + def is_internal_transfer(party_details, doctype): - if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): destination_gstin = party_details.company_gstin elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): destination_gstin = party_details.supplier_gstin @@ -256,66 +325,93 @@ def is_internal_transfer(party_details, doctype): else: False + def get_tax_template_based_on_category(master_doctype, company, party_details): - if not party_details.get('tax_category'): + if not party_details.get("tax_category"): return - default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, - 'name') + default_tax = frappe.db.get_value( + master_doctype, {"company": company, "tax_category": party_details.get("tax_category")}, "name" + ) return default_tax + def get_tax_template(master_doctype, company, is_inter_state, state_code): - tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], - filters = {'is_inter_state': is_inter_state, 'is_reverse_charge': 0}) + tax_categories = frappe.get_all( + "Tax Category", + fields=["name", "is_inter_state", "gst_state"], + filters={"is_inter_state": is_inter_state, "is_reverse_charge": 0, "disabled": 0}, + ) - default_tax = '' + default_tax = "" for tax_category in tax_categories: - if tax_category.gst_state == number_state_mapping[state_code] or \ - (not default_tax and not tax_category.gst_state): - default_tax = frappe.db.get_value(master_doctype, - {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') + if tax_category.gst_state == number_state_mapping[state_code] or ( + not default_tax and not tax_category.gst_state + ): + default_tax = frappe.db.get_value( + master_doctype, {"company": company, "disabled": 0, "tax_category": tax_category.name}, "name" + ) return default_tax + def calculate_annual_eligible_hra_exemption(doc): - basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"]) + basic_component, hra_component = frappe.db.get_value( + "Company", doc.company, ["basic_component", "hra_component"] + ) if not (basic_component and hra_component): frappe.throw(_("Please mention Basic and HRA component in Company")) annual_exemption, monthly_exemption, hra_amount = 0, 0, 0 if hra_component and basic_component: assignment = get_salary_assignment(doc.employee, nowdate()) if assignment: - hra_component_exists = frappe.db.exists("Salary Detail", { - "parent": assignment.salary_structure, - "salary_component": hra_component, - "parentfield": "earnings", - "parenttype": "Salary Structure" - }) + hra_component_exists = frappe.db.exists( + "Salary Detail", + { + "parent": assignment.salary_structure, + "salary_component": hra_component, + "parentfield": "earnings", + "parenttype": "Salary Structure", + }, + ) if hra_component_exists: - basic_amount, hra_amount = get_component_amt_from_salary_slip(doc.employee, - assignment.salary_structure, basic_component, hra_component) + basic_amount, hra_amount = get_component_amt_from_salary_slip( + doc.employee, assignment.salary_structure, basic_component, hra_component + ) if hra_amount: if doc.monthly_house_rent: - annual_exemption = calculate_hra_exemption(assignment.salary_structure, - basic_amount, hra_amount, doc.monthly_house_rent, doc.rented_in_metro_city) + annual_exemption = calculate_hra_exemption( + assignment.salary_structure, + basic_amount, + hra_amount, + doc.monthly_house_rent, + doc.rented_in_metro_city, + ) if annual_exemption > 0: monthly_exemption = annual_exemption / 12 else: annual_exemption = 0 elif doc.docstatus == 1: - frappe.throw(_("Salary Structure must be submitted before submission of Tax Ememption Declaration")) + frappe.throw( + _("Salary Structure must be submitted before submission of Tax Ememption Declaration") + ) + + return frappe._dict( + { + "hra_amount": hra_amount, + "annual_exemption": annual_exemption, + "monthly_exemption": monthly_exemption, + } + ) - return frappe._dict({ - "hra_amount": hra_amount, - "annual_exemption": annual_exemption, - "monthly_exemption": monthly_exemption - }) def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component): - salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1, ignore_permissions=True) + salary_slip = make_salary_slip( + salary_structure, employee=employee, for_preview=1, ignore_permissions=True + ) basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: if earning.salary_component == basic_component: @@ -326,7 +422,10 @@ def get_component_amt_from_salary_slip(employee, salary_structure, basic_compone return basic_amt, hra_amt return basic_amt, hra_amt -def calculate_hra_exemption(salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city): + +def calculate_hra_exemption( + salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city +): # TODO make this configurable exemptions = [] frequency = frappe.get_value("Salary Structure", salary_structure, "payroll_frequency") @@ -343,6 +442,7 @@ def calculate_hra_exemption(salary_structure, basic, monthly_hra, monthly_house_ # return minimum of 3 cases return min(exemptions) + def get_annual_component_pay(frequency, amount): if frequency == "Daily": return amount * 365 @@ -355,6 +455,7 @@ def get_annual_component_pay(frequency, amount): elif frequency == "Bimonthly": return amount * 6 + def validate_house_rent_dates(doc): if not doc.rented_to_date or not doc.rented_from_date: frappe.throw(_("House rented dates required for exemption calculation")) @@ -362,30 +463,34 @@ def validate_house_rent_dates(doc): if date_diff(doc.rented_to_date, doc.rented_from_date) < 14: frappe.throw(_("House rented dates should be atleast 15 days apart")) - proofs = frappe.db.sql(""" + proofs = frappe.db.sql( + """ select name from `tabEmployee Tax Exemption Proof Submission` where docstatus=1 and employee=%(employee)s and payroll_period=%(payroll_period)s and (rented_from_date between %(from_date)s and %(to_date)s or rented_to_date between %(from_date)s and %(to_date)s) - """, { - "employee": doc.employee, - "payroll_period": doc.payroll_period, - "from_date": doc.rented_from_date, - "to_date": doc.rented_to_date - }) + """, + { + "employee": doc.employee, + "payroll_period": doc.payroll_period, + "from_date": doc.rented_from_date, + "to_date": doc.rented_to_date, + }, + ) if proofs: frappe.throw(_("House rent paid days overlapping with {0}").format(proofs[0][0])) + def calculate_hra_exemption_for_period(doc): monthly_rent, eligible_hra = 0, 0 if doc.house_rent_payment_amount: validate_house_rent_dates(doc) # TODO receive rented months or validate dates are start and end of months? # Calc monthly rent, round to nearest .5 - factor = flt(date_diff(doc.rented_to_date, doc.rented_from_date) + 1)/30 - factor = round(factor * 2)/2 + factor = flt(date_diff(doc.rented_to_date, doc.rented_from_date) + 1) / 30 + factor = round(factor * 2) / 2 monthly_rent = doc.house_rent_payment_amount / factor # update field used by calculate_annual_eligible_hra_exemption doc.monthly_house_rent = monthly_rent @@ -398,6 +503,7 @@ def calculate_hra_exemption_for_period(doc): exemptions["total_eligible_hra_exemption"] = eligible_hra return exemptions + def get_ewb_data(dt, dn): ewaybills = [] @@ -406,32 +512,38 @@ def get_ewb_data(dt, dn): validate_doc(doc) - data = frappe._dict({ - "transporterId": "", - "TotNonAdvolVal": 0, - }) + data = frappe._dict( + { + "transporterId": "", + "TotNonAdvolVal": 0, + } + ) data.userGstin = data.fromGstin = doc.company_gstin - data.supplyType = 'O' + data.supplyType = "O" - if dt == 'Delivery Note': + if dt == "Delivery Note": data.subSupplyType = 1 - elif doc.gst_category in ['Registered Regular', 'SEZ']: + elif doc.gst_category in ["Registered Regular", "SEZ"]: data.subSupplyType = 1 - elif doc.gst_category in ['Overseas', 'Deemed Export']: + elif doc.gst_category in ["Overseas", "Deemed Export"]: data.subSupplyType = 3 else: - frappe.throw(_('Unsupported GST Category for E-Way Bill JSON generation')) + frappe.throw(_("Unsupported GST Category for E-Way Bill JSON generation")) - data.docType = 'INV' - data.docDate = frappe.utils.formatdate(doc.posting_date, 'dd/mm/yyyy') + data.docType = "INV" + data.docDate = frappe.utils.formatdate(doc.posting_date, "dd/mm/yyyy") - company_address = frappe.get_doc('Address', doc.company_address) - billing_address = frappe.get_doc('Address', doc.customer_address) + company_address = frappe.get_doc("Address", doc.company_address) + billing_address = frappe.get_doc("Address", doc.customer_address) - #added dispatch address - dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address - shipping_address = frappe.get_doc('Address', doc.shipping_address_name) + # added dispatch address + dispatch_address = ( + frappe.get_doc("Address", doc.dispatch_address_name) + if doc.dispatch_address_name + else company_address + ) + shipping_address = frappe.get_doc("Address", doc.shipping_address_name) data = get_address_details(data, doc, company_address, billing_address, dispatch_address) @@ -440,75 +552,78 @@ def get_ewb_data(dt, dn): data = get_item_list(data, doc, hsn_wise=True) - disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') + disable_rounded = frappe.db.get_single_value("Global Defaults", "disable_rounded_total") data.totInvValue = doc.grand_total if disable_rounded else doc.rounded_total data = get_transport_details(data, doc) fields = { "/. -": { - 'docNo': doc.name, - 'fromTrdName': doc.company, - 'toTrdName': doc.customer_name, - 'transDocNo': doc.lr_no, + "docNo": doc.name, + "fromTrdName": doc.company, + "toTrdName": doc.customer_name, + "transDocNo": doc.lr_no, }, "@#/,&. -": { - 'fromAddr1': company_address.address_line1, - 'fromAddr2': company_address.address_line2, - 'fromPlace': company_address.city, - 'toAddr1': shipping_address.address_line1, - 'toAddr2': shipping_address.address_line2, - 'toPlace': shipping_address.city, - 'transporterName': doc.transporter_name - } + "fromAddr1": company_address.address_line1, + "fromAddr2": company_address.address_line2, + "fromPlace": company_address.city, + "toAddr1": shipping_address.address_line1, + "toAddr2": shipping_address.address_line2, + "toPlace": shipping_address.city, + "transporterName": doc.transporter_name, + }, } for allowed_chars, field_map in fields.items(): for key, value in field_map.items(): if not value: - data[key] = '' + data[key] = "" else: - data[key] = re.sub(r'[^\w' + allowed_chars + ']', '', value) + data[key] = re.sub(r"[^\w" + allowed_chars + "]", "", value) ewaybills.append(data) - data = { - 'version': '1.0.0421', - 'billLists': ewaybills - } + data = {"version": "1.0.0421", "billLists": ewaybills} return data + @frappe.whitelist() def generate_ewb_json(dt, dn): dn = json.loads(dn) return get_ewb_data(dt, dn) + @frappe.whitelist() def download_ewb_json(): data = json.loads(frappe.local.form_dict.data) frappe.local.response.filecontent = json.dumps(data, indent=4, sort_keys=True) - frappe.local.response.type = 'download' + frappe.local.response.type = "download" - filename_prefix = 'Bulk' + filename_prefix = "Bulk" docname = frappe.local.form_dict.docname if docname: - if docname.startswith('['): + if docname.startswith("["): docname = json.loads(docname) if len(docname) == 1: docname = docname[0] if not isinstance(docname, list): # removes characters not allowed in a filename (https://stackoverflow.com/a/38766141/4767738) - filename_prefix = re.sub(r'[^\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) + ) - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(filename_prefix, frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): - company_gstins =[] + company_gstins = [] if company: - company_gstins = frappe.db.sql("""select + company_gstins = frappe.db.sql( + """select distinct `tabAddress`.gstin from `tabAddress`, `tabDynamic Link` @@ -516,56 +631,66 @@ def get_gstins_for_company(company): `tabDynamic Link`.parent = `tabAddress`.name and `tabDynamic Link`.parenttype = 'Address' and `tabDynamic Link`.link_doctype = 'Company' and - `tabDynamic Link`.link_name = %(company)s""", {"company": company}) + `tabDynamic Link`.link_name = %(company)s""", + {"company": company}, + ) return company_gstins + def get_address_details(data, doc, company_address, billing_address, dispatch_address): - data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') - data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address') - data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address') + data.fromPincode = validate_pincode(company_address.pincode, "Company Address") + data.fromStateCode = validate_state_code(company_address.gst_state_number, "Company Address") + data.actualFromStateCode = validate_state_code( + dispatch_address.gst_state_number, "Dispatch Address" + ) if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: - data.toGstin = 'URP' + data.toGstin = "URP" set_gst_state_and_state_number(billing_address) else: data.toGstin = doc.billing_address_gstin - data.toPincode = validate_pincode(billing_address.pincode, 'Customer Address') - data.toStateCode = validate_state_code(billing_address.gst_state_number, 'Customer Address') + data.toPincode = validate_pincode(billing_address.pincode, "Customer Address") + data.toStateCode = validate_state_code(billing_address.gst_state_number, "Customer Address") if doc.customer_address != doc.shipping_address_name: data.transType = 2 - shipping_address = frappe.get_doc('Address', doc.shipping_address_name) + shipping_address = frappe.get_doc("Address", doc.shipping_address_name) set_gst_state_and_state_number(shipping_address) - data.toPincode = validate_pincode(shipping_address.pincode, 'Shipping Address') - data.actualToStateCode = validate_state_code(shipping_address.gst_state_number, 'Shipping Address') + data.toPincode = validate_pincode(shipping_address.pincode, "Shipping Address") + data.actualToStateCode = validate_state_code( + shipping_address.gst_state_number, "Shipping Address" + ) else: data.transType = 1 data.actualToStateCode = data.toStateCode shipping_address = billing_address - if doc.gst_category == 'SEZ': + if doc.gst_category == "SEZ": data.toStateCode = 99 return data + def get_item_list(data, doc, hsn_wise=False): - for attr in ['cgstValue', 'sgstValue', 'igstValue', 'cessValue', 'OthValue']: + for attr in ["cgstValue", "sgstValue", "igstValue", "cessValue", "OthValue"]: data[attr] = 0 gst_accounts = get_gst_accounts(doc.company, account_wise=True) tax_map = { - 'sgst_account': ['sgstRate', 'sgstValue'], - 'cgst_account': ['cgstRate', 'cgstValue'], - 'igst_account': ['igstRate', 'igstValue'], - 'cess_account': ['cessRate', 'cessValue'] + "sgst_account": ["sgstRate", "sgstValue"], + "cgst_account": ["cgstRate", "cgstValue"], + "igst_account": ["igstRate", "igstValue"], + "cess_account": ["cessRate", "cessValue"], } - item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol'] - hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise) + item_data_attrs = ["sgstRate", "cgstRate", "igstRate", "cessRate", "cessNonAdvol"] + hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data( + doc, account_wise=True, hsn_wise=hsn_wise + ) for item_or_hsn, taxable_amount in hsn_taxable_amount.items(): item_data = frappe._dict() if not item_or_hsn: - frappe.throw(_('GST HSN Code does not exist for one or more items')) + frappe.throw(_("GST HSN Code does not exist for one or more items")) item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn item_data.taxableAmount = taxable_amount item_data.qtyUnit = "" @@ -573,87 +698,89 @@ def get_item_list(data, doc, hsn_wise=False): item_data[attr] = 0 for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items(): - account_type = gst_accounts.get(account, '') + account_type = gst_accounts.get(account, "") for tax_acc, attrs in tax_map.items(): if account_type == tax_acc: - item_data[attrs[0]] = tax_detail.get('tax_rate') - data[attrs[1]] += tax_detail.get('tax_amount') + item_data[attrs[0]] = tax_detail.get("tax_rate") + data[attrs[1]] += tax_detail.get("tax_amount") break else: - data.OthValue += tax_detail.get('tax_amount') + data.OthValue += tax_detail.get("tax_amount") data.itemList.append(item_data) # Tax amounts rounded to 2 decimals to avoid exceeding max character limit - for attr in ['sgstValue', 'cgstValue', 'igstValue', 'cessValue']: + for attr in ["sgstValue", "cgstValue", "igstValue", "cessValue"]: data[attr] = flt(data[attr], 2) return data + def validate_doc(doc): if doc.docstatus != 1: - frappe.throw(_('E-Way Bill JSON can only be generated from submitted document')) + frappe.throw(_("E-Way Bill JSON can only be generated from submitted document")) if doc.is_return: - frappe.throw(_('E-Way Bill JSON cannot be generated for Sales Return as of now')) + frappe.throw(_("E-Way Bill JSON cannot be generated for Sales Return as of now")) if doc.ewaybill: - frappe.throw(_('e-Way Bill already exists for this document')) + frappe.throw(_("e-Way Bill already exists for this document")) - reqd_fields = ['company_gstin', 'company_address', 'customer_address', - 'shipping_address_name', 'mode_of_transport', 'distance'] + reqd_fields = [ + "company_gstin", + "company_address", + "customer_address", + "shipping_address_name", + "mode_of_transport", + "distance", + ] for fieldname in reqd_fields: if not doc.get(fieldname): - frappe.throw(_('{} is required to generate E-Way Bill JSON').format( - doc.meta.get_label(fieldname) - )) + frappe.throw( + _("{} is required to generate E-Way Bill JSON").format(doc.meta.get_label(fieldname)) + ) if len(doc.company_gstin) < 15: - frappe.throw(_('You must be a registered supplier to generate e-Way Bill')) + frappe.throw(_("You must be a registered supplier to generate e-Way Bill")) + def get_transport_details(data, doc): if doc.distance > 4000: - frappe.throw(_('Distance cannot be greater than 4000 kms')) + frappe.throw(_("Distance cannot be greater than 4000 kms")) data.transDistance = int(round(doc.distance)) - transport_modes = { - 'Road': 1, - 'Rail': 2, - 'Air': 3, - 'Ship': 4 - } + transport_modes = {"Road": 1, "Rail": 2, "Air": 3, "Ship": 4} - vehicle_types = { - 'Regular': 'R', - 'Over Dimensional Cargo (ODC)': 'O' - } + vehicle_types = {"Regular": "R", "Over Dimensional Cargo (ODC)": "O"} data.transMode = transport_modes.get(doc.mode_of_transport) - if doc.mode_of_transport == 'Road': + if doc.mode_of_transport == "Road": if not doc.gst_transporter_id and not doc.vehicle_no: - frappe.throw(_('Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road')) + frappe.throw( + _("Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road") + ) if doc.vehicle_no: - data.vehicleNo = doc.vehicle_no.replace(' ', '') + data.vehicleNo = doc.vehicle_no.replace(" ", "") if not doc.gst_vehicle_type: - frappe.throw(_('Vehicle Type is required if Mode of Transport is Road')) + frappe.throw(_("Vehicle Type is required if Mode of Transport is Road")) else: data.vehicleType = vehicle_types.get(doc.gst_vehicle_type) else: if not doc.lr_no or not doc.lr_date: - frappe.throw(_('Transport Receipt No and Date are mandatory for your chosen Mode of Transport')) + frappe.throw(_("Transport Receipt No and Date are mandatory for your chosen Mode of Transport")) if doc.lr_no: data.transDocNo = doc.lr_no if doc.lr_date: - data.transDocDate = frappe.utils.formatdate(doc.lr_date, 'dd/mm/yyyy') + data.transDocDate = frappe.utils.formatdate(doc.lr_date, "dd/mm/yyyy") if doc.gst_transporter_id: if doc.gst_transporter_id[0:2] != "88": - validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID') + validate_gstin_check_digit(doc.gst_transporter_id, label="GST Transporter ID") data.transporterId = doc.gst_transporter_id return data @@ -666,12 +793,13 @@ def validate_pincode(pincode, address): if not pincode: frappe.throw(_(pin_not_found.format(address))) - pincode = pincode.replace(' ', '') + pincode = pincode.replace(" ", "") if not pincode.isdigit() or len(pincode) != 6: frappe.throw(_(incorrect_pin.format(address))) else: return int(pincode) + def validate_state_code(state_code, address): no_state_code = "GST State Code not found for {0}. Please set GST State in {0}" if not state_code: @@ -679,21 +807,26 @@ def validate_state_code(state_code, address): else: return int(state_code) + @frappe.whitelist() -def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0): - filters={"parent": "GST Settings"} +def get_gst_accounts( + company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0 +): + filters = {"parent": "GST Settings"} if company: - filters.update({'company': company}) + filters.update({"company": company}) if only_reverse_charge: - filters.update({'is_reverse_charge_account': 1}) + filters.update({"is_reverse_charge_account": 1}) elif only_non_reverse_charge: - filters.update({'is_reverse_charge_account': 0}) + filters.update({"is_reverse_charge_account": 0}) gst_accounts = frappe._dict() - gst_settings_accounts = frappe.get_all("GST Account", + gst_settings_accounts = frappe.get_all( + "GST Account", filters=filters, - fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) + fields=["cgst_account", "sgst_account", "igst_account", "cess_account", "utgst_account"], + ) if not gst_settings_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: frappe.throw(_("Please set GST Accounts in GST Settings")) @@ -707,32 +840,39 @@ def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, on return gst_accounts -def validate_reverse_charge_transaction(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def validate_reverse_charge_transaction(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return base_gst_tax = 0 base_reverse_charge_booked = 0 - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": gst_accounts = get_gst_accounts(doc.company, only_reverse_charge=1) - reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ - + gst_accounts.get('igst_account') + reverse_charge_accounts = ( + gst_accounts.get("cgst_account") + + gst_accounts.get("sgst_account") + + gst_accounts.get("igst_account") + ) gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1) - non_reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ - + gst_accounts.get('igst_account') + non_reverse_charge_accounts = ( + gst_accounts.get("cgst_account") + + gst_accounts.get("sgst_account") + + gst_accounts.get("igst_account") + ) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.account_head in non_reverse_charge_accounts: - if tax.add_deduct_tax == 'Add': + if tax.add_deduct_tax == "Add": base_gst_tax += tax.base_tax_amount_after_discount_amount else: base_gst_tax += tax.base_tax_amount_after_discount_amount elif tax.account_head in reverse_charge_accounts: - if tax.add_deduct_tax == 'Add': + if tax.add_deduct_tax == "Add": base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount else: base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount @@ -740,57 +880,65 @@ def validate_reverse_charge_transaction(doc, method): if base_gst_tax != base_reverse_charge_booked: msg = _("Booked reverse charge is not equal to applied tax amount") msg += "
    " - msg += _("Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice").format( - gst_document_link='GST Documentation') + msg += _( + "Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice" + ).format( + gst_document_link='GST Documentation' + ) frappe.throw(msg) -def update_itc_availed_fields(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def update_itc_availed_fields(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return # Initialize values doc.itc_integrated_tax = doc.itc_state_tax = doc.itc_central_tax = doc.itc_cess_amount = 0 gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1) - for tax in doc.get('taxes'): - if tax.account_head in gst_accounts.get('igst_account', []): + for tax in doc.get("taxes"): + if tax.account_head in gst_accounts.get("igst_account", []): doc.itc_integrated_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('sgst_account', []): + if tax.account_head in gst_accounts.get("sgst_account", []): doc.itc_state_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('cgst_account', []): + if tax.account_head in gst_accounts.get("cgst_account", []): doc.itc_central_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('cess_account', []): + if tax.account_head in gst_accounts.get("cess_account", []): doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount) + def update_place_of_supply(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': + country = frappe.get_cached_value("Company", doc.company, "country") + if country != "India": return - address = frappe.db.get_value("Address", doc.get('customer_address'), ["gst_state", "gst_state_number"], as_dict=1) + address = frappe.db.get_value( + "Address", doc.get("customer_address"), ["gst_state", "gst_state_number"], as_dict=1 + ) if address and address.gst_state and address.gst_state_number: doc.place_of_supply = cstr(address.gst_state_number) + "-" + cstr(address.gst_state) + @frappe.whitelist() def get_regional_round_off_accounts(company, account_list): - country = frappe.get_cached_value('Company', company, 'country') + country = frappe.get_cached_value("Company", company, "country") - if country != 'India': + if country != "India": return if isinstance(account_list, string_types): account_list = json.loads(account_list) - if not frappe.db.get_single_value('GST Settings', 'round_off_gst_values'): + if not frappe.db.get_single_value("GST Settings", "round_off_gst_values"): return gst_accounts = get_gst_accounts(company) gst_account_list = [] - for account in ['cgst_account', 'sgst_account', 'igst_account']: + for account in ["cgst_account", "sgst_account", "igst_account"]: if account in gst_accounts: gst_account_list += gst_accounts.get(account) @@ -798,94 +946,113 @@ def get_regional_round_off_accounts(company, account_list): return account_list -def update_taxable_values(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def update_taxable_values(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return gst_accounts = get_gst_accounts(doc.company) # Only considering sgst account to avoid inflating taxable value - gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \ - + gst_accounts.get('igst_account', []) + gst_account_list = ( + gst_accounts.get("sgst_account", []) + + gst_accounts.get("sgst_account", []) + + gst_accounts.get("igst_account", []) + ) additional_taxes = 0 total_charges = 0 item_count = 0 considered_rows = [] - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): prev_row_id = cint(tax.row_id) - 1 if tax.account_head in gst_account_list and prev_row_id not in considered_rows: - if tax.charge_type == 'On Previous Row Amount': - additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount + if tax.charge_type == "On Previous Row Amount": + additional_taxes += doc.get("taxes")[prev_row_id].tax_amount_after_discount_amount considered_rows.append(prev_row_id) - if tax.charge_type == 'On Previous Row Total': - additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total + if tax.charge_type == "On Previous Row Total": + additional_taxes += doc.get("taxes")[prev_row_id].base_total - doc.base_net_total considered_rows.append(prev_row_id) - for item in doc.get('items'): + for item in doc.get("items"): proportionate_value = item.base_net_amount if doc.base_net_total else item.qty total_value = doc.base_net_total if doc.base_net_total else doc.total_qty - applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)), - item.precision('taxable_value'))) + applicable_charges = flt( + flt( + proportionate_value * (flt(additional_taxes) / flt(total_value)), + item.precision("taxable_value"), + ) + ) item.taxable_value = applicable_charges + proportionate_value total_charges += applicable_charges item_count += 1 if total_charges != additional_taxes: diff = additional_taxes - total_charges - doc.get('items')[item_count - 1].taxable_value += diff + doc.get("items")[item_count - 1].taxable_value += diff + def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - - flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) + depreciation_amount = ( + flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + depreciation_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) else: rate_of_depreciation = row.rate_of_depreciation # if its the first depreciation if depreciable_value == asset.gross_purchase_amount: - if row.finance_book and frappe.db.get_value('Finance Book', row.finance_book, 'for_income_tax'): + if row.finance_book and frappe.db.get_value("Finance Book", row.finance_book, "for_income_tax"): # as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2 diff = date_diff(row.depreciation_start_date, asset.available_for_use_date) if diff <= 180: rate_of_depreciation = rate_of_depreciation / 2 frappe.msgprint( - _('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.')) + _( + "As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%." + ) + ) depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100)) return depreciation_amount + def set_item_tax_from_hsn_code(item): if not item.taxes and item.gst_hsn_code: hsn_doc = frappe.get_doc("GST HSN Code", item.gst_hsn_code) for tax in hsn_doc.taxes: - item.append('taxes', { - 'item_tax_template': tax.item_tax_template, - 'tax_category': tax.tax_category, - 'valid_from': tax.valid_from - }) + item.append( + "taxes", + { + "item_tax_template": tax.item_tax_template, + "tax_category": tax.tax_category, + "valid_from": tax.valid_from, + }, + ) + def delete_gst_settings_for_company(doc, method): - if doc.country != 'India': + if doc.country != "India": return gst_settings = frappe.get_doc("GST Settings") records_to_delete = [] - for d in reversed(gst_settings.get('gst_accounts')): + for d in reversed(gst_settings.get("gst_accounts")): if d.company == doc.name: records_to_delete.append(d) @@ -893,4 +1060,3 @@ def delete_gst_settings_for_company(doc, method): gst_settings.remove(d) gst_settings.save() - diff --git a/erpnext/regional/italy/__init__.py b/erpnext/regional/italy/__init__.py index 4932f660ca5..833dcfa13aa 100644 --- a/erpnext/regional/italy/__init__.py +++ b/erpnext/regional/italy/__init__.py @@ -1,79 +1,174 @@ # coding=utf-8 fiscal_regimes = [ - "RF01-Ordinario", - "RF02-Contribuenti minimi (art.1, c.96-117, L. 244/07)", - "RF04-Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)", - "RF05-Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)", - "RF06-Commercio fiammiferi (art.74, c.1, DPR 633/72)", - "RF07-Editoria (art.74, c.1, DPR 633/72)", - "RF08-Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)", - "RF09-Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)", - "RF10-Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)", - "RF11-Agenzie viaggi e turismo (art.74-ter, DPR 633/72)", - "RF12-Agriturismo (art.5, c.2, L. 413/91)", - "RF13-Vendite a domicilio (art.25-bis, c.6, DPR 600/73)", - "RF14-Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)", - "RF15-Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)", - "RF16-IVA per cassa P.A. (art.6, c.5, DPR 633/72)", - "RF17-IVA per cassa (art. 32-bis, DL 83/2012)", - "RF18-Altro", - "RF19-Regime forfettario (art.1, c.54-89, L. 190/2014)" + "RF01-Ordinario", + "RF02-Contribuenti minimi (art.1, c.96-117, L. 244/07)", + "RF04-Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)", + "RF05-Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)", + "RF06-Commercio fiammiferi (art.74, c.1, DPR 633/72)", + "RF07-Editoria (art.74, c.1, DPR 633/72)", + "RF08-Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)", + "RF09-Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)", + "RF10-Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)", + "RF11-Agenzie viaggi e turismo (art.74-ter, DPR 633/72)", + "RF12-Agriturismo (art.5, c.2, L. 413/91)", + "RF13-Vendite a domicilio (art.25-bis, c.6, DPR 600/73)", + "RF14-Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)", + "RF15-Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)", + "RF16-IVA per cassa P.A. (art.6, c.5, DPR 633/72)", + "RF17-IVA per cassa (art. 32-bis, DL 83/2012)", + "RF18-Altro", + "RF19-Regime forfettario (art.1, c.54-89, L. 190/2014)", ] tax_exemption_reasons = [ - "N1-Escluse ex art. 15", - "N2-Non Soggette", - "N3-Non Imponibili", - "N4-Esenti", - "N5-Regime del margine / IVA non esposta in fattura", - "N6-Inversione Contabile", - "N7-IVA assolta in altro stato UE" + "N1-Escluse ex art. 15", + "N2-Non Soggette", + "N3-Non Imponibili", + "N4-Esenti", + "N5-Regime del margine / IVA non esposta in fattura", + "N6-Inversione Contabile", + "N7-IVA assolta in altro stato UE", ] mode_of_payment_codes = [ - "MP01-Contanti", - "MP02-Assegno", - "MP03-Assegno circolare", - "MP04-Contanti presso Tesoreria", - "MP05-Bonifico", - "MP06-Vaglia cambiario", - "MP07-Bollettino bancario", - "MP08-Carta di pagamento", - "MP09-RID", - "MP10-RID utenze", - "MP11-RID veloce", - "MP12-RIBA", - "MP13-MAV", - "MP14-Quietanza erario", - "MP15-Giroconto su conti di contabilità speciale", - "MP16-Domiciliazione bancaria", - "MP17-Domiciliazione postale", - "MP18-Bollettino di c/c postale", - "MP19-SEPA Direct Debit", - "MP20-SEPA Direct Debit CORE", - "MP21-SEPA Direct Debit B2B", - "MP22-Trattenuta su somme già riscosse" + "MP01-Contanti", + "MP02-Assegno", + "MP03-Assegno circolare", + "MP04-Contanti presso Tesoreria", + "MP05-Bonifico", + "MP06-Vaglia cambiario", + "MP07-Bollettino bancario", + "MP08-Carta di pagamento", + "MP09-RID", + "MP10-RID utenze", + "MP11-RID veloce", + "MP12-RIBA", + "MP13-MAV", + "MP14-Quietanza erario", + "MP15-Giroconto su conti di contabilità speciale", + "MP16-Domiciliazione bancaria", + "MP17-Domiciliazione postale", + "MP18-Bollettino di c/c postale", + "MP19-SEPA Direct Debit", + "MP20-SEPA Direct Debit CORE", + "MP21-SEPA Direct Debit B2B", + "MP22-Trattenuta su somme già riscosse", ] -vat_collectability_options = [ - "I-Immediata", - "D-Differita", - "S-Scissione dei Pagamenti" -] +vat_collectability_options = ["I-Immediata", "D-Differita", "S-Scissione dei Pagamenti"] -state_codes = {'Siracusa': 'SR', 'Bologna': 'BO', 'Grosseto': 'GR', 'Caserta': 'CE', 'Alessandria': 'AL', 'Ancona': 'AN', 'Pavia': 'PV', - 'Benevento or Beneventum': 'BN', 'Modena': 'MO', 'Lodi': 'LO', 'Novara': 'NO', 'Avellino': 'AV', 'Verona': 'VR', 'Forli-Cesena': 'FC', - 'Caltanissetta': 'CL', 'Brescia': 'BS', 'Rieti': 'RI', 'Treviso': 'TV', 'Ogliastra': 'OG', 'Olbia-Tempio': 'OT', 'Bergamo': 'BG', - 'Napoli': 'NA', 'Campobasso': 'CB', 'Fermo': 'FM', 'Roma': 'RM', 'Lucca': 'LU', 'Rovigo': 'RO', 'Piacenza': 'PC', 'Monza and Brianza': 'MB', - 'La Spezia': 'SP', 'Pescara': 'PE', 'Vercelli': 'VC', 'Enna': 'EN', 'Nuoro': 'NU', 'Medio Campidano': 'MD', 'Trieste': 'TS', 'Aosta': 'AO', - 'Firenze': 'FI', 'Trapani': 'TP', 'Messina': 'ME', 'Teramo': 'TE', 'Udine': 'UD', 'Verbano-Cusio-Ossola': 'VB', 'Padua': 'PD', - 'Reggio Emilia': 'RE', 'Frosinone': 'FR', 'Taranto': 'TA', 'Catanzaro': 'CZ', 'Belluno': 'BL', 'Pordenone': 'PN', 'Viterbo': 'VT', - 'Gorizia': 'GO', 'Vatican City': 'SCV', 'Ferrara': 'FE', 'Chieti': 'CH', 'Crotone': 'KR', 'Foggia': 'FG', 'Perugia': 'PG', 'Bari': 'BA', - 'Massa-Carrara': 'MS', 'Pisa': 'PI', 'Latina': 'LT', 'Salerno': 'SA', 'Turin': 'TO', 'Lecco': 'LC', 'Lecce': 'LE', 'Pistoia': 'PT', 'Como': 'CO', - 'Barletta-Andria-Trani': 'BT', 'Mantua': 'MN', 'Ragusa': 'RG', 'Macerata': 'MC', 'Imperia': 'IM', 'Palermo': 'PA', 'Matera': 'MT', "L'Aquila": 'AQ', - 'Milano': 'MI', 'Catania': 'CT', 'Pesaro e Urbino': 'PU', 'Potenza': 'PZ', 'Republic of San Marino': 'RSM', 'Genoa': 'GE', 'Brindisi': 'BR', - 'Cagliari': 'CA', 'Siena': 'SI', 'Vibo Valentia': 'VV', 'Reggio Calabria': 'RC', 'Ascoli Piceno': 'AP', 'Carbonia-Iglesias': 'CI', 'Oristano': 'OR', - 'Asti': 'AT', 'Ravenna': 'RA', 'Vicenza': 'VI', 'Savona': 'SV', 'Biella': 'BI', 'Rimini': 'RN', 'Agrigento': 'AG', 'Prato': 'PO', 'Cuneo': 'CN', - 'Cosenza': 'CS', 'Livorno or Leghorn': 'LI', 'Sondrio': 'SO', 'Cremona': 'CR', 'Isernia': 'IS', 'Trento': 'TN', 'Terni': 'TR', 'Bolzano/Bozen': 'BZ', - 'Parma': 'PR', 'Varese': 'VA', 'Venezia': 'VE', 'Sassari': 'SS', 'Arezzo': 'AR'} +state_codes = { + "Siracusa": "SR", + "Bologna": "BO", + "Grosseto": "GR", + "Caserta": "CE", + "Alessandria": "AL", + "Ancona": "AN", + "Pavia": "PV", + "Benevento or Beneventum": "BN", + "Modena": "MO", + "Lodi": "LO", + "Novara": "NO", + "Avellino": "AV", + "Verona": "VR", + "Forli-Cesena": "FC", + "Caltanissetta": "CL", + "Brescia": "BS", + "Rieti": "RI", + "Treviso": "TV", + "Ogliastra": "OG", + "Olbia-Tempio": "OT", + "Bergamo": "BG", + "Napoli": "NA", + "Campobasso": "CB", + "Fermo": "FM", + "Roma": "RM", + "Lucca": "LU", + "Rovigo": "RO", + "Piacenza": "PC", + "Monza and Brianza": "MB", + "La Spezia": "SP", + "Pescara": "PE", + "Vercelli": "VC", + "Enna": "EN", + "Nuoro": "NU", + "Medio Campidano": "MD", + "Trieste": "TS", + "Aosta": "AO", + "Firenze": "FI", + "Trapani": "TP", + "Messina": "ME", + "Teramo": "TE", + "Udine": "UD", + "Verbano-Cusio-Ossola": "VB", + "Padua": "PD", + "Reggio Emilia": "RE", + "Frosinone": "FR", + "Taranto": "TA", + "Catanzaro": "CZ", + "Belluno": "BL", + "Pordenone": "PN", + "Viterbo": "VT", + "Gorizia": "GO", + "Vatican City": "SCV", + "Ferrara": "FE", + "Chieti": "CH", + "Crotone": "KR", + "Foggia": "FG", + "Perugia": "PG", + "Bari": "BA", + "Massa-Carrara": "MS", + "Pisa": "PI", + "Latina": "LT", + "Salerno": "SA", + "Turin": "TO", + "Lecco": "LC", + "Lecce": "LE", + "Pistoia": "PT", + "Como": "CO", + "Barletta-Andria-Trani": "BT", + "Mantua": "MN", + "Ragusa": "RG", + "Macerata": "MC", + "Imperia": "IM", + "Palermo": "PA", + "Matera": "MT", + "L'Aquila": "AQ", + "Milano": "MI", + "Catania": "CT", + "Pesaro e Urbino": "PU", + "Potenza": "PZ", + "Republic of San Marino": "RSM", + "Genoa": "GE", + "Brindisi": "BR", + "Cagliari": "CA", + "Siena": "SI", + "Vibo Valentia": "VV", + "Reggio Calabria": "RC", + "Ascoli Piceno": "AP", + "Carbonia-Iglesias": "CI", + "Oristano": "OR", + "Asti": "AT", + "Ravenna": "RA", + "Vicenza": "VI", + "Savona": "SV", + "Biella": "BI", + "Rimini": "RN", + "Agrigento": "AG", + "Prato": "PO", + "Cuneo": "CN", + "Cosenza": "CS", + "Livorno or Leghorn": "LI", + "Sondrio": "SO", + "Cremona": "CR", + "Isernia": "IS", + "Trento": "TN", + "Terni": "TR", + "Bolzano/Bozen": "BZ", + "Parma": "PR", + "Varese": "VA", + "Venezia": "VE", + "Sassari": "SS", + "Arezzo": "AR", +} diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 9453a2340ad..23406ea85a6 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -21,206 +21,476 @@ def setup(company=None, patch=True): setup_report() add_permissions() + def make_custom_fields(update=True): invoice_item_fields = [ - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Float', insert_after='description', - print_hide=1, hidden=1, read_only=1), - dict(fieldname='tax_amount', label='Tax Amount', - fieldtype='Currency', insert_after='tax_rate', - print_hide=1, hidden=1, read_only=1, options="currency"), - dict(fieldname='total_amount', label='Total Amount', - fieldtype='Currency', insert_after='tax_amount', - print_hide=1, hidden=1, read_only=1, options="currency") + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Float", + insert_after="description", + print_hide=1, + hidden=1, + read_only=1, + ), + dict( + fieldname="tax_amount", + label="Tax Amount", + fieldtype="Currency", + insert_after="tax_rate", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), + dict( + fieldname="total_amount", + label="Total Amount", + fieldtype="Currency", + insert_after="tax_amount", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), ] customer_po_fields = [ - dict(fieldname='customer_po_details', label='Customer PO', - fieldtype='Section Break', insert_after='image'), - dict(fieldname='customer_po_no', label='Customer PO No', - fieldtype='Data', insert_after='customer_po_details', - fetch_from = 'sales_order.po_no', - print_hide=1, allow_on_submit=1, fetch_if_empty= 1, read_only=1, no_copy=1), - dict(fieldname='customer_po_clm_brk', label='', - fieldtype='Column Break', insert_after='customer_po_no', - print_hide=1, read_only=1), - dict(fieldname='customer_po_date', label='Customer PO Date', - fieldtype='Date', insert_after='customer_po_clm_brk', - fetch_from = 'sales_order.po_date', - print_hide=1, allow_on_submit=1, fetch_if_empty= 1, read_only=1, no_copy=1) + dict( + fieldname="customer_po_details", + label="Customer PO", + fieldtype="Section Break", + insert_after="image", + ), + dict( + fieldname="customer_po_no", + label="Customer PO No", + fieldtype="Data", + insert_after="customer_po_details", + fetch_from="sales_order.po_no", + print_hide=1, + allow_on_submit=1, + fetch_if_empty=1, + read_only=1, + no_copy=1, + ), + dict( + fieldname="customer_po_clm_brk", + label="", + fieldtype="Column Break", + insert_after="customer_po_no", + print_hide=1, + read_only=1, + ), + dict( + fieldname="customer_po_date", + label="Customer PO Date", + fieldtype="Date", + insert_after="customer_po_clm_brk", + fetch_from="sales_order.po_date", + print_hide=1, + allow_on_submit=1, + fetch_if_empty=1, + read_only=1, + no_copy=1, + ), ] custom_fields = { - 'Company': [ - dict(fieldname='sb_e_invoicing', label='E-Invoicing', - fieldtype='Section Break', insert_after='date_of_establishment', print_hide=1), - dict(fieldname='fiscal_regime', label='Fiscal Regime', - fieldtype='Select', insert_after='sb_e_invoicing', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), fiscal_regimes))), - dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='fiscal_regime', print_hide=1, - description=_("Applicable if the company is an Individual or a Proprietorship")), - dict(fieldname='vat_collectability', label='VAT Collectability', - fieldtype='Select', insert_after='fiscal_code', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options))), - dict(fieldname='cb_e_invoicing1', fieldtype='Column Break', insert_after='vat_collectability', print_hide=1), - dict(fieldname='registrar_office_province', label='Province of the Registrar Office', - fieldtype='Data', insert_after='cb_e_invoicing1', print_hide=1, length=2), - dict(fieldname='registration_number', label='Registration Number', - fieldtype='Data', insert_after='registrar_office_province', print_hide=1, length=20), - dict(fieldname='share_capital_amount', label='Share Capital', - fieldtype='Currency', insert_after='registration_number', print_hide=1, - description=_('Applicable if the company is SpA, SApA or SRL')), - dict(fieldname='no_of_members', label='No of Members', - fieldtype='Select', insert_after='share_capital_amount', print_hide=1, - options="\nSU-Socio Unico\nSM-Piu Soci", description=_("Applicable if the company is a limited liability company")), - dict(fieldname='liquidation_state', label='Liquidation State', - fieldtype='Select', insert_after='no_of_members', print_hide=1, - options="\nLS-In Liquidazione\nLN-Non in Liquidazione") + "Company": [ + dict( + fieldname="sb_e_invoicing", + label="E-Invoicing", + fieldtype="Section Break", + insert_after="date_of_establishment", + print_hide=1, + ), + dict( + fieldname="fiscal_regime", + label="Fiscal Regime", + fieldtype="Select", + insert_after="sb_e_invoicing", + print_hide=1, + options="\n".join(map(lambda x: frappe.safe_decode(x, encoding="utf-8"), fiscal_regimes)), + ), + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="fiscal_regime", + print_hide=1, + description=_("Applicable if the company is an Individual or a Proprietorship"), + ), + dict( + fieldname="vat_collectability", + label="VAT Collectability", + fieldtype="Select", + insert_after="fiscal_code", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), vat_collectability_options) + ), + ), + dict( + fieldname="cb_e_invoicing1", + fieldtype="Column Break", + insert_after="vat_collectability", + print_hide=1, + ), + dict( + fieldname="registrar_office_province", + label="Province of the Registrar Office", + fieldtype="Data", + insert_after="cb_e_invoicing1", + print_hide=1, + length=2, + ), + dict( + fieldname="registration_number", + label="Registration Number", + fieldtype="Data", + insert_after="registrar_office_province", + print_hide=1, + length=20, + ), + dict( + fieldname="share_capital_amount", + label="Share Capital", + fieldtype="Currency", + insert_after="registration_number", + print_hide=1, + description=_("Applicable if the company is SpA, SApA or SRL"), + ), + dict( + fieldname="no_of_members", + label="No of Members", + fieldtype="Select", + insert_after="share_capital_amount", + print_hide=1, + options="\nSU-Socio Unico\nSM-Piu Soci", + description=_("Applicable if the company is a limited liability company"), + ), + dict( + fieldname="liquidation_state", + label="Liquidation State", + fieldtype="Select", + insert_after="no_of_members", + print_hide=1, + options="\nLS-In Liquidazione\nLN-Non in Liquidazione", + ), ], - 'Sales Taxes and Charges': [ - dict(fieldname='tax_exemption_reason', label='Tax Exemption Reason', - fieldtype='Select', insert_after='included_in_print_rate', print_hide=1, + "Sales Taxes and Charges": [ + dict( + fieldname="tax_exemption_reason", + label="Tax Exemption Reason", + fieldtype="Select", + insert_after="included_in_print_rate", + print_hide=1, depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0', - options="\n" + "\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), tax_exemption_reasons))), - dict(fieldname='tax_exemption_law', label='Tax Exempt Under', - fieldtype='Text', insert_after='tax_exemption_reason', print_hide=1, - depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0') + options="\n" + + "\n".join(map(lambda x: frappe.safe_decode(x, encoding="utf-8"), tax_exemption_reasons)), + ), + dict( + fieldname="tax_exemption_law", + label="Tax Exempt Under", + fieldtype="Text", + insert_after="tax_exemption_reason", + print_hide=1, + depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0', + ), ], - 'Customer': [ - dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='tax_id', print_hide=1), - dict(fieldname='recipient_code', label='Recipient Code', - fieldtype='Data', insert_after='fiscal_code', print_hide=1, default="0000000"), - dict(fieldname='pec', label='Recipient PEC', - fieldtype='Data', insert_after='fiscal_code', print_hide=1), - dict(fieldname='is_public_administration', label='Is Public Administration', - fieldtype='Check', insert_after='is_internal_customer', print_hide=1, + "Customer": [ + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="tax_id", + print_hide=1, + ), + dict( + fieldname="recipient_code", + label="Recipient Code", + fieldtype="Data", + insert_after="fiscal_code", + print_hide=1, + default="0000000", + ), + dict( + fieldname="pec", + label="Recipient PEC", + fieldtype="Data", + insert_after="fiscal_code", + print_hide=1, + ), + dict( + fieldname="is_public_administration", + label="Is Public Administration", + fieldtype="Check", + insert_after="is_internal_customer", + print_hide=1, description=_("Set this if the customer is a Public Administration company."), - depends_on='eval:doc.customer_type=="Company"'), - dict(fieldname='first_name', label='First Name', fieldtype='Data', - insert_after='salutation', print_hide=1, depends_on='eval:doc.customer_type!="Company"'), - dict(fieldname='last_name', label='Last Name', fieldtype='Data', - insert_after='first_name', print_hide=1, depends_on='eval:doc.customer_type!="Company"') + depends_on='eval:doc.customer_type=="Company"', + ), + dict( + fieldname="first_name", + label="First Name", + fieldtype="Data", + insert_after="salutation", + print_hide=1, + depends_on='eval:doc.customer_type!="Company"', + ), + dict( + fieldname="last_name", + label="Last Name", + fieldtype="Data", + insert_after="first_name", + print_hide=1, + depends_on='eval:doc.customer_type!="Company"', + ), ], - 'Mode of Payment': [ - dict(fieldname='mode_of_payment_code', label='Code', - fieldtype='Select', insert_after='included_in_print_rate', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), mode_of_payment_codes))) + "Mode of Payment": [ + dict( + fieldname="mode_of_payment_code", + label="Code", + fieldtype="Select", + insert_after="included_in_print_rate", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), mode_of_payment_codes) + ), + ) ], - 'Payment Schedule': [ - dict(fieldname='mode_of_payment_code', label='Code', - fieldtype='Select', insert_after='mode_of_payment', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), mode_of_payment_codes)), - fetch_from="mode_of_payment.mode_of_payment_code", read_only=1), - dict(fieldname='bank_account', label='Bank Account', - fieldtype='Link', insert_after='mode_of_payment_code', print_hide=1, - options="Bank Account"), - dict(fieldname='bank_account_name', label='Bank Name', - fieldtype='Data', insert_after='bank_account', print_hide=1, - fetch_from="bank_account.bank", read_only=1), - dict(fieldname='bank_account_no', label='Bank Account No', - fieldtype='Data', insert_after='bank_account_name', print_hide=1, - fetch_from="bank_account.bank_account_no", read_only=1), - dict(fieldname='bank_account_iban', label='IBAN', - fieldtype='Data', insert_after='bank_account_name', print_hide=1, - fetch_from="bank_account.iban", read_only=1), - dict(fieldname='bank_account_swift_number', label='Swift Code (BIC)', - fieldtype='Data', insert_after='bank_account_iban', print_hide=1, - fetch_from="bank_account.swift_number", read_only=1), + "Payment Schedule": [ + dict( + fieldname="mode_of_payment_code", + label="Code", + fieldtype="Select", + insert_after="mode_of_payment", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), mode_of_payment_codes) + ), + fetch_from="mode_of_payment.mode_of_payment_code", + read_only=1, + ), + dict( + fieldname="bank_account", + label="Bank Account", + fieldtype="Link", + insert_after="mode_of_payment_code", + print_hide=1, + options="Bank Account", + ), + dict( + fieldname="bank_account_name", + label="Bank Name", + fieldtype="Data", + insert_after="bank_account", + print_hide=1, + fetch_from="bank_account.bank", + read_only=1, + ), + dict( + fieldname="bank_account_no", + label="Bank Account No", + fieldtype="Data", + insert_after="bank_account_name", + print_hide=1, + fetch_from="bank_account.bank_account_no", + read_only=1, + ), + dict( + fieldname="bank_account_iban", + label="IBAN", + fieldtype="Data", + insert_after="bank_account_name", + print_hide=1, + fetch_from="bank_account.iban", + read_only=1, + ), + dict( + fieldname="bank_account_swift_number", + label="Swift Code (BIC)", + fieldtype="Data", + insert_after="bank_account_iban", + print_hide=1, + fetch_from="bank_account.swift_number", + read_only=1, + ), ], "Sales Invoice": [ - dict(fieldname='vat_collectability', label='VAT Collectability', - fieldtype='Select', insert_after='taxes_and_charges', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options)), - fetch_from="company.vat_collectability"), - dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing', - fieldtype='Section Break', insert_after='against_income_account', print_hide=1), - dict(fieldname='company_fiscal_code', label='Company Fiscal Code', - fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1, - fetch_from="company.fiscal_code"), - dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime', - fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1, - fetch_from="company.fiscal_regime"), - dict(fieldname='cb_e_invoicing_reference', fieldtype='Column Break', - insert_after='company_fiscal_regime', print_hide=1), - dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code', - fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1, - fetch_from="customer.fiscal_code"), - dict(fieldname='type_of_document', label='Type of Document', - fieldtype='Select', insert_after='customer_fiscal_code', - options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'), - ], - 'Purchase Invoice Item': invoice_item_fields, - 'Sales Order Item': invoice_item_fields, - 'Delivery Note Item': invoice_item_fields, - 'Sales Invoice Item': invoice_item_fields + customer_po_fields, - 'Quotation Item': invoice_item_fields, - 'Purchase Order Item': invoice_item_fields, - 'Purchase Receipt Item': invoice_item_fields, - 'Supplier Quotation Item': invoice_item_fields, - 'Address': [ - dict(fieldname='country_code', label='Country Code', - fieldtype='Data', insert_after='country', print_hide=1, read_only=0, - fetch_from="country.code"), - dict(fieldname='state_code', label='State Code', - fieldtype='Data', insert_after='state', print_hide=1) - ], - 'Purchase Invoice': [ - dict(fieldname='document_type', label='Document Type', - fieldtype='Data', insert_after='company', print_hide=1, read_only=1 + dict( + fieldname="vat_collectability", + label="VAT Collectability", + fieldtype="Select", + insert_after="taxes_and_charges", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), vat_collectability_options) ), - dict(fieldname='destination_code', label='Destination Code', - fieldtype='Data', insert_after='company', print_hide=1, read_only=1 - ), - dict(fieldname='imported_grand_total', label='Imported Grand Total', - fieldtype='Data', insert_after='update_auto_repeat_reference', print_hide=1, read_only=1 - ) + fetch_from="company.vat_collectability", + ), + dict( + fieldname="sb_e_invoicing_reference", + label="E-Invoicing", + fieldtype="Section Break", + insert_after="against_income_account", + print_hide=1, + ), + dict( + fieldname="company_fiscal_code", + label="Company Fiscal Code", + fieldtype="Data", + insert_after="sb_e_invoicing_reference", + print_hide=1, + read_only=1, + fetch_from="company.fiscal_code", + ), + dict( + fieldname="company_fiscal_regime", + label="Company Fiscal Regime", + fieldtype="Data", + insert_after="company_fiscal_code", + print_hide=1, + read_only=1, + fetch_from="company.fiscal_regime", + ), + dict( + fieldname="cb_e_invoicing_reference", + fieldtype="Column Break", + insert_after="company_fiscal_regime", + print_hide=1, + ), + dict( + fieldname="customer_fiscal_code", + label="Customer Fiscal Code", + fieldtype="Data", + insert_after="cb_e_invoicing_reference", + read_only=1, + fetch_from="customer.fiscal_code", + ), + dict( + fieldname="type_of_document", + label="Type of Document", + fieldtype="Select", + insert_after="customer_fiscal_code", + options="\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27", + ), ], - 'Purchase Taxes and Charges': [ - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Data', insert_after='parenttype', print_hide=1, read_only=0 - ) + "Purchase Invoice Item": invoice_item_fields, + "Sales Order Item": invoice_item_fields, + "Delivery Note Item": invoice_item_fields, + "Sales Invoice Item": invoice_item_fields + customer_po_fields, + "Quotation Item": invoice_item_fields, + "Purchase Order Item": invoice_item_fields, + "Purchase Receipt Item": invoice_item_fields, + "Supplier Quotation Item": invoice_item_fields, + "Address": [ + dict( + fieldname="country_code", + label="Country Code", + fieldtype="Data", + insert_after="country", + print_hide=1, + read_only=0, + fetch_from="country.code", + ), + dict( + fieldname="state_code", + label="State Code", + fieldtype="Data", + insert_after="state", + print_hide=1, + ), + ], + "Purchase Invoice": [ + dict( + fieldname="document_type", + label="Document Type", + fieldtype="Data", + insert_after="company", + print_hide=1, + read_only=1, + ), + dict( + fieldname="destination_code", + label="Destination Code", + fieldtype="Data", + insert_after="company", + print_hide=1, + read_only=1, + ), + dict( + fieldname="imported_grand_total", + label="Imported Grand Total", + fieldtype="Data", + insert_after="update_auto_repeat_reference", + print_hide=1, + read_only=1, + ), + ], + "Purchase Taxes and Charges": [ + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Data", + insert_after="parenttype", + print_hide=1, + read_only=0, + ) + ], + "Supplier": [ + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="tax_id", + print_hide=1, + read_only=1, + ), + dict( + fieldname="fiscal_regime", + label="Fiscal Regime", + fieldtype="Select", + insert_after="fiscal_code", + print_hide=1, + read_only=1, + options="\nRF01\nRF02\nRF04\nRF05\nRF06\nRF07\nRF08\nRF09\nRF10\nRF11\nRF12\nRF13\nRF14\nRF15\nRF16\nRF17\nRF18\nRF19", + ), ], - 'Supplier': [ - dict(fieldname='fiscal_code', label='Fiscal Code', - fieldtype='Data', insert_after='tax_id', print_hide=1, read_only=1 - ), - dict(fieldname='fiscal_regime', label='Fiscal Regime', - fieldtype='Select', insert_after='fiscal_code', print_hide=1, read_only=1, - options= "\nRF01\nRF02\nRF04\nRF05\nRF06\nRF07\nRF08\nRF09\nRF10\nRF11\nRF12\nRF13\nRF14\nRF15\nRF16\nRF17\nRF18\nRF19" - ) - ] } - create_custom_fields(custom_fields, ignore_validate = frappe.flags.in_patch, update=update) + create_custom_fields(custom_fields, ignore_validate=frappe.flags.in_patch, update=update) + def setup_report(): - report_name = 'Electronic Invoice Register' + report_name = "Electronic Invoice Register" frappe.db.set_value("Report", report_name, "disabled", 0) - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() + def add_permissions(): - doctype = 'Import Supplier Invoice' - add_permission(doctype, 'All', 0) + doctype = "Import Supplier Invoice" + add_permission(doctype, "All", 0) - for role in ('Accounts Manager', 'Accounts User','Purchase User', 'Auditor'): + for role in ("Accounts Manager", "Accounts User", "Purchase User", "Auditor"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'print', 1) - update_permission_property(doctype, role, 0, 'report', 1) + update_permission_property(doctype, role, 0, "print", 1) + update_permission_property(doctype, role, 0, "report", 1) - if role in ('Accounts Manager', 'Accounts User'): - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + if role in ("Accounts Manager", "Accounts User"): + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1) - add_permission(doctype, 'Accounts Manager', 1) - update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1) - update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1) + update_permission_property(doctype, "Accounts Manager", 0, "delete", 1) + add_permission(doctype, "Accounts Manager", 1) + update_permission_property(doctype, "Accounts Manager", 1, "write", 1) + update_permission_property(doctype, "Accounts Manager", 1, "create", 1) diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index 8d1558be227..8dffd99eadb 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -1,4 +1,3 @@ - import io import json @@ -13,41 +12,41 @@ from erpnext.regional.italy import state_codes def update_itemised_tax_data(doc): - if not doc.taxes: return + if not doc.taxes: + return - if doc.doctype == "Purchase Invoice": return + if doc.doctype == "Purchase Invoice": + return itemised_tax = get_itemised_tax(doc.taxes) for row in doc.items: tax_rate = 0.0 if itemised_tax.get(row.item_code): - tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()]) + tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()]) row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + @frappe.whitelist() def export_invoices(filters=None): - frappe.has_permission('Sales Invoice', throw=True) + frappe.has_permission("Sales Invoice", throw=True) invoices = frappe.get_all( - "Sales Invoice", - filters=get_conditions(filters), - fields=["name", "company_tax_id"] + "Sales Invoice", filters=get_conditions(filters), fields=["name", "company_tax_id"] ) attachments = get_e_invoice_attachments(invoices) - zip_filename = "{0}-einvoices.zip".format( - frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) + zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) download_zip(attachments, zip_filename) def prepare_invoice(invoice, progressive_number): - #set company information + # set company information company = frappe.get_doc("Company", invoice.company) invoice.progressive_number = progressive_number @@ -56,15 +55,17 @@ def prepare_invoice(invoice, progressive_number): company_address = frappe.get_doc("Address", invoice.company_address) invoice.company_address_data = company_address - #Set invoice type + # Set invoice type if not invoice.type_of_document: if invoice.is_return and invoice.return_against: - invoice.type_of_document = "TD04" #Credit Note (Nota di Credito) - invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against)) + invoice.type_of_document = "TD04" # Credit Note (Nota di Credito) + invoice.return_against_unamended = get_unamended_name( + frappe.get_doc("Sales Invoice", invoice.return_against) + ) else: - invoice.type_of_document = "TD01" #Sales Invoice (Fattura) + invoice.type_of_document = "TD01" # Sales Invoice (Fattura) - #set customer information + # set customer information invoice.customer_data = frappe.get_doc("Customer", invoice.customer) customer_address = frappe.get_doc("Address", invoice.customer_address) invoice.customer_address_data = customer_address @@ -81,8 +82,10 @@ def prepare_invoice(invoice, progressive_number): tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes) invoice.tax_data = tax_data - #Check if stamp duty (Bollo) of 2 EUR exists. - stamp_duty_charge_row = next((tax for tax in invoice.taxes if tax.charge_type == "Actual" and tax.tax_amount == 2.0 ), None) + # Check if stamp duty (Bollo) of 2 EUR exists. + stamp_duty_charge_row = next( + (tax for tax in invoice.taxes if tax.charge_type == "Actual" and tax.tax_amount == 2.0), None + ) if stamp_duty_charge_row: invoice.stamp_duty = stamp_duty_charge_row.tax_amount @@ -92,24 +95,28 @@ def prepare_invoice(invoice, progressive_number): customer_po_data = {} for d in invoice.e_invoice_items: - if (d.customer_po_no and d.customer_po_date - and d.customer_po_no not in customer_po_data): + if d.customer_po_no and d.customer_po_date and d.customer_po_no not in customer_po_data: customer_po_data[d.customer_po_no] = d.customer_po_date invoice.customer_po_data = customer_po_data return invoice + def get_conditions(filters): filters = json.loads(filters) conditions = {"docstatus": 1, "company_tax_id": ("!=", "")} - if filters.get("company"): conditions["company"] = filters["company"] - if filters.get("customer"): conditions["customer"] = filters["customer"] + if filters.get("company"): + conditions["company"] = filters["company"] + if filters.get("customer"): + conditions["customer"] = filters["customer"] - if filters.get("from_date"): conditions["posting_date"] = (">=", filters["from_date"]) - if filters.get("to_date"): conditions["posting_date"] = ("<=", filters["to_date"]) + if filters.get("from_date"): + conditions["posting_date"] = (">=", filters["from_date"]) + if filters.get("to_date"): + conditions["posting_date"] = ("<=", filters["to_date"]) if filters.get("from_date") and filters.get("to_date"): conditions["posting_date"] = ("between", [filters.get("from_date"), filters.get("to_date")]) @@ -121,10 +128,9 @@ def download_zip(files, output_filename): import zipfile zip_stream = io.BytesIO() - with zipfile.ZipFile(zip_stream, 'w', zipfile.ZIP_DEFLATED) as zip_file: + with zipfile.ZipFile(zip_stream, "w", zipfile.ZIP_DEFLATED) as zip_file: for file in files: - file_path = frappe.utils.get_files_path( - file.file_name, is_private=file.is_private) + file_path = frappe.utils.get_files_path(file.file_name, is_private=file.is_private) zip_file.write(file_path, arcname=file.file_name) @@ -133,20 +139,21 @@ def download_zip(files, output_filename): frappe.local.response.type = "download" zip_stream.close() + def get_invoice_summary(items, taxes): summary_data = frappe._dict() for tax in taxes: - #Include only VAT charges. + # Include only VAT charges. if tax.charge_type == "Actual": continue - #Charges to appear as items in the e-invoice. + # Charges to appear as items in the e-invoice. if tax.charge_type in ["On Previous Row Total", "On Previous Row Amount"]: reference_row = next((row for row in taxes if row.idx == int(tax.row_id or 0)), None) if reference_row: items.append( frappe._dict( - idx=len(items)+1, + idx=len(items) + 1, item_code=reference_row.description, item_name=reference_row.description, description=reference_row.description, @@ -159,11 +166,11 @@ def get_invoice_summary(items, taxes): net_amount=reference_row.tax_amount, taxable_amount=reference_row.tax_amount, item_tax_rate={tax.account_head: tax.rate}, - charges=True + charges=True, ) ) - #Check item tax rates if tax rate is zero. + # Check item tax rates if tax rate is zero. if tax.rate == 0: for item in items: item_tax_rate = item.item_tax_rate @@ -173,8 +180,15 @@ def get_invoice_summary(items, taxes): if item_tax_rate and tax.account_head in item_tax_rate: key = cstr(item_tax_rate[tax.account_head]) if key not in summary_data: - summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0, - "tax_exemption_reason": "", "tax_exemption_law": ""}) + summary_data.setdefault( + key, + { + "tax_amount": 0.0, + "taxable_amount": 0.0, + "tax_exemption_reason": "", + "tax_exemption_law": "", + }, + ) summary_data[key]["tax_amount"] += item.tax_amount summary_data[key]["taxable_amount"] += item.net_amount @@ -182,93 +196,138 @@ def get_invoice_summary(items, taxes): summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law - if summary_data.get("0.0") and tax.charge_type in ["On Previous Row Total", - "On Previous Row Amount"]: + if summary_data.get("0.0") and tax.charge_type in [ + "On Previous Row Total", + "On Previous Row Amount", + ]: summary_data[key]["taxable_amount"] = tax.total - if summary_data == {}: #Implies that Zero VAT has not been set on any item. - summary_data.setdefault("0.0", {"tax_amount": 0.0, "taxable_amount": tax.total, - "tax_exemption_reason": tax.tax_exemption_reason, "tax_exemption_law": tax.tax_exemption_law}) + if summary_data == {}: # Implies that Zero VAT has not been set on any item. + summary_data.setdefault( + "0.0", + { + "tax_amount": 0.0, + "taxable_amount": tax.total, + "tax_exemption_reason": tax.tax_exemption_reason, + "tax_exemption_law": tax.tax_exemption_law, + }, + ) else: item_wise_tax_detail = json.loads(tax.item_wise_tax_detail) - for rate_item in [tax_item for tax_item in item_wise_tax_detail.items() if tax_item[1][0] == tax.rate]: + for rate_item in [ + tax_item for tax_item in item_wise_tax_detail.items() if tax_item[1][0] == tax.rate + ]: key = cstr(tax.rate) - if not summary_data.get(key): summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0}) + if not summary_data.get(key): + summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0}) summary_data[key]["tax_amount"] += rate_item[1][1] - summary_data[key]["taxable_amount"] += sum([item.net_amount for item in items if item.item_code == rate_item[0]]) + summary_data[key]["taxable_amount"] += sum( + [item.net_amount for item in items if item.item_code == rate_item[0]] + ) for item in items: key = cstr(tax.rate) if item.get("charges"): - if not summary_data.get(key): summary_data.setdefault(key, {"taxable_amount": 0.0}) + if not summary_data.get(key): + summary_data.setdefault(key, {"taxable_amount": 0.0}) summary_data[key]["taxable_amount"] += item.taxable_amount return summary_data -#Preflight for successful e-invoice export. + +# Preflight for successful e-invoice export. def sales_invoice_validate(doc): - #Validate company - if doc.doctype != 'Sales Invoice': + # Validate company + if doc.doctype != "Sales Invoice": return if not doc.company_address: - frappe.throw(_("Please set an Address on the Company '%s'" % doc.company), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set an Address on the Company '%s'" % doc.company), + title=_("E-Invoicing Information Missing"), + ) else: validate_address(doc.company_address) - company_fiscal_regime = frappe.get_cached_value("Company", doc.company, 'fiscal_regime') + company_fiscal_regime = frappe.get_cached_value("Company", doc.company, "fiscal_regime") if not company_fiscal_regime: - frappe.throw(_("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}") - .format(doc.company)) + frappe.throw( + _("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}").format( + doc.company + ) + ) else: doc.company_fiscal_regime = company_fiscal_regime - doc.company_tax_id = frappe.get_cached_value("Company", doc.company, 'tax_id') - doc.company_fiscal_code = frappe.get_cached_value("Company", doc.company, 'fiscal_code') + doc.company_tax_id = frappe.get_cached_value("Company", doc.company, "tax_id") + doc.company_fiscal_code = frappe.get_cached_value("Company", doc.company, "fiscal_code") if not doc.company_tax_id and not doc.company_fiscal_code: - frappe.throw(_("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), + title=_("E-Invoicing Information Missing"), + ) - #Validate customer details + # Validate customer details customer = frappe.get_doc("Customer", doc.customer) if customer.customer_type == "Individual": doc.customer_fiscal_code = customer.fiscal_code if not doc.customer_fiscal_code: - frappe.throw(_("Please set Fiscal Code for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Fiscal Code for the customer '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) else: if customer.is_public_administration: doc.customer_fiscal_code = customer.fiscal_code if not doc.customer_fiscal_code: - frappe.throw(_("Please set Fiscal Code for the public administration '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Fiscal Code for the public administration '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) else: doc.tax_id = customer.tax_id if not doc.tax_id: - frappe.throw(_("Please set Tax ID for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Tax ID for the customer '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) if not doc.customer_address: - frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing")) + frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing")) else: validate_address(doc.customer_address) if not len(doc.taxes): - frappe.throw(_("Please set at least one row in the Taxes and Charges Table"), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set at least one row in the Taxes and Charges Table"), + title=_("E-Invoicing Information Missing"), + ) else: for row in doc.taxes: if row.rate == 0 and row.tax_amount == 0 and not row.tax_exemption_reason: - frappe.throw(_("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges").format(row.idx), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges").format(row.idx), + title=_("E-Invoicing Information Missing"), + ) for schedule in doc.payment_schedule: if schedule.mode_of_payment and not schedule.mode_of_payment_code: - schedule.mode_of_payment_code = frappe.get_cached_value('Mode of Payment', - schedule.mode_of_payment, 'mode_of_payment_code') + schedule.mode_of_payment_code = frappe.get_cached_value( + "Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code" + ) -#Ensure payment details are valid for e-invoice. + +# Ensure payment details are valid for e-invoice. def sales_invoice_on_submit(doc, method): - #Validate payment details - if get_company_country(doc.company) not in ['Italy', - 'Italia', 'Italian Republic', 'Repubblica Italiana']: + # Validate payment details + if get_company_country(doc.company) not in [ + "Italy", + "Italia", + "Italian Republic", + "Repubblica Italiana", + ]: return if not len(doc.payment_schedule): @@ -276,38 +335,53 @@ def sales_invoice_on_submit(doc, method): else: for schedule in doc.payment_schedule: if not schedule.mode_of_payment: - frappe.throw(_("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), - title=_("E-Invoicing Information Missing")) - elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"): - frappe.throw(_("Row {0}: Please set the correct code on Mode of Payment {1}").format(schedule.idx, schedule.mode_of_payment), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), + title=_("E-Invoicing Information Missing"), + ) + elif not frappe.db.get_value( + "Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code" + ): + frappe.throw( + _("Row {0}: Please set the correct code on Mode of Payment {1}").format( + schedule.idx, schedule.mode_of_payment + ), + title=_("E-Invoicing Information Missing"), + ) prepare_and_attach_invoice(doc) + def prepare_and_attach_invoice(doc, replace=False): progressive_name, progressive_number = get_progressive_name_and_number(doc, replace) invoice = prepare_invoice(doc, progressive_number) item_meta = frappe.get_meta("Sales Invoice Item") - invoice_xml = frappe.render_template('erpnext/regional/italy/e-invoice.xml', - context={"doc": invoice, "item_meta": item_meta}, is_path=True) + invoice_xml = frappe.render_template( + "erpnext/regional/italy/e-invoice.xml", + context={"doc": invoice, "item_meta": item_meta}, + is_path=True, + ) invoice_xml = invoice_xml.replace("&", "&") xml_filename = progressive_name + ".xml" - _file = frappe.get_doc({ - "doctype": "File", - "file_name": xml_filename, - "attached_to_doctype": doc.doctype, - "attached_to_name": doc.name, - "is_private": True, - "content": invoice_xml - }) + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": xml_filename, + "attached_to_doctype": doc.doctype, + "attached_to_name": doc.name, + "is_private": True, + "content": invoice_xml, + } + ) _file.save() return _file + @frappe.whitelist() def generate_single_invoice(docname): doc = frappe.get_doc("Sales Invoice", docname) @@ -316,17 +390,24 @@ def generate_single_invoice(docname): e_invoice = prepare_and_attach_invoice(doc, True) return e_invoice.file_url + # Delete e-invoice attachment on cancel. def sales_invoice_on_cancel(doc, method): - if get_company_country(doc.company) not in ['Italy', - 'Italia', 'Italian Republic', 'Repubblica Italiana']: + if get_company_country(doc.company) not in [ + "Italy", + "Italia", + "Italian Republic", + "Repubblica Italiana", + ]: return for attachment in get_e_invoice_attachments(doc): remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name) + def get_company_country(company): - return frappe.get_cached_value('Company', company, 'country') + return frappe.get_cached_value("Company", company, "country") + def get_e_invoice_attachments(invoices): if not isinstance(invoices, list): @@ -340,16 +421,14 @@ def get_e_invoice_attachments(invoices): invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id - ) for invoice in invoices + ) + for invoice in invoices } attachments = frappe.get_all( "File", fields=("name", "file_name", "attached_to_name", "is_private"), - filters= { - "attached_to_name": ('in', tax_id_map), - "attached_to_doctype": 'Sales Invoice' - } + filters={"attached_to_name": ("in", tax_id_map), "attached_to_doctype": "Sales Invoice"}, ) out = [] @@ -357,21 +436,24 @@ def get_e_invoice_attachments(invoices): if ( attachment.file_name and attachment.file_name.endswith(".xml") - and attachment.file_name.startswith( - tax_id_map.get(attachment.attached_to_name)) + and attachment.file_name.startswith(tax_id_map.get(attachment.attached_to_name)) ): out.append(attachment) return out + def validate_address(address_name): fields = ["pincode", "city", "country_code"] data = frappe.get_cached_value("Address", address_name, fields, as_dict=1) or {} for field in fields: if not data.get(field): - frappe.throw(_("Please set {0} for address {1}").format(field.replace('-',''), address_name), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set {0} for address {1}").format(field.replace("-", ""), address_name), + title=_("E-Invoicing Information Missing"), + ) + def get_unamended_name(doc): attributes = ["naming_series", "amended_from"] @@ -384,6 +466,7 @@ def get_unamended_name(doc): else: return doc.name + def get_progressive_name_and_number(doc, replace=False): if replace: for attachment in get_e_invoice_attachments(doc): @@ -391,24 +474,30 @@ def get_progressive_name_and_number(doc, replace=False): filename = attachment.file_name.split(".xml")[0] return filename, filename.split("_")[1] - company_tax_id = doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id + company_tax_id = ( + doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id + ) progressive_name = frappe.model.naming.make_autoname(company_tax_id + "_.#####") progressive_number = progressive_name.split("_")[1] return progressive_name, progressive_number + def set_state_code(doc, method): - if doc.get('country_code'): + if doc.get("country_code"): doc.country_code = doc.country_code.upper() - if not doc.get('state'): + if not doc.get("state"): return - if not (hasattr(doc, "state_code") and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"]): + if not ( + hasattr(doc, "state_code") + and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"] + ): return - state_codes_lower = {key.lower():value for key,value in state_codes.items()} + state_codes_lower = {key.lower(): value for key, value in state_codes.items()} - state = doc.get('state','').lower() + state = doc.get("state", "").lower() if state_codes_lower.get(state): doc.state_code = state_codes_lower.get(state) diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json index a8da0bd2097..2343b4ec5cc 100644 --- a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json +++ b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json @@ -10,10 +10,10 @@ "docstatus": 0, "doctype": "Print Format", "font": "Default", - "html": "{% if letter_head and not no_letterhead -%}\n
    {{ letter_head }}
    \n{%- endif %}\n\n
    \n

    {{ doc.company }} 80G Donor Certificate

    \n
    \n

    \n\n
    \n

    {{ _(\"Certificate No. : \") }} {{ doc.name }}

    \n

    \n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
    \n

    \n

    \n \n
    \n\n This is to confirm that the {{ doc.company }} received an amount of {{doc.get_formatted(\"amount\")}}\n from {{ doc.donor_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n

    \n \n

    \n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n

    \n\n
    \n
    \n\n

    \n

    {{doc.company_address_display }}

    \n\n", + "html": "{% if letter_head and not no_letterhead -%}\n
    {{ letter_head }}
    \n{%- endif %}\n\n
    \n

    {{ doc.company }} 80G Donor Certificate

    \n
    \n

    \n\n
    \n

    {{ _(\"Certificate No. : \") }} {{ doc.name }}

    \n

    \n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
    \n

    \n

    \n \n
    \n\n This is to confirm that the {{ doc.company }} received an amount of {{doc.get_formatted(\"amount\")}}\n from {{ doc.donor_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.payment_id -%}\n bearing Payment ID {{ doc.payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n

    \n \n

    \n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n

    \n\n
    \n
    \n\n

    \n

    {{doc.company_address_display }}

    \n\n", "idx": 0, "line_breaks": 0, - "modified": "2021-02-22 00:20:08.516600", + "modified": "2022-03-16 17:25:33.420509", "modified_by": "Administrator", "module": "Regional", "name": "80G Certificate for Donation", diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index adc77e8921c..fb2f1cdcf05 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -27,134 +27,104 @@ COLUMNS = [ "label": "Umsatz (ohne Soll/Haben-Kz)", "fieldname": "Umsatz (ohne Soll/Haben-Kz)", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "label": "Soll/Haben-Kennzeichen", "fieldname": "Soll/Haben-Kennzeichen", "fieldtype": "Data", - "width": 100 - }, - { - "label": "Konto", - "fieldname": "Konto", - "fieldtype": "Data", - "width": 100 + "width": 100, }, + {"label": "Konto", "fieldname": "Konto", "fieldtype": "Data", "width": 100}, { "label": "Gegenkonto (ohne BU-Schlüssel)", "fieldname": "Gegenkonto (ohne BU-Schlüssel)", "fieldtype": "Data", - "width": 100 - }, - { - "label": "BU-Schlüssel", - "fieldname": "BU-Schlüssel", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Belegdatum", - "fieldname": "Belegdatum", - "fieldtype": "Date", - "width": 100 - }, - { - "label": "Belegfeld 1", - "fieldname": "Belegfeld 1", - "fieldtype": "Data", - "width": 150 - }, - { - "label": "Buchungstext", - "fieldname": "Buchungstext", - "fieldtype": "Text", - "width": 300 + "width": 100, }, + {"label": "BU-Schlüssel", "fieldname": "BU-Schlüssel", "fieldtype": "Data", "width": 100}, + {"label": "Belegdatum", "fieldname": "Belegdatum", "fieldtype": "Date", "width": 100}, + {"label": "Belegfeld 1", "fieldname": "Belegfeld 1", "fieldtype": "Data", "width": 150}, + {"label": "Buchungstext", "fieldname": "Buchungstext", "fieldtype": "Text", "width": 300}, { "label": "Beleginfo - Art 1", "fieldname": "Beleginfo - Art 1", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 1", "fieldname": "Beleginfo - Inhalt 1", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 1", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 2", "fieldname": "Beleginfo - Art 2", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 2", "fieldname": "Beleginfo - Inhalt 2", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 2", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 3", "fieldname": "Beleginfo - Art 3", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 3", "fieldname": "Beleginfo - Inhalt 3", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 3", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 4", "fieldname": "Beleginfo - Art 4", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 4", "fieldname": "Beleginfo - Inhalt 4", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 5", "fieldname": "Beleginfo - Art 5", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Inhalt 5", "fieldname": "Beleginfo - Inhalt 5", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Art 6", "fieldname": "Beleginfo - Art 6", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Inhalt 6", "fieldname": "Beleginfo - Inhalt 6", "fieldtype": "Date", - "width": 100 + "width": 100, }, - { - "label": "Fälligkeit", - "fieldname": "Fälligkeit", - "fieldtype": "Date", - "width": 100 - } + {"label": "Fälligkeit", "fieldname": "Fälligkeit", "fieldtype": "Date", "width": 100}, ] @@ -162,8 +132,8 @@ def execute(filters=None): """Entry point for frappe.""" data = [] if filters and validate(filters): - fn = 'temporary_against_account_number' - filters[fn] = frappe.get_value('DATEV Settings', filters.get('company'), fn) + fn = "temporary_against_account_number" + filters[fn] = frappe.get_value("DATEV Settings", filters.get("company"), fn) data = get_transactions(filters, as_dict=0) return COLUMNS, data @@ -171,23 +141,23 @@ def execute(filters=None): def validate(filters): """Make sure all mandatory filters and settings are present.""" - company = filters.get('company') + company = filters.get("company") if not company: - frappe.throw(_('Company is a mandatory filter.')) + frappe.throw(_("Company is a mandatory filter.")) - from_date = filters.get('from_date') + from_date = filters.get("from_date") if not from_date: - frappe.throw(_('From Date is a mandatory filter.')) + frappe.throw(_("From Date is a mandatory filter.")) - to_date = filters.get('to_date') + to_date = filters.get("to_date") if not to_date: - frappe.throw(_('To Date is a mandatory filter.')) + frappe.throw(_("To Date is a mandatory filter.")) validate_fiscal_year(from_date, to_date, company) - if not frappe.db.exists('DATEV Settings', filters.get('company')): - msg = 'Please create DATEV Settings for Company {}'.format(filters.get('company')) - frappe.log_error(msg, title='DATEV Settings missing') + if not frappe.db.exists("DATEV Settings", filters.get("company")): + msg = "Please create DATEV Settings for Company {}".format(filters.get("company")) + frappe.log_error(msg, title="DATEV Settings missing") return False return True @@ -197,7 +167,7 @@ def validate_fiscal_year(from_date, to_date, company): from_fiscal_year = get_fiscal_year(date=from_date, company=company) to_fiscal_year = get_fiscal_year(date=to_date, company=company) if from_fiscal_year != to_fiscal_year: - frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date)) + frappe.throw(_("Dates {} and {} are not in the same fiscal year.").format(from_date, to_date)) def get_transactions(filters, as_dict=1): @@ -213,7 +183,7 @@ def get_transactions(filters, as_dict=1): # specific query methods for some voucher types "Payment Entry": get_payment_entry_params, "Sales Invoice": get_sales_invoice_params, - "Purchase Invoice": get_purchase_invoice_params + "Purchase Invoice": get_purchase_invoice_params, } only_voucher_type = filters.get("voucher_type") @@ -309,7 +279,9 @@ def get_generic_params(filters): if filters.get("exclude_voucher_types"): # exclude voucher types that are queried by a dedicated method - exclude = "({})".format(', '.join("'{}'".format(key) for key in filters.get("exclude_voucher_types"))) + exclude = "({})".format( + ", ".join("'{}'".format(key) for key in filters.get("exclude_voucher_types")) + ) extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude) # if voucher type filter is set, allow only this type @@ -345,8 +317,7 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): /* against number or, if empty, party against number */ %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', - /* disable automatic VAT deduction */ - '40' as 'BU-Schlüssel', + '' as 'BU-Schlüssel', gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', @@ -382,10 +353,8 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): {extra_filters} ORDER BY 'Belegdatum', gl.voucher_no""".format( - extra_fields=extra_fields, - extra_joins=extra_joins, - extra_filters=extra_filters - ) + extra_fields=extra_fields, extra_joins=extra_joins, extra_filters=extra_filters + ) gl_entries = frappe.db.sql(query, filters, as_dict=as_dict) @@ -399,7 +368,8 @@ def get_customers(filters): Arguments: filters -- dict of filters to be passed to the sql query """ - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT par.debtor_creditor_number as 'Konto', @@ -451,7 +421,10 @@ def get_customers(filters): on country.name = adr.country WHERE adr.is_primary_address = '1' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) def get_suppliers(filters): @@ -461,7 +434,8 @@ def get_suppliers(filters): Arguments: filters -- dict of filters to be passed to the sql query """ - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT par.debtor_creditor_number as 'Konto', @@ -514,11 +488,15 @@ def get_suppliers(filters): on country.name = adr.country WHERE adr.is_primary_address = '1' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) def get_account_names(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT account_number as 'Konto', @@ -529,7 +507,10 @@ def get_account_names(filters): WHERE company = %(company)s AND is_group = 0 AND account_number != '' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) @frappe.whitelist() @@ -549,40 +530,43 @@ def download_datev_csv(filters): filters = json.loads(filters) validate(filters) - company = filters.get('company') + company = filters.get("company") - fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=company) - filters['fiscal_year_start'] = fiscal_year[1] + fiscal_year = get_fiscal_year(date=filters.get("from_date"), company=company) + filters["fiscal_year_start"] = fiscal_year[1] # set chart of accounts used - coa = frappe.get_value('Company', company, 'chart_of_accounts') - filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + coa = frappe.get_value("Company", company, "chart_of_accounts") + filters["skr"] = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") - datev_settings = frappe.get_doc('DATEV Settings', company) - filters['account_number_length'] = datev_settings.account_number_length - filters['temporary_against_account_number'] = datev_settings.temporary_against_account_number + datev_settings = frappe.get_doc("DATEV Settings", company) + filters["account_number_length"] = datev_settings.account_number_length + filters["temporary_against_account_number"] = datev_settings.temporary_against_account_number transactions = get_transactions(filters) account_names = get_account_names(filters) customers = get_customers(filters) suppliers = get_suppliers(filters) - zip_name = '{} DATEV.zip'.format(frappe.utils.datetime.date.today()) - zip_and_download(zip_name, [ - { - 'file_name': 'EXTF_Buchungsstapel.csv', - 'csv_data': get_datev_csv(transactions, filters, csv_class=Transactions) - }, - { - 'file_name': 'EXTF_Kontenbeschriftungen.csv', - 'csv_data': get_datev_csv(account_names, filters, csv_class=AccountNames) - }, - { - 'file_name': 'EXTF_Kunden.csv', - 'csv_data': get_datev_csv(customers, filters, csv_class=DebtorsCreditors) - }, - { - 'file_name': 'EXTF_Lieferanten.csv', - 'csv_data': get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors) - }, - ]) + zip_name = "{} DATEV.zip".format(frappe.utils.datetime.date.today()) + zip_and_download( + zip_name, + [ + { + "file_name": "EXTF_Buchungsstapel.csv", + "csv_data": get_datev_csv(transactions, filters, csv_class=Transactions), + }, + { + "file_name": "EXTF_Kontenbeschriftungen.csv", + "csv_data": get_datev_csv(account_names, filters, csv_class=AccountNames), + }, + { + "file_name": "EXTF_Kunden.csv", + "csv_data": get_datev_csv(customers, filters, csv_class=DebtorsCreditors), + }, + { + "file_name": "EXTF_Lieferanten.csv", + "csv_data": get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors), + }, + ], + ) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index 7ca0b1b50fe..6e6847b896b 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -25,15 +25,17 @@ from erpnext.regional.report.datev.datev import ( def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "EUR", - "country": "Germany", - "create_chart_of_accounts_based_on": "Standard Template", - "chart_of_accounts": "SKR04 mit Kontonummern" - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "EUR", + "country": "Germany", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "SKR04 mit Kontonummern", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -47,17 +49,20 @@ def make_company(company_name, abbr): company.save() return company + def setup_fiscal_year(): fiscal_year = None year = cstr(now_datetime().year) if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"): try: - fiscal_year = frappe.get_doc({ - "doctype": "Fiscal Year", - "year": year, - "year_start_date": "{0}-01-01".format(year), - "year_end_date": "{0}-12-31".format(year) - }) + fiscal_year = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": year, + "year_start_date": "{0}-01-01".format(year), + "year_end_date": "{0}-12-31".format(year), + } + ) fiscal_year.insert() except frappe.NameError: pass @@ -65,75 +70,78 @@ def setup_fiscal_year(): if fiscal_year: fiscal_year.set_as_default() + def make_customer_with_account(customer_name, company): - acc_name = frappe.db.get_value("Account", { - "account_name": customer_name, - "company": company.name - }, "name") + acc_name = frappe.db.get_value( + "Account", {"account_name": customer_name, "company": company.name}, "name" + ) if not acc_name: - acc = frappe.get_doc({ - "doctype": "Account", - "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", - "account_name": customer_name, - "company": company.name, - "account_type": "Receivable", - "account_number": "10001" - }) + acc = frappe.get_doc( + { + "doctype": "Account", + "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", + "account_name": customer_name, + "company": company.name, + "account_type": "Receivable", + "account_number": "10001", + } + ) acc.insert() acc_name = acc.name if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": customer_name, - "customer_type": "Company", - "accounts": [{ - "company": company.name, - "account": acc_name - }] - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": customer_name, + "customer_type": "Company", + "accounts": [{"company": company.name, "account": acc_name}], + } + ) customer.insert() else: customer = frappe.get_doc("Customer", customer_name) return customer + def make_item(item_code, company): - warehouse_name = frappe.db.get_value("Warehouse", { - "warehouse_name": "Stores", - "company": company.name - }, "name") + warehouse_name = frappe.db.get_value( + "Warehouse", {"warehouse_name": "Stores", "company": company.name}, "name" + ) if not frappe.db.exists("Item", item_code): - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "All Item Groups", - "is_stock_item": 0, - "is_purchase_item": 0, - "is_customer_provided_item": 0, - "item_defaults": [{ - "default_warehouse": warehouse_name, - "company": company.name - }] - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "All Item Groups", + "is_stock_item": 0, + "is_purchase_item": 0, + "is_customer_provided_item": 0, + "item_defaults": [{"default_warehouse": warehouse_name, "company": company.name}], + } + ) item.insert() else: item = frappe.get_doc("Item", item_code) return item + def make_datev_settings(company): if not frappe.db.exists("DATEV Settings", company.name): - frappe.get_doc({ - "doctype": "DATEV Settings", - "client": company.name, - "client_number": "12345", - "consultant_number": "67890", - "temporary_against_account_number": "9999" - }).insert() + frappe.get_doc( + { + "doctype": "DATEV Settings", + "client": company.name, + "client_number": "12345", + "consultant_number": "67890", + "temporary_against_account_number": "9999", + } + ).insert() class TestDatev(TestCase): @@ -144,27 +152,24 @@ class TestDatev(TestCase): "company": self.company.name, "from_date": today(), "to_date": today(), - "temporary_against_account_number": "9999" + "temporary_against_account_number": "9999", } make_datev_settings(self.company) item = make_item("_Test Item", self.company) setup_fiscal_year() - warehouse = frappe.db.get_value("Item Default", { - "parent": item.name, - "company": self.company.name - }, "default_warehouse") + warehouse = frappe.db.get_value( + "Item Default", {"parent": item.name, "company": self.company.name}, "default_warehouse" + ) - income_account = frappe.db.get_value("Account", { - "account_number": "4200", - "company": self.company.name - }, "name") + income_account = frappe.db.get_value( + "Account", {"account_number": "4200", "company": self.company.name}, "name" + ) - tax_account = frappe.db.get_value("Account", { - "account_number": "3806", - "company": self.company.name - }, "name") + tax_account = frappe.db.get_value( + "Account", {"account_number": "3806", "company": self.company.name}, "name" + ) si = create_sales_invoice( company=self.company.name, @@ -176,16 +181,19 @@ class TestDatev(TestCase): cost_center=self.company.cost_center, warehouse=warehouse, item=item.name, - do_not_save=1 + do_not_save=1, ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": tax_account, - "description": "Umsatzsteuer 19 %", - "rate": 19, - "cost_center": self.company.cost_center - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": "Umsatzsteuer 19 %", + "rate": 19, + "cost_center": self.company.cost_center, + }, + ) si.cost_center = self.company.cost_center @@ -221,16 +229,18 @@ class TestDatev(TestCase): self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors)) def test_csv(self): - test_data = [{ - "Umsatz (ohne Soll/Haben-Kz)": 100, - "Soll/Haben-Kennzeichen": "H", - "Kontonummer": "4200", - "Gegenkonto (ohne BU-Schlüssel)": "10000", - "Belegdatum": today(), - "Buchungstext": "No remark", - "Beleginfo - Art 1": "Sales Invoice", - "Beleginfo - Inhalt 1": "SINV-0001" - }] + test_data = [ + { + "Umsatz (ohne Soll/Haben-Kz)": 100, + "Soll/Haben-Kennzeichen": "H", + "Kontonummer": "4200", + "Gegenkonto (ohne BU-Schlüssel)": "10000", + "Belegdatum": today(), + "Buchungstext": "No remark", + "Beleginfo - Art 1": "Sales Invoice", + "Beleginfo - Inhalt 1": "SINV-0001", + } + ] get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions) def test_download(self): @@ -239,6 +249,6 @@ class TestDatev(TestCase): # zipfile.is_zipfile() expects a file-like object zip_buffer = BytesIO() - zip_buffer.write(frappe.response['filecontent']) + zip_buffer.write(frappe.response["filecontent"]) self.assertTrue(zipfile.is_zipfile(zip_buffer)) diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py index 5ec7d85b9dd..694615d7422 100644 --- a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py @@ -14,96 +14,68 @@ def execute(filters=None): return columns, data + def validate_filters(filters=None): filters = frappe._dict(filters or {}) if not filters.company: - frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter')) + frappe.throw( + _("{} is mandatory for generating E-Invoice Summary Report").format(_("Company")), + title=_("Invalid Filter"), + ) if filters.company: # validate if company has e-invoicing enabled pass if not filters.from_date or not filters.to_date: - frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter')) + frappe.throw( + _("From Date & To Date is mandatory for generating E-Invoice Summary Report"), + title=_("Invalid Filter"), + ) if filters.from_date > filters.to_date: - frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter')) + frappe.throw(_("From Date must be before To Date"), title=_("Invalid Filter")) + def get_data(filters=None): if not filters: filters = {} query_filters = { - 'posting_date': ['between', [filters.from_date, filters.to_date]], - 'einvoice_status': ['is', 'set'], - 'company': filters.company + "posting_date": ["between", [filters.from_date, filters.to_date]], + "einvoice_status": ["is", "set"], + "company": filters.company, } if filters.customer: - query_filters['customer'] = filters.customer + query_filters["customer"] = filters.customer if filters.status: - query_filters['einvoice_status'] = filters.status + query_filters["einvoice_status"] = filters.status data = frappe.get_all( - 'Sales Invoice', - filters=query_filters, - fields=[d.get('fieldname') for d in get_columns()] + "Sales Invoice", filters=query_filters, fields=[d.get("fieldname") for d in get_columns()] ) return data + def get_columns(): return [ - { - "fieldtype": "Date", - "fieldname": "posting_date", - "label": _("Posting Date"), - "width": 0 - }, + {"fieldtype": "Date", "fieldname": "posting_date", "label": _("Posting Date"), "width": 0}, { "fieldtype": "Link", "fieldname": "name", "label": _("Sales Invoice"), "options": "Sales Invoice", - "width": 140 - }, - { - "fieldtype": "Data", - "fieldname": "einvoice_status", - "label": _("Status"), - "width": 100 - }, - { - "fieldtype": "Link", - "fieldname": "customer", - "options": "Customer", - "label": _("Customer") - }, - { - "fieldtype": "Check", - "fieldname": "is_return", - "label": _("Is Return"), - "width": 85 - }, - { - "fieldtype": "Data", - "fieldname": "ack_no", - "label": "Ack. No.", - "width": 145 - }, - { - "fieldtype": "Data", - "fieldname": "ack_date", - "label": "Ack. Date", - "width": 165 - }, - { - "fieldtype": "Data", - "fieldname": "irn", - "label": _("IRN No."), - "width": 250 + "width": 140, }, + {"fieldtype": "Data", "fieldname": "einvoice_status", "label": _("Status"), "width": 100}, + {"fieldtype": "Link", "fieldname": "customer", "options": "Customer", "label": _("Customer")}, + {"fieldtype": "Check", "fieldname": "is_return", "label": _("Is Return"), "width": 85}, + {"fieldtype": "Data", "fieldname": "ack_no", "label": "Ack. No.", "width": 145}, + {"fieldtype": "Data", "fieldname": "ack_date", "label": "Ack. Date", "width": 165}, + {"fieldtype": "Data", "fieldname": "irn", "label": _("IRN No."), "width": 250}, { "fieldtype": "Currency", "options": "Company:company:default_currency", "fieldname": "base_grand_total", "label": _("Grand Total"), - "width": 120 - } + "width": 120, + }, ] diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py index f3fe5e88488..8dcd6a365df 100644 --- a/erpnext/regional/report/eway_bill/eway_bill.py +++ b/erpnext/regional/report/eway_bill/eway_bill.py @@ -11,35 +11,41 @@ from frappe.utils import nowdate def execute(filters=None): - if not filters: filters.setdefault('posting_date', [nowdate(), nowdate()]) + if not filters: + filters.setdefault("posting_date", [nowdate(), nowdate()]) columns, data = [], [] columns = get_columns() data = get_data(filters) return columns, data + def get_data(filters): conditions = get_conditions(filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT dn.name as dn_id, dn.posting_date, dn.company, dn.company_gstin, dn.customer, dn.customer_gstin, dni.item_code, dni.item_name, dni.description, dni.gst_hsn_code, dni.uom, dni.qty, dni.amount, dn.mode_of_transport, dn.distance, dn.transporter_name, dn.gst_transporter_id, dn.lr_no, dn.lr_date, dn.vehicle_no, dn.gst_vehicle_type, dn.company_address, dn.shipping_address_name FROM `tabDelivery Note` AS dn join `tabDelivery Note Item` AS dni on (dni.parent = dn.name) WHERE dn.docstatus < 2 - %s """ % conditions, as_dict=1) + %s """ + % conditions, + as_dict=1, + ) unit = { - 'Bag': "BAGS", - 'Bottle': "BOTTLES", - 'Kg': "KILOGRAMS", - 'Liter': "LITERS", - 'Meter': "METERS", - 'Nos': "NUMBERS", - 'PKT': "PACKS", - 'Roll': "ROLLS", - 'Set': "SETS" + "Bag": "BAGS", + "Bottle": "BOTTLES", + "Kg": "KILOGRAMS", + "Liter": "LITERS", + "Meter": "METERS", + "Nos": "NUMBERS", + "PKT": "PACKS", + "Roll": "ROLLS", + "Set": "SETS", } # Regular expression set to remove all the special characters @@ -51,11 +57,11 @@ def get_data(filters): set_address_details(row, special_characters) # Eway Bill accepts date as dd/mm/yyyy and not dd-mm-yyyy - row.posting_date = '/'.join(str(row.posting_date).replace("-", "/").split('/')[::-1]) - row.lr_date = '/'.join(str(row.lr_date).replace("-", "/").split('/')[::-1]) + row.posting_date = "/".join(str(row.posting_date).replace("-", "/").split("/")[::-1]) + row.lr_date = "/".join(str(row.lr_date).replace("-", "/").split("/")[::-1]) - if row.gst_vehicle_type == 'Over Dimensional Cargo (ODC)': - row.gst_vehicle_type = 'ODC' + if row.gst_vehicle_type == "Over Dimensional Cargo (ODC)": + row.gst_vehicle_type = "ODC" row.item_name = re.sub(special_characters, " ", row.item_name) row.description = row.item_name @@ -67,58 +73,80 @@ def get_data(filters): return data + def get_conditions(filters): conditions = "" - conditions += filters.get('company') and " AND dn.company = '%s' " % filters.get('company') or "" - conditions += filters.get('posting_date') and " AND dn.posting_date >= '%s' AND dn.posting_date <= '%s' " % (filters.get('posting_date')[0], filters.get('posting_date')[1]) or "" - conditions += filters.get('delivery_note') and " AND dn.name = '%s' " % filters.get('delivery_note') or "" - conditions += filters.get('customer') and " AND dn.customer = '%s' " % filters.get('customer').replace("'", "\'") or "" + conditions += filters.get("company") and " AND dn.company = '%s' " % filters.get("company") or "" + conditions += ( + filters.get("posting_date") + and " AND dn.posting_date >= '%s' AND dn.posting_date <= '%s' " + % (filters.get("posting_date")[0], filters.get("posting_date")[1]) + or "" + ) + conditions += ( + filters.get("delivery_note") and " AND dn.name = '%s' " % filters.get("delivery_note") or "" + ) + conditions += ( + filters.get("customer") + and " AND dn.customer = '%s' " % filters.get("customer").replace("'", "'") + or "" + ) return conditions + def set_defaults(row): - row.setdefault(u'supply_type', "Outward") - row.setdefault(u'sub_type', "Supply") - row.setdefault(u'doc_type', "Delivery Challan") + row.setdefault("supply_type", "Outward") + row.setdefault("sub_type", "Supply") + row.setdefault("doc_type", "Delivery Challan") + def set_address_details(row, special_characters): - if row.get('company_address'): - address_line1, address_line2, city, pincode, state = frappe.db.get_value("Address", row.get('company_address'), ['address_line1', 'address_line2', 'city', 'pincode', 'state']) + if row.get("company_address"): + address_line1, address_line2, city, pincode, state = frappe.db.get_value( + "Address", + row.get("company_address"), + ["address_line1", "address_line2", "city", "pincode", "state"], + ) - row.update({'from_address_1': re.sub(special_characters, "", address_line1 or '')}) - row.update({'from_address_2': re.sub(special_characters, "", address_line2 or '')}) - row.update({'from_place': city and city.upper() or ''}) - row.update({'from_pin_code': pincode and pincode.replace(" ", "") or ''}) - row.update({'from_state': state and state.upper() or ''}) - row.update({'dispatch_state': row.from_state}) + row.update({"from_address_1": re.sub(special_characters, "", address_line1 or "")}) + row.update({"from_address_2": re.sub(special_characters, "", address_line2 or "")}) + row.update({"from_place": city and city.upper() or ""}) + row.update({"from_pin_code": pincode and pincode.replace(" ", "") or ""}) + row.update({"from_state": state and state.upper() or ""}) + row.update({"dispatch_state": row.from_state}) - if row.get('shipping_address_name'): - address_line1, address_line2, city, pincode, state = frappe.db.get_value("Address", row.get('shipping_address_name'), ['address_line1', 'address_line2', 'city', 'pincode', 'state']) + if row.get("shipping_address_name"): + address_line1, address_line2, city, pincode, state = frappe.db.get_value( + "Address", + row.get("shipping_address_name"), + ["address_line1", "address_line2", "city", "pincode", "state"], + ) + + row.update({"to_address_1": re.sub(special_characters, "", address_line1 or "")}) + row.update({"to_address_2": re.sub(special_characters, "", address_line2 or "")}) + row.update({"to_place": city and city.upper() or ""}) + row.update({"to_pin_code": pincode and pincode.replace(" ", "") or ""}) + row.update({"to_state": state and state.upper() or ""}) + row.update({"ship_to_state": row.to_state}) - row.update({'to_address_1': re.sub(special_characters, "", address_line1 or '')}) - row.update({'to_address_2': re.sub(special_characters, "", address_line2 or '')}) - row.update({'to_place': city and city.upper() or ''}) - row.update({'to_pin_code': pincode and pincode.replace(" ", "") or ''}) - row.update({'to_state': state and state.upper() or ''}) - row.update({'ship_to_state': row.to_state}) def set_taxes(row, filters): - taxes = frappe.get_all("Sales Taxes and Charges", - filters={ - 'parent': row.dn_id - }, - fields=('item_wise_tax_detail', 'account_head')) + taxes = frappe.get_all( + "Sales Taxes and Charges", + filters={"parent": row.dn_id}, + fields=("item_wise_tax_detail", "account_head"), + ) account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"] - taxes_list = frappe.get_all("GST Account", - filters={ - "parent": "GST Settings", - "company": filters.company - }, - fields=account_list) + taxes_list = frappe.get_all( + "GST Account", + filters={"parent": "GST Settings", "company": filters.company}, + fields=account_list, + ) if not taxes_list: frappe.throw(_("Please set GST Accounts in GST Settings")) @@ -141,253 +169,89 @@ def set_taxes(row, filters): item_tax_rate.pop(tax[key]) row.amount = float(row.amount) + sum(i[1] for i in item_tax_rate.values()) - row.update({'tax_rate': '+'.join(tax_rate)}) + row.update({"tax_rate": "+".join(tax_rate)}) + def get_columns(): columns = [ - { - "fieldname": "supply_type", - "label": _("Supply Type"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "sub_type", - "label": _("Sub Type"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "doc_type", - "label": _("Doc Type"), - "fieldtype": "Data", - "width": 100 - }, + {"fieldname": "supply_type", "label": _("Supply Type"), "fieldtype": "Data", "width": 100}, + {"fieldname": "sub_type", "label": _("Sub Type"), "fieldtype": "Data", "width": 100}, + {"fieldname": "doc_type", "label": _("Doc Type"), "fieldtype": "Data", "width": 100}, { "fieldname": "dn_id", "label": _("Doc Name"), "fieldtype": "Link", "options": "Delivery Note", - "width": 140 - }, - { - "fieldname": "posting_date", - "label": _("Doc Date"), - "fieldtype": "Data", - "width": 100 + "width": 140, }, + {"fieldname": "posting_date", "label": _("Doc Date"), "fieldtype": "Data", "width": 100}, { "fieldname": "company", "label": _("From Party Name"), "fieldtype": "Link", "options": "Company", - "width": 120 - }, - { - "fieldname": "company_gstin", - "label": _("From GSTIN"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "from_address_1", - "label": _("From Address 1"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "from_address_2", - "label": _("From Address 2"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "from_place", - "label": _("From Place"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "from_pin_code", - "label": _("From Pin Code"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "from_state", - "label": _("From State"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "dispatch_state", - "label": _("Dispatch State"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "customer", - "label": _("To Party Name"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "customer_gstin", - "label": _("To GSTIN"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_address_1", - "label": _("To Address 1"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_address_2", - "label": _("To Address 2"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_place", - "label": _("To Place"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "to_pin_code", - "label": _("To Pin Code"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "to_state", - "label": _("To State"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "ship_to_state", - "label": _("Ship To State"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "company_gstin", "label": _("From GSTIN"), "fieldtype": "Data", "width": 100}, + {"fieldname": "from_address_1", "label": _("From Address 1"), "fieldtype": "Data", "width": 120}, + {"fieldname": "from_address_2", "label": _("From Address 2"), "fieldtype": "Data", "width": 120}, + {"fieldname": "from_place", "label": _("From Place"), "fieldtype": "Data", "width": 80}, + {"fieldname": "from_pin_code", "label": _("From Pin Code"), "fieldtype": "Data", "width": 80}, + {"fieldname": "from_state", "label": _("From State"), "fieldtype": "Data", "width": 80}, + {"fieldname": "dispatch_state", "label": _("Dispatch State"), "fieldtype": "Data", "width": 100}, + {"fieldname": "customer", "label": _("To Party Name"), "fieldtype": "Data", "width": 120}, + {"fieldname": "customer_gstin", "label": _("To GSTIN"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_address_1", "label": _("To Address 1"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_address_2", "label": _("To Address 2"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_place", "label": _("To Place"), "fieldtype": "Data", "width": 80}, + {"fieldname": "to_pin_code", "label": _("To Pin Code"), "fieldtype": "Data", "width": 80}, + {"fieldname": "to_state", "label": _("To State"), "fieldtype": "Data", "width": 80}, + {"fieldname": "ship_to_state", "label": _("Ship To State"), "fieldtype": "Data", "width": 100}, { "fieldname": "item_name", "label": _("Product"), "fieldtype": "Link", "options": "Item", - "width": 120 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "gst_hsn_code", - "label": _("HSN"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "uom", - "label": _("Unit"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "qty", - "label": _("Qty"), - "fieldtype": "Float", - "width": 100 - }, - { - "fieldname": "amount", - "label": _("Accessable Value"), - "fieldtype": "Float", - "width": 120 - }, - { - "fieldname": "tax_rate", - "label": _("Tax Rate"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "cgst_amount", - "label": _("CGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "sgst_amount", - "label": _("SGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "igst_amount", - "label": _("IGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "cess_amount", - "label": _("CESS Amount"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 100}, + {"fieldname": "gst_hsn_code", "label": _("HSN"), "fieldtype": "Data", "width": 120}, + {"fieldname": "uom", "label": _("Unit"), "fieldtype": "Data", "width": 100}, + {"fieldname": "qty", "label": _("Qty"), "fieldtype": "Float", "width": 100}, + {"fieldname": "amount", "label": _("Accessable Value"), "fieldtype": "Float", "width": 120}, + {"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 100}, + {"fieldname": "cgst_amount", "label": _("CGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "sgst_amount", "label": _("SGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "igst_amount", "label": _("IGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "cess_amount", "label": _("CESS Amount"), "fieldtype": "Data", "width": 100}, { "fieldname": "mode_of_transport", "label": _("Mode of Transport"), "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "distance", - "label": _("Distance"), - "fieldtype": "Data", - "width": 100 + "width": 100, }, + {"fieldname": "distance", "label": _("Distance"), "fieldtype": "Data", "width": 100}, { "fieldname": "transporter_name", "label": _("Transporter Name"), "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "gst_transporter_id", "label": _("Transporter ID"), "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "lr_no", - "label": _("Transport Receipt No"), - "fieldtype": "Data", - "width": 120 + "width": 100, }, + {"fieldname": "lr_no", "label": _("Transport Receipt No"), "fieldtype": "Data", "width": 120}, { "fieldname": "lr_date", "label": _("Transport Receipt Date"), "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "vehicle_no", - "label": _("Vehicle No"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "gst_vehicle_type", - "label": _("Vehicle Type"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "vehicle_no", "label": _("Vehicle No"), "fieldtype": "Data", "width": 100}, + {"fieldname": "gst_vehicle_type", "label": _("Vehicle Type"), "fieldtype": "Data", "width": 100}, ] return columns diff --git a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py index 59888ff94e7..c75179ee5d1 100644 --- a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py +++ b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py @@ -26,31 +26,42 @@ def execute(filters=None): def validate_filters(filters, account_details): - if not filters.get('company'): - frappe.throw(_('{0} is mandatory').format(_('Company'))) + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) - if not filters.get('fiscal_year'): - frappe.throw(_('{0} is mandatory').format(_('Fiscal Year'))) + if not filters.get("fiscal_year"): + frappe.throw(_("{0} is mandatory").format(_("Fiscal Year"))) def set_account_currency(filters): - filters["company_currency"] = frappe.get_cached_value('Company', filters.company, "default_currency") + filters["company_currency"] = frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) return filters def get_columns(filters): columns = [ - "JournalCode" + "::90", "JournalLib" + "::90", - "EcritureNum" + ":Dynamic Link:90", "EcritureDate" + "::90", - "CompteNum" + ":Link/Account:100", "CompteLib" + ":Link/Account:200", - "CompAuxNum" + "::90", "CompAuxLib" + "::90", - "PieceRef" + "::90", "PieceDate" + "::90", - "EcritureLib" + "::90", "Debit" + "::90", "Credit" + "::90", - "EcritureLet" + "::90", "DateLet" + - "::90", "ValidDate" + "::90", - "Montantdevise" + "::90", "Idevise" + "::90" + "JournalCode" + "::90", + "JournalLib" + "::90", + "EcritureNum" + ":Dynamic Link:90", + "EcritureDate" + "::90", + "CompteNum" + ":Link/Account:100", + "CompteLib" + ":Link/Account:200", + "CompAuxNum" + "::90", + "CompAuxLib" + "::90", + "PieceRef" + "::90", + "PieceDate" + "::90", + "EcritureLib" + "::90", + "Debit" + "::90", + "Credit" + "::90", + "EcritureLet" + "::90", + "DateLet" + "::90", + "ValidDate" + "::90", + "Montantdevise" + "::90", + "Idevise" + "::90", ] return columns @@ -66,10 +77,14 @@ def get_result(filters): def get_gl_entries(filters): - group_by_condition = "group by voucher_type, voucher_no, account" \ - if filters.get("group_by_voucher") else "group by gl.name" + group_by_condition = ( + "group by voucher_type, voucher_no, account" + if filters.get("group_by_voucher") + else "group by gl.name" + ) - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date, sum(gl.debit) as debit, sum(gl.credit) as credit, @@ -99,8 +114,12 @@ def get_gl_entries(filters): left join `tabMember` mem on gl.party = mem.name where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s {group_by_condition} - order by GlPostDate, voucher_no"""\ - .format(group_by_condition=group_by_condition), filters, as_dict=1) + order by GlPostDate, voucher_no""".format( + group_by_condition=group_by_condition + ), + filters, + as_dict=1, + ) return gl_entries @@ -108,25 +127,37 @@ def get_gl_entries(filters): def get_result_as_list(data, filters): result = [] - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") - accounts = frappe.get_all("Account", filters={"Company": filters.company}, fields=["name", "account_number"]) + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") + accounts = frappe.get_all( + "Account", filters={"Company": filters.company}, fields=["name", "account_number"] + ) for d in data: JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0] - if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith("{0}/".format(JournalCode)): + if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith( + "{0}/".format(JournalCode) + ): EcritureNum = re.split("-|/", d.get("voucher_no"))[1] else: - EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE).group(1) + EcritureNum = re.search( + r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE + ).group(1) EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") - account_number = [account.account_number for account in accounts if account.name == d.get("account")] + account_number = [ + account.account_number for account in accounts if account.name == d.get("account") + ] if account_number[0] is not None: - CompteNum = account_number[0] + CompteNum = account_number[0] else: - frappe.throw(_("Account number for account {0} is not available.
    Please setup your Chart of Accounts correctly.").format(d.get("account"))) + frappe.throw( + _( + "Account number for account {0} is not available.
    Please setup your Chart of Accounts correctly." + ).format(d.get("account")) + ) if d.get("party_type") == "Customer": CompAuxNum = d.get("cusName") @@ -172,19 +203,45 @@ def get_result_as_list(data, filters): PieceDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") - debit = '{:.2f}'.format(d.get("debit")).replace(".", ",") + debit = "{:.2f}".format(d.get("debit")).replace(".", ",") - credit = '{:.2f}'.format(d.get("credit")).replace(".", ",") + credit = "{:.2f}".format(d.get("credit")).replace(".", ",") Idevise = d.get("account_currency") if Idevise != company_currency: - Montantdevise = '{:.2f}'.format(d.get("debitCurr")).replace(".", ",") if d.get("debitCurr") != 0 else '{:.2f}'.format(d.get("creditCurr")).replace(".", ",") + Montantdevise = ( + "{:.2f}".format(d.get("debitCurr")).replace(".", ",") + if d.get("debitCurr") != 0 + else "{:.2f}".format(d.get("creditCurr")).replace(".", ",") + ) else: - Montantdevise = '{:.2f}'.format(d.get("debit")).replace(".", ",") if d.get("debit") != 0 else '{:.2f}'.format(d.get("credit")).replace(".", ",") + Montantdevise = ( + "{:.2f}".format(d.get("debit")).replace(".", ",") + if d.get("debit") != 0 + else "{:.2f}".format(d.get("credit")).replace(".", ",") + ) - row = [JournalCode, d.get("voucher_type"), EcritureNum, EcritureDate, CompteNum, d.get("account"), CompAuxNum, CompAuxLib, - PieceRef, PieceDate, EcritureLib, debit, credit, "", "", ValidDate, Montantdevise, Idevise] + row = [ + JournalCode, + d.get("voucher_type"), + EcritureNum, + EcritureDate, + CompteNum, + d.get("account"), + CompAuxNum, + CompAuxLib, + PieceRef, + PieceDate, + EcritureLib, + debit, + credit, + "", + "", + ValidDate, + Montantdevise, + Idevise, + ] result.append(row) diff --git a/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py b/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py index 528868cf176..fec63f2f18a 100644 --- a/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py +++ b/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py @@ -8,24 +8,28 @@ from erpnext.accounts.report.item_wise_purchase_register.item_wise_purchase_regi def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Supplier GSTIN', fieldname="supplier_gstin", width=120), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130), - dict(fieldtype='Data', label='HSN Code', fieldname="gst_hsn_code", width=120), - dict(fieldtype='Data', label='Supplier Invoice No', fieldname="bill_no", width=120), - dict(fieldtype='Date', label='Supplier Invoice Date', fieldname="bill_date", width=100) - ], additional_query_columns=[ - 'supplier_gstin', - 'company_gstin', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin', - 'gst_hsn_code', - 'bill_no', - 'bill_date' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Supplier GSTIN", fieldname="supplier_gstin", width=120), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120), + dict(fieldtype="Data", label="Supplier Invoice No", fieldname="bill_no", width=120), + dict(fieldtype="Date", label="Supplier Invoice Date", fieldname="bill_date", width=100), + ], + additional_query_columns=[ + "supplier_gstin", + "company_gstin", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + "gst_hsn_code", + "bill_no", + "bill_date", + ], + ) diff --git a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py index 386e2197569..bb1843f1bd9 100644 --- a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py +++ b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py @@ -6,24 +6,30 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Customer GSTIN', fieldname="customer_gstin", width=120), - dict(fieldtype='Data', label='Billing Address GSTIN', fieldname="billing_address_gstin", width=140), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Place of Supply', fieldname="place_of_supply", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130), - dict(fieldtype='Data', label='HSN Code', fieldname="gst_hsn_code", width=120) - ], additional_query_columns=[ - 'customer_gstin', - 'billing_address_gstin', - 'company_gstin', - 'place_of_supply', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin', - 'gst_hsn_code' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120), + dict( + fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140 + ), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120), + ], + additional_query_columns=[ + "customer_gstin", + "billing_address_gstin", + "company_gstin", + "place_of_supply", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + "gst_hsn_code", + ], + ) diff --git a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py index 2d994082c31..609dbbaf73b 100644 --- a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py +++ b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py @@ -6,18 +6,22 @@ from erpnext.accounts.report.purchase_register.purchase_register import _execute def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Supplier GSTIN', fieldname="supplier_gstin", width=120), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130) - ], additional_query_columns=[ - 'supplier_gstin', - 'company_gstin', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Supplier GSTIN", fieldname="supplier_gstin", width=120), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + ], + additional_query_columns=[ + "supplier_gstin", + "company_gstin", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + ], + ) diff --git a/erpnext/regional/report/gst_sales_register/gst_sales_register.py b/erpnext/regional/report/gst_sales_register/gst_sales_register.py index a6f2b3dbf4d..94ceb197b1a 100644 --- a/erpnext/regional/report/gst_sales_register/gst_sales_register.py +++ b/erpnext/regional/report/gst_sales_register/gst_sales_register.py @@ -6,22 +6,28 @@ from erpnext.accounts.report.sales_register.sales_register import _execute def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Customer GSTIN', fieldname="customer_gstin", width=120), - dict(fieldtype='Data', label='Billing Address GSTIN', fieldname="billing_address_gstin", width=140), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Place of Supply', fieldname="place_of_supply", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130) - ], additional_query_columns=[ - 'customer_gstin', - 'billing_address_gstin', - 'company_gstin', - 'place_of_supply', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120), + dict( + fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140 + ), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + ], + additional_query_columns=[ + "customer_gstin", + "billing_address_gstin", + "company_gstin", + "place_of_supply", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + ], + ) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 9999a6d167b..943bd2c3d20 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -78,8 +78,9 @@ frappe.query_reports["GSTR-1"] = { } }); - report.page.add_inner_button(__("Download as JSON"), function () { + let filters = report.get_values(); + frappe.call({ method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', args: { diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 1ba3d20bdbb..0aece86a681 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -16,6 +16,7 @@ from erpnext.regional.india.utils import get_gst_accounts def execute(filters=None): return Gstr1Report(filters).run() + class Gstr1Report(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -60,7 +61,7 @@ class Gstr1Report(object): return self.columns, self.data def get_data(self): - if self.filters.get("type_of_business") in ("B2C Small", "B2C Large"): + if self.filters.get("type_of_business") in ("B2C Small", "B2C Large"): self.get_b2c_data() elif self.filters.get("type_of_business") == "Advances": self.get_advance_data() @@ -84,15 +85,17 @@ class Gstr1Report(object): advances = self.get_advance_entries() for entry in advances: # only consider IGST and SGST so as to avoid duplication of taxable amount - if entry.account_head in self.gst_accounts.igst_account or \ - entry.account_head in self.gst_accounts.sgst_account: + if ( + entry.account_head in self.gst_accounts.igst_account + or entry.account_head in self.gst_accounts.sgst_account + ): advances_data.setdefault((entry.place_of_supply, entry.rate), [0.0, 0.0]) - advances_data[(entry.place_of_supply, entry.rate)][0] += (entry.amount * 100 / entry.rate) + advances_data[(entry.place_of_supply, entry.rate)][0] += entry.amount * 100 / entry.rate elif entry.account_head in self.gst_accounts.cess_account: advances_data[(entry.place_of_supply, entry.rate)][1] += entry.amount for key, value in advances_data.items(): - row= [key[0], key[1], value[0], value[1]] + row = [key[0], key[1], value[0], value[1]] self.data.append(row) def get_nil_rated_invoices(self): @@ -101,31 +104,31 @@ class Gstr1Report(object): "description": "Inter-State supplies to registered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Intra-State supplies to registered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Inter-State supplies to unregistered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Intra-State supplies to unregistered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 - } + "non_gst": 0.0, + }, ] for invoice, details in self.nil_exempt_non_gst.items(): invoice_detail = self.invoices.get(invoice) - if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"): + if invoice_detail.get("gst_category") in ("Registered Regular", "Deemed Export", "SEZ"): if is_inter_state(invoice_detail): nil_exempt_output[0]["nil_rated"] += details[0] nil_exempt_output[0]["exempted"] += details[1] @@ -154,26 +157,34 @@ class Gstr1Report(object): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): place_of_supply = invoice_details.get("place_of_supply") - ecommerce_gstin = invoice_details.get("ecommerce_gstin") + ecommerce_gstin = invoice_details.get("ecommerce_gstin") - b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin), { - "place_of_supply": "", - "ecommerce_gstin": "", - "rate": "", - "taxable_value": 0, - "cess_amount": 0, - "type": "", - "invoice_number": invoice_details.get("invoice_number"), - "posting_date": invoice_details.get("posting_date"), - "invoice_value": invoice_details.get("base_grand_total"), - }) + b2cs_output.setdefault( + (rate, place_of_supply, ecommerce_gstin), + { + "place_of_supply": "", + "ecommerce_gstin": "", + "rate": "", + "taxable_value": 0, + "cess_amount": 0, + "type": "", + "invoice_number": invoice_details.get("invoice_number"), + "posting_date": invoice_details.get("posting_date"), + "invoice_value": invoice_details.get("base_grand_total"), + }, + ) row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) row["place_of_supply"] = place_of_supply row["ecommerce_gstin"] = ecommerce_gstin row["rate"] = rate - row["taxable_value"] += sum([abs(net_amount) - for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items]) + row["taxable_value"] += sum( + [ + abs(net_amount) + for item_code, net_amount in self.invoice_items.get(inv).items() + if item_code in items + ] + ) row["cess_amount"] += flt(self.invoice_cess.get(inv), 2) row["type"] = "E" if ecommerce_gstin else "OE" @@ -183,14 +194,17 @@ class Gstr1Report(object): def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items): row = [] for fieldname in self.invoice_fields: - if self.filters.get("type_of_business") in ("CDNR-REG", "CDNR-UNREG") and fieldname == "invoice_value": + if ( + self.filters.get("type_of_business") in ("CDNR-REG", "CDNR-UNREG") + and fieldname == "invoice_value" + ): row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total)) elif fieldname == "invoice_value": row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total) - elif fieldname in ('posting_date', 'shipping_bill_date'): - row.append(formatdate(invoice_details.get(fieldname), 'dd-MMM-YY')) + elif fieldname in ("posting_date", "shipping_bill_date"): + row.append(formatdate(invoice_details.get(fieldname), "dd-MMM-YY")) elif fieldname == "export_type": - export_type = "WPAY" if invoice_details.get(fieldname)=="With Payment of Tax" else "WOPAY" + export_type = "WPAY" if invoice_details.get(fieldname) == "With Payment of Tax" else "WOPAY" row.append(export_type) else: row.append(invoice_details.get(fieldname)) @@ -203,20 +217,25 @@ class Gstr1Report(object): for item_code, net_amount in self.invoice_items.get(invoice).items(): if item_code in items: - if self.item_tax_rate.get(invoice) and tax_rate/division_factor in self.item_tax_rate.get(invoice, {}).get(item_code, []): + if self.item_tax_rate.get(invoice) and tax_rate / division_factor in self.item_tax_rate.get( + invoice, {} + ).get(item_code, []): taxable_value += abs(net_amount) elif not self.item_tax_rate.get(invoice): taxable_value += abs(net_amount) elif tax_rate: taxable_value += abs(net_amount) - elif not tax_rate and self.filters.get('type_of_business') == 'EXPORT' \ - and invoice_details.get('export_type') == "Without Payment of Tax": + elif ( + not tax_rate + and self.filters.get("type_of_business") == "EXPORT" + and invoice_details.get("export_type") == "Without Payment of Tax" + ): taxable_value += abs(net_amount) row += [tax_rate or 0, taxable_value] for column in self.other_columns: - if column.get('fieldname') == 'cess_amount': + if column.get("fieldname") == "cess_amount": row.append(flt(self.invoice_cess.get(invoice), 2)) return row, taxable_value @@ -225,68 +244,82 @@ class Gstr1Report(object): self.invoices = frappe._dict() conditions = self.get_conditions() - invoice_data = frappe.db.sql(""" + invoice_data = frappe.db.sql( + """ select {select_columns} from `tab{doctype}` where docstatus = 1 {where_conditions} and is_opening = 'No' order by posting_date desc - """.format(select_columns=self.select_columns, doctype=self.doctype, - where_conditions=conditions), self.filters, as_dict=1) + """.format( + select_columns=self.select_columns, doctype=self.doctype, where_conditions=conditions + ), + self.filters, + as_dict=1, + ) for d in invoice_data: self.invoices.setdefault(d.invoice_number, d) def get_advance_entries(self): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT SUM(a.base_tax_amount) as amount, a.account_head, a.rate, p.place_of_supply FROM `tabPayment Entry` p, `tabAdvance Taxes and Charges` a WHERE p.docstatus = 1 AND p.name = a.parent AND posting_date between %s and %s GROUP BY a.account_head, p.place_of_supply, a.rate - """, (self.filters.get('from_date'), self.filters.get('to_date')), as_dict=1) + """, + (self.filters.get("from_date"), self.filters.get("to_date")), + as_dict=1, + ) def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), ("to_date", " and posting_date<=%(to_date)s"), ("company_address", " and company_address=%(company_address)s"), - ("company_gstin", " and company_gstin=%(company_gstin)s")): - if self.filters.get(opts[0]): - conditions += opts[1] + ("company_gstin", " and company_gstin=%(company_gstin)s"), + ): + if self.filters.get(opts[0]): + conditions += opts[1] - - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Registered Composition', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1" if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): - b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') + b2c_limit = frappe.db.get_single_value("GST Settings", "b2c_limit") if not b2c_limit: frappe.throw(_("Please set B2C Limit in GST Settings.")) - if self.filters.get("type_of_business") == "B2C Large": + if self.filters.get("type_of_business") == "B2C Large": conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') - AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format(flt(b2c_limit)) + AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format( + flt(b2c_limit) + ) - elif self.filters.get("type_of_business") == "B2C Small": + elif self.filters.get("type_of_business") == "B2C Small": conditions += """ AND ( SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2) - OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format(flt(b2c_limit)) + OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format( + flt(b2c_limit) + ) elif self.filters.get("type_of_business") == "CDNR-REG": conditions += """ AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ')""" elif self.filters.get("type_of_business") == "CDNR-UNREG": - b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') + b2c_limit = frappe.db.get_single_value("GST Settings", "b2c_limit") conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""" - elif self.filters.get("type_of_business") == "EXPORT": + elif self.filters.get("type_of_business") == "EXPORT": conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ conditions += " AND IFNULL(billing_address_gstin, '') != company_gstin" @@ -298,15 +331,22 @@ class Gstr1Report(object): self.item_tax_rate = frappe._dict() self.nil_exempt_non_gst = {} - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, is_non_gst from `tab%s Item` where parent in (%s) - """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (self.doctype, ", ".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in items: self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) - self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get( + "base_net_amount", 0 + ) item_tax_rate = {} @@ -320,15 +360,16 @@ class Gstr1Report(object): if d.is_nil_exempt: self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) if item_tax_rate: - self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][0] += d.get("taxable_value", 0) else: - self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][1] += d.get("taxable_value", 0) elif d.is_non_gst: self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) - self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0) def get_items_based_on_tax_rate(self): - self.tax_details = frappe.db.sql(""" + self.tax_details = frappe.db.sql( + """ select parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` @@ -336,8 +377,10 @@ class Gstr1Report(object): parenttype = %s and docstatus = 1 and parent in (%s) order by account_head - """ % (self.tax_doctype, '%s', ', '.join(['%s']*len(self.invoices.keys()))), - tuple([self.doctype] + list(self.invoices.keys()))) + """ + % (self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))), + tuple([self.doctype] + list(self.invoices.keys())), + ) self.items_based_on_tax_rate = {} self.invoice_cess = frappe._dict() @@ -353,8 +396,7 @@ class Gstr1Report(object): try: item_wise_tax_detail = json.loads(item_wise_tax_detail) cgst_or_sgst = False - if account in self.gst_accounts.cgst_account \ - or account in self.gst_accounts.sgst_account: + if account in self.gst_accounts.cgst_account or account in self.gst_accounts.sgst_account: cgst_or_sgst = True if not (cgst_or_sgst or account in self.gst_accounts.igst_account): @@ -371,22 +413,30 @@ class Gstr1Report(object): if parent not in self.cgst_sgst_invoices: self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( + tax_rate, [] + ) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: continue if unidentified_gst_accounts: - frappe.msgprint(_("Following accounts might be selected in GST Settings:") - + "
    " + "
    ".join(unidentified_gst_accounts), alert=True) + frappe.msgprint( + _("Following accounts might be selected in GST Settings:") + + "
    " + + "
    ".join(unidentified_gst_accounts), + alert=True, + ) # Build itemised tax for export invoices where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ - and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \ - and self.invoices.get(invoice, {}).get('gst_category') in ("Overseas", "SEZ"): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) + if ( + invoice not in self.items_based_on_tax_rate + and invoice not in unidentified_gst_accounts_invoice + and self.invoices.get(invoice, {}).get("export_type") == "Without Payment of Tax" + and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ") + ): + self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): self.other_columns = [] @@ -394,417 +444,258 @@ class Gstr1Report(object): if self.filters.get("type_of_business") != "NIL Rated": self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, + {"fieldname": "rate", "label": "Rate", "fieldtype": "Int", "width": 60}, { "fieldname": "taxable_value", "label": "Taxable Value", "fieldtype": "Currency", - "width": 100 - } + "width": 100, + }, ] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ { "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width":100 + "width": 150, }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 100}, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":100 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width":80 + "width": 100, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 80}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width":100 + "width": 100, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width":100 - }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data" - }, - { - "fieldname": "gst_category", - "label": "Invoice Type", - "fieldtype": "Data" + "width": 100, }, + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data"}, + {"fieldname": "gst_category", "label": "Invoice Type", "fieldtype": "Data"}, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width":120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } - ] + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} + ] - elif self.filters.get("type_of_business") == "B2C Large": + elif self.filters.get("type_of_business") == "B2C Large": self.invoice_columns = [ { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 100}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width": 130 - } + "width": 130, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } - ] + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} + ] elif self.filters.get("type_of_business") == "CDNR-REG": self.invoice_columns = [ { "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width": 120 + "width": 150, }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 120}, { "fieldname": "return_against", "label": "Invoice/Advance Receipt Number", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 + "width": 120, }, { "fieldname": "posting_date", "label": "Invoice/Advance Receipt date", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Invoice/Advance Receipt Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data" - }, - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "hidden": 1 + "width": 120, }, + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data"}, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "hidden": 1}, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 140 + "width": 140, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "gst_category", - "label": "GST Category", - "fieldtype": "Data" + "width": 120, }, + {"fieldname": "gst_category", "label": "GST Category", "fieldtype": "Data"}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 80}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 80}, ] elif self.filters.get("type_of_business") == "CDNR-UNREG": self.invoice_columns = [ - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width": 120 - }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 120}, { "fieldname": "return_against", "label": "Issued Against", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Note Date", - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Note Date", "fieldtype": "Date", "width": 120}, { "fieldname": "invoice_number", "label": "Note Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "hidden": 1 + "width": 120, }, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "hidden": 1}, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 140 + "width": 140, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "gst_category", - "label": "GST Category", - "fieldtype": "Data" + "width": 120, }, + {"fieldname": "gst_category", "label": "GST Category", "fieldtype": "Data"}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 80}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 80}, ] - elif self.filters.get("type_of_business") == "B2C Small": + elif self.filters.get("type_of_business") == "B2C Small": self.invoice_columns = [ { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width": 130 - } + "width": 130, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "type", - "label": "Type", - "fieldtype": "Data", - "width": 50 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "type", "label": "Type", "fieldtype": "Data", "width": 50}, ] - elif self.filters.get("type_of_business") == "EXPORT": + elif self.filters.get("type_of_business") == "EXPORT": self.invoice_columns = [ - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "width":120 - }, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - }, - { - "fieldname": "port_code", - "label": "Port Code", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "port_code", "label": "Port Code", "fieldtype": "Data", "width": 120}, { "fieldname": "shipping_bill_number", "label": "Shipping Bill Number", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "shipping_bill_date", "label": "Shipping Bill Date", "fieldtype": "Data", - "width": 120 - } + "width": 120, + }, ] elif self.filters.get("type_of_business") == "Advances": self.invoice_columns = [ - { - "fieldname": "place_of_supply", - "label": "Place Of Supply", - "fieldtype": "Data", - "width": 120 - } + {"fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", "width": 120} ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} ] elif self.filters.get("type_of_business") == "NIL Rated": self.invoice_columns = [ - { - "fieldname": "description", - "label": "Description", - "fieldtype": "Data", - "width": 420 - }, - { - "fieldname": "nil_rated", - "label": "Nil Rated", - "fieldtype": "Currency", - "width": 200 - }, - { - "fieldname": "exempted", - "label": "Exempted", - "fieldtype": "Currency", - "width": 200 - }, - { - "fieldname": "non_gst", - "label": "Non GST", - "fieldtype": "Currency", - "width": 200 - } + {"fieldname": "description", "label": "Description", "fieldtype": "Data", "width": 420}, + {"fieldname": "nil_rated", "label": "Nil Rated", "fieldtype": "Currency", "width": 200}, + {"fieldname": "exempted", "label": "Exempted", "fieldtype": "Currency", "width": 200}, + {"fieldname": "non_gst", "label": "Non GST", "fieldtype": "Currency", "width": 200}, ] self.columns = self.invoice_columns + self.tax_columns + self.other_columns + @frappe.whitelist() def get_json(filters, report_name, data): filters = json.loads(filters) @@ -813,13 +704,14 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"version": "GST3.0.4", - "hash": "hash", "gstin": gstin, "fp": fp} + gst_json = {"version": "GST3.0.4", "hash": "hash", "gstin": gstin, "fp": fp} res = {} if filters["type_of_business"] == "B2B": for item in report_data[:-1]: - res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"], []).append( + item + ) out = get_b2b_json(res, gstin) gst_json["b2b"] = out @@ -843,13 +735,15 @@ def get_json(filters, report_name, data): gst_json["exp"] = out elif filters["type_of_business"] == "CDNR-REG": for item in report_data[:-1]: - res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"], []).append( + item + ) out = get_cdnr_reg_json(res, gstin) gst_json["cdnr"] = out elif filters["type_of_business"] == "CDNR-UNREG": for item in report_data[:-1]: - res.setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["invoice_number"], []).append(item) out = get_cdnr_unreg_json(res, gstin) gst_json["cdnur"] = out @@ -857,10 +751,14 @@ def get_json(filters, report_name, data): elif filters["type_of_business"] == "Advances": for item in report_data[:-1]: if not item.get("place_of_supply"): - frappe.throw(_("""{0} not entered in some entries. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some entries. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - res.setdefault(item["place_of_supply"],[]).append(item) + res.setdefault(item["place_of_supply"], []).append(item) out = get_advances_json(res, gstin) gst_json["at"] = out @@ -870,30 +768,34 @@ def get_json(filters, report_name, data): out = get_exempted_json(res) gst_json["nil"] = out - return { - 'report_name': report_name, - 'report_type': filters['type_of_business'], - 'data': gst_json - } + return {"report_name": report_name, "report_type": filters["type_of_business"], "data": gst_json} + def get_b2b_json(res, gstin): out = [] for gst_in in res: b2b_item, inv = {"ctin": gst_in, "inv": []}, [] - if not gst_in: continue + if not gst_in: + continue for number, invoice in iteritems(res[gst_in]): if not invoice[0]["place_of_supply"]: - frappe.throw(_("""{0} not entered in Invoice {1}. - Please update and try again""").format(frappe.bold("Place Of Supply"), - frappe.bold(invoice[0]['invoice_number']))) + frappe.throw( + _( + """{0} not entered in Invoice {1}. + Please update and try again""" + ).format( + frappe.bold("Place Of Supply"), frappe.bold(invoice[0]["invoice_number"]) + ) + ) inv_item = get_basic_invoice_detail(invoice[0]) - inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]) + inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split("-")[0]) inv_item["rchrg"] = invoice[0]["reverse_charge"] inv_item["inv_typ"] = get_invoice_type(invoice[0]) - if inv_item["pos"]=="00": continue + if inv_item["pos"] == "00": + continue inv_item["itms"] = [] for item in invoice: @@ -901,95 +803,101 @@ def get_b2b_json(res, gstin): inv.append(inv_item) - if not inv: continue + if not inv: + continue b2b_item["inv"] = inv out.append(b2b_item) return out + def get_b2cs_json(data, gstin): company_state_number = gstin[0:2] out = [] for d in data: if not d.get("place_of_supply"): - frappe.throw(_("""{0} not entered in some invoices. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some invoices. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - pos = d.get('place_of_supply').split('-')[0] + pos = d.get("place_of_supply").split("-")[0] tax_details = {} - rate = d.get('rate', 0) - tax = flt((d["taxable_value"]*rate)/100.0, 2) + rate = d.get("rate", 0) + tax = flt((d["taxable_value"] * rate) / 100.0, 2) if company_state_number == pos: - tax_details.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)}) + tax_details.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)}) else: tax_details.update({"iamt": tax}) inv = { "sply_ty": "INTRA" if company_state_number == pos else "INTER", "pos": pos, - "typ": d.get('type'), - "txval": flt(d.get('taxable_value'), 2), + "typ": d.get("type"), + "txval": flt(d.get("taxable_value"), 2), "rt": rate, - "iamt": flt(tax_details.get('iamt'), 2), - "camt": flt(tax_details.get('camt'), 2), - "samt": flt(tax_details.get('samt'), 2), - "csamt": flt(d.get('cess_amount'), 2) + "iamt": flt(tax_details.get("iamt"), 2), + "camt": flt(tax_details.get("camt"), 2), + "samt": flt(tax_details.get("samt"), 2), + "csamt": flt(d.get("cess_amount"), 2), } - if d.get('type') == "E" and d.get('ecommerce_gstin'): - inv.update({ - "etin": d.get('ecommerce_gstin') - }) + if d.get("type") == "E" and d.get("ecommerce_gstin"): + inv.update({"etin": d.get("ecommerce_gstin")}) out.append(inv) return out + def get_advances_json(data, gstin): company_state_number = gstin[0:2] out = [] for place_of_supply, items in iteritems(data): - supply_type = "INTRA" if company_state_number == place_of_supply.split('-')[0] else "INTER" - row = { - "pos": place_of_supply.split('-')[0], - "itms": [], - "sply_ty": supply_type - } + supply_type = "INTRA" if company_state_number == place_of_supply.split("-")[0] else "INTER" + row = {"pos": place_of_supply.split("-")[0], "itms": [], "sply_ty": supply_type} for item in items: itms = { - 'rt': item['rate'], - 'ad_amount': flt(item.get('taxable_value')), - 'csamt': flt(item.get('cess_amount')) + "rt": item["rate"], + "ad_amount": flt(item.get("taxable_value")), + "csamt": flt(item.get("cess_amount")), } if supply_type == "INTRA": - itms.update({ - "samt": flt((itms["ad_amount"] * itms["rt"]) / 100), - "camt": flt((itms["ad_amount"] * itms["rt"]) / 100), - "rt": itms["rt"] * 2 - }) + itms.update( + { + "samt": flt((itms["ad_amount"] * itms["rt"]) / 100), + "camt": flt((itms["ad_amount"] * itms["rt"]) / 100), + "rt": itms["rt"] * 2, + } + ) else: - itms.update({ - "iamt": flt((itms["ad_amount"] * itms["rt"]) / 100) - }) + itms.update({"iamt": flt((itms["ad_amount"] * itms["rt"]) / 100)}) - row['itms'].append(itms) + row["itms"].append(itms) out.append(row) return out + def get_b2cl_json(res, gstin): out = [] for pos in res: if not pos: - frappe.throw(_("""{0} not entered in some invoices. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some invoices. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - b2cl_item, inv = {"pos": "%02d" % int(pos.split('-')[0]), "inv": []}, [] + b2cl_item, inv = {"pos": "%02d" % int(pos.split("-")[0]), "inv": []}, [] for row in res[pos]: inv_item = get_basic_invoice_detail(row) @@ -1005,6 +913,7 @@ def get_b2cl_json(res, gstin): return out + def get_export_json(res): out = [] for exp_type in res: @@ -1012,12 +921,9 @@ def get_export_json(res): for row in res[exp_type]: inv_item = get_basic_invoice_detail(row) - inv_item["itms"] = [{ - "txval": flt(row["taxable_value"], 2), - "rt": row["rate"] or 0, - "iamt": 0, - "csamt": 0 - }] + inv_item["itms"] = [ + {"txval": flt(row["taxable_value"], 2), "rt": row["rate"] or 0, "iamt": 0, "csamt": 0} + ] inv.append(inv_item) @@ -1026,27 +932,34 @@ def get_export_json(res): return out + def get_cdnr_reg_json(res, gstin): out = [] for gst_in in res: cdnr_item, inv = {"ctin": gst_in, "nt": []}, [] - if not gst_in: continue + if not gst_in: + continue for number, invoice in iteritems(res[gst_in]): if not invoice[0]["place_of_supply"]: - frappe.throw(_("""{0} not entered in Invoice {1}. - Please update and try again""").format(frappe.bold("Place Of Supply"), - frappe.bold(invoice[0]['invoice_number']))) + frappe.throw( + _( + """{0} not entered in Invoice {1}. + Please update and try again""" + ).format( + frappe.bold("Place Of Supply"), frappe.bold(invoice[0]["invoice_number"]) + ) + ) inv_item = { "nt_num": invoice[0]["invoice_number"], - "nt_dt": getdate(invoice[0]["posting_date"]).strftime('%d-%m-%Y'), + "nt_dt": getdate(invoice[0]["posting_date"]).strftime("%d-%m-%Y"), "val": abs(flt(invoice[0]["invoice_value"])), "ntty": invoice[0]["document_type"], - "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]), + "pos": "%02d" % int(invoice[0]["place_of_supply"].split("-")[0]), "rchrg": invoice[0]["reverse_charge"], - "inv_typ": get_invoice_type(invoice[0]) + "inv_typ": get_invoice_type(invoice[0]), } inv_item["itms"] = [] @@ -1055,23 +968,25 @@ def get_cdnr_reg_json(res, gstin): inv.append(inv_item) - if not inv: continue + if not inv: + continue cdnr_item["nt"] = inv out.append(cdnr_item) return out + def get_cdnr_unreg_json(res, gstin): out = [] for invoice, items in iteritems(res): inv_item = { "nt_num": items[0]["invoice_number"], - "nt_dt": getdate(items[0]["posting_date"]).strftime('%d-%m-%Y'), + "nt_dt": getdate(items[0]["posting_date"]).strftime("%d-%m-%Y"), "val": abs(flt(items[0]["invoice_value"])), "ntty": items[0]["document_type"], - "pos": "%02d" % int(items[0]["place_of_supply"].split('-')[0]), - "typ": get_invoice_type(items[0]) + "pos": "%02d" % int(items[0]["place_of_supply"].split("-")[0]), + "typ": get_invoice_type(items[0]), } inv_item["itms"] = [] @@ -1082,63 +997,62 @@ def get_cdnr_unreg_json(res, gstin): return out + def get_exempted_json(data): out = { "inv": [ - { - "sply_ty": "INTRB2B" - }, - { - "sply_ty": "INTRAB2B" - }, - { - "sply_ty": "INTRB2C" - }, - { - "sply_ty": "INTRAB2C" - } + {"sply_ty": "INTRB2B"}, + {"sply_ty": "INTRAB2B"}, + {"sply_ty": "INTRB2C"}, + {"sply_ty": "INTRAB2C"}, ] } for i, v in enumerate(data): - if data[i].get('nil_rated'): - out['inv'][i]['nil_amt'] = data[i]['nil_rated'] + if data[i].get("nil_rated"): + out["inv"][i]["nil_amt"] = data[i]["nil_rated"] - if data[i].get('exempted'): - out['inv'][i]['expt_amt'] = data[i]['exempted'] + if data[i].get("exempted"): + out["inv"][i]["expt_amt"] = data[i]["exempted"] - if data[i].get('non_gst'): - out['inv'][i]['ngsup_amt'] = data[i]['non_gst'] + if data[i].get("non_gst"): + out["inv"][i]["ngsup_amt"] = data[i]["non_gst"] return out + def get_invoice_type(row): - gst_category = row.get('gst_category') + gst_category = row.get("gst_category") - if gst_category == 'SEZ': - return 'SEWP' if row.get('export_type') == 'WPAY' else 'SEWOP' + if gst_category == "SEZ": + return "SEWP" if row.get("export_type") == "WPAY" else "SEWOP" - if gst_category == 'Overseas': - return 'EXPWP' if row.get('export_type') == 'WPAY' else 'EXPWOP' + if gst_category == "Overseas": + return "EXPWP" if row.get("export_type") == "WPAY" else "EXPWOP" + + return ( + { + "Deemed Export": "DE", + "Registered Regular": "R", + "Registered Composition": "R", + "Unregistered": "B2CL", + } + ).get(gst_category) - return ({ - 'Deemed Export': 'DE', - 'Registered Regular': 'R', - 'Registered Composition': 'R', - 'Unregistered': 'B2CL' - }).get(gst_category) def get_basic_invoice_detail(row): return { "inum": row["invoice_number"], - "idt": getdate(row["posting_date"]).strftime('%d-%m-%Y'), - "val": flt(row["invoice_value"], 2) + "idt": getdate(row["posting_date"]).strftime("%d-%m-%Y"), + "val": flt(row["invoice_value"], 2), } + def get_rate_and_tax_details(row, gstin): - itm_det = {"txval": flt(row["taxable_value"], 2), + itm_det = { + "txval": flt(row["taxable_value"], 2), "rt": row["rate"], - "csamt": (flt(row.get("cess_amount"), 2) or 0) + "csamt": (flt(row.get("cess_amount"), 2) or 0), } # calculate rate @@ -1146,17 +1060,18 @@ def get_rate_and_tax_details(row, gstin): rate = row.get("rate") or 0 # calculate tax amount added - tax = flt((row["taxable_value"]*rate)/100.0, 2) - frappe.errprint([tax, tax/2]) + tax = flt((row["taxable_value"] * rate) / 100.0, 2) + frappe.errprint([tax, tax / 2]) if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]: - itm_det.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)}) + itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)}) else: itm_det.update({"iamt": tax}) return {"num": int(num), "itm_det": itm_det} + def get_company_gstin_number(company, address=None, all_gstins=False): - gstin = '' + gstin = "" if address: gstin = frappe.db.get_value("Address", address, "gstin") @@ -1166,28 +1081,36 @@ def get_company_gstin_number(company, address=None, all_gstins=False): ["Dynamic Link", "link_doctype", "=", "Company"], ["Dynamic Link", "link_name", "=", company], ["Dynamic Link", "parenttype", "=", "Address"], - ["gstin", "!=", ''] + ["gstin", "!=", ""], ] - gstin = frappe.get_all("Address", filters=filters, pluck="gstin", order_by="is_primary_address desc") + gstin = frappe.get_all( + "Address", filters=filters, pluck="gstin", order_by="is_primary_address desc" + ) 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( - address, frappe.bold(company) - )) + frappe.throw( + _("Please set valid GSTIN No. in Company Address {} for company {}").format( + address, frappe.bold(company) + ) + ) return gstin + @frappe.whitelist() def download_json_file(): - ''' download json content in a file ''' + """download json content in a file""" data = frappe._dict(frappe.local.form_dict) - frappe.response['filename'] = frappe.scrub("{0} {1}".format(data['report_name'], data['report_type'])) + '.json' - frappe.response['filecontent'] = data['data'] - frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' + frappe.response["filename"] = ( + frappe.scrub("{0} {1}".format(data["report_name"], data["report_type"])) + ".json" + ) + frappe.response["filecontent"] = data["data"] + frappe.response["content_type"] = "application/json" + frappe.response["type"] = "download" + def is_inter_state(invoice_detail): if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]: @@ -1201,16 +1124,16 @@ def get_company_gstins(company): address = frappe.qb.DocType("Address") links = frappe.qb.DocType("Dynamic Link") - addresses = frappe.qb.from_(address).inner_join(links).on( - address.name == links.parent - ).select( - address.gstin - ).where( - links.link_doctype == 'Company' - ).where( - links.link_name == company - ).run(as_dict=1) + addresses = ( + frappe.qb.from_(address) + .inner_join(links) + .on(address.name == links.parent) + .select(address.gstin) + .where(links.link_doctype == "Company") + .where(links.link_name == company) + .run(as_dict=1) + ) - address_list = [''] + [d.gstin for d in addresses] + address_list = [""] + [d.gstin for d in addresses] - return address_list \ No newline at end of file + return address_list diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py index 47c856dfaae..a189d2a500b 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.py +++ b/erpnext/regional/report/gstr_2/gstr_2.py @@ -12,6 +12,7 @@ from erpnext.regional.report.gstr_1.gstr_1 import Gstr1Report def execute(filters=None): return Gstr2Report(filters).run() + class Gstr2Report(Gstr1Report): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -47,7 +48,7 @@ class Gstr2Report(Gstr1Report): for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): - if rate or invoice_details.get('gst_category') == 'Registered Composition': + if rate or invoice_details.get("gst_category") == "Registered Composition": if inv not in self.igst_invoices: rate = rate / 2 row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) @@ -60,13 +61,13 @@ class Gstr2Report(Gstr1Report): row += [ self.invoice_cess.get(inv), - invoice_details.get('eligibility_for_itc'), - invoice_details.get('itc_integrated_tax'), - invoice_details.get('itc_central_tax'), - invoice_details.get('itc_state_tax'), - invoice_details.get('itc_cess_amount') + invoice_details.get("eligibility_for_itc"), + invoice_details.get("itc_integrated_tax"), + invoice_details.get("itc_central_tax"), + invoice_details.get("itc_state_tax"), + invoice_details.get("itc_cess_amount"), ] - if self.filters.get("type_of_business") == "CDNR": + if self.filters.get("type_of_business") == "CDNR": row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N") row.append("C" if invoice_details.return_against else "R") @@ -82,201 +83,158 @@ class Gstr2Report(Gstr1Report): def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): - if self.filters.get(opts[0]): - conditions += opts[1] + ("to_date", " and posting_date<=%(to_date)s"), + ): + if self.filters.get(opts[0]): + conditions += opts[1] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ', 'Registered Composition') and is_return != 1 " - elif self.filters.get("type_of_business") == "CDNR": + elif self.filters.get("type_of_business") == "CDNR": conditions += """ and is_return = 1 """ return conditions def get_columns(self): self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, - { - "fieldname": "taxable_value", - "label": "Taxable Value", - "fieldtype": "Currency", - "width": 100 - }, + {"fieldname": "rate", "label": "Rate", "fieldtype": "Int", "width": 60}, + {"fieldname": "taxable_value", "label": "Taxable Value", "fieldtype": "Currency", "width": 100}, { "fieldname": "integrated_tax_paid", "label": "Integrated Tax Paid", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "central_tax_paid", "label": "Central Tax Paid", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "state_tax_paid", "label": "State/UT Tax Paid", "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "cess_amount", - "label": "Cess Paid", - "fieldtype": "Currency", - "width": 100 + "width": 100, }, + {"fieldname": "cess_amount", "label": "Cess Paid", "fieldtype": "Currency", "width": 100}, { "fieldname": "eligibility_for_itc", "label": "Eligibility For ITC", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "fieldname": "itc_integrated_tax", "label": "Availed ITC Integrated Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_central_tax", "label": "Availed ITC Central Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_state_tax", "label": "Availed ITC State/UT Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_cess_amount", "label": "Availed ITC Cess ", "fieldtype": "Currency", - "width": 100 - } + "width": 100, + }, ] self.other_columns = [] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ { "fieldname": "supplier_gstin", "label": "GSTIN of Supplier", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Purchase Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Date", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 + "width": 120, }, { "fieldname": "place_of_supply", "label": "Place of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "gst_category", - "label": "Invoice Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data", "width": 80}, + {"fieldname": "gst_category", "label": "Invoice Type", "fieldtype": "Data", "width": 80}, ] - elif self.filters.get("type_of_business") == "CDNR": + elif self.filters.get("type_of_business") == "CDNR": self.invoice_columns = [ { "fieldname": "supplier_gstin", "label": "GSTIN of Supplier", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Note/Refund Voucher Number", "fieldtype": "Link", - "options": "Purchase Invoice" + "options": "Purchase Invoice", }, { "fieldname": "posting_date", "label": "Note/Refund Voucher date", "fieldtype": "Date", - "width": 120 + "width": 120, }, { "fieldname": "return_against", "label": "Invoice/Advance Payment Voucher Number", "fieldtype": "Link", "options": "Purchase Invoice", - "width": 120 + "width": 120, }, { "fieldname": "posting_date", "label": "Invoice/Advance Payment Voucher date", "fieldtype": "Date", - "width": 120 + "width": 120, }, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "supply_type", - "label": "Supply Type", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "supply_type", "label": "Supply Type", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 50 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 50 - } + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 50}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 50}, ] self.columns = self.invoice_columns + self.tax_columns + self.other_columns diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index f074920d110..81fc386f7af 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -18,8 +18,10 @@ from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) + def _execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() company_currency = erpnext.get_company_currency(filters.company) @@ -45,9 +47,10 @@ def _execute(filters=None): data.append(row) added_item.append((d.parent, d.item_code)) if data: - data = get_merged_data(columns, data) # merge same hsn code data + data = get_merged_data(columns, data) # merge same hsn code data return columns, data + def get_columns(): columns = [ { @@ -55,69 +58,52 @@ def get_columns(): "label": _("HSN/SAC"), "fieldtype": "Link", "options": "GST HSN Code", - "width": 100 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 300 - }, - { - "fieldname": "stock_uom", - "label": _("Stock UOM"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "stock_qty", - "label": _("Stock Qty"), - "fieldtype": "Float", - "width": 90 - }, - { - "fieldname": "tax_rate", - "label": _("Tax Rate"), - "fieldtype": "Data", - "width": 90 - }, - { - "fieldname": "total_amount", - "label": _("Total Amount"), - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, + {"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, + {"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90}, + {"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 90}, + {"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, + {"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, + {"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90}, + {"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120}, { "fieldname": "taxable_amount", "label": _("Total Taxable Amount"), "fieldtype": "Currency", - "width": 170 - } + "width": 170, + }, ] return columns + def get_conditions(filters): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("gst_hsn_code", " and gst_hsn_code=%(gst_hsn_code)s"), ("company_gstin", " and company_gstin=%(company_gstin)s"), ("from_date", " and posting_date >= %(from_date)s"), - ("to_date", "and posting_date <= %(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] + ("to_date", "and posting_date <= %(to_date)s"), + ): + if filters.get(opts[0]): + conditions += opts[1] return conditions + def get_items(filters): conditions = get_conditions(filters) match_conditions = frappe.build_match_conditions("Sales Invoice") if match_conditions: match_conditions = " and {0} ".format(match_conditions) - - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_uom, @@ -143,25 +129,41 @@ def get_items(filters): group by `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code - """ % (conditions, match_conditions), filters, as_dict=1) + """ + % (conditions, match_conditions), + filters, + as_dict=1, + ) return items -def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): + +def get_tax_accounts( + item_list, + columns, + company_currency, + doctype="Sales Invoice", + tax_doctype="Sales Taxes and Charges", +): item_row_map = {} tax_columns = [] invoice_item_row = {} itemised_tax = {} conditions = "" - tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"), - currency=company_currency) or 2 + tax_amount_precision = ( + get_field_precision( + frappe.get_meta(tax_doctype).get_field("tax_amount"), currency=company_currency + ) + or 2 + ) for d in item_list: invoice_item_row.setdefault(d.parent, []).append(d) item_row_map.setdefault(d.parent, {}).setdefault(d.item_code or d.item_name, []).append(d) - tax_details = frappe.db.sql(""" + tax_details = frappe.db.sql( + """ select parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount @@ -172,8 +174,10 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic and parent in (%s) %s order by description - """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), - tuple([doctype] + list(invoice_item_row))) + """ + % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), + tuple([doctype] + list(invoice_item_row)), + ) for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: @@ -197,75 +201,75 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ - "tax_amount": flt(item_tax_amount, tax_amount_precision) - }) + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict( + {"tax_amount": flt(item_tax_amount, tax_amount_precision)} + ) except ValueError: continue tax_columns.sort() for account_head in tax_columns: - columns.append({ - "label": account_head, - "fieldname": frappe.scrub(account_head), - "fieldtype": "Float", - "width": 110 - }) + columns.append( + { + "label": account_head, + "fieldname": frappe.scrub(account_head), + "fieldtype": "Float", + "width": 110, + } + ) return itemised_tax, tax_columns + def get_merged_data(columns, data): - merged_hsn_dict = {} # to group same hsn under one key and perform row addition + merged_hsn_dict = {} # to group same hsn under one key and perform row addition result = [] for row in data: - key = row[0] + '-' + str(row[4]) + key = row[0] + "-" + str(row[4]) merged_hsn_dict.setdefault(key, {}) for i, d in enumerate(columns): - if d['fieldtype'] not in ('Int', 'Float', 'Currency'): - merged_hsn_dict[key][d['fieldname']] = row[i] + if d["fieldtype"] not in ("Int", "Float", "Currency"): + merged_hsn_dict[key][d["fieldname"]] = row[i] else: - if merged_hsn_dict.get(key, {}).get(d['fieldname'], ''): - merged_hsn_dict[key][d['fieldname']] += row[i] + if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""): + merged_hsn_dict[key][d["fieldname"]] += row[i] else: - merged_hsn_dict[key][d['fieldname']] = row[i] + merged_hsn_dict[key][d["fieldname"]] = row[i] for key, value in iteritems(merged_hsn_dict): result.append(value) return result + @frappe.whitelist() def get_json(filters, report_name, data): filters = json.loads(filters) report_data = json.loads(data) - gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + gstin = filters.get("company_gstin") or get_company_gstin_number(filters["company"]) - if not filters.get('from_date') or not filters.get('to_date'): + if not filters.get("from_date") or not filters.get("to_date"): frappe.throw(_("Please enter From Date and To Date to generate JSON")) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"version": "GST3.0.3", - "hash": "hash", "gstin": gstin, "fp": fp} + gst_json = {"version": "GST3.0.3", "hash": "hash", "gstin": gstin, "fp": fp} - gst_json["hsn"] = { - "data": get_hsn_wise_json_data(filters, report_data) - } + gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)} + + return {"report_name": report_name, "data": gst_json} - return { - 'report_name': report_name, - 'data': gst_json - } @frappe.whitelist() def download_json_file(): - '''download json content in a file''' + """download json content in a file""" data = frappe._dict(frappe.local.form_dict) - frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' - frappe.response['filecontent'] = data['data'] - frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' + frappe.response["filename"] = frappe.scrub("{0}".format(data["report_name"])) + ".json" + frappe.response["filecontent"] = data["data"] + frappe.response["content_type"] = "application/json" + frappe.response["type"] = "download" + def get_hsn_wise_json_data(filters, report_data): @@ -286,23 +290,22 @@ def get_hsn_wise_json_data(filters, report_data): "iamt": 0.0, "camt": 0.0, "samt": 0.0, - "csamt": 0.0 - + "csamt": 0.0, } - for account in gst_accounts.get('igst_account'): - row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("igst_account"): + row["iamt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('cgst_account'): - row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("cgst_account"): + row["camt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('sgst_account'): - row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("sgst_account"): + row["samt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('cess_account'): - row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("cess_account"): + row["csamt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) data.append(row) - count +=1 + count += 1 return data diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py index 86dc458bdb1..090473f4fdc 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py @@ -28,7 +28,7 @@ class TestHSNWiseSummaryReport(TestCase): setup_company() setup_customers() setup_gst_settings() - make_item("Golf Car", properties={ "gst_hsn_code": "999900" }) + make_item("Golf Car", properties={"gst_hsn_code": "999900"}) @classmethod def tearDownClass(cls): @@ -37,53 +37,66 @@ class TestHSNWiseSummaryReport(TestCase): def test_hsn_summary_for_invoice_with_duplicate_items(self): si = create_sales_invoice( company="_Test Company GST", - customer = "_Test GST Customer", - currency = "INR", - warehouse = "Finished Goods - _GST", - debit_to = "Debtors - _GST", - income_account = "Sales - _GST", - expense_account = "Cost of Goods Sold - _GST", - cost_center = "Main - _GST", - do_not_save=1 + customer="_Test GST Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, ) si.items = [] - si.append("items", { - "item_code": "Golf Car", - "gst_hsn_code": "999900", - "qty": "1", - "rate": "120", - "cost_center": "Main - _GST" - }) - si.append("items", { - "item_code": "Golf Car", - "gst_hsn_code": "999900", - "qty": "1", - "rate": "140", - "cost_center": "Main - _GST" - }) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) + si.append( + "items", + { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "120", + "cost_center": "Main - _GST", + }, + ) + si.append( + "items", + { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "140", + "cost_center": "Main - _GST", + }, + ) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) si.posting_date = "2020-11-17" si.submit() si.reload() - [columns, data] = run_report(filters=frappe._dict({ - "company": "_Test Company GST", - "gst_hsn_code": "999900", - "company_gstin": si.company_gstin, - "from_date": si.posting_date, - "to_date": si.posting_date - })) + [columns, data] = run_report( + filters=frappe._dict( + { + "company": "_Test Company GST", + "gst_hsn_code": "999900", + "company_gstin": si.company_gstin, + "from_date": si.posting_date, + "to_date": si.posting_date, + } + ) + ) - filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data)) + filtered_rows = list(filter(lambda row: row["gst_hsn_code"] == "999900", data)) self.assertTrue(filtered_rows) hsn_row = filtered_rows[0] - self.assertEquals(hsn_row['stock_qty'], 2.0) - self.assertEquals(hsn_row['total_amount'], 306.8) + self.assertEquals(hsn_row["stock_qty"], 2.0) + self.assertEquals(hsn_row["total_amount"], 306.8) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index b1a5d109621..749bf95e969 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -20,23 +20,22 @@ IRS_1099_FORMS_FILE_EXTENSION = ".pdf" def execute(filters=None): filters = filters if isinstance(filters, frappe._dict) else frappe._dict(filters) if not filters: - filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) - filters.setdefault('company', frappe.db.get_default("company")) + filters.setdefault("fiscal_year", get_fiscal_year(nowdate())[0]) + filters.setdefault("company", frappe.db.get_default("company")) - region = frappe.db.get_value("Company", - filters={"name": filters.company}, - fieldname=["country"]) + region = frappe.db.get_value("Company", filters={"name": filters.company}, fieldname=["country"]) - if region != 'United States': + if region != "United States": return [], [] data = [] columns = get_columns() conditions = "" if filters.supplier_group: - conditions += "AND s.supplier_group = %s" %frappe.db.escape(filters.get("supplier_group")) + conditions += "AND s.supplier_group = %s" % frappe.db.escape(filters.get("supplier_group")) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT s.supplier_group as "supplier_group", gl.party AS "supplier", @@ -57,10 +56,12 @@ def execute(filters=None): gl.party ORDER BY - gl.party DESC""".format(conditions=conditions), { - "fiscal_year": filters.fiscal_year, - "company": filters.company - }, as_dict=True) + gl.party DESC""".format( + conditions=conditions + ), + {"fiscal_year": filters.fiscal_year, "company": filters.company}, + as_dict=True, + ) return columns, data @@ -72,37 +73,29 @@ def get_columns(): "label": _("Supplier Group"), "fieldtype": "Link", "options": "Supplier Group", - "width": 200 + "width": 200, }, { "fieldname": "supplier", "label": _("Supplier"), "fieldtype": "Link", "options": "Supplier", - "width": 200 + "width": 200, }, - { - "fieldname": "tax_id", - "label": _("Tax ID"), - "fieldtype": "Data", - "width": 200 - }, - { - "fieldname": "payments", - "label": _("Total Payments"), - "fieldtype": "Currency", - "width": 200 - } + {"fieldname": "tax_id", "label": _("Tax ID"), "fieldtype": "Data", "width": 200}, + {"fieldname": "payments", "label": _("Total Payments"), "fieldtype": "Currency", "width": 200}, ] @frappe.whitelist() def irs_1099_print(filters): if not filters: - frappe._dict({ - "company": frappe.db.get_default("Company"), - "fiscal_year": frappe.db.get_default("Fiscal Year") - }) + frappe._dict( + { + "company": frappe.db.get_default("Company"), + "fiscal_year": frappe.db.get_default("Fiscal Year"), + } + ) else: filters = frappe._dict(json.loads(filters)) @@ -122,17 +115,21 @@ def irs_1099_print(filters): row["company_tin"] = company_tin row["payer_street_address"] = company_address row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html( - "Supplier", row.supplier) + "Supplier", row.supplier + ) row["payments"] = fmt_money(row["payments"], precision=0, currency="USD") pdf = get_pdf(render_template(template, row), output=output if output else None) - frappe.local.response.filename = f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}" + frappe.local.response.filename = ( + f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}" + ) frappe.local.response.filecontent = read_multi_pdf(output) frappe.local.response.type = "download" def get_payer_address_html(company): - address_list = frappe.db.sql(""" + address_list = frappe.db.sql( + """ SELECT name FROM @@ -142,7 +139,10 @@ def get_payer_address_html(company): ORDER BY address_type="Postal" DESC, address_type="Billing" DESC LIMIT 1 - """, {"company": company}, as_dict=True) + """, + {"company": company}, + as_dict=True, + ) address_display = "" if address_list: @@ -153,7 +153,8 @@ def get_payer_address_html(company): def get_street_address_html(party_type, party): - address_list = frappe.db.sql(""" + address_list = frappe.db.sql( + """ SELECT link.parent FROM @@ -166,7 +167,10 @@ def get_street_address_html(party_type, party): address.address_type="Postal" DESC, address.address_type="Billing" DESC LIMIT 1 - """, {"party": party}, as_dict=True) + """, + {"party": party}, + as_dict=True, + ) street_address = city_state = "" if address_list: diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py index cc26bd7a57a..15996d2d1f8 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ b/erpnext/regional/report/ksa_vat/ksa_vat.py @@ -14,6 +14,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(): return [ { @@ -48,101 +49,136 @@ def get_columns(): "label": _("Currency"), "fieldtype": "Currency", "width": 150, - "hidden": 1 - } + "hidden": 1, + }, ] + def get_data(filters): data = [] # Validate if vat settings exist - company = filters.get('company') - company_currency = frappe.get_cached_value('Company', company, "default_currency") + company = filters.get("company") + company_currency = frappe.get_cached_value("Company", company, "default_currency") - if frappe.db.exists('KSA VAT Setting', company) is None: - url = get_url_to_list('KSA VAT Setting') + if frappe.db.exists("KSA VAT Setting", company) is None: + url = get_url_to_list("KSA VAT Setting") frappe.msgprint(_('Create KSA VAT Setting for this company').format(url)) return data - ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company) + ksa_vat_setting = frappe.get_doc("KSA VAT Setting", company) # Sales Heading - append_data(data, 'VAT on Sales', '', '', '', company_currency) + append_data(data, "VAT on Sales", "", "", "", company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 grand_total_tax = 0 for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts: - total_taxable_amount, total_taxable_adjustment_amount, \ - total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Sales Invoice') + ( + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Sales Invoice") # Adding results to data - append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax, company_currency) + append_data( + data, + vat_setting.title, + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + company_currency, + ) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount grand_total_tax += total_tax # Sales Grand Total - append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) + append_data( + data, + "Grand Total", + grand_total_taxable_amount, + grand_total_taxable_adjustment_amount, + grand_total_tax, + company_currency, + ) # Blank Line - append_data(data, '', '', '', '', company_currency) + append_data(data, "", "", "", "", company_currency) # Purchase Heading - append_data(data, 'VAT on Purchases', '', '', '', company_currency) + append_data(data, "VAT on Purchases", "", "", "", company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 grand_total_tax = 0 for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts: - total_taxable_amount, total_taxable_adjustment_amount, \ - total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Purchase Invoice') + ( + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Purchase Invoice") # Adding results to data - append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax, company_currency) + append_data( + data, + vat_setting.title, + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + company_currency, + ) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount grand_total_tax += total_tax # Purchase Grand Total - append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) + append_data( + data, + "Grand Total", + grand_total_taxable_amount, + grand_total_taxable_adjustment_amount, + grand_total_tax, + company_currency, + ) return data + def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): - ''' + """ (KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n calculates and returns \n - total_taxable_amount, total_taxable_adjustment_amount, total_tax''' - from_date = filters.get('from_date') - to_date = filters.get('to_date') + total_taxable_amount, total_taxable_adjustment_amount, total_tax""" + from_date = filters.get("from_date") + to_date = filters.get("to_date") # Initiate variables total_taxable_amount = 0 total_taxable_adjustment_amount = 0 total_tax = 0 # Fetch All Invoices - invoices = frappe.get_all(doctype, - filters ={ - 'docstatus': 1, - 'posting_date': ['between', [from_date, to_date]] - }, fields =['name', 'is_return']) + invoices = frappe.get_all( + doctype, + filters={"docstatus": 1, "posting_date": ["between", [from_date, to_date]]}, + fields=["name", "is_return"], + ) for invoice in invoices: - invoice_items = frappe.get_all(f'{doctype} Item', - filters ={ - 'docstatus': 1, - 'parent': invoice.name, - 'item_tax_template': vat_setting.item_tax_template - }, fields =['item_code', 'net_amount']) + invoice_items = frappe.get_all( + f"{doctype} Item", + filters={ + "docstatus": 1, + "parent": invoice.name, + "item_tax_template": vat_setting.item_tax_template, + }, + fields=["item_code", "net_amount"], + ) for item in invoice_items: # Summing up total taxable amount @@ -158,24 +194,31 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): return total_taxable_amount, total_taxable_adjustment_amount, total_tax - def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency): """Returns data with appended value.""" - data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount, - "currency": company_currency}) + data.append( + { + "title": _(title), + "amount": amount, + "adjustment_amount": adjustment_amount, + "vat_amount": vat_amount, + "currency": company_currency, + } + ) + def get_tax_amount(item_code, account_head, doctype, parent): - if doctype == 'Sales Invoice': - tax_doctype = 'Sales Taxes and Charges' + if doctype == "Sales Invoice": + tax_doctype = "Sales Taxes and Charges" - elif doctype == 'Purchase Invoice': - tax_doctype = 'Purchase Taxes and Charges' + elif doctype == "Purchase Invoice": + tax_doctype = "Purchase Taxes and Charges" - item_wise_tax_detail = frappe.get_value(tax_doctype, { - 'docstatus': 1, - 'parent': parent, - 'account_head': account_head - }, 'item_wise_tax_detail') + item_wise_tax_detail = frappe.get_value( + tax_doctype, + {"docstatus": 1, "parent": parent, "account_head": account_head}, + "item_wise_tax_detail", + ) tax_amount = 0 if item_wise_tax_detail and len(item_wise_tax_detail) > 0: diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py index def43798289..17a62d5e5da 100644 --- a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py @@ -16,6 +16,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -23,53 +24,54 @@ def get_columns(filters): "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 + "width": 160, }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 140 - } + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 140}, ] return columns + def get_data(filters): data = [] - component_type_dict = frappe._dict(frappe.db.sql(""" select name, component_type from `tabSalary Component` - where component_type = 'Professional Tax' """)) + component_type_dict = frappe._dict( + frappe.db.sql( + """ select name, component_type from `tabSalary Component` + where component_type = 'Professional Tax' """ + ) + ) if not len(component_type_dict): return [] conditions = get_conditions(filters) - entry = frappe.db.sql(""" select sal.employee, sal.employee_name, ded.salary_component, ded.amount + entry = frappe.db.sql( + """ select sal.employee, sal.employee_name, ded.salary_component, ded.amount from `tabSalary Slip` sal, `tabSalary Detail` ded where sal.name = ded.parent and ded.parentfield = 'deductions' and ded.parenttype = 'Salary Slip' and sal.docstatus = 1 %s and ded.salary_component in (%s) - """ % (conditions , ", ".join(['%s']*len(component_type_dict))), tuple(component_type_dict.keys()), as_dict=1) + """ + % (conditions, ", ".join(["%s"] * len(component_type_dict))), + tuple(component_type_dict.keys()), + as_dict=1, + ) for d in entry: - employee = { - "employee": d.employee, - "employee_name": d.employee_name, - "amount": d.amount - } + employee = {"employee": d.employee, "employee_name": d.employee_name, "amount": d.amount} data.append(employee) diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py index 190f408fe0e..ab4b6e73b83 100644 --- a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py @@ -13,6 +13,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -20,57 +21,38 @@ def get_columns(filters): "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 - }, - { - "label": _("PF Account"), - "fieldname": "pf_account", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("PF Amount"), - "fieldname": "pf_amount", - "fieldtype": "Currency", - "width": 140 + "width": 160, }, + {"label": _("PF Account"), "fieldname": "pf_account", "fieldtype": "Data", "width": 140}, + {"label": _("PF Amount"), "fieldname": "pf_amount", "fieldtype": "Currency", "width": 140}, { "label": _("Additional PF"), "fieldname": "additional_pf", "fieldtype": "Currency", - "width": 140 + "width": 140, }, - { - "label": _("PF Loan"), - "fieldname": "pf_loan", - "fieldtype": "Currency", - "width": 140 - }, - { - "label": _("Total"), - "fieldname": "total", - "fieldtype": "Currency", - "width": 140 - } + {"label": _("PF Loan"), "fieldname": "pf_loan", "fieldtype": "Currency", "width": 140}, + {"label": _("Total"), "fieldname": "total", "fieldtype": "Currency", "width": 140}, ] return columns + def get_conditions(filters): conditions = [""] if filters.get("department"): - conditions.append("sal.department = '%s' " % (filters["department"]) ) + conditions.append("sal.department = '%s' " % (filters["department"])) if filters.get("branch"): - conditions.append("sal.branch = '%s' " % (filters["branch"]) ) + conditions.append("sal.branch = '%s' " % (filters["branch"])) if filters.get("company"): conditions.append("sal.company = '%s' " % (filters["company"])) @@ -86,10 +68,13 @@ def get_conditions(filters): return " and ".join(conditions) -def prepare_data(entry,component_type_dict): + +def prepare_data(entry, component_type_dict): data_list = {} - employee_account_dict = frappe._dict(frappe.db.sql(""" select name, provident_fund_account from `tabEmployee`""")) + employee_account_dict = frappe._dict( + frappe.db.sql(""" select name, provident_fund_account from `tabEmployee`""") + ) for d in entry: @@ -98,40 +83,57 @@ def prepare_data(entry,component_type_dict): if data_list.get(d.name): data_list[d.name][component_type] = d.amount else: - data_list.setdefault(d.name,{ - "employee": d.employee, - "employee_name": d.employee_name, - "pf_account": employee_account_dict.get(d.employee), - component_type: d.amount - }) + data_list.setdefault( + d.name, + { + "employee": d.employee, + "employee_name": d.employee_name, + "pf_account": employee_account_dict.get(d.employee), + component_type: d.amount, + }, + ) return data_list + def get_data(filters): data = [] conditions = get_conditions(filters) - salary_slips = frappe.db.sql(""" select sal.name from `tabSalary Slip` sal + salary_slips = frappe.db.sql( + """ select sal.name from `tabSalary Slip` sal where docstatus = 1 %s - """ % (conditions), as_dict=1) + """ + % (conditions), + as_dict=1, + ) - component_type_dict = frappe._dict(frappe.db.sql(""" select name, component_type from `tabSalary Component` - where component_type in ('Provident Fund', 'Additional Provident Fund', 'Provident Fund Loan')""")) + component_type_dict = frappe._dict( + frappe.db.sql( + """ select name, component_type from `tabSalary Component` + where component_type in ('Provident Fund', 'Additional Provident Fund', 'Provident Fund Loan')""" + ) + ) if not len(component_type_dict): return [] - entry = frappe.db.sql(""" select sal.name, sal.employee, sal.employee_name, ded.salary_component, ded.amount + entry = frappe.db.sql( + """ select sal.name, sal.employee, sal.employee_name, ded.salary_component, ded.amount from `tabSalary Slip` sal, `tabSalary Detail` ded where sal.name = ded.parent and ded.parentfield = 'deductions' and ded.parenttype = 'Salary Slip' and sal.docstatus = 1 %s and ded.salary_component in (%s) - """ % (conditions, ", ".join(['%s']*len(component_type_dict))), tuple(component_type_dict.keys()), as_dict=1) + """ + % (conditions, ", ".join(["%s"] * len(component_type_dict))), + tuple(component_type_dict.keys()), + as_dict=1, + ) - data_list = prepare_data(entry,component_type_dict) + data_list = prepare_data(entry, component_type_dict) for d in salary_slips: total = 0 @@ -139,7 +141,7 @@ def get_data(filters): employee = { "employee": data_list.get(d.name).get("employee"), "employee_name": data_list.get(d.name).get("employee_name"), - "pf_account": data_list.get(d.name).get("pf_account") + "pf_account": data_list.get(d.name).get("pf_account"), } if data_list.get(d.name).get("Provident Fund"): @@ -160,9 +162,12 @@ def get_data(filters): return data + @frappe.whitelist() def get_years(): - year_list = frappe.db.sql_list("""select distinct YEAR(end_date) from `tabSalary Slip` ORDER BY YEAR(end_date) DESC""") + year_list = frappe.db.sql_list( + """select distinct YEAR(end_date) from `tabSalary Slip` ORDER BY YEAR(end_date) DESC""" + ) if not year_list: year_list = [getdate().year] diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 62d694ba7ba..e021bc8789d 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -20,6 +20,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse_account test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] + class TestUaeVat201(TestCase): def setUp(self): frappe.set_user("Administrator") @@ -36,9 +37,9 @@ class TestUaeVat201(TestCase): create_warehouse("_Test UAE VAT Supplier Warehouse", company="_Test Company UAE VAT") - make_item("_Test UAE VAT Item", properties = {"is_zero_rated": 0, "is_exempt": 0}) - make_item("_Test UAE VAT Zero Rated Item", properties = {"is_zero_rated": 1, "is_exempt": 0}) - make_item("_Test UAE VAT Exempt Item", properties = {"is_zero_rated": 0, "is_exempt": 1}) + make_item("_Test UAE VAT Item", properties={"is_zero_rated": 0, "is_exempt": 0}) + make_item("_Test UAE VAT Zero Rated Item", properties={"is_zero_rated": 1, "is_exempt": 0}) + make_item("_Test UAE VAT Exempt Item", properties={"is_zero_rated": 0, "is_exempt": 1}) make_sales_invoices() @@ -54,27 +55,30 @@ class TestUaeVat201(TestCase): "raw_amount": amount, "raw_vat_amount": vat, } - self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"],100) - self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"],5) - self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200) - self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"],10) - self.assertEqual(get_tourist_tax_return_total(filters),100) - self.assertEqual(get_tourist_tax_return_tax(filters),2) - self.assertEqual(get_zero_rated_total(filters),100) - self.assertEqual(get_exempt_total(filters),100) - self.assertEqual(get_standard_rated_expenses_total(filters),250) - self.assertEqual(get_standard_rated_expenses_tax(filters),1) + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"], 100) + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"], 5) + self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"], 200) + self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"], 10) + self.assertEqual(get_tourist_tax_return_total(filters), 100) + self.assertEqual(get_tourist_tax_return_tax(filters), 2) + self.assertEqual(get_zero_rated_total(filters), 100) + self.assertEqual(get_exempt_total(filters), 100) + self.assertEqual(get_standard_rated_expenses_total(filters), 250) + self.assertEqual(get_standard_rated_expenses_tax(filters), 1) + def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "AED", - "country": "United Arab Emirates", - "create_chart_of_accounts_based_on": "Standard Template", - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "AED", + "country": "United Arab Emirates", + "create_chart_of_accounts_based_on": "Standard Template", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -87,50 +91,53 @@ def make_company(company_name, abbr): company.save() return company + def set_vat_accounts(): if not frappe.db.exists("UAE VAT Settings", "_Test Company UAE VAT"): vat_accounts = frappe.get_all( "Account", fields=["name"], - filters = { - "company": "_Test Company UAE VAT", - "is_group": 0, - "account_type": "Tax" - } + filters={"company": "_Test Company UAE VAT", "is_group": 0, "account_type": "Tax"}, ) uae_vat_accounts = [] for account in vat_accounts: - uae_vat_accounts.append({ - "doctype": "UAE VAT Account", - "account": account.name - }) + uae_vat_accounts.append({"doctype": "UAE VAT Account", "account": account.name}) + + frappe.get_doc( + { + "company": "_Test Company UAE VAT", + "uae_vat_accounts": uae_vat_accounts, + "doctype": "UAE VAT Settings", + } + ).insert() - frappe.get_doc({ - "company": "_Test Company UAE VAT", - "uae_vat_accounts": uae_vat_accounts, - "doctype": "UAE VAT Settings", - }).insert() def make_customer(): if not frappe.db.exists("Customer", "_Test UAE Customer"): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test UAE Customer", - "customer_type": "Company", - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test UAE Customer", + "customer_type": "Company", + } + ) customer.insert() else: customer = frappe.get_doc("Customer", "_Test UAE Customer") + def make_supplier(): if not frappe.db.exists("Supplier", "_Test UAE Supplier"): - frappe.get_doc({ - "supplier_group": "Local", - "supplier_name": "_Test UAE Supplier", - "supplier_type": "Individual", - "doctype": "Supplier", - }).insert() + frappe.get_doc( + { + "supplier_group": "Local", + "supplier_name": "_Test UAE Supplier", + "supplier_type": "Individual", + "doctype": "Supplier", + } + ).insert() + def create_warehouse(warehouse_name, properties=None, company=None): if not company: @@ -150,17 +157,20 @@ def create_warehouse(warehouse_name, properties=None, company=None): else: return warehouse_id + def make_item(item_code, properties=None): if frappe.db.exists("Item", item_code): return frappe.get_doc("Item", item_code) - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) @@ -169,71 +179,77 @@ def make_item(item_code, properties=None): return item + def make_sales_invoices(): - def make_sales_invoices_wrapper(emirate, item, tax = True, tourist_tax= False): + def make_sales_invoices_wrapper(emirate, item, tax=True, tourist_tax=False): si = create_sales_invoice( company="_Test Company UAE VAT", - customer = '_Test UAE Customer', - currency = 'AED', - warehouse = 'Finished Goods - _TCUV', - debit_to = 'Debtors - _TCUV', - income_account = 'Sales - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - cost_center = 'Main - _TCUV', - item = item, - do_not_save=1 + customer="_Test UAE Customer", + currency="AED", + warehouse="Finished Goods - _TCUV", + debit_to="Debtors - _TCUV", + income_account="Sales - _TCUV", + expense_account="Cost of Goods Sold - _TCUV", + cost_center="Main - _TCUV", + item=item, + do_not_save=1, ) si.vat_emirate = emirate if tax: si.append( - "taxes", { + "taxes", + { "charge_type": "On Net Total", "account_head": "VAT 5% - _TCUV", "cost_center": "Main - _TCUV", "description": "VAT 5% @ 5.0", - "rate": 5.0 - } + "rate": 5.0, + }, ) if tourist_tax: si.tourist_tax_return = 2 si.submit() - #Define Item Names + # Define Item Names uae_item = "_Test UAE VAT Item" uae_exempt_item = "_Test UAE VAT Exempt Item" uae_zero_rated_item = "_Test UAE VAT Zero Rated Item" - #Sales Invoice with standard rated expense in Dubai - make_sales_invoices_wrapper('Dubai', uae_item) - #Sales Invoice with standard rated expense in Sharjah - make_sales_invoices_wrapper('Sharjah', uae_item) - #Sales Invoice with Tourist Tax Return - make_sales_invoices_wrapper('Dubai', uae_item, True, True) - #Sales Invoice with Exempt Item - make_sales_invoices_wrapper('Sharjah', uae_exempt_item, False) - #Sales Invoice with Zero Rated Item - make_sales_invoices_wrapper('Sharjah', uae_zero_rated_item, False) + # Sales Invoice with standard rated expense in Dubai + make_sales_invoices_wrapper("Dubai", uae_item) + # Sales Invoice with standard rated expense in Sharjah + make_sales_invoices_wrapper("Sharjah", uae_item) + # Sales Invoice with Tourist Tax Return + make_sales_invoices_wrapper("Dubai", uae_item, True, True) + # Sales Invoice with Exempt Item + make_sales_invoices_wrapper("Sharjah", uae_exempt_item, False) + # Sales Invoice with Zero Rated Item + make_sales_invoices_wrapper("Sharjah", uae_zero_rated_item, False) + def create_purchase_invoices(): pi = make_purchase_invoice( company="_Test Company UAE VAT", - supplier = '_Test UAE Supplier', - supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - currency = 'AED', - cost_center = 'Main - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - item = "_Test UAE VAT Item", + supplier="_Test UAE Supplier", + supplier_warehouse="_Test UAE VAT Supplier Warehouse - _TCUV", + warehouse="_Test UAE VAT Supplier Warehouse - _TCUV", + currency="AED", + cost_center="Main - _TCUV", + expense_account="Cost of Goods Sold - _TCUV", + item="_Test UAE VAT Item", do_not_save=1, - uom = "Nos" + uom="Nos", + ) + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0, + }, ) - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT 5% - _TCUV", - "cost_center": "Main - _TCUV", - "description": "VAT 5% @ 5.0", - "rate": 5.0 - }) pi.recoverable_standard_rated_expenses = 1 diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index f8379aa17ab..59ef58bfde3 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -11,21 +11,12 @@ def execute(filters=None): data, emirates, amounts_by_emirate = get_data(filters) return columns, data + def get_columns(): """Creates a list of dictionaries that are used to generate column headers of the data table.""" return [ - { - "fieldname": "no", - "label": _("No"), - "fieldtype": "Data", - "width": 50 - }, - { - "fieldname": "legend", - "label": _("Legend"), - "fieldtype": "Data", - "width": 300 - }, + {"fieldname": "no", "label": _("No"), "fieldtype": "Data", "width": 50}, + {"fieldname": "legend", "label": _("Legend"), "fieldtype": "Data", "width": 300}, { "fieldname": "amount", "label": _("Amount (AED)"), @@ -37,41 +28,53 @@ def get_columns(): "label": _("VAT Amount (AED)"), "fieldtype": "Currency", "width": 150, - } + }, ] -def get_data(filters = None): + +def get_data(filters=None): """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data.""" data = [] emirates, amounts_by_emirate = append_vat_on_sales(data, filters) append_vat_on_expenses(data, filters) return data, emirates, amounts_by_emirate + def append_vat_on_sales(data, filters): """Appends Sales and All Other Outputs.""" - append_data(data, '', _('VAT on Sales and All Other Outputs'), '', '') + append_data(data, "", _("VAT on Sales and All Other Outputs"), "", "") emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters) - append_data(data, '2', - _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), - frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'), - frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency')) + append_data( + data, + "2", + _("Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme"), + frappe.format((-1) * get_tourist_tax_return_total(filters), "Currency"), + frappe.format((-1) * get_tourist_tax_return_tax(filters), "Currency"), + ) - append_data(data, '3', _('Supplies subject to the reverse charge provision'), - frappe.format(get_reverse_charge_total(filters), 'Currency'), - frappe.format(get_reverse_charge_tax(filters), 'Currency')) + append_data( + data, + "3", + _("Supplies subject to the reverse charge provision"), + frappe.format(get_reverse_charge_total(filters), "Currency"), + frappe.format(get_reverse_charge_tax(filters), "Currency"), + ) - append_data(data, '4', _('Zero Rated'), - frappe.format(get_zero_rated_total(filters), 'Currency'), "-") + append_data( + data, "4", _("Zero Rated"), frappe.format(get_zero_rated_total(filters), "Currency"), "-" + ) - append_data(data, '5', _('Exempt Supplies'), - frappe.format(get_exempt_total(filters), 'Currency'),"-") + append_data( + data, "5", _("Exempt Supplies"), frappe.format(get_exempt_total(filters), "Currency"), "-" + ) - append_data(data, '', '', '', '') + append_data(data, "", "", "", "") return emirates, amounts_by_emirate + def standard_rated_expenses_emiratewise(data, filters): """Append emiratewise standard rated expenses and vat.""" total_emiratewise = get_total_emiratewise(filters) @@ -82,44 +85,61 @@ def standard_rated_expenses_emiratewise(data, filters): "legend": emirate, "raw_amount": amount, "raw_vat_amount": vat, - "amount": frappe.format(amount, 'Currency'), - "vat_amount": frappe.format(vat, 'Currency'), + "amount": frappe.format(amount, "Currency"), + "vat_amount": frappe.format(vat, "Currency"), } amounts_by_emirate = append_emiratewise_expenses(data, emirates, amounts_by_emirate) return emirates, amounts_by_emirate + def append_emiratewise_expenses(data, emirates, amounts_by_emirate): """Append emiratewise standard rated expenses and vat.""" for no, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: - amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(no)) - amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) + amounts_by_emirate[emirate]["no"] = _("1{0}").format(chr(no)) + amounts_by_emirate[emirate]["legend"] = _("Standard rated supplies in {0}").format(emirate) data.append(amounts_by_emirate[emirate]) else: - append_data(data, _('1{0}').format(chr(no)), - _('Standard rated supplies in {0}').format(emirate), - frappe.format(0, 'Currency'), frappe.format(0, 'Currency')) + append_data( + data, + _("1{0}").format(chr(no)), + _("Standard rated supplies in {0}").format(emirate), + frappe.format(0, "Currency"), + frappe.format(0, "Currency"), + ) return amounts_by_emirate + def append_vat_on_expenses(data, filters): """Appends Expenses and All Other Inputs.""" - append_data(data, '', _('VAT on Expenses and All Other Inputs'), '', '') - append_data(data, '9', _('Standard Rated Expenses'), - frappe.format(get_standard_rated_expenses_total(filters), 'Currency'), - frappe.format(get_standard_rated_expenses_tax(filters), 'Currency')) - append_data(data, '10', _('Supplies subject to the reverse charge provision'), - frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'), - frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency')) + append_data(data, "", _("VAT on Expenses and All Other Inputs"), "", "") + append_data( + data, + "9", + _("Standard Rated Expenses"), + frappe.format(get_standard_rated_expenses_total(filters), "Currency"), + frappe.format(get_standard_rated_expenses_tax(filters), "Currency"), + ) + append_data( + data, + "10", + _("Supplies subject to the reverse charge provision"), + frappe.format(get_reverse_charge_recoverable_total(filters), "Currency"), + frappe.format(get_reverse_charge_recoverable_tax(filters), "Currency"), + ) + def append_data(data, no, legend, amount, vat_amount): """Returns data with appended value.""" - data.append({"no": no, "legend":legend, "amount": amount, "vat_amount": vat_amount}) + data.append({"no": no, "legend": legend, "amount": amount, "vat_amount": vat_amount}) + def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return frappe.db.sql( + """ select s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount) from @@ -131,52 +151,54 @@ def get_total_emiratewise(filters): {where_conditions} group by s.vat_emirate; - """.format(where_conditions=conditions), filters) + """.format( + where_conditions=conditions + ), + filters, + ) except (IndexError, TypeError): return 0 + def get_emirates(): """Returns a List of emirates in the order that they are to be displayed.""" - return [ - 'Abu Dhabi', - 'Dubai', - 'Sharjah', - 'Ajman', - 'Umm Al Quwain', - 'Ras Al Khaimah', - 'Fujairah' - ] + return ["Abu Dhabi", "Dubai", "Sharjah", "Ajman", "Umm Al Quwain", "Ras Al Khaimah", "Fujairah"] + def get_filters(filters): """The conditions to be used to filter data to calculate the total sale.""" query_filters = [] if filters.get("company"): - query_filters.append(["company", '=', filters['company']]) + query_filters.append(["company", "=", filters["company"]]) if filters.get("from_date"): - query_filters.append(["posting_date", '>=', filters['from_date']]) + query_filters.append(["posting_date", ">=", filters["from_date"]]) if filters.get("from_date"): - query_filters.append(["posting_date", '<=', filters['to_date']]) + query_filters.append(["posting_date", "<=", filters["to_date"]]) return query_filters + def get_reverse_charge_total(filters): """Returns the sum of the total of each Purchase invoice made.""" query_filters = get_filters(filters) - query_filters.append(['reverse_charge', '=', 'Y']) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["reverse_charge", "=", "Y"]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_reverse_charge_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(debit) from `tabPurchase Invoice` p inner join `tabGL Entry` gl on @@ -187,28 +209,38 @@ def get_reverse_charge_tax(filters): and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) + def get_reverse_charge_recoverable_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) - query_filters.append(['reverse_charge', '=', 'Y']) - query_filters.append(['recoverable_reverse_charge', '>', '0']) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["reverse_charge", "=", "Y"]) + query_filters.append(["recoverable_reverse_charge", ">", "0"]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_reverse_charge_recoverable_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(debit * p.recoverable_reverse_charge / 100) from @@ -222,83 +254,107 @@ def get_reverse_charge_recoverable_tax(filters): and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) + def get_conditions_join(filters): """The conditions to be used to filter data to calculate the total vat.""" conditions = "" - for opts in (("company", " and p.company=%(company)s"), + for opts in ( + ("company", " and p.company=%(company)s"), ("from_date", " and p.posting_date>=%(from_date)s"), - ("to_date", " and p.posting_date<=%(to_date)s")): + ("to_date", " and p.posting_date<=%(to_date)s"), + ): if filters.get(opts[0]): conditions += opts[1] return conditions + def get_standard_rated_expenses_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) - query_filters.append(['recoverable_standard_rated_expenses', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["recoverable_standard_rated_expenses", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" query_filters = get_filters(filters) - query_filters.append(['recoverable_standard_rated_expenses', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["recoverable_standard_rated_expenses", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(recoverable_standard_rated_expenses)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", + filters=query_filters, + fields=["sum(recoverable_standard_rated_expenses)"], + as_list=True, + limit=1, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) - query_filters.append(['tourist_tax_return', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["tourist_tax_return", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Sales Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Sales Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_tourist_tax_return_tax(filters): """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) - query_filters.append(['tourist_tax_return', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["tourist_tax_return", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Sales Invoice', - filters = query_filters, - fields = ['sum(tourist_tax_return)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Sales Invoice", + filters=query_filters, + fields=["sum(tourist_tax_return)"], + as_list=True, + limit=1, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_zero_rated_total(filters): """Returns the sum of each Sales Invoice Item Amount which is zero rated.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(i.base_amount) as total from @@ -308,15 +364,24 @@ def get_zero_rated_total(filters): where s.docstatus = 1 and i.is_zero_rated = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_exempt_total(filters): """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(i.base_amount) as total from @@ -326,15 +391,25 @@ def get_exempt_total(filters): where s.docstatus = 1 and i.is_exempt = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + + def get_conditions(filters): """The conditions to be used to filter data to calculate the total sale.""" conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): + ("to_date", " and posting_date<=%(to_date)s"), + ): if filters.get(opts[0]): conditions += opts[1] return conditions diff --git a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py index f22abae1ff8..a898a251043 100644 --- a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py @@ -18,14 +18,22 @@ class TestVATAuditReport(TestCase): frappe.set_user("Administrator") make_company("_Test Company SA VAT", "_TCSV") - create_account(account_name="VAT - 0%", account_type="Tax", - parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") - create_account(account_name="VAT - 15%", account_type="Tax", - parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + create_account( + account_name="VAT - 0%", + account_type="Tax", + parent_account="Duties and Taxes - _TCSV", + company="_Test Company SA VAT", + ) + create_account( + account_name="VAT - 15%", + account_type="Tax", + parent_account="Duties and Taxes - _TCSV", + company="_Test Company SA VAT", + ) set_sa_vat_accounts() make_item("_Test SA VAT Item") - make_item("_Test SA VAT Zero Rated Item", properties = {"is_zero_rated": 1}) + make_item("_Test SA VAT Zero Rated Item", properties={"is_zero_rated": 1}) make_customer() make_supplier() @@ -38,34 +46,33 @@ class TestVATAuditReport(TestCase): frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company SA VAT'") def test_vat_audit_report(self): - filters = { - "company": "_Test Company SA VAT", - "from_date": today(), - "to_date": today() - } + filters = {"company": "_Test Company SA VAT", "from_date": today(), "to_date": today()} columns, data = execute(filters) total_tax_amount = 0 total_row_tax = 0 for row in data: keys = row.keys() # skips total row tax_amount in if.. and skips section header in elif.. - if 'voucher_no' in keys: - total_tax_amount = total_tax_amount + row['tax_amount'] - elif 'tax_amount' in keys: - total_row_tax = total_row_tax + row['tax_amount'] + if "voucher_no" in keys: + total_tax_amount = total_tax_amount + row["tax_amount"] + elif "tax_amount" in keys: + total_row_tax = total_row_tax + row["tax_amount"] self.assertEqual(total_tax_amount, total_row_tax) + def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "ZAR", - "country": "South Africa", - "create_chart_of_accounts_based_on": "Standard Template" - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "ZAR", + "country": "South Africa", + "create_chart_of_accounts_based_on": "Standard Template", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -79,86 +86,95 @@ def make_company(company_name, abbr): return company + def set_sa_vat_accounts(): if not frappe.db.exists("South Africa VAT Settings", "_Test Company SA VAT"): vat_accounts = frappe.get_all( "Account", fields=["name"], - filters = { - "company": "_Test Company SA VAT", - "is_group": 0, - "account_type": "Tax" - } + filters={"company": "_Test Company SA VAT", "is_group": 0, "account_type": "Tax"}, ) sa_vat_accounts = [] for account in vat_accounts: - sa_vat_accounts.append({ - "doctype": "South Africa VAT Account", - "account": account.name - }) + sa_vat_accounts.append({"doctype": "South Africa VAT Account", "account": account.name}) + + frappe.get_doc( + { + "company": "_Test Company SA VAT", + "vat_accounts": sa_vat_accounts, + "doctype": "South Africa VAT Settings", + } + ).insert() - frappe.get_doc({ - "company": "_Test Company SA VAT", - "vat_accounts": sa_vat_accounts, - "doctype": "South Africa VAT Settings", - }).insert() def make_customer(): if not frappe.db.exists("Customer", "_Test SA Customer"): - frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test SA Customer", - "customer_type": "Company", - }).insert() + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test SA Customer", + "customer_type": "Company", + } + ).insert() + def make_supplier(): if not frappe.db.exists("Supplier", "_Test SA Supplier"): - frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": "_Test SA Supplier", - "supplier_type": "Company", - "supplier_group":"All Supplier Groups" - }).insert() + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": "_Test SA Supplier", + "supplier_type": "Company", + "supplier_group": "All Supplier Groups", + } + ).insert() + def make_item(item_code, properties=None): if not frappe.db.exists("Item", item_code): - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) item.insert() + def make_sales_invoices(): def make_sales_invoices_wrapper(item, rate, tax_account, tax_rate, tax=True): si = create_sales_invoice( company="_Test Company SA VAT", - customer = "_Test SA Customer", - currency = "ZAR", + customer="_Test SA Customer", + currency="ZAR", item=item, rate=rate, - warehouse = "Finished Goods - _TCSV", - debit_to = "Debtors - _TCSV", - income_account = "Sales - _TCSV", - expense_account = "Cost of Goods Sold - _TCSV", - cost_center = "Main - _TCSV", - do_not_save=1 + warehouse="Finished Goods - _TCSV", + debit_to="Debtors - _TCSV", + income_account="Sales - _TCSV", + expense_account="Cost of Goods Sold - _TCSV", + cost_center="Main - _TCSV", + do_not_save=1, ) if tax: - si.append("taxes", { + si.append( + "taxes", + { "charge_type": "On Net Total", "account_head": tax_account, "cost_center": "Main - _TCSV", "description": "VAT 15% @ 15.0", - "rate": tax_rate - }) + "rate": tax_rate, + }, + ) si.submit() @@ -168,27 +184,31 @@ def make_sales_invoices(): make_sales_invoices_wrapper(test_item, 100.0, "VAT - 15% - _TCSV", 15.0) make_sales_invoices_wrapper(test_zero_rated_item, 100.0, "VAT - 0% - _TCSV", 0.0) + def create_purchase_invoices(): pi = make_purchase_invoice( - company = "_Test Company SA VAT", - supplier = "_Test SA Supplier", - supplier_warehouse = "Finished Goods - _TCSV", - warehouse = "Finished Goods - _TCSV", - currency = "ZAR", - cost_center = "Main - _TCSV", - expense_account = "Cost of Goods Sold - _TCSV", - item = "_Test SA VAT Item", - qty = 1, - rate = 100, - uom = "Nos", - do_not_save = 1 + company="_Test Company SA VAT", + supplier="_Test SA Supplier", + supplier_warehouse="Finished Goods - _TCSV", + warehouse="Finished Goods - _TCSV", + currency="ZAR", + cost_center="Main - _TCSV", + expense_account="Cost of Goods Sold - _TCSV", + item="_Test SA VAT Item", + qty=1, + rate=100, + uom="Nos", + do_not_save=1, + ) + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "VAT - 15% - _TCSV", + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": 15.0, + }, ) - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT - 15% - _TCSV", - "cost_center": "Main - _TCSV", - "description": "VAT 15% @ 15.0", - "rate": 15.0 - }) pi.submit() diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 17e50648b3b..70f2c0a3339 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -12,8 +12,8 @@ from frappe.utils import formatdate, get_link_to_form def execute(filters=None): return VATAuditReport(filters).run() -class VATAuditReport(object): +class VATAuditReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.columns = [] @@ -27,8 +27,11 @@ class VATAuditReport(object): self.select_columns = """ name as voucher_no, posting_date, remarks""" - columns = ", supplier as party, credit_to as account" if doctype=="Purchase Invoice" \ + columns = ( + ", supplier as party, credit_to as account" + if doctype == "Purchase Invoice" else ", customer as party, debit_to as account" + ) self.select_columns += columns self.get_invoice_data(doctype) @@ -41,17 +44,21 @@ class VATAuditReport(object): return self.columns, self.data def get_sa_vat_accounts(self): - self.sa_vat_accounts = frappe.get_all("South Africa VAT Account", - filters = {"parent": self.filters.company}, pluck="account") + self.sa_vat_accounts = frappe.get_all( + "South Africa VAT Account", filters={"parent": self.filters.company}, pluck="account" + ) if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: - link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") + link_to_settings = get_link_to_form( + "South Africa VAT Settings", "", label="South Africa VAT Settings" + ) frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings)) def get_invoice_data(self, doctype): conditions = self.get_conditions() self.invoices = frappe._dict() - invoice_data = frappe.db.sql(""" + invoice_data = frappe.db.sql( + """ SELECT {select_columns} FROM @@ -61,8 +68,12 @@ class VATAuditReport(object): and is_opening = "No" ORDER BY posting_date DESC - """.format(select_columns=self.select_columns, doctype=doctype, - where_conditions=conditions), self.filters, as_dict=1) + """.format( + select_columns=self.select_columns, doctype=doctype, where_conditions=conditions + ), + self.filters, + as_dict=1, + ) for d in invoice_data: self.invoices.setdefault(d.voucher_no, d) @@ -70,30 +81,35 @@ class VATAuditReport(object): def get_invoice_items(self, doctype): self.invoice_items = frappe._dict() - items = frappe.db.sql(""" + items = frappe.db.sql( + """ SELECT item_code, parent, base_net_amount, is_zero_rated FROM `tab%s Item` WHERE parent in (%s) - """ % (doctype, ", ".join(["%s"]*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (doctype, ", ".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in items: - if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, { - 'net_amount': 0.0}) - self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('base_net_amount', 0) - self.invoice_items[d.parent][d.item_code]['is_zero_rated'] = d.is_zero_rated + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0}) + self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0) + self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated def get_items_based_on_tax_rate(self, doctype): self.items_based_on_tax_rate = frappe._dict() self.item_tax_rate = frappe._dict() - self.tax_doctype = "Purchase Taxes and Charges" if doctype=="Purchase Invoice" \ - else "Sales Taxes and Charges" + self.tax_doctype = ( + "Purchase Taxes and Charges" if doctype == "Purchase Invoice" else "Sales Taxes and Charges" + ) - self.tax_details = frappe.db.sql(""" + self.tax_details = frappe.db.sql( + """ SELECT - parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount + parent, account_head, item_wise_tax_detail FROM `tab%s` WHERE @@ -101,10 +117,12 @@ class VATAuditReport(object): and parent in (%s) ORDER BY account_head - """ % (self.tax_doctype, "%s", ", ".join(["%s"]*len(self.invoices.keys()))), - tuple([doctype] + list(self.invoices.keys()))) + """ + % (self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))), + tuple([doctype] + list(self.invoices.keys())), + ) - for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: + for parent, account, item_wise_tax_detail in self.tax_details: if item_wise_tax_detail: try: if account in self.sa_vat_accounts: @@ -113,14 +131,15 @@ class VATAuditReport(object): continue for item_code, taxes in item_wise_tax_detail.items(): is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated") - #to skip items with non-zero tax rate in multiple rows + # to skip items with non-zero tax rate in multiple rows if taxes[0] == 0 and not is_zero_rated: continue - tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes) + tax_rate = self.get_item_amount_map(parent, item_code, taxes) if tax_rate is not None: - rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}) \ - .setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( + tax_rate, [] + ) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: @@ -131,23 +150,30 @@ class VATAuditReport(object): tax_rate = taxes[0] tax_amount = taxes[1] gross_amount = net_amount + tax_amount - item_amount_map = self.item_tax_rate.setdefault(parent, {}) \ - .setdefault(item_code, []) - amount_dict = { - "tax_rate": tax_rate, - "gross_amount": gross_amount, - "tax_amount": tax_amount, - "net_amount": net_amount - } - item_amount_map.append(amount_dict) - return tax_rate, item_amount_map + self.item_tax_rate.setdefault(parent, {}).setdefault( + item_code, + { + "tax_rate": tax_rate, + "gross_amount": 0.0, + "tax_amount": 0.0, + "net_amount": 0.0, + }, + ) + + self.item_tax_rate[parent][item_code]["net_amount"] += net_amount + self.item_tax_rate[parent][item_code]["tax_amount"] += tax_amount + self.item_tax_rate[parent][item_code]["gross_amount"] += gross_amount + + return tax_rate def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): + ("to_date", " and posting_date<=%(to_date)s"), + ): if self.filters.get(opts[0]): conditions += opts[1] @@ -174,19 +200,20 @@ class VATAuditReport(object): "gross_amount": total_gross, "tax_amount": total_tax, "net_amount": total_net, - "bold":1 + "bold": 1, } self.data.append(total) self.data.append({}) def get_consolidated_data(self, doctype): - consolidated_data_map={} + consolidated_data_map = {} for inv, inv_data in self.invoices.items(): if self.items_based_on_tax_rate.get(inv): for rate, items in self.items_based_on_tax_rate.get(inv).items(): + row = {"tax_amount": 0.0, "gross_amount": 0.0, "net_amount": 0.0} + consolidated_data_map.setdefault(rate, {"data": []}) for item in items: - row = {} item_details = self.item_tax_rate.get(inv).get(item) row["account"] = inv_data.get("account") row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy") @@ -195,78 +222,54 @@ class VATAuditReport(object): row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier" row["party"] = inv_data.get("party") row["remarks"] = inv_data.get("remarks") - row["gross_amount"]= item_details[0].get("gross_amount") - row["tax_amount"]= item_details[0].get("tax_amount") - row["net_amount"]= item_details[0].get("net_amount") - consolidated_data_map[rate]["data"].append(row) + row["gross_amount"] += item_details.get("gross_amount") + row["tax_amount"] += item_details.get("tax_amount") + row["net_amount"] += item_details.get("net_amount") + + consolidated_data_map[rate]["data"].append(row) return consolidated_data_map def get_columns(self): self.columns = [ - { - "fieldname": "posting_date", - "label": "Posting Date", - "fieldtype": "Data", - "width": 200 - }, + {"fieldname": "posting_date", "label": "Posting Date", "fieldtype": "Data", "width": 200}, { "fieldname": "account", "label": "Account", "fieldtype": "Link", "options": "Account", - "width": 150 + "width": 150, }, { "fieldname": "voucher_type", "label": "Voucher Type", "fieldtype": "Data", "width": 140, - "hidden": 1 + "hidden": 1, }, { "fieldname": "voucher_no", "label": "Reference", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": 150 + "width": 150, }, { "fieldname": "party_type", "label": "Party Type", "fieldtype": "Data", "width": 140, - "hidden": 1 + "hidden": 1, }, { "fieldname": "party", "label": "Party", "fieldtype": "Dynamic Link", "options": "party_type", - "width": 150 - }, - { - "fieldname": "remarks", - "label": "Details", - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "net_amount", - "label": "Net Amount", - "fieldtype": "Currency", - "width": 130 - }, - { - "fieldname": "tax_amount", - "label": "Tax Amount", - "fieldtype": "Currency", - "width": 130 - }, - { - "fieldname": "gross_amount", - "label": "Gross Amount", - "fieldtype": "Currency", - "width": 130 + "width": 150, }, + {"fieldname": "remarks", "label": "Details", "fieldtype": "Data", "width": 150}, + {"fieldname": "net_amount", "label": "Net Amount", "fieldtype": "Currency", "width": 130}, + {"fieldname": "tax_amount", "label": "Tax Amount", "fieldtype": "Currency", "width": 130}, + {"fieldname": "gross_amount", "label": "Gross Amount", "fieldtype": "Currency", "width": 130}, ] diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 2e31c03d5c6..0b9f753dcc3 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -4,15 +4,19 @@ import frappe from frappe.permissions import add_permission, update_permission_property from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields -from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting +from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import ( + create_ksa_vat_setting, +) from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + def setup(company=None, patch=True): uae_custom_fields() add_print_formats() add_permissions() make_custom_fields() + def add_print_formats(): frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True) frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True) @@ -20,19 +24,27 @@ def add_print_formats(): frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'): + for d in ( + "Simplified Tax Invoice", + "Detailed Tax Invoice", + "Tax Invoice", + "KSA VAT Invoice", + "KSA POS Invoice", + ): frappe.db.set_value("Print Format", d, "disabled", 0) + def add_permissions(): """Add Permissions for KSA VAT Setting.""" - add_permission('KSA VAT Setting', 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): - add_permission('KSA VAT Setting', role, 0) - update_permission_property('KSA VAT Setting', role, 0, 'write', 1) - update_permission_property('KSA VAT Setting', role, 0, 'create', 1) + add_permission("KSA VAT Setting", "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): + add_permission("KSA VAT Setting", role, 0) + update_permission_property("KSA VAT Setting", role, 0, "write", 1) + update_permission_property("KSA VAT Setting", role, 0, "create", 1) """Enable KSA VAT Report""" - frappe.db.set_value('Report', 'KSA VAT', 'disabled', 0) + frappe.db.set_value("Report", "KSA VAT", "disabled", 0) + def make_custom_fields(): """Create Custom fields @@ -41,41 +53,46 @@ def make_custom_fields(): - Address in Arabic """ custom_fields = { - 'Sales Invoice': [ + "Sales Invoice": [ dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, ) ], - 'POS Invoice': [ + "POS Invoice": [ dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, ) ], - 'Address': [ + "Address": [ dict( - fieldname='address_in_arabic', - label='Address in Arabic', - fieldtype='Data', - insert_after='address_line2' + fieldname="address_in_arabic", + label="Address in Arabic", + fieldtype="Data", + insert_after="address_line2", ) ], - 'Company': [ + "Company": [ dict( - fieldname='company_name_in_arabic', - label='Company Name In Arabic', - fieldtype='Data', - insert_after='company_name' + fieldname="company_name_in_arabic", + label="Company Name In Arabic", + fieldtype="Data", + insert_after="company_name", ) - ] + ], } create_custom_fields(custom_fields, update=True) + def update_regional_tax_settings(country, company): create_ksa_vat_setting(company) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 674ea83cc65..4557730e4da 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -16,21 +16,25 @@ from erpnext import get_region def create_qr_code(doc, method=None): region = get_region(doc.company) - if region not in ['Saudi Arabia']: + if region not in ["Saudi Arabia"]: return # if QR Code field not present, create it. Invoices without QR are invalid as per law. - if not hasattr(doc, 'ksa_einv_qr'): - create_custom_fields({ - doc.doctype: [ - dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 - ) - ] - }) + if not hasattr(doc, "ksa_einv_qr"): + create_custom_fields( + { + doc.doctype: [ + dict( + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, + ) + ] + } + ) # Don't create QR Code if it already exists qr_code = doc.get("ksa_einv_qr") @@ -40,113 +44,129 @@ def create_qr_code(doc, method=None): meta = frappe.get_meta(doc.doctype) if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]: - ''' TLV conversion for + """TLV conversion for 1. Seller's Name 2. VAT Number 3. Time Stamp 4. Invoice Amount 5. VAT Amount - ''' + """ tlv_array = [] # Sellers Name - seller_name = frappe.db.get_value( - 'Company', - doc.company, - 'company_name_in_arabic') + seller_name = frappe.db.get_value("Company", doc.company, "company_name_in_arabic") if not seller_name: - frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) + frappe.throw(_("Arabic name missing for {} in the company document").format(doc.company)) tag = bytes([1]).hex() - length = bytes([len(seller_name.encode('utf-8'))]).hex() - value = seller_name.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + length = bytes([len(seller_name.encode("utf-8"))]).hex() + value = seller_name.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # VAT Number - tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') + tax_id = frappe.db.get_value("Company", doc.company, "tax_id") if not tax_id: - frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) + frappe.throw(_("Tax ID missing for {} in the company document").format(doc.company)) tag = bytes([2]).hex() length = bytes([len(tax_id)]).hex() - value = tax_id.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = tax_id.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # Time Stamp posting_date = getdate(doc.posting_date) time = get_time(doc.posting_time) seconds = time.hour * 60 * 60 + time.minute * 60 + time.second time_stamp = add_to_date(posting_date, seconds=seconds) - time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') + time_stamp = time_stamp.strftime("%Y-%m-%dT%H:%M:%SZ") tag = bytes([3]).hex() length = bytes([len(time_stamp)]).hex() - value = time_stamp.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = time_stamp.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # Invoice Amount invoice_amount = str(doc.grand_total) tag = bytes([4]).hex() length = bytes([len(invoice_amount)]).hex() - value = invoice_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = invoice_amount.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # VAT Amount - vat_amount = str(doc.total_taxes_and_charges) + vat_amount = str(get_vat_amount(doc)) tag = bytes([5]).hex() length = bytes([len(vat_amount)]).hex() - value = vat_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = vat_amount.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # Joining bytes into one - tlv_buff = ''.join(tlv_array) + tlv_buff = "".join(tlv_array) # base64 conversion for QR Code base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() qr_image = io.BytesIO() - url = qr_create(base64_string, error='L') + url = qr_create(base64_string, error="L") url.png(qr_image, scale=2, quiet_zone=1) name = frappe.generate_hash(doc.name, 5) # making file filename = f"QRCode-{name}.png".replace(os.path.sep, "__") - _file = frappe.get_doc({ - "doctype": "File", - "file_name": filename, - "is_private": 0, - "content": qr_image.getvalue(), - "attached_to_doctype": doc.get("doctype"), - "attached_to_name": doc.get("name"), - "attached_to_field": "ksa_einv_qr" - }) + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": filename, + "is_private": 0, + "content": qr_image.getvalue(), + "attached_to_doctype": doc.get("doctype"), + "attached_to_name": doc.get("name"), + "attached_to_field": "ksa_einv_qr", + } + ) _file.save() # assigning to document - doc.db_set('ksa_einv_qr', _file.file_url) + doc.db_set("ksa_einv_qr", _file.file_url) doc.notify_update() +def get_vat_amount(doc): + vat_settings = frappe.db.get_value("KSA VAT Setting", {"company": doc.company}) + vat_accounts = [] + vat_amount = 0 + + if vat_settings: + vat_settings_doc = frappe.get_cached_doc("KSA VAT Setting", vat_settings) + + for row in vat_settings_doc.get("ksa_vat_sales_accounts"): + vat_accounts.append(row.account) + + for tax in doc.get("taxes"): + if tax.account_head in vat_accounts: + vat_amount += tax.tax_amount + + return vat_amount + + def delete_qr_code_file(doc, method=None): region = get_region(doc.company) - if region not in ['Saudi Arabia']: + if region not in ["Saudi Arabia"]: return - if hasattr(doc, 'ksa_einv_qr'): - if doc.get('ksa_einv_qr'): - file_doc = frappe.get_list('File', { - 'file_url': doc.get('ksa_einv_qr') - }) + if hasattr(doc, "ksa_einv_qr"): + if doc.get("ksa_einv_qr"): + file_doc = frappe.get_list("File", {"file_url": doc.get("ksa_einv_qr")}) if len(file_doc): - frappe.delete_doc('File', file_doc[0].name) + frappe.delete_doc("File", file_doc[0].name) + def delete_vat_settings_for_company(doc, method=None): - if doc.country != 'Saudi Arabia': + if doc.country != "Saudi Arabia": return - if frappe.db.exists('KSA VAT Setting', doc.name): - frappe.delete_doc('KSA VAT Setting', doc.name) + if frappe.db.exists("KSA VAT Setting", doc.name): + frappe.delete_doc("KSA VAT Setting", doc.name) diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py index 97300dc3782..66d9df224e7 100644 --- a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py +++ b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py @@ -5,39 +5,42 @@ import frappe def create_ksa_vat_setting(company): - """On creation of first company. Creates KSA VAT Setting""" + """On creation of first company. Creates KSA VAT Setting""" - company = frappe.get_doc('Company', company) + company = frappe.get_doc("Company", company) - file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'ksa_vat_settings.json') - with open(file_path, 'r') as json_file: - account_data = json.load(json_file) + file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json") + with open(file_path, "r") as json_file: + account_data = json.load(json_file) - # Creating KSA VAT Setting - ksa_vat_setting = frappe.get_doc({ - 'doctype': 'KSA VAT Setting', - 'company': company.name - }) + # Creating KSA VAT Setting + ksa_vat_setting = frappe.get_doc({"doctype": "KSA VAT Setting", "company": company.name}) - for data in account_data: - if data['type'] == 'Sales Account': - for row in data['accounts']: - item_tax_template = row['item_tax_template'] - account = row['account'] - ksa_vat_setting.append('ksa_vat_sales_accounts', { - 'title': row['title'], - 'item_tax_template': f'{item_tax_template} - {company.abbr}', - 'account': f'{account} - {company.abbr}' - }) + for data in account_data: + if data["type"] == "Sales Account": + for row in data["accounts"]: + item_tax_template = row["item_tax_template"] + account = row["account"] + ksa_vat_setting.append( + "ksa_vat_sales_accounts", + { + "title": row["title"], + "item_tax_template": f"{item_tax_template} - {company.abbr}", + "account": f"{account} - {company.abbr}", + }, + ) - elif data['type'] == 'Purchase Account': - for row in data['accounts']: - item_tax_template = row['item_tax_template'] - account = row['account'] - ksa_vat_setting.append('ksa_vat_purchase_accounts', { - 'title': row['title'], - 'item_tax_template': f'{item_tax_template} - {company.abbr}', - 'account': f'{account} - {company.abbr}' - }) + elif data["type"] == "Purchase Account": + for row in data["accounts"]: + item_tax_template = row["item_tax_template"] + account = row["account"] + ksa_vat_setting.append( + "ksa_vat_purchase_accounts", + { + "title": row["title"], + "item_tax_template": f"{item_tax_template} - {company.abbr}", + "account": f"{account} - {company.abbr}", + }, + ) - ksa_vat_setting.save() + ksa_vat_setting.save() diff --git a/erpnext/regional/south_africa/setup.py b/erpnext/regional/south_africa/setup.py index f018de99272..289f2726e9b 100644 --- a/erpnext/regional/south_africa/setup.py +++ b/erpnext/regional/south_africa/setup.py @@ -11,40 +11,48 @@ def setup(company=None, patch=True): make_custom_fields() add_permissions() + def make_custom_fields(update=True): - is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', fetch_from='item_code.is_zero_rated', - insert_after='description', print_hide=1) + is_zero_rated = dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + fetch_from="item_code.is_zero_rated", + insert_after="description", + print_hide=1, + ) custom_fields = { - 'Item': [ - dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', insert_after='item_group', - print_hide=1) + "Item": [ + dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + insert_after="item_group", + print_hide=1, + ) ], - 'Sales Invoice Item': is_zero_rated, - 'Purchase Invoice Item': is_zero_rated + "Sales Invoice Item": is_zero_rated, + "Purchase Invoice Item": is_zero_rated, } create_custom_fields(custom_fields, update=update) + def add_permissions(): """Add Permissions for South Africa VAT Settings and South Africa VAT Account - and VAT Audit Report""" - for doctype in ('South Africa VAT Settings', 'South Africa VAT Account'): - add_permission(doctype, 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + and VAT Audit Report""" + for doctype in ("South Africa VAT Settings", "South Africa VAT Account"): + add_permission(doctype, "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - - if not frappe.db.get_value('Custom Role', dict(report="VAT Audit Report")): - frappe.get_doc(dict( - doctype='Custom Role', - report="VAT Audit Report", - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() \ No newline at end of file + if not frappe.db.get_value("Custom Role", dict(report="VAT Audit Report")): + frappe.get_doc( + dict( + doctype="Custom Role", + report="VAT Audit Report", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() diff --git a/erpnext/regional/turkey/setup.py b/erpnext/regional/turkey/setup.py index c57ea06599d..c915189352c 100644 --- a/erpnext/regional/turkey/setup.py +++ b/erpnext/regional/turkey/setup.py @@ -1,4 +1,2 @@ - - def setup(company=None, patch=True): - pass + pass diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 11f25065eb0..4b9623f014e 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -16,145 +16,270 @@ def setup(company=None, patch=True): add_permissions() create_gratuity_rule() + def make_custom_fields(): - is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description', - print_hide=1) - is_exempt = dict(fieldname='is_exempt', label='Is Exempt', - fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated', - print_hide=1) + is_zero_rated = dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + fetch_from="item_code.is_zero_rated", + insert_after="description", + print_hide=1, + ) + is_exempt = dict( + fieldname="is_exempt", + label="Is Exempt", + fieldtype="Check", + fetch_from="item_code.is_exempt", + insert_after="is_zero_rated", + print_hide=1, + ) invoice_fields = [ - dict(fieldname='vat_section', label='VAT Details', fieldtype='Section Break', - insert_after='group_same_items', print_hide=1, collapsible=1), - dict(fieldname='permit_no', label='Permit Number', - fieldtype='Data', insert_after='vat_section', print_hide=1), + dict( + fieldname="vat_section", + label="VAT Details", + fieldtype="Section Break", + insert_after="group_same_items", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="permit_no", + label="Permit Number", + fieldtype="Data", + insert_after="vat_section", + print_hide=1, + ), ] purchase_invoice_fields = [ - dict(fieldname='company_trn', label='Company TRN', - fieldtype='Read Only', insert_after='shipping_address', - fetch_from='company.tax_id', print_hide=1), - dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', - fieldtype='Read Only', insert_after='supplier_name', - fetch_from='supplier.supplier_name_in_arabic', print_hide=1), - dict(fieldname='recoverable_standard_rated_expenses', print_hide=1, default='0', - label='Recoverable Standard Rated Expenses (AED)', insert_after='permit_no', - fieldtype='Currency', ), - dict(fieldname='reverse_charge', label='Reverse Charge Applicable', - fieldtype='Select', insert_after='recoverable_standard_rated_expenses', print_hide=1, - options='Y\nN', default='N'), - dict(fieldname='recoverable_reverse_charge', label='Recoverable Reverse Charge (Percentage)', - insert_after='reverse_charge', fieldtype='Percent', print_hide=1, - depends_on="eval:doc.reverse_charge=='Y'", default='100.000'), - ] + dict( + fieldname="company_trn", + label="Company TRN", + fieldtype="Read Only", + insert_after="shipping_address", + fetch_from="company.tax_id", + print_hide=1, + ), + dict( + fieldname="supplier_name_in_arabic", + label="Supplier Name in Arabic", + fieldtype="Read Only", + insert_after="supplier_name", + fetch_from="supplier.supplier_name_in_arabic", + print_hide=1, + ), + dict( + fieldname="recoverable_standard_rated_expenses", + print_hide=1, + default="0", + label="Recoverable Standard Rated Expenses (AED)", + insert_after="permit_no", + fieldtype="Currency", + ), + dict( + fieldname="reverse_charge", + label="Reverse Charge Applicable", + fieldtype="Select", + insert_after="recoverable_standard_rated_expenses", + print_hide=1, + options="Y\nN", + default="N", + ), + dict( + fieldname="recoverable_reverse_charge", + label="Recoverable Reverse Charge (Percentage)", + insert_after="reverse_charge", + fieldtype="Percent", + print_hide=1, + depends_on="eval:doc.reverse_charge=='Y'", + default="100.000", + ), + ] sales_invoice_fields = [ - dict(fieldname='company_trn', label='Company TRN', - fieldtype='Read Only', insert_after='company_address', - fetch_from='company.tax_id', print_hide=1), - dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', - fieldtype='Read Only', insert_after='customer_name', - fetch_from='customer.customer_name_in_arabic', print_hide=1), - dict(fieldname='vat_emirate', label='VAT Emirate', insert_after='permit_no', fieldtype='Select', - options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain', - fetch_from='company_address.emirate'), - dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', - insert_after='vat_emirate', fieldtype='Currency', print_hide=1, default='0'), - ] + dict( + fieldname="company_trn", + label="Company TRN", + fieldtype="Read Only", + insert_after="company_address", + fetch_from="company.tax_id", + print_hide=1, + ), + dict( + fieldname="customer_name_in_arabic", + label="Customer Name in Arabic", + fieldtype="Read Only", + insert_after="customer_name", + fetch_from="customer.customer_name_in_arabic", + print_hide=1, + ), + dict( + fieldname="vat_emirate", + label="VAT Emirate", + insert_after="permit_no", + fieldtype="Select", + options="\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain", + fetch_from="company_address.emirate", + ), + dict( + fieldname="tourist_tax_return", + label="Tax Refund provided to Tourists (AED)", + insert_after="vat_emirate", + fieldtype="Currency", + print_hide=1, + default="0", + ), + ] invoice_item_fields = [ - dict(fieldname='tax_code', label='Tax Code', - fieldtype='Read Only', fetch_from='item_code.tax_code', insert_after='description', - allow_on_submit=1, print_hide=1), - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Float', insert_after='tax_code', - print_hide=1, hidden=1, read_only=1), - dict(fieldname='tax_amount', label='Tax Amount', - fieldtype='Currency', insert_after='tax_rate', - print_hide=1, hidden=1, read_only=1, options="currency"), - dict(fieldname='total_amount', label='Total Amount', - fieldtype='Currency', insert_after='tax_amount', - print_hide=1, hidden=1, read_only=1, options="currency"), + dict( + fieldname="tax_code", + label="Tax Code", + fieldtype="Read Only", + fetch_from="item_code.tax_code", + insert_after="description", + allow_on_submit=1, + print_hide=1, + ), + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Float", + insert_after="tax_code", + print_hide=1, + hidden=1, + read_only=1, + ), + dict( + fieldname="tax_amount", + label="Tax Amount", + fieldtype="Currency", + insert_after="tax_rate", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), + dict( + fieldname="total_amount", + label="Total Amount", + fieldtype="Currency", + insert_after="tax_amount", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), ] delivery_date_field = [ - dict(fieldname='delivery_date', label='Delivery Date', - fieldtype='Date', insert_after='item_name', print_hide=1) + dict( + fieldname="delivery_date", + label="Delivery Date", + fieldtype="Date", + insert_after="item_name", + print_hide=1, + ) ] custom_fields = { - 'Item': [ - dict(fieldname='tax_code', label='Tax Code', - fieldtype='Data', insert_after='item_group'), - dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', insert_after='tax_code', - print_hide=1), - dict(fieldname='is_exempt', label='Is Exempt', - fieldtype='Check', insert_after='is_zero_rated', - print_hide=1) + "Item": [ + dict(fieldname="tax_code", label="Tax Code", fieldtype="Data", insert_after="item_group"), + dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + insert_after="tax_code", + print_hide=1, + ), + dict( + fieldname="is_exempt", + label="Is Exempt", + fieldtype="Check", + insert_after="is_zero_rated", + print_hide=1, + ), ], - 'Customer': [ - dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', - fieldtype='Data', insert_after='customer_name'), + "Customer": [ + dict( + fieldname="customer_name_in_arabic", + label="Customer Name in Arabic", + fieldtype="Data", + insert_after="customer_name", + ), ], - 'Supplier': [ - dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', - fieldtype='Data', insert_after='supplier_name'), + "Supplier": [ + dict( + fieldname="supplier_name_in_arabic", + label="Supplier Name in Arabic", + fieldtype="Data", + insert_after="supplier_name", + ), ], - 'Address': [ - dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', - options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') + "Address": [ + dict( + fieldname="emirate", + label="Emirate", + fieldtype="Select", + insert_after="state", + options="\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain", + ) ], - 'Purchase Invoice': purchase_invoice_fields + invoice_fields, - 'Purchase Order': purchase_invoice_fields + invoice_fields, - 'Purchase Receipt': purchase_invoice_fields + invoice_fields, - 'Sales Invoice': sales_invoice_fields + invoice_fields, - 'POS Invoice': sales_invoice_fields + invoice_fields, - 'Sales Order': sales_invoice_fields + invoice_fields, - 'Delivery Note': sales_invoice_fields + invoice_fields, - 'Sales Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], - 'POS Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], - 'Purchase Invoice Item': invoice_item_fields, - 'Sales Order Item': invoice_item_fields, - 'Delivery Note Item': invoice_item_fields, - 'Quotation Item': invoice_item_fields, - 'Purchase Order Item': invoice_item_fields, - 'Purchase Receipt Item': invoice_item_fields, - 'Supplier Quotation Item': invoice_item_fields, + "Purchase Invoice": purchase_invoice_fields + invoice_fields, + "Purchase Order": purchase_invoice_fields + invoice_fields, + "Purchase Receipt": purchase_invoice_fields + invoice_fields, + "Sales Invoice": sales_invoice_fields + invoice_fields, + "POS Invoice": sales_invoice_fields + invoice_fields, + "Sales Order": sales_invoice_fields + invoice_fields, + "Delivery Note": sales_invoice_fields + invoice_fields, + "Sales Invoice Item": invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], + "POS Invoice Item": invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], + "Purchase Invoice Item": invoice_item_fields, + "Sales Order Item": invoice_item_fields, + "Delivery Note Item": invoice_item_fields, + "Quotation Item": invoice_item_fields, + "Purchase Order Item": invoice_item_fields, + "Purchase Receipt Item": invoice_item_fields, + "Supplier Quotation Item": invoice_item_fields, } - create_custom_fields(custom_fields) + create_custom_fields(custom_fields, ignore_validate=True) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "detailed_tax_invoice") frappe.reload_doc("regional", "print_format", "simplified_tax_invoice") frappe.reload_doc("regional", "print_format", "tax_invoice") - frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """) + frappe.db.sql( + """ update `tabPrint Format` set disabled = 0 where + name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """ + ) + def add_custom_roles_for_reports(): """Add Access Control to UAE VAT 201.""" - if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 201')): - frappe.get_doc(dict( - doctype='Custom Role', - report='UAE VAT 201', - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report="UAE VAT 201")): + frappe.get_doc( + dict( + doctype="Custom Role", + report="UAE VAT 201", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() + def add_permissions(): """Add Permissions for UAE VAT Settings and UAE VAT Account.""" - for doctype in ('UAE VAT Settings', 'UAE VAT Account'): - add_permission(doctype, 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + for doctype in ("UAE VAT Settings", "UAE VAT Account"): + add_permission(doctype, "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) + def create_gratuity_rule(): rule_1 = rule_2 = rule_3 = None @@ -162,7 +287,11 @@ def create_gratuity_rule(): # Rule Under Limited Contract slabs = get_slab_for_limited_contract() if not frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)"): - rule_1 = get_gratuity_rule("Rule Under Limited Contract (UAE)", slabs, calculate_gratuity_amount_based_on="Sum of all previous slabs") + rule_1 = get_gratuity_rule( + "Rule Under Limited Contract (UAE)", + slabs, + calculate_gratuity_amount_based_on="Sum of all previous slabs", + ) # Rule Under Unlimited Contract on termination slabs = get_slab_for_unlimited_contract_on_termination() @@ -174,7 +303,7 @@ def create_gratuity_rule(): if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on resignation (UAE)"): rule_3 = get_gratuity_rule("Rule Under Unlimited Contract on resignation (UAE)", slabs) - #for applicable salary component user need to set this by its own + # for applicable salary component user need to set this by its own if rule_1: rule_1.flags.ignore_mandatory = True rule_1.save() @@ -187,61 +316,29 @@ def create_gratuity_rule(): def get_slab_for_limited_contract(): - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 5, "fraction_of_applicable_earnings": 21 / 30}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": 1}, + ] + def get_slab_for_unlimited_contract_on_termination(): - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 5, "fraction_of_applicable_earnings": 21 / 30}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": 1}, + ] + def get_slab_for_unlimited_contract_on_resignation(): - fraction_1 = 1/3 * 21/30 - fraction_2 = 2/3 * 21/30 - fraction_3 = 21/30 + fraction_1 = 1 / 3 * 21 / 30 + fraction_2 = 2 / 3 * 21 / 30 + fraction_3 = 21 / 30 - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":3, - "fraction_of_applicable_earnings": fraction_1 - }, - { - "from_year": 3, - "to_year":5, - "fraction_of_applicable_earnings": fraction_2 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": fraction_3 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 3, "fraction_of_applicable_earnings": fraction_1}, + {"from_year": 3, "to_year": 5, "fraction_of_applicable_earnings": fraction_2}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": fraction_3}, + ] diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 891e75e0033..b0312f7972b 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ from frappe.utils import flt, money_in_words, round_based_on_smallest_currency_fraction @@ -9,7 +8,8 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax def update_itemised_tax_data(doc): - if not doc.taxes: return + if not doc.taxes: + return itemised_tax = get_itemised_tax(doc.taxes) @@ -26,37 +26,39 @@ def update_itemised_tax_data(doc): for account, rate in iteritems(item_tax_rate): tax_rate += rate elif row.item_code and itemised_tax.get(row.item_code): - tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()]) + tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()]) + + meta = frappe.get_meta(row.doctype) + + if meta.has_field("tax_rate"): + row.tax_rate = flt(tax_rate, row.precision("tax_rate")) + row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) + row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) - row.tax_rate = flt(tax_rate, row.precision("tax_rate")) - row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) - row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) def get_account_currency(account): """Helper function to get account currency.""" if not account: return + def generator(): account_currency, company = frappe.get_cached_value( - "Account", - account, - ["account_currency", - "company"] + "Account", account, ["account_currency", "company"] ) if not account_currency: - account_currency = frappe.get_cached_value('Company', company, "default_currency") + account_currency = frappe.get_cached_value("Company", company, "default_currency") return account_currency return frappe.local_cache("account_currency", account, generator) + def get_tax_accounts(company): """Get the list of tax accounts for a specific company.""" tax_accounts_dict = frappe._dict() - tax_accounts_list = frappe.get_all("UAE VAT Account", - filters={"parent": company}, - fields=["Account"] - ) + tax_accounts_list = frappe.get_all( + "UAE VAT Account", filters={"parent": company}, fields=["Account"] + ) if not tax_accounts_list and not frappe.flags.in_test: frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company)) @@ -66,23 +68,24 @@ def get_tax_accounts(company): return tax_accounts_dict + def update_grand_total_for_rcm(doc, method): """If the Reverse Charge is Applicable subtract the tax amount from the grand total and update in the form.""" - country = frappe.get_cached_value('Company', doc.company, 'country') + country = frappe.get_cached_value("Company", doc.company, "country") - if country != 'United Arab Emirates': + if country != "United Arab Emirates": return if not doc.total_taxes_and_charges: return - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": tax_accounts = get_tax_accounts(doc.company) base_vat_tax = 0 vat_tax = 0 - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.category not in ("Total", "Valuation and Total"): continue @@ -97,6 +100,7 @@ def update_grand_total_for_rcm(doc, method): update_totals(vat_tax, base_vat_tax, doc) + def update_totals(vat_tax, base_vat_tax, doc): """Update the grand total values in the form.""" doc.base_grand_total -= base_vat_tax @@ -108,56 +112,67 @@ def update_totals(vat_tax, base_vat_tax, doc): doc.outstanding_amount = doc.grand_total else: - doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total, - doc.currency, doc.precision("rounded_total")) - doc.rounding_adjustment = flt(doc.rounded_total - doc.grand_total, - doc.precision("rounding_adjustment")) + doc.rounded_total = round_based_on_smallest_currency_fraction( + doc.grand_total, doc.currency, doc.precision("rounded_total") + ) + doc.rounding_adjustment = flt( + doc.rounded_total - doc.grand_total, doc.precision("rounding_adjustment") + ) doc.outstanding_amount = doc.rounded_total or doc.grand_total doc.in_words = money_in_words(doc.grand_total, doc.currency) - doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) + doc.base_in_words = money_in_words( + doc.base_grand_total, erpnext.get_company_currency(doc.company) + ) doc.set_payment_schedule() + def make_regional_gl_entries(gl_entries, doc): """Hooked to make_regional_gl_entries in Purchase Invoice.It appends the region specific general ledger entries to the list of GL Entries.""" - country = frappe.get_cached_value('Company', doc.company, 'country') + country = frappe.get_cached_value("Company", doc.company, "country") - if country != 'United Arab Emirates': + if country != "United Arab Emirates": return gl_entries - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": tax_accounts = get_tax_accounts(doc.company) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.category not in ("Total", "Valuation and Total"): continue gl_entries = make_gl_entry(tax, gl_entries, doc, tax_accounts) return gl_entries + def make_gl_entry(tax, gl_entries, doc, tax_accounts): dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: account_currency = get_account_currency(tax.account_head) - gl_entries.append(doc.get_gl_dict({ - "account": tax.account_head, - "cost_center": tax.cost_center, - "posting_date": doc.posting_date, - "against": doc.supplier, - dr_or_cr: tax.base_tax_amount_after_discount_amount, - dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ - if account_currency==doc.company_currency \ - else tax.tax_amount_after_discount_amount - }, account_currency, item=tax - )) + gl_entries.append( + doc.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "posting_date": doc.posting_date, + "against": doc.supplier, + dr_or_cr: tax.base_tax_amount_after_discount_amount, + dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount + if account_currency == doc.company_currency + else tax.tax_amount_after_discount_amount, + }, + account_currency, + item=tax, + ) + ) return gl_entries def validate_returns(doc, method): """Standard Rated expenses should not be set when Reverse Charge Applicable is set.""" - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'United Arab Emirates': + country = frappe.get_cached_value("Company", doc.company, "country") + if country != "United Arab Emirates": return - if doc.reverse_charge == 'Y' and flt(doc.recoverable_standard_rated_expenses) != 0: - frappe.throw(_( - "Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y" - )) + if doc.reverse_charge == "Y" and flt(doc.recoverable_standard_rated_expenses) != 0: + frappe.throw( + _("Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y") + ) diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index e2eb79b05b0..a7dee9deabc 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -11,38 +11,61 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def setup(company=None, patch=True): # Company independent fixtures should be called only once at the first company setup - if frappe.db.count('Company', {'country': 'United States'}) <=1: + if frappe.db.count("Company", {"country": "United States"}) <= 1: setup_company_independent_fixtures(patch=patch) + def setup_company_independent_fixtures(company=None, patch=True): make_custom_fields() add_print_formats() + def make_custom_fields(update=True): custom_fields = { - 'Supplier': [ - dict(fieldname='irs_1099', fieldtype='Check', insert_after='tax_id', - label='Is IRS 1099 reporting required for supplier?') + "Supplier": [ + dict( + fieldname="irs_1099", + fieldtype="Check", + insert_after="tax_id", + label="Is IRS 1099 reporting required for supplier?", + ) ], - 'Sales Order': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges', - label='Is customer exempted from sales tax?') + "Sales Order": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_and_charges", + label="Is customer exempted from sales tax?", + ) ], - 'Sales Invoice': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_section', - label='Is customer exempted from sales tax?') + "Sales Invoice": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_section", + label="Is customer exempted from sales tax?", + ) ], - 'Customer': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='represents_company', - label='Is customer exempted from sales tax?') + "Customer": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="represents_company", + label="Is customer exempted from sales tax?", + ) + ], + "Quotation": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_and_charges", + label="Is customer exempted from sales tax?", + ) ], - 'Quotation': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges', - label='Is customer exempted from sales tax?') - ] } create_custom_fields(custom_fields, update=update) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0) diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 652b4835a18..83ba6ed3ad1 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -9,49 +9,51 @@ from erpnext.regional.report.irs_1099.irs_1099 import execute as execute_1099_re class TestUnitedStates(unittest.TestCase): - def test_irs_1099_custom_field(self): + def test_irs_1099_custom_field(self): - if not frappe.db.exists("Supplier", "_US 1099 Test Supplier"): - doc = frappe.new_doc("Supplier") - doc.supplier_name = "_US 1099 Test Supplier" - doc.supplier_group = "Services" - doc.supplier_type = "Company" - doc.country = "United States" - doc.tax_id = "04-1234567" - doc.irs_1099 = 1 - doc.save() - frappe.db.commit() - supplier = frappe.get_doc('Supplier', "_US 1099 Test Supplier") - self.assertEqual(supplier.irs_1099, 1) + if not frappe.db.exists("Supplier", "_US 1099 Test Supplier"): + doc = frappe.new_doc("Supplier") + doc.supplier_name = "_US 1099 Test Supplier" + doc.supplier_group = "Services" + doc.supplier_type = "Company" + doc.country = "United States" + doc.tax_id = "04-1234567" + doc.irs_1099 = 1 + doc.save() + frappe.db.commit() + supplier = frappe.get_doc("Supplier", "_US 1099 Test Supplier") + self.assertEqual(supplier.irs_1099, 1) - def test_irs_1099_report(self): - make_payment_entry_to_irs_1099_supplier() - filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) - columns, data = execute_1099_report(filters) - expected_row = {'supplier': '_US 1099 Test Supplier', - 'supplier_group': 'Services', - 'payments': 100.0, - 'tax_id': '04-1234567'} - self.assertEqual(data[0], expected_row) + def test_irs_1099_report(self): + make_payment_entry_to_irs_1099_supplier() + filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) + columns, data = execute_1099_report(filters) + expected_row = { + "supplier": "_US 1099 Test Supplier", + "supplier_group": "Services", + "payments": 100.0, + "tax_id": "04-1234567", + } + self.assertEqual(data[0], expected_row) def make_payment_entry_to_irs_1099_supplier(): - frappe.db.sql("delete from `tabGL Entry` where party='_US 1099 Test Supplier'") - frappe.db.sql("delete from `tabGL Entry` where against='_US 1099 Test Supplier'") - frappe.db.sql("delete from `tabPayment Entry` where party='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabGL Entry` where party='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabGL Entry` where against='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabPayment Entry` where party='_US 1099 Test Supplier'") - pe = frappe.new_doc("Payment Entry") - pe.payment_type = "Pay" - pe.company = "_Test Company 1" - pe.posting_date = "2016-01-10" - pe.paid_from = "_Test Bank USD - _TC1" - pe.paid_to = "_Test Payable USD - _TC1" - pe.paid_amount = 100 - pe.received_amount = 100 - pe.reference_no = "For IRS 1099 testing" - pe.reference_date = "2016-01-10" - pe.party_type = "Supplier" - pe.party = "_US 1099 Test Supplier" - pe.insert() - pe.submit() + pe = frappe.new_doc("Payment Entry") + pe.payment_type = "Pay" + pe.company = "_Test Company 1" + pe.posting_date = "2016-01-10" + pe.paid_from = "_Test Bank USD - _TC1" + pe.paid_to = "_Test Payable USD - _TC1" + pe.paid_amount = 100 + pe.received_amount = 100 + pe.reference_no = "For IRS 1099 testing" + pe.reference_date = "2016-01-10" + pe.party_type = "Supplier" + pe.party = "_US 1099 Test Supplier" + pe.insert() + pe.submit() diff --git a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py b/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py index c91ef56142d..a2ebec0a4d6 100644 --- a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py +++ b/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py @@ -1,18 +1,11 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'restaurant', - 'transactions': [ - { - 'label': _('Setup'), - 'items': ['Restaurant Menu', 'Restaurant Table'] - }, - { - 'label': _('Operations'), - 'items': ['Restaurant Reservation', 'Sales Invoice'] - } - ] + "fieldname": "restaurant", + "transactions": [ + {"label": _("Setup"), "items": ["Restaurant Menu", "Restaurant Table"]}, + {"label": _("Operations"), "items": ["Restaurant Reservation", "Sales Invoice"]}, + ], } diff --git a/erpnext/restaurant/doctype/restaurant/test_restaurant.py b/erpnext/restaurant/doctype/restaurant/test_restaurant.py index f88f9801290..0276179323d 100644 --- a/erpnext/restaurant/doctype/restaurant/test_restaurant.py +++ b/erpnext/restaurant/doctype/restaurant/test_restaurant.py @@ -4,11 +4,22 @@ import unittest test_records = [ - dict(doctype='Restaurant', name='Test Restaurant 1', company='_Test Company 1', - invoice_series_prefix='Test-Rest-1-Inv-', default_customer='_Test Customer 1'), - dict(doctype='Restaurant', name='Test Restaurant 2', company='_Test Company 1', - invoice_series_prefix='Test-Rest-2-Inv-', default_customer='_Test Customer 1'), + dict( + doctype="Restaurant", + name="Test Restaurant 1", + company="_Test Company 1", + invoice_series_prefix="Test-Rest-1-Inv-", + default_customer="_Test Customer 1", + ), + dict( + doctype="Restaurant", + name="Test Restaurant 2", + company="_Test Company 1", + invoice_series_prefix="Test-Rest-2-Inv-", + default_customer="_Test Customer 1", + ), ] + class TestRestaurant(unittest.TestCase): pass diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py index 64eb40f3645..893c5123c6b 100644 --- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py +++ b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py @@ -10,45 +10,44 @@ class RestaurantMenu(Document): def validate(self): for d in self.items: if not d.rate: - d.rate = frappe.db.get_value('Item', d.item, 'standard_rate') + d.rate = frappe.db.get_value("Item", d.item, "standard_rate") def on_update(self): - '''Sync Price List''' + """Sync Price List""" self.make_price_list() def on_trash(self): - '''clear prices''' + """clear prices""" self.clear_item_price() def clear_item_price(self, price_list=None): - '''clear all item prices for this menu''' + """clear all item prices for this menu""" if not price_list: price_list = self.get_price_list().name - frappe.db.sql('delete from `tabItem Price` where price_list = %s', price_list) + frappe.db.sql("delete from `tabItem Price` where price_list = %s", price_list) def make_price_list(self): # create price list for menu price_list = self.get_price_list() - self.db_set('price_list', price_list.name) + self.db_set("price_list", price_list.name) # delete old items self.clear_item_price(price_list.name) for d in self.items: - frappe.get_doc(dict( - doctype = 'Item Price', - price_list = price_list.name, - item_code = d.item, - price_list_rate = d.rate - )).insert() + frappe.get_doc( + dict( + doctype="Item Price", price_list=price_list.name, item_code=d.item, price_list_rate=d.rate + ) + ).insert() def get_price_list(self): - '''Create price list for menu if missing''' - price_list_name = frappe.db.get_value('Price List', dict(restaurant_menu=self.name)) + """Create price list for menu if missing""" + price_list_name = frappe.db.get_value("Price List", dict(restaurant_menu=self.name)) if price_list_name: - price_list = frappe.get_doc('Price List', price_list_name) + price_list = frappe.get_doc("Price List", price_list_name) else: - price_list = frappe.new_doc('Price List') + price_list = frappe.new_doc("Price List") price_list.restaurant_menu = self.name price_list.price_list_name = self.name diff --git a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py index 27020eb869f..d8e23eb9a45 100644 --- a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py +++ b/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py @@ -6,47 +6,70 @@ import unittest import frappe test_records = [ - dict(doctype='Item', item_code='Food Item 1', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 2', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 3', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 4', - item_group='Products', is_stock_item=0), - dict(doctype='Restaurant Menu', restaurant='Test Restaurant 1', name='Test Restaurant 1 Menu 1', - items = [ - dict(item='Food Item 1', rate=400), - dict(item='Food Item 2', rate=300), - dict(item='Food Item 3', rate=200), - dict(item='Food Item 4', rate=100), - ]), - dict(doctype='Restaurant Menu', restaurant='Test Restaurant 1', name='Test Restaurant 1 Menu 2', - items = [ - dict(item='Food Item 1', rate=450), - dict(item='Food Item 2', rate=350), - ]) + dict(doctype="Item", item_code="Food Item 1", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Food Item 2", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Food Item 3", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Food Item 4", item_group="Products", is_stock_item=0), + dict( + doctype="Restaurant Menu", + restaurant="Test Restaurant 1", + name="Test Restaurant 1 Menu 1", + items=[ + dict(item="Food Item 1", rate=400), + dict(item="Food Item 2", rate=300), + dict(item="Food Item 3", rate=200), + dict(item="Food Item 4", rate=100), + ], + ), + dict( + doctype="Restaurant Menu", + restaurant="Test Restaurant 1", + name="Test Restaurant 1 Menu 2", + items=[ + dict(item="Food Item 1", rate=450), + dict(item="Food Item 2", rate=350), + ], + ), ] + class TestRestaurantMenu(unittest.TestCase): def test_price_list_creation_and_editing(self): - menu1 = frappe.get_doc('Restaurant Menu', 'Test Restaurant 1 Menu 1') + menu1 = frappe.get_doc("Restaurant Menu", "Test Restaurant 1 Menu 1") menu1.save() - menu2 = frappe.get_doc('Restaurant Menu', 'Test Restaurant 1 Menu 2') + menu2 = frappe.get_doc("Restaurant Menu", "Test Restaurant 1 Menu 2") menu2.save() - self.assertTrue(frappe.db.get_value('Price List', 'Test Restaurant 1 Menu 1')) - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 1', item_code='Food Item 1'), 'price_list_rate'), 400) - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 2', item_code='Food Item 1'), 'price_list_rate'), 450) + self.assertTrue(frappe.db.get_value("Price List", "Test Restaurant 1 Menu 1")) + self.assertEqual( + frappe.db.get_value( + "Item Price", + dict(price_list="Test Restaurant 1 Menu 1", item_code="Food Item 1"), + "price_list_rate", + ), + 400, + ) + self.assertEqual( + frappe.db.get_value( + "Item Price", + dict(price_list="Test Restaurant 1 Menu 2", item_code="Food Item 1"), + "price_list_rate", + ), + 450, + ) menu1.items[0].rate = 401 menu1.save() - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 1', item_code='Food Item 1'), 'price_list_rate'), 401) + self.assertEqual( + frappe.db.get_value( + "Item Price", + dict(price_list="Test Restaurant 1 Menu 1", item_code="Food Item 1"), + "price_list_rate", + ), + 401, + ) menu1.items[0].rate = 400 menu1.save() diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py index f9e75b47a0f..b22f164382f 100644 --- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py +++ b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py @@ -14,78 +14,84 @@ from erpnext.controllers.queries import item_query class RestaurantOrderEntry(Document): pass + @frappe.whitelist() def get_invoice(table): - '''returns the active invoice linked to the given table''' - invoice_name = frappe.get_value('Sales Invoice', dict(restaurant_table = table, docstatus=0)) + """returns the active invoice linked to the given table""" + invoice_name = frappe.get_value("Sales Invoice", dict(restaurant_table=table, docstatus=0)) restaurant, menu_name = get_restaurant_and_menu_name(table) if invoice_name: - invoice = frappe.get_doc('Sales Invoice', invoice_name) + invoice = frappe.get_doc("Sales Invoice", invoice_name) else: - invoice = frappe.new_doc('Sales Invoice') - invoice.naming_series = frappe.db.get_value('Restaurant', restaurant, 'invoice_series_prefix') + invoice = frappe.new_doc("Sales Invoice") + invoice.naming_series = frappe.db.get_value("Restaurant", restaurant, "invoice_series_prefix") invoice.is_pos = 1 - default_customer = frappe.db.get_value('Restaurant', restaurant, 'default_customer') + default_customer = frappe.db.get_value("Restaurant", restaurant, "default_customer") if not default_customer: - frappe.throw(_('Please set default customer in Restaurant Settings')) + frappe.throw(_("Please set default customer in Restaurant Settings")) invoice.customer = default_customer - invoice.taxes_and_charges = frappe.db.get_value('Restaurant', restaurant, 'default_tax_template') - invoice.selling_price_list = frappe.db.get_value('Price List', dict(restaurant_menu=menu_name, enabled=1)) + invoice.taxes_and_charges = frappe.db.get_value("Restaurant", restaurant, "default_tax_template") + invoice.selling_price_list = frappe.db.get_value( + "Price List", dict(restaurant_menu=menu_name, enabled=1) + ) return invoice + @frappe.whitelist() def sync(table, items): - '''Sync the sales order related to the table''' + """Sync the sales order related to the table""" invoice = get_invoice(table) items = json.loads(items) invoice.items = [] invoice.restaurant_table = table for d in items: - invoice.append('items', dict( - item_code = d.get('item'), - qty = d.get('qty') - )) + invoice.append("items", dict(item_code=d.get("item"), qty=d.get("qty"))) invoice.save() return invoice.as_dict() + @frappe.whitelist() def make_invoice(table, customer, mode_of_payment): - '''Make table based on Sales Order''' + """Make table based on Sales Order""" restaurant, menu = get_restaurant_and_menu_name(table) invoice = get_invoice(table) invoice.customer = customer invoice.restaurant = restaurant invoice.calculate_taxes_and_totals() - invoice.append('payments', dict(mode_of_payment=mode_of_payment, amount=invoice.grand_total)) + invoice.append("payments", dict(mode_of_payment=mode_of_payment, amount=invoice.grand_total)) invoice.save() invoice.submit() - frappe.msgprint(_('Invoice Created'), indicator='green', alert=True) + frappe.msgprint(_("Invoice Created"), indicator="green", alert=True) return invoice.name -@frappe.whitelist() -def item_query_restaurant(doctype='Item', txt='', searchfield='name', start=0, page_len=20, filters=None, as_dict=False): - '''Return items that are selected in active menu of the restaurant''' - restaurant, menu = get_restaurant_and_menu_name(filters['table']) - items = frappe.db.get_all('Restaurant Menu Item', ['item'], dict(parent = menu)) - del filters['table'] - filters['name'] = ('in', [d.item for d in items]) - return item_query('Item', txt, searchfield, start, page_len, filters, as_dict) +@frappe.whitelist() +def item_query_restaurant( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=None, as_dict=False +): + """Return items that are selected in active menu of the restaurant""" + restaurant, menu = get_restaurant_and_menu_name(filters["table"]) + items = frappe.db.get_all("Restaurant Menu Item", ["item"], dict(parent=menu)) + del filters["table"] + filters["name"] = ("in", [d.item for d in items]) + + return item_query("Item", txt, searchfield, start, page_len, filters, as_dict) + def get_restaurant_and_menu_name(table): if not table: - frappe.throw(_('Please select a table')) + frappe.throw(_("Please select a table")) - restaurant = frappe.db.get_value('Restaurant Table', table, 'restaurant') - menu = frappe.db.get_value('Restaurant', restaurant, 'active_menu') + restaurant = frappe.db.get_value("Restaurant Table", table, "restaurant") + menu = frappe.db.get_value("Restaurant", restaurant, "active_menu") if not menu: - frappe.throw(_('Please set an active menu for Restaurant {0}').format(restaurant)) + frappe.throw(_("Please set an active menu for Restaurant {0}").format(restaurant)) return restaurant, menu diff --git a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py index 29f8a1a12b1..79255253c5a 100644 --- a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py +++ b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py @@ -10,5 +10,5 @@ from frappe.model.naming import make_autoname class RestaurantTable(Document): def autoname(self): - prefix = re.sub('-+', '-', self.restaurant.replace(' ', '-')) - self.name = make_autoname(prefix + '-.##') + prefix = re.sub("-+", "-", self.restaurant.replace(" ", "-")) + self.name = make_autoname(prefix + "-.##") diff --git a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py b/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py index 00d14d2bb2a..761c37f83fc 100644 --- a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py +++ b/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py @@ -4,11 +4,12 @@ import unittest test_records = [ - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), + dict(restaurant="Test Restaurant 1", no_of_seats=5, minimum_seating=1), + dict(restaurant="Test Restaurant 1", no_of_seats=5, minimum_seating=1), + dict(restaurant="Test Restaurant 1", no_of_seats=5, minimum_seating=1), + dict(restaurant="Test Restaurant 1", no_of_seats=5, minimum_seating=1), ] + class TestRestaurantTable(unittest.TestCase): pass diff --git a/erpnext/selling/doctype/campaign/campaign.py b/erpnext/selling/doctype/campaign/campaign.py index 1bc7e69a18f..efea8bfe472 100644 --- a/erpnext/selling/doctype/campaign/campaign.py +++ b/erpnext/selling/doctype/campaign/campaign.py @@ -9,7 +9,7 @@ from frappe.model.naming import set_name_by_naming_series class Campaign(Document): def autoname(self): - if frappe.defaults.get_global_default('campaign_naming_by') != 'Naming Series': + if frappe.defaults.get_global_default("campaign_naming_by") != "Naming Series": self.name = self.campaign_name else: set_name_by_naming_series(self) diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py index fd04e0ff6e2..e5c490ff356 100644 --- a/erpnext/selling/doctype/campaign/campaign_dashboard.py +++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py @@ -1,18 +1,11 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'campaign_name', - 'transactions': [ - { - 'label': _('Email Campaigns'), - 'items': ['Email Campaign'] - }, - { - 'label': _('Social Media Campaigns'), - 'items': ['Social Media Post'] - } - ] + "fieldname": "campaign_name", + "transactions": [ + {"label": _("Email Campaigns"), "items": ["Email Campaign"]}, + {"label": _("Social Media Campaigns"), "items": ["Social Media Post"]}, + ], } diff --git a/erpnext/selling/doctype/campaign/test_campaign.py b/erpnext/selling/doctype/campaign/test_campaign.py index 25001802857..f0c051e2b3e 100644 --- a/erpnext/selling/doctype/campaign/test_campaign.py +++ b/erpnext/selling/doctype/campaign/test_campaign.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Campaign') +test_records = frappe.get_test_records("Campaign") diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index e23b5b50fe7..2b8d45b070f 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -155,7 +155,6 @@ frappe.ui.form.on("Customer", { if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name); }, - get_customer_group_details: function(frm) { frappe.call({ method: "get_customer_group_details", @@ -203,4 +202,5 @@ frappe.ui.form.on("Customer", { }); dialog.show(); } -}); \ No newline at end of file +}); + diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index df871491422..8889a5f939a 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -37,13 +37,13 @@ class Customer(TransactionBase): def load_dashboard_info(self): info = get_dashboard_info(self.doctype, self.name, self.loyalty_program) - self.set_onload('dashboard_info', info) + self.set_onload("dashboard_info", info) def autoname(self): - cust_master_name = frappe.defaults.get_global_default('cust_master_name') - if cust_master_name == 'Customer Name': + cust_master_name = frappe.defaults.get_global_default("cust_master_name") + if cust_master_name == "Customer Name": self.name = self.get_customer_name() - elif cust_master_name == 'Naming Series': + elif cust_master_name == "Naming Series": set_name_by_naming_series(self) else: self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) @@ -51,22 +51,30 @@ class Customer(TransactionBase): def get_customer_name(self): if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import: - count = frappe.db.sql("""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer - where name like %s""", "%{0} - %".format(self.customer_name), as_list=1)[0][0] + count = frappe.db.sql( + """select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer + where name like %s""", + "%{0} - %".format(self.customer_name), + as_list=1, + )[0][0] count = cint(count) + 1 new_customer_name = "{0} - {1}".format(self.customer_name, cstr(count)) - msgprint(_("Changed customer name to '{}' as '{}' already exists.") - .format(new_customer_name, self.customer_name), - title=_("Note"), indicator="yellow") + msgprint( + _("Changed customer name to '{}' as '{}' already exists.").format( + new_customer_name, self.customer_name + ), + title=_("Note"), + indicator="yellow", + ) return new_customer_name return self.customer_name def after_insert(self): - '''If customer created from Lead, update customer id in quotations, opportunities''' + """If customer created from Lead, update customer id in quotations, opportunities""" self.update_lead_status() def validate(self): @@ -80,8 +88,8 @@ class Customer(TransactionBase): self.validate_internal_customer() # set loyalty program tier - if frappe.db.exists('Customer', self.name): - customer = frappe.get_doc('Customer', self.name) + if frappe.db.exists("Customer", self.name): + customer = frappe.get_doc("Customer", self.name) if self.loyalty_program == customer.loyalty_program and not self.loyalty_program_tier: self.loyalty_program_tier = customer.loyalty_program_tier @@ -91,8 +99,9 @@ class Customer(TransactionBase): @frappe.whitelist() def get_customer_group_details(self): - doc = frappe.get_doc('Customer Group', self.customer_group) - self.accounts = self.credit_limits = [] + doc = frappe.get_doc("Customer Group", self.customer_group) + self.accounts = [] + self.credit_limits = [] self.payment_terms = self.default_price_list = "" tables = [["accounts", "account"], ["credit_limits", "credit_limit"]] @@ -100,14 +109,16 @@ class Customer(TransactionBase): for row in tables: table, field = row[0], row[1] - if not doc.get(table): continue + if not doc.get(table): + continue for entry in doc.get(table): child = self.append(table) child.update({"company": entry.company, field: entry.get(field)}) for field in fields: - if not doc.get(field): continue + if not doc.get(field): + continue self.update({field: doc.get(field)}) self.save() @@ -115,23 +126,37 @@ class Customer(TransactionBase): def check_customer_group_change(self): frappe.flags.customer_group_changed = False - if not self.get('__islocal'): - if self.customer_group != frappe.db.get_value('Customer', self.name, 'customer_group'): + if not self.get("__islocal"): + if self.customer_group != frappe.db.get_value("Customer", self.name, "customer_group"): frappe.flags.customer_group_changed = True def validate_default_bank_account(self): if self.default_bank_account: - is_company_account = frappe.db.get_value('Bank Account', self.default_bank_account, 'is_company_account') + is_company_account = frappe.db.get_value( + "Bank Account", self.default_bank_account, "is_company_account" + ) if not is_company_account: - frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) + frappe.throw( + _("{0} is not a company bank account").format(frappe.bold(self.default_bank_account)) + ) def validate_internal_customer(self): - internal_customer = frappe.db.get_value("Customer", - {"is_internal_customer": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + internal_customer = frappe.db.get_value( + "Customer", + { + "is_internal_customer": 1, + "represents_company": self.represents_company, + "name": ("!=", self.name), + }, + "name", + ) if internal_customer: - frappe.throw(_("Internal Customer for company {0} already exists").format( - frappe.bold(self.represents_company))) + frappe.throw( + _("Internal Customer for company {0} already exists").format( + frappe.bold(self.represents_company) + ) + ) def on_update(self): self.validate_name_with_customer_group() @@ -149,21 +174,22 @@ class Customer(TransactionBase): def update_customer_groups(self): ignore_doctypes = ["Lead", "Opportunity", "POS Profile", "Tax Rule", "Pricing Rule"] if frappe.flags.customer_group_changed: - update_linked_doctypes('Customer', self.name, 'Customer Group', - self.customer_group, ignore_doctypes) + update_linked_doctypes( + "Customer", self.name, "Customer Group", self.customer_group, ignore_doctypes + ) def create_primary_contact(self): if not self.customer_primary_contact and not self.lead_name: if self.mobile_no or self.email_id: contact = make_contact(self) - self.db_set('customer_primary_contact', contact.name) - self.db_set('mobile_no', self.mobile_no) - self.db_set('email_id', self.email_id) + self.db_set("customer_primary_contact", contact.name) + self.db_set("mobile_no", self.mobile_no) + self.db_set("email_id", self.email_id) def create_primary_address(self): from frappe.contacts.doctype.address.address import get_address_display - if self.flags.is_new_doc and self.get('address_line1'): + if self.flags.is_new_doc and self.get("address_line1"): address = make_address(self) address_display = get_address_display(address.name) @@ -171,8 +197,8 @@ class Customer(TransactionBase): self.db_set("primary_address", address_display) def update_lead_status(self): - '''If Customer created from Lead, update lead status to "Converted" - update Customer link in Quotation, Opportunity''' + """If Customer created from Lead, update lead status to "Converted" + update Customer link in Quotation, Opportunity""" if self.lead_name: frappe.db.set_value("Lead", self.lead_name, "status", "Converted") @@ -191,23 +217,36 @@ class Customer(TransactionBase): for row in linked_contacts_and_addresses: linked_doc = frappe.get_doc(row.doctype, row.name) - if not linked_doc.has_link('Customer', self.name): - linked_doc.append('links', dict(link_doctype='Customer', link_name=self.name)) + if not linked_doc.has_link("Customer", self.name): + linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name)) linked_doc.save(ignore_permissions=self.flags.ignore_permissions) - def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): - frappe.throw(_("A Customer Group exists with same name please change the Customer name or rename the Customer Group"), frappe.NameError) + frappe.throw( + _( + "A Customer Group exists with same name please change the Customer name or rename the Customer Group" + ), + frappe.NameError, + ) def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - past_credit_limits = [d.credit_limit - for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] + past_credit_limits = [ + d.credit_limit + for d in frappe.db.get_all( + "Customer Credit Limit", + filters={"parent": self.name}, + fields=["credit_limit"], + order_by="company", + ) + ] - current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] + current_credit_limits = [ + d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company) + ] if past_credit_limits == current_credit_limits: return @@ -215,17 +254,26 @@ class Customer(TransactionBase): company_record = [] for limit in self.credit_limits: if limit.company in company_record: - frappe.throw(_("Credit limit is already defined for the Company {0}").format(limit.company, self.name)) + frappe.throw( + _("Credit limit is already defined for the Company {0}").format(limit.company, self.name) + ) else: company_record.append(limit.company) - outstanding_amt = get_customer_outstanding(self.name, limit.company) + outstanding_amt = get_customer_outstanding( + self.name, limit.company, ignore_outstanding_sales_order=limit.bypass_credit_limit_check + ) if flt(limit.credit_limit) < outstanding_amt: - frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt)) + frappe.throw( + _( + """New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""" + ).format(outstanding_amt) + ) def on_trash(self): if self.customer_primary_contact: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustomer` SET customer_primary_contact=null, @@ -233,14 +281,16 @@ class Customer(TransactionBase): mobile_no=null, email_id=null, primary_address=null - WHERE name=%(name)s""", {"name": self.name}) + WHERE name=%(name)s""", + {"name": self.name}, + ) - delete_contact_and_address('Customer', self.name) + delete_contact_and_address("Customer", self.name) if self.lead_name: frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name) def after_rename(self, olddn, newdn, merge=False): - if frappe.defaults.get_global_default('cust_master_name') == 'Customer Name': + if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": frappe.db.set(self, "customer_name", newdn) def set_loyalty_program(self): @@ -255,43 +305,49 @@ class Customer(TransactionBase): self.loyalty_program = loyalty_program[0] else: frappe.msgprint( - _("Multiple Loyalty Programs found for Customer {}. Please select manually.") - .format(frappe.bold(self.customer_name)) + _("Multiple Loyalty Programs found for Customer {}. Please select manually.").format( + frappe.bold(self.customer_name) + ) ) + def create_contact(contact, party_type, party, email): """Create contact based on given contact name""" - contact = contact.split(' ') + contact = contact.split(" ") - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': contact[0], - 'last_name': len(contact) > 1 and contact[1] or "" - }) - contact.append('email_ids', dict(email_id=email, is_primary=1)) - contact.append('links', dict(link_doctype=party_type, link_name=party)) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": contact[0], + "last_name": len(contact) > 1 and contact[1] or "", + } + ) + contact.append("email_ids", dict(email_id=email, is_primary=1)) + contact.append("links", dict(link_doctype=party_type, link_name=party)) contact.insert() + @frappe.whitelist() def make_quotation(source_name, target_doc=None): - def set_missing_values(source, target): _set_missing_values(source, target) - target_doc = get_mapped_doc("Customer", source_name, - {"Customer": { - "doctype": "Quotation", - "field_map": { - "name":"party_name" - } - }}, target_doc, set_missing_values) + target_doc = get_mapped_doc( + "Customer", + source_name, + {"Customer": {"doctype": "Quotation", "field_map": {"name": "party_name"}}}, + target_doc, + set_missing_values, + ) target_doc.quotation_to = "Customer" target_doc.run_method("set_missing_values") target_doc.run_method("set_other_charges") target_doc.run_method("calculate_taxes_and_totals") - price_list, currency = frappe.db.get_value("Customer", {'name': source_name}, ['default_price_list', 'default_currency']) + price_list, currency = frappe.db.get_value( + "Customer", {"name": source_name}, ["default_price_list", "default_currency"] + ) if price_list: target_doc.selling_price_list = price_list if currency: @@ -299,34 +355,53 @@ def make_quotation(source_name, target_doc=None): return target_doc + @frappe.whitelist() def make_opportunity(source_name, target_doc=None): def set_missing_values(source, target): _set_missing_values(source, target) - target_doc = get_mapped_doc("Customer", source_name, - {"Customer": { - "doctype": "Opportunity", - "field_map": { - "name": "party_name", - "doctype": "opportunity_from", + target_doc = get_mapped_doc( + "Customer", + source_name, + { + "Customer": { + "doctype": "Opportunity", + "field_map": { + "name": "party_name", + "doctype": "opportunity_from", + }, } - }}, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return target_doc -def _set_missing_values(source, target): - address = frappe.get_all('Dynamic Link', { - 'link_doctype': source.doctype, - 'link_name': source.name, - 'parenttype': 'Address', - }, ['parent'], limit=1) - contact = frappe.get_all('Dynamic Link', { - 'link_doctype': source.doctype, - 'link_name': source.name, - 'parenttype': 'Contact', - }, ['parent'], limit=1) +def _set_missing_values(source, target): + address = frappe.get_all( + "Dynamic Link", + { + "link_doctype": source.doctype, + "link_name": source.name, + "parenttype": "Address", + }, + ["parent"], + limit=1, + ) + + contact = frappe.get_all( + "Dynamic Link", + { + "link_doctype": source.doctype, + "link_name": source.name, + "parenttype": "Contact", + }, + ["parent"], + limit=1, + ) if address: target.customer_address = address[0].parent @@ -334,35 +409,41 @@ def _set_missing_values(source, target): if contact: target.contact_person = contact[0].parent + @frappe.whitelist() def get_loyalty_programs(doc): - ''' returns applicable loyalty programs for a customer ''' + """returns applicable loyalty programs for a customer""" lp_details = [] - loyalty_programs = frappe.get_all("Loyalty Program", + loyalty_programs = frappe.get_all( + "Loyalty Program", fields=["name", "customer_group", "customer_territory"], - filters={"auto_opt_in": 1, "from_date": ["<=", today()], - "ifnull(to_date, '2500-01-01')": [">=", today()]}) + filters={ + "auto_opt_in": 1, + "from_date": ["<=", today()], + "ifnull(to_date, '2500-01-01')": [">=", today()], + }, + ) for loyalty_program in loyalty_programs: if ( - (not loyalty_program.customer_group - or doc.customer_group in get_nested_links( - "Customer Group", - loyalty_program.customer_group, - doc.flags.ignore_permissions - )) - and (not loyalty_program.customer_territory - or doc.territory in get_nested_links( - "Territory", - loyalty_program.customer_territory, - doc.flags.ignore_permissions - )) + not loyalty_program.customer_group + or doc.customer_group + in get_nested_links( + "Customer Group", loyalty_program.customer_group, doc.flags.ignore_permissions + ) + ) and ( + not loyalty_program.customer_territory + or doc.territory + in get_nested_links( + "Territory", loyalty_program.customer_territory, doc.flags.ignore_permissions + ) ): lp_details.append(loyalty_program.name) return lp_details + def get_nested_links(link_doctype, link_name, ignore_permissions=False): from frappe.desk.treeview import _get_children @@ -372,10 +453,12 @@ def get_nested_links(link_doctype, link_name, ignore_permissions=False): return links + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): from erpnext.controllers.queries import get_fields + fields = ["name", "customer_name", "customer_group", "territory"] if frappe.db.get_default("cust_master_name") == "Customer Name": @@ -390,7 +473,8 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): filter_conditions = get_filters_cond(doctype, filters, []) match_conditions += "{}".format(filter_conditions) - return frappe.db.sql(""" + return frappe.db.sql( + """ select %s from `tabCustomer` where docstatus < 2 @@ -400,8 +484,12 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): case when name like %s then 0 else 1 end, case when customer_name like %s then 0 else 1 end, name, customer_name limit %s, %s - """.format(match_conditions=match_conditions) % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), - ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len)) + """.format( + match_conditions=match_conditions + ) + % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), + ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len), + ) def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0): @@ -414,63 +502,87 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, customer_outstanding += flt(extra_amount) if credit_limit > 0 and flt(customer_outstanding) > credit_limit: - msgprint(_("Credit limit has been crossed for customer {0} ({1}/{2})") - .format(customer, customer_outstanding, credit_limit)) + msgprint( + _("Credit limit has been crossed for customer {0} ({1}/{2})").format( + customer, customer_outstanding, credit_limit + ) + ) # If not authorized person raise exception - credit_controller_role = frappe.db.get_single_value('Accounts Settings', 'credit_controller') + credit_controller_role = frappe.db.get_single_value("Accounts Settings", "credit_controller") if not credit_controller_role or credit_controller_role not in frappe.get_roles(): # form a list of emails for the credit controller users credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") # form a list of emails and names to show to the user - credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + credit_controller_users_formatted = [ + get_formatted_email(user).replace("<", "(").replace(">", ")") + for user in credit_controller_users + ] if not credit_controller_users_formatted: - frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer)) + frappe.throw( + _("Please contact your administrator to extend the credit limits for {0}.").format(customer) + ) message = """Please contact any of the following users to extend the credit limits for {0}: -

    • {1}
    """.format(customer, '
  • '.join(credit_controller_users_formatted)) +

    • {1}
    """.format( + customer, "
  • ".join(credit_controller_users_formatted) + ) # if the current user does not have permissions to override credit limit, # prompt them to send out an email to the controller users - frappe.msgprint(message, + frappe.msgprint( + message, title="Notify", raise_exception=1, primary_action={ - 'label': 'Send Email', - 'server_action': 'erpnext.selling.doctype.customer.customer.send_emails', - 'args': { - 'customer': customer, - 'customer_outstanding': customer_outstanding, - 'credit_limit': credit_limit, - 'credit_controller_users_list': credit_controller_users - } - } + "label": "Send Email", + "server_action": "erpnext.selling.doctype.customer.customer.send_emails", + "args": { + "customer": customer, + "customer_outstanding": customer_outstanding, + "credit_limit": credit_limit, + "credit_controller_users_list": credit_controller_users, + }, + }, ) + @frappe.whitelist() def send_emails(args): args = json.loads(args) - subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) - message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") - .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) - frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message) + subject = _("Credit limit reached for customer {0}").format(args.get("customer")) + message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format( + args.get("customer"), args.get("customer_outstanding"), args.get("credit_limit") + ) + frappe.sendmail( + recipients=args.get("credit_controller_users_list"), subject=subject, message=message + ) -def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): + +def get_customer_outstanding( + customer, company, ignore_outstanding_sales_order=False, cost_center=None +): # Outstanding based on GL Entries cond = "" if cost_center: - lft, rgt = frappe.get_cached_value("Cost Center", - cost_center, ['lft', 'rgt']) + lft, rgt = frappe.get_cached_value("Cost Center", cost_center, ["lft", "rgt"]) cond = """ and cost_center in (select name from `tabCost Center` where - lft >= {0} and rgt <= {1})""".format(lft, rgt) + lft >= {0} and rgt <= {1})""".format( + lft, rgt + ) - outstanding_based_on_gle = frappe.db.sql(""" + outstanding_based_on_gle = frappe.db.sql( + """ select sum(debit) - sum(credit) from `tabGL Entry` where party_type = 'Customer' - and party = %s and company=%s {0}""".format(cond), (customer, company)) + and party = %s and company=%s {0}""".format( + cond + ), + (customer, company), + ) outstanding_based_on_gle = flt(outstanding_based_on_gle[0][0]) if outstanding_based_on_gle else 0 @@ -480,18 +592,22 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F # if credit limit check is bypassed at sales order level, # we should not consider outstanding Sales Orders, when customer credit balance report is run if not ignore_outstanding_sales_order: - outstanding_based_on_so = frappe.db.sql(""" + outstanding_based_on_so = frappe.db.sql( + """ select sum(base_grand_total*(100 - per_billed)/100) from `tabSales Order` where customer=%s and docstatus = 1 and company=%s - and per_billed < 100 and status != 'Closed'""", (customer, company)) + and per_billed < 100 and status != 'Closed'""", + (customer, company), + ) outstanding_based_on_so = flt(outstanding_based_on_so[0][0]) if outstanding_based_on_so else 0 # Outstanding based on Delivery Note, which are not created against Sales Order outstanding_based_on_dn = 0 - unmarked_delivery_note_items = frappe.db.sql("""select + unmarked_delivery_note_items = frappe.db.sql( + """select dn_item.name, dn_item.amount, dn.base_net_total, dn.base_grand_total from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item where @@ -500,21 +616,24 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F and dn.docstatus = 1 and dn.status not in ('Closed', 'Stopped') and ifnull(dn_item.against_sales_order, '') = '' and ifnull(dn_item.against_sales_invoice, '') = '' - """, (customer, company), as_dict=True) + """, + (customer, company), + as_dict=True, + ) if not unmarked_delivery_note_items: return outstanding_based_on_gle + outstanding_based_on_so - si_amounts = frappe.db.sql(""" + si_amounts = frappe.db.sql( + """ SELECT dn_detail, sum(amount) from `tabSales Invoice Item` WHERE docstatus = 1 and dn_detail in ({}) - GROUP BY dn_detail""".format(", ".join( - frappe.db.escape(dn_item.name) - for dn_item in unmarked_delivery_note_items - )) + GROUP BY dn_detail""".format( + ", ".join(frappe.db.escape(dn_item.name) for dn_item in unmarked_delivery_note_items) + ) ) si_amounts = {si_item[0]: si_item[1] for si_item in si_amounts} @@ -524,8 +643,9 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F si_amount = flt(si_amounts.get(dn_item.name)) if dn_amount > si_amount and dn_item.base_net_total: - outstanding_based_on_dn += ((dn_amount - si_amount) - / dn_item.base_net_total) * dn_item.base_grand_total + outstanding_based_on_dn += ( + (dn_amount - si_amount) / dn_item.base_net_total + ) * dn_item.base_grand_total return outstanding_based_on_gle + outstanding_based_on_so + outstanding_based_on_dn @@ -534,75 +654,84 @@ def get_credit_limit(customer, company): credit_limit = None if customer: - credit_limit = frappe.db.get_value("Customer Credit Limit", - {'parent': customer, 'parenttype': 'Customer', 'company': company}, 'credit_limit') + credit_limit = frappe.db.get_value( + "Customer Credit Limit", + {"parent": customer, "parenttype": "Customer", "company": company}, + "credit_limit", + ) if not credit_limit: - customer_group = frappe.get_cached_value("Customer", customer, 'customer_group') - credit_limit = frappe.db.get_value("Customer Credit Limit", - {'parent': customer_group, 'parenttype': 'Customer Group', 'company': company}, 'credit_limit') + customer_group = frappe.get_cached_value("Customer", customer, "customer_group") + credit_limit = frappe.db.get_value( + "Customer Credit Limit", + {"parent": customer_group, "parenttype": "Customer Group", "company": company}, + "credit_limit", + ) if not credit_limit: - credit_limit = frappe.get_cached_value('Company', company, "credit_limit") + credit_limit = frappe.get_cached_value("Company", company, "credit_limit") return flt(credit_limit) + def make_contact(args, is_primary_contact=1): - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': args.get('name'), - 'is_primary_contact': is_primary_contact, - 'links': [{ - 'link_doctype': args.get('doctype'), - 'link_name': args.get('name') - }] - }) - if args.get('email_id'): - contact.add_email(args.get('email_id'), is_primary=True) - if args.get('mobile_no'): - contact.add_phone(args.get('mobile_no'), is_primary_mobile_no=True) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": args.get("name"), + "is_primary_contact": is_primary_contact, + "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], + } + ) + if args.get("email_id"): + contact.add_email(args.get("email_id"), is_primary=True) + if args.get("mobile_no"): + contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True) contact.insert() return contact + def make_address(args, is_primary_address=1): reqd_fields = [] - for field in ['city', 'country']: + for field in ["city", "country"]: if not args.get(field): - reqd_fields.append( '
  • ' + field.title() + '
  • ') + reqd_fields.append("
  • " + field.title() + "
  • ") if reqd_fields: msg = _("Following fields are mandatory to create address:") - frappe.throw("{0}

      {1}
    ".format(msg, '\n'.join(reqd_fields)), - title = _("Missing Values Required")) + frappe.throw( + "{0}

      {1}
    ".format(msg, "\n".join(reqd_fields)), + title=_("Missing Values Required"), + ) - address = frappe.get_doc({ - 'doctype': 'Address', - 'address_title': args.get('name'), - 'address_line1': args.get('address_line1'), - 'address_line2': args.get('address_line2'), - 'city': args.get('city'), - 'state': args.get('state'), - 'pincode': args.get('pincode'), - 'country': args.get('country'), - 'links': [{ - 'link_doctype': args.get('doctype'), - 'link_name': args.get('name') - }] - }).insert() + address = frappe.get_doc( + { + "doctype": "Address", + "address_title": args.get("name"), + "address_line1": args.get("address_line1"), + "address_line2": args.get("address_line2"), + "city": args.get("city"), + "state": args.get("state"), + "pincode": args.get("pincode"), + "country": args.get("country"), + "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], + } + ).insert() return address + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): - customer = filters.get('customer') - return frappe.db.sql(""" + customer = filters.get("customer") + return frappe.db.sql( + """ select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabContact`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s and `tabDynamic Link`.link_doctype = 'Customer' and `tabContact`.name like %(txt)s - """, { - 'customer': customer, - 'txt': '%%%s%%' % txt - }) + """, + {"customer": customer, "txt": "%%%s%%" % txt}, + ) diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index faf8a4403ca..1b2296381e8 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -1,50 +1,31 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Customer. See timeline below for details'), - 'fieldname': 'customer', - 'non_standard_fieldnames': { - 'Payment Entry': 'party', - 'Quotation': 'party_name', - 'Opportunity': 'party_name', - 'Bank Account': 'party', - 'Subscription': 'party' + "heatmap": True, + "heatmap_message": _( + "This is based on transactions against this Customer. See timeline below for details" + ), + "fieldname": "customer", + "non_standard_fieldnames": { + "Payment Entry": "party", + "Quotation": "party_name", + "Opportunity": "party_name", + "Bank Account": "party", + "Subscription": "party", }, - 'dynamic_links': { - 'party_name': ['Customer', 'quotation_to'] - }, - 'transactions': [ + "dynamic_links": {"party_name": ["Customer", "quotation_to"]}, + "transactions": [ + {"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]}, + {"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Payments"), "items": ["Payment Entry", "Bank Account"]}, { - 'label': _('Pre Sales'), - 'items': ['Opportunity', 'Quotation'] + "label": _("Support"), + "items": ["Issue", "Maintenance Visit", "Installation Note", "Warranty Claim"], }, - { - 'label': _('Orders'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Payments'), - 'items': ['Payment Entry', 'Bank Account'] - }, - { - 'label': _('Support'), - 'items': ['Issue', 'Maintenance Visit', 'Installation Note', 'Warranty Claim'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - }, - { - 'label': _('Pricing'), - 'items': ['Pricing Rule'] - }, - { - 'label': _('Subscriptions'), - 'items': ['Subscription'] - } - ] + {"label": _("Projects"), "items": ["Project"]}, + {"label": _("Pricing"), "items": ["Pricing Rule"]}, + {"label": _("Subscriptions"), "items": ["Subscription"]}, + ], } diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7802a3fea44..f631a6ef568 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -4,27 +4,28 @@ import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding -from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address test_ignore = ["Price List"] -test_dependencies = ['Payment Term', 'Payment Terms Template'] -test_records = frappe.get_test_records('Customer') +test_dependencies = ["Payment Term", "Payment Terms Template"] +test_records = frappe.get_test_records("Customer") from six import iteritems -class TestCustomer(ERPNextTestCase): +class TestCustomer(FrappeTestCase): def setUp(self): - if not frappe.get_value('Item', '_Test Item'): - make_test_records('Item') + if not frappe.get_value("Item", "_Test Item"): + make_test_records("Item") def tearDown(self): - set_credit_limit('_Test Customer', '_Test Company', 0) + set_credit_limit("_Test Customer", "_Test Company", 0) def test_get_customer_group_details(self): doc = frappe.new_doc("Customer Group") @@ -37,10 +38,7 @@ class TestCustomer(ERPNextTestCase): "company": "_Test Company", "account": "Creditors - _TC", } - test_credit_limits = { - "company": "_Test Company", - "credit_limit": 350000 - } + test_credit_limits = {"company": "_Test Company", "credit_limit": 350000} doc.append("accounts", test_account_details) doc.append("credit_limits", test_credit_limits) doc.insert() @@ -49,7 +47,8 @@ class TestCustomer(ERPNextTestCase): c_doc.customer_name = "Testing Customer" c_doc.customer_group = "_Testing Customer Group" c_doc.payment_terms = c_doc.default_price_list = "" - c_doc.accounts = c_doc.credit_limits= [] + c_doc.accounts = [] + c_doc.credit_limits = [] c_doc.insert() c_doc.get_customer_group_details() self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") @@ -66,25 +65,26 @@ class TestCustomer(ERPNextTestCase): from erpnext.accounts.party import get_party_details to_check = { - 'selling_price_list': None, - 'customer_group': '_Test Customer Group', - 'contact_designation': None, - 'customer_address': '_Test Address for Customer-Office', - 'contact_department': None, - 'contact_email': 'test_contact_customer@example.com', - 'contact_mobile': None, - 'sales_team': [], - 'contact_display': '_Test Contact for _Test Customer', - 'contact_person': '_Test Contact for _Test Customer-_Test Customer', - 'territory': u'_Test Territory', - 'contact_phone': '+91 0000000000', - 'customer_name': '_Test Customer' + "selling_price_list": None, + "customer_group": "_Test Customer Group", + "contact_designation": None, + "customer_address": "_Test Address for Customer-Office", + "contact_department": None, + "contact_email": "test_contact_customer@example.com", + "contact_mobile": None, + "sales_team": [], + "contact_display": "_Test Contact for _Test Customer", + "contact_person": "_Test Contact for _Test Customer-_Test Customer", + "territory": "_Test Territory", + "contact_phone": "+91 0000000000", + "customer_name": "_Test Customer", } create_test_contact_and_address() - frappe.db.set_value("Contact", "_Test Contact for _Test Customer-_Test Customer", - "is_primary_contact", 1) + frappe.db.set_value( + "Contact", "_Test Contact for _Test Customer-_Test Customer", "is_primary_contact", 1 + ) details = get_party_details("_Test Customer") @@ -105,32 +105,30 @@ class TestCustomer(ERPNextTestCase): details = get_party_details("_Test Customer With Tax Category") self.assertEqual(details.tax_category, "_Test Tax Category 1") - billing_address = frappe.get_doc(dict( - doctype='Address', - address_title='_Test Address With Tax Category', - tax_category='_Test Tax Category 2', - address_type='Billing', - address_line1='Station Road', - city='_Test City', - country='India', - links=[dict( - link_doctype='Customer', - link_name='_Test Customer With Tax Category' - )] - )).insert() - shipping_address = frappe.get_doc(dict( - doctype='Address', - address_title='_Test Address With Tax Category', - tax_category='_Test Tax Category 3', - address_type='Shipping', - address_line1='Station Road', - city='_Test City', - country='India', - links=[dict( - link_doctype='Customer', - link_name='_Test Customer With Tax Category' - )] - )).insert() + billing_address = frappe.get_doc( + dict( + doctype="Address", + address_title="_Test Address With Tax Category", + tax_category="_Test Tax Category 2", + address_type="Billing", + address_line1="Station Road", + city="_Test City", + country="India", + links=[dict(link_doctype="Customer", link_name="_Test Customer With Tax Category")], + ) + ).insert() + shipping_address = frappe.get_doc( + dict( + doctype="Address", + address_title="_Test Address With Tax Category", + tax_category="_Test Tax Category 3", + address_type="Shipping", + address_line1="Station Road", + city="_Test City", + country="India", + links=[dict(link_doctype="Customer", link_name="_Test Customer With Tax Category")], + ) + ).insert() settings = frappe.get_single("Accounts Settings") rollback_setting = settings.determine_address_tax_category_from @@ -158,12 +156,16 @@ class TestCustomer(ERPNextTestCase): new_name = "_Test Customer 1 Renamed" for name in ("_Test Customer 1", new_name): - frappe.db.sql("""delete from `tabComment` + frappe.db.sql( + """delete from `tabComment` where reference_doctype=%s and reference_name=%s""", - ("Customer", name)) + ("Customer", name), + ) # add comments - comment = frappe.get_doc("Customer", "_Test Customer 1").add_comment("Comment", "Test Comment for Rename") + comment = frappe.get_doc("Customer", "_Test Customer 1").add_comment( + "Comment", "Test Comment for Rename" + ) # rename frappe.rename_doc("Customer", "_Test Customer 1", new_name) @@ -173,11 +175,17 @@ class TestCustomer(ERPNextTestCase): self.assertFalse(frappe.db.exists("Customer", "_Test Customer 1")) # test that comment gets linked to renamed doc - self.assertEqual(frappe.db.get_value("Comment", { - "reference_doctype": "Customer", - "reference_name": new_name, - "content": "Test Comment for Rename" - }), comment.name) + self.assertEqual( + frappe.db.get_value( + "Comment", + { + "reference_doctype": "Customer", + "reference_name": new_name, + "content": "Test Comment for Rename", + }, + ), + comment.name, + ) # rename back to original frappe.rename_doc("Customer", new_name, "_Test Customer 1") @@ -191,7 +199,7 @@ class TestCustomer(ERPNextTestCase): from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order - so = make_sales_order(do_not_save= True) + so = make_sales_order(do_not_save=True) self.assertRaises(PartyFrozen, so.save) @@ -200,13 +208,14 @@ class TestCustomer(ERPNextTestCase): so.save() def test_delete_customer_contact(self): - customer = frappe.get_doc( - get_customer_dict('_Test Customer for delete')).insert(ignore_permissions=True) + customer = frappe.get_doc(get_customer_dict("_Test Customer for delete")).insert( + ignore_permissions=True + ) customer.mobile_no = "8989889890" customer.save() self.assertTrue(customer.customer_primary_contact) - frappe.delete_doc('Customer', customer.name) + frappe.delete_doc("Customer", customer.name) def test_disabled_customer(self): make_test_records("Item") @@ -227,13 +236,15 @@ class TestCustomer(ERPNextTestCase): frappe.db.sql("delete from `tabCustomer` where customer_name='_Test Customer 1'") if not frappe.db.get_value("Customer", "_Test Customer 1"): - test_customer_1 = frappe.get_doc( - get_customer_dict('_Test Customer 1')).insert(ignore_permissions=True) + test_customer_1 = frappe.get_doc(get_customer_dict("_Test Customer 1")).insert( + ignore_permissions=True + ) else: test_customer_1 = frappe.get_doc("Customer", "_Test Customer 1") - duplicate_customer = frappe.get_doc( - get_customer_dict('_Test Customer 1')).insert(ignore_permissions=True) + duplicate_customer = frappe.get_doc(get_customer_dict("_Test Customer 1")).insert( + ignore_permissions=True + ) self.assertEqual("_Test Customer 1", test_customer_1.name) self.assertEqual("_Test Customer 1 - 1", duplicate_customer.name) @@ -241,15 +252,16 @@ class TestCustomer(ERPNextTestCase): def get_customer_outstanding_amount(self): from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order - outstanding_amt = get_customer_outstanding('_Test Customer', '_Test Company') + + outstanding_amt = get_customer_outstanding("_Test Customer", "_Test Company") # If outstanding is negative make a transaction to get positive outstanding amount if outstanding_amt > 0.0: return outstanding_amt - item_qty = int((abs(outstanding_amt) + 200)/100) + item_qty = int((abs(outstanding_amt) + 200) / 100) make_sales_order(qty=item_qty) - return get_customer_outstanding('_Test Customer', '_Test Company') + return get_customer_outstanding("_Test Customer", "_Test Company") def test_customer_credit_limit(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -258,14 +270,14 @@ class TestCustomer(ERPNextTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note outstanding_amt = self.get_customer_outstanding_amount() - credit_limit = get_credit_limit('_Test Customer', '_Test Company') + credit_limit = get_credit_limit("_Test Customer", "_Test Company") if outstanding_amt <= 0.0: - item_qty = int((abs(outstanding_amt) + 200)/100) + item_qty = int((abs(outstanding_amt) + 200) / 100) make_sales_order(qty=item_qty) if not credit_limit: - set_credit_limit('_Test Customer', '_Test Company', outstanding_amt - 50) + set_credit_limit("_Test Customer", "_Test Company", outstanding_amt - 50) # Sales Order so = make_sales_order(do_not_submit=True) @@ -280,7 +292,7 @@ class TestCustomer(ERPNextTestCase): self.assertRaises(frappe.ValidationError, si.submit) if credit_limit > outstanding_amt: - set_credit_limit('_Test Customer', '_Test Company', credit_limit) + set_credit_limit("_Test Customer", "_Test Company", credit_limit) # Makes Sales invoice from Sales Order so.save(ignore_permissions=True) @@ -290,16 +302,21 @@ class TestCustomer(ERPNextTestCase): def test_customer_credit_limit_on_change(self): outstanding_amt = self.get_customer_outstanding_amount() - customer = frappe.get_doc("Customer", '_Test Customer') - customer.append('credit_limits', {'credit_limit': flt(outstanding_amt - 100), 'company': '_Test Company'}) + customer = frappe.get_doc("Customer", "_Test Customer") + customer.append( + "credit_limits", {"credit_limit": flt(outstanding_amt - 100), "company": "_Test Company"} + ) - ''' define new credit limit for same company ''' - customer.append('credit_limits', {'credit_limit': flt(outstanding_amt - 100), 'company': '_Test Company'}) + """ define new credit limit for same company """ + customer.append( + "credit_limits", {"credit_limit": flt(outstanding_amt - 100), "company": "_Test Company"} + ) self.assertRaises(frappe.ValidationError, customer.save) def test_customer_payment_terms(self): frappe.db.set_value( - "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3") + "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3" + ) due_date = get_due_date("2016-01-22", "Customer", "_Test Customer With Template") self.assertEqual(due_date, "2016-02-21") @@ -308,7 +325,8 @@ class TestCustomer(ERPNextTestCase): self.assertEqual(due_date, "2017-02-21") frappe.db.set_value( - "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 1") + "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 1" + ) due_date = get_due_date("2016-01-22", "Customer", "_Test Customer With Template") self.assertEqual(due_date, "2016-02-29") @@ -328,13 +346,14 @@ class TestCustomer(ERPNextTestCase): def get_customer_dict(customer_name): return { - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", } + def set_credit_limit(customer, company, credit_limit): customer = frappe.get_doc("Customer", customer) existing_row = None @@ -346,31 +365,29 @@ def set_credit_limit(customer, company, credit_limit): break if not existing_row: - customer.append('credit_limits', { - 'company': company, - 'credit_limit': credit_limit - }) + customer.append("credit_limits", {"company": company, "credit_limit": credit_limit}) customer.credit_limits[-1].db_insert() + def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": represents_company - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": represents_company, + } + ) - customer.append("companies", { - "company": allowed_to_interact_with - }) + customer.append("companies", {"company": allowed_to_interact_with}) customer.insert() customer_name = customer.name else: customer_name = frappe.db.get_value("Customer", customer_name) - return customer_name \ No newline at end of file + return customer_name diff --git a/erpnext/selling/doctype/industry_type/test_industry_type.py b/erpnext/selling/doctype/industry_type/test_industry_type.py index 250c2bec485..eb5f905f104 100644 --- a/erpnext/selling/doctype/industry_type/test_industry_type.py +++ b/erpnext/selling/doctype/industry_type/test_industry_type.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Industry Type') +test_records = frappe.get_test_records("Industry Type") diff --git a/erpnext/selling/doctype/installation_note/installation_note.py b/erpnext/selling/doctype/installation_note/installation_note.py index 36acdbea612..dd0b1e87517 100644 --- a/erpnext/selling/doctype/installation_note/installation_note.py +++ b/erpnext/selling/doctype/installation_note/installation_note.py @@ -13,26 +13,29 @@ from erpnext.utilities.transaction_base import TransactionBase class InstallationNote(TransactionBase): def __init__(self, *args, **kwargs): super(InstallationNote, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Installation Note Item', - 'target_dt': 'Delivery Note Item', - 'target_field': 'installed_qty', - 'target_ref_field': 'qty', - 'join_field': 'prevdoc_detail_docname', - 'target_parent_dt': 'Delivery Note', - 'target_parent_field': 'per_installed', - 'source_field': 'qty', - 'percent_join_field': 'prevdoc_docname', - 'status_field': 'installation_status', - 'keyword': 'Installed', - 'overflow_type': 'installation' - }] + self.status_updater = [ + { + "source_dt": "Installation Note Item", + "target_dt": "Delivery Note Item", + "target_field": "installed_qty", + "target_ref_field": "qty", + "join_field": "prevdoc_detail_docname", + "target_parent_dt": "Delivery Note", + "target_parent_field": "per_installed", + "source_field": "qty", + "percent_join_field": "prevdoc_docname", + "status_field": "installation_status", + "keyword": "Installed", + "overflow_type": "installation", + } + ] def validate(self): self.validate_installation_date() self.check_item_table() from erpnext.controllers.selling_controller import set_default_income_account_for_item + set_default_income_account_for_item(self) def is_serial_no_added(self, item_code, serial_no): @@ -48,18 +51,19 @@ class InstallationNote(TransactionBase): frappe.throw(_("Serial No {0} does not exist").format(x)) def get_prevdoc_serial_no(self, prevdoc_detail_docname): - serial_nos = frappe.db.get_value("Delivery Note Item", - prevdoc_detail_docname, "serial_no") + serial_nos = frappe.db.get_value("Delivery Note Item", prevdoc_detail_docname, "serial_no") return get_valid_serial_nos(serial_nos) def is_serial_no_match(self, cur_s_no, prevdoc_s_no, prevdoc_docname): for sr in cur_s_no: if sr not in prevdoc_s_no: - frappe.throw(_("Serial No {0} does not belong to Delivery Note {1}").format(sr, prevdoc_docname)) + frappe.throw( + _("Serial No {0} does not belong to Delivery Note {1}").format(sr, prevdoc_docname) + ) def validate_serial_no(self): prevdoc_s_no, sr_list = [], [] - for d in self.get('items'): + for d in self.get("items"): self.is_serial_no_added(d.item_code, d.serial_no) if d.serial_no: sr_list = get_valid_serial_nos(d.serial_no, d.qty, d.item_code) @@ -69,26 +73,27 @@ class InstallationNote(TransactionBase): if prevdoc_s_no: self.is_serial_no_match(sr_list, prevdoc_s_no, d.prevdoc_docname) - def validate_installation_date(self): - for d in self.get('items'): + for d in self.get("items"): if d.prevdoc_docname: d_date = frappe.db.get_value("Delivery Note", d.prevdoc_docname, "posting_date") if d_date > getdate(self.inst_date): - frappe.throw(_("Installation date cannot be before delivery date for Item {0}").format(d.item_code)) + frappe.throw( + _("Installation date cannot be before delivery date for Item {0}").format(d.item_code) + ) def check_item_table(self): - if not(self.get('items')): + if not (self.get("items")): frappe.throw(_("Please pull items from Delivery Note")) def on_update(self): - frappe.db.set(self, 'status', 'Draft') + frappe.db.set(self, "status", "Draft") def on_submit(self): self.validate_serial_no() self.update_prevdoc_status() - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") def on_cancel(self): self.update_prevdoc_status() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") diff --git a/erpnext/selling/doctype/installation_note/test_installation_note.py b/erpnext/selling/doctype/installation_note/test_installation_note.py index d3c8be53574..56e0fe160ab 100644 --- a/erpnext/selling/doctype/installation_note/test_installation_note.py +++ b/erpnext/selling/doctype/installation_note/test_installation_note.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Installation Note') + class TestInstallationNote(unittest.TestCase): pass diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.py b/erpnext/selling/doctype/party_specific_item/party_specific_item.py index a408af56420..0aef7d362e4 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.py @@ -8,12 +8,14 @@ from frappe.model.document import Document class PartySpecificItem(Document): def validate(self): - exists = frappe.db.exists({ - 'doctype': 'Party Specific Item', - 'party_type': self.party_type, - 'party': self.party, - 'restrict_based_on': self.restrict_based_on, - 'based_on': self.based_on_value, - }) + exists = frappe.db.exists( + { + "doctype": "Party Specific Item", + "party_type": self.party_type, + "party": self.party, + "restrict_based_on": self.restrict_based_on, + "based_on": self.based_on_value, + } + ) if exists: frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type)) diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index b951044f332..f98cbd7e9a3 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -1,39 +1,53 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.controllers.queries import item_query -from erpnext.tests.utils import ERPNextTestCase -test_dependencies = ['Item', 'Customer', 'Supplier'] +test_dependencies = ["Item", "Customer", "Supplier"] + def create_party_specific_item(**args): psi = frappe.new_doc("Party Specific Item") - psi.party_type = args.get('party_type') - psi.party = args.get('party') - psi.restrict_based_on = args.get('restrict_based_on') - psi.based_on_value = args.get('based_on_value') + psi.party_type = args.get("party_type") + psi.party = args.get("party") + psi.restrict_based_on = args.get("restrict_based_on") + psi.based_on_value = args.get("based_on_value") psi.insert() -class TestPartySpecificItem(ERPNextTestCase): + +class TestPartySpecificItem(FrappeTestCase): def setUp(self): self.customer = frappe.get_last_doc("Customer") self.supplier = frappe.get_last_doc("Supplier") self.item = frappe.get_last_doc("Item") def test_item_query_for_customer(self): - create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name) - filters = {'is_sales_item': 1, 'customer': self.customer.name} - items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False) + create_party_specific_item( + party_type="Customer", + party=self.customer.name, + restrict_based_on="Item", + based_on_value=self.item.name, + ) + filters = {"is_sales_item": 1, "customer": self.customer.name} + items = item_query( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False + ) for item in items: self.assertEqual(item[0], self.item.name) def test_item_query_for_supplier(self): - create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group) - filters = {'supplier': self.supplier.name, 'is_purchase_item': 1} - items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False) + create_party_specific_item( + party_type="Supplier", + party=self.supplier.name, + restrict_based_on="Item Group", + based_on_value=self.item.item_group, + ) + filters = {"supplier": self.supplier.name, "is_purchase_item": 1} + items = item_query( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False + ) for item in items: self.assertEqual(item[2], self.item.item_group) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 2bb876e6d0f..575b956686a 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -16,11 +16,22 @@ class ProductBundle(Document): self.validate_main_item() self.validate_child_items() from erpnext.utilities.transaction_base import validate_uom_is_integer + validate_uom_is_integer(self, "uom", "qty") def on_trash(self): - linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice", - "Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"] + linked_doctypes = [ + "Delivery Note", + "Sales Invoice", + "POS Invoice", + "Purchase Receipt", + "Purchase Invoice", + "Stock Entry", + "Stock Reconciliation", + "Sales Order", + "Purchase Order", + "Material Request", + ] invoice_links = [] for doctype in linked_doctypes: @@ -29,15 +40,20 @@ class ProductBundle(Document): if doctype == "Stock Entry": item_doctype = doctype + " Detail" - invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"]) + invoices = frappe.db.get_all( + item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"] + ) for invoice in invoices: - invoice_links.append(get_link_to_form(doctype, invoice['parent'])) + invoice_links.append(get_link_to_form(doctype, invoice["parent"])) if len(invoice_links): frappe.throw( - "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" - .format(", ".join(invoice_links)), title=_("Not Allowed")) + "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle".format( + ", ".join(invoice_links) + ), + title=_("Not Allowed"), + ) def validate_main_item(self): """Validates, main Item is not a stock item""" @@ -47,15 +63,22 @@ class ProductBundle(Document): def validate_child_items(self): for item in self.items: if frappe.db.exists("Product Bundle", item.item_code): - frappe.throw(_("Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save").format(item.idx, frappe.bold(item.item_code))) + frappe.throw( + _( + "Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save" + ).format(item.idx, frappe.bold(item.item_code)) + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond - return frappe.db.sql("""select name, item_name, description from tabItem + return frappe.db.sql( + """select name, item_name, description from tabItem where is_stock_item=0 and name not in (select name from `tabProduct Bundle`) - and %s like %s %s limit %s, %s""" % (searchfield, "%s", - get_match_cond(doctype),"%s", "%s"), - ("%%%s%%" % txt, start, page_len)) + and %s like %s %s limit %s, %s""" + % (searchfield, "%s", get_match_cond(doctype), "%s", "%s"), + ("%%%s%%" % txt, start, page_len), + ) diff --git a/erpnext/selling/doctype/product_bundle/test_product_bundle.py b/erpnext/selling/doctype/product_bundle/test_product_bundle.py index b966c62f66c..82fe892edf7 100644 --- a/erpnext/selling/doctype/product_bundle/test_product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/test_product_bundle.py @@ -1,19 +1,16 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt import frappe -test_records = frappe.get_test_records('Product Bundle') +test_records = frappe.get_test_records("Product Bundle") + def make_product_bundle(parent, items, qty=None): if frappe.db.exists("Product Bundle", parent): return frappe.get_doc("Product Bundle", parent) - product_bundle = frappe.get_doc({ - "doctype": "Product Bundle", - "new_item_code": parent - }) + product_bundle = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": parent}) for item in items: product_bundle.append("items", {"item_code": item, "qty": qty or 1}) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 16a5e0b756d..dcc0c784639 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -31,6 +31,8 @@ "col_break98", "shipping_address_name", "shipping_address", + "company_address", + "company_address_display", "customer_group", "territory", "currency_and_price_list", @@ -116,7 +118,9 @@ { "fieldname": "customer_section", "fieldtype": "Section Break", - "options": "fa fa-user" + "options": "fa fa-user", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -126,7 +130,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -138,7 +144,9 @@ "options": "SAL-QTN-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "Customer", @@ -150,7 +158,9 @@ "oldfieldtype": "Select", "options": "DocType", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -163,7 +173,9 @@ "oldfieldtype": "Link", "options": "quotation_to", "print_hide": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -172,12 +184,16 @@ "hidden": 1, "in_global_search": 1, "label": "Customer Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -191,6 +207,8 @@ "options": "Quotation", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { @@ -203,6 +221,8 @@ "print_hide": 1, "remember_last_selected_value": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "150px" }, { @@ -217,12 +237,16 @@ "oldfieldtype": "Date", "reqd": 1, "search_index": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { "fieldname": "valid_till", "fieldtype": "Date", - "label": "Valid Till" + "label": "Valid Till", + "show_days": 1, + "show_seconds": 1 }, { "default": "Sales", @@ -234,7 +258,9 @@ "oldfieldtype": "Select", "options": "\nSales\nMaintenance\nShopping Cart", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -242,14 +268,18 @@ "fieldname": "contact_section", "fieldtype": "Section Break", "label": "Address and Contact", - "options": "fa fa-bullhorn" + "options": "fa fa-bullhorn", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_address", "fieldtype": "Link", "label": "Customer Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", @@ -257,7 +287,9 @@ "label": "Address", "oldfieldname": "customer_address", "oldfieldtype": "Small Text", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", @@ -266,20 +298,26 @@ "oldfieldname": "contact_person", "oldfieldtype": "Link", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "in_global_search": 1, "label": "Contact", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_email", @@ -288,12 +326,16 @@ "label": "Contact Email", "options": "Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", "fieldname": "col_break98", "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -301,14 +343,18 @@ "fieldtype": "Link", "label": "Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", @@ -319,21 +365,27 @@ "oldfieldname": "customer_group", "oldfieldtype": "Link", "options": "Customer Group", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "territory", "fieldtype": "Link", "label": "Territory", "options": "Territory", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "currency", @@ -344,6 +396,8 @@ "options": "Currency", "print_hide": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -356,11 +410,15 @@ "precision": "9", "print_hide": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { "fieldname": "column_break2", "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -372,6 +430,8 @@ "options": "Price List", "print_hide": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -381,7 +441,9 @@ "options": "Currency", "print_hide": 1, "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "Rate at which Price list currency is converted to company's base currency", @@ -390,7 +452,9 @@ "label": "Price List Exchange Rate", "precision": "9", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -399,13 +463,17 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" + "options": "fa fa-shopping-cart", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -416,29 +484,39 @@ "oldfieldtype": "Table", "options": "Quotation Item", "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "40px" }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_break23", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total", @@ -446,7 +524,9 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_net_total", @@ -457,18 +537,24 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { "fieldname": "column_break_28", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_total", @@ -476,32 +562,42 @@ "label": "Net Total", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "label": "Taxes and Charges", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_34", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", @@ -509,11 +605,15 @@ "label": "Shipping Rule", "oldfieldtype": "Button", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_36", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -522,7 +622,9 @@ "oldfieldname": "charge", "oldfieldtype": "Link", "options": "Sales Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -530,13 +632,17 @@ "label": "Sales Taxes and Charges", "oldfieldname": "other_charges", "oldfieldtype": "Table", - "options": "Sales Taxes and Charges" + "options": "Sales Taxes and Charges", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup" + "label": "Tax Breakup", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "other_charges_calculation", @@ -545,11 +651,15 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_39", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -559,11 +669,15 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_42", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -571,26 +685,34 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount and Coupon Code" + "label": "Additional Discount and Coupon Code", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "coupon_code", "fieldtype": "Link", "label": "Coupon Code", - "options": "Coupon Code" + "options": "Coupon Code", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "referral_sales_partner", "fieldtype": "Link", "label": "Referral Sales Partner", - "options": "Sales Partner" + "options": "Sales Partner", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -598,7 +720,9 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_discount_amount", @@ -606,31 +730,41 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -641,6 +775,8 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -650,7 +786,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "In Words will be visible once you save the Quotation.", @@ -662,6 +800,8 @@ "oldfieldtype": "Data", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -673,6 +813,8 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -680,6 +822,8 @@ "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -691,6 +835,8 @@ "oldfieldtype": "Currency", "options": "currency", "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -700,7 +846,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -711,6 +859,8 @@ "oldfieldtype": "Currency", "options": "currency", "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { @@ -722,19 +872,25 @@ "oldfieldtype": "Data", "print_hide": 1, "read_only": 1, + "show_days": 1, + "show_seconds": 1, "width": "200px" }, { "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms" + "label": "Payment Terms", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", "options": "Payment Terms Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_schedule", @@ -742,7 +898,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -751,7 +909,9 @@ "fieldtype": "Section Break", "label": "Terms and Conditions", "oldfieldtype": "Section Break", - "options": "fa fa-legal" + "options": "fa fa-legal", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", @@ -761,20 +921,26 @@ "oldfieldtype": "Link", "options": "Terms and Conditions", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", "label": "Term Details", "oldfieldname": "terms", - "oldfieldtype": "Text Editor" + "oldfieldtype": "Text Editor", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "print_settings", "fieldtype": "Section Break", - "label": "Print Settings" + "label": "Print Settings", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -784,7 +950,9 @@ "oldfieldname": "letter_head", "oldfieldtype": "Select", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -792,11 +960,15 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_73", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -808,19 +980,25 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Auto Repeat Section" + "label": "Auto Repeat Section", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -829,14 +1007,18 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference" + "label": "Update Auto Repeat Reference", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -845,7 +1027,9 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "campaign", @@ -854,7 +1038,9 @@ "oldfieldname": "campaign", "oldfieldtype": "Link", "options": "Campaign", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "source", @@ -863,7 +1049,9 @@ "oldfieldname": "source", "oldfieldtype": "Select", "options": "Lead Source", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -874,13 +1062,17 @@ "no_copy": 1, "oldfieldname": "order_lost_reason", "oldfieldtype": "Small Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break4", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -895,7 +1087,9 @@ "options": "Draft\nOpen\nReplied\nOrdered\nLost\nCancelled\nExpired", "print_hide": 1, "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "enq_det", @@ -905,13 +1099,17 @@ "oldfieldname": "enq_det", "oldfieldtype": "Text", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_quotation", "fieldtype": "Link", "label": "Supplier Quotation", - "options": "Supplier Quotation" + "options": "Supplier Quotation", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "opportunity", @@ -919,7 +1117,9 @@ "label": "Opportunity", "options": "Opportunity", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -927,7 +1127,9 @@ "fieldtype": "Table MultiSelect", "label": "Lost Reasons", "options": "Quotation Lost Reason Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "packed_items", @@ -935,7 +1137,9 @@ "fieldtype": "Table", "label": "Bundle Items", "options": "Packed Item", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -945,14 +1149,32 @@ "fieldtype": "Section Break", "label": "Bundle Items", "options": "fa fa-suitcase", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "company_address", + "fieldtype": "Link", + "label": "Company Address Name", + "options": "Address", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "company_address_display", + "fieldtype": "Small Text", + "label": "Company Address", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2021-11-30 01:33:21.106073", + "modified": "2022-03-23 16:49:36.297403", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index f191d9323ee..5759b504cee 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -9,35 +9,56 @@ from frappe.utils import flt, getdate, nowdate from erpnext.controllers.selling_controller import SellingController -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class Quotation(SellingController): def set_indicator(self): - if self.docstatus==1: - self.indicator_color = 'blue' - self.indicator_title = 'Submitted' + if self.docstatus == 1: + self.indicator_color = "blue" + self.indicator_title = "Submitted" if self.valid_till and getdate(self.valid_till) < getdate(nowdate()): - self.indicator_color = 'gray' - self.indicator_title = 'Expired' + self.indicator_color = "gray" + self.indicator_title = "Expired" def validate(self): super(Quotation, self).validate() self.set_status() self.validate_uom_is_integer("stock_uom", "qty") self.validate_valid_till() + self.validate_shopping_cart_items() self.set_customer_name() if self.items: self.with_items = 1 from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) def validate_valid_till(self): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): frappe.throw(_("Valid till date cannot be before transaction date")) + def validate_shopping_cart_items(self): + if self.order_type != "Shopping Cart": + return + + for item in self.items: + has_web_item = frappe.db.exists("Website Item", {"item_code": item.item_code}) + + # If variant is unpublished but template is published: valid + template = frappe.get_cached_value("Item", item.item_code, "variant_of") + if template and not has_web_item: + has_web_item = frappe.db.exists("Website Item", {"item_code": template}) + + if not has_web_item: + frappe.throw( + _("Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Unpublished Item"), + ) + def has_sales_order(self): return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1}) @@ -46,10 +67,12 @@ class Quotation(SellingController): frappe.get_doc("Lead", self.party_name).set_status(update=True) def set_customer_name(self): - if self.party_name and self.quotation_to == 'Customer': + if self.party_name and self.quotation_to == "Customer": self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name") - elif self.party_name and self.quotation_to == 'Lead': - lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"]) + elif self.party_name and self.quotation_to == "Lead": + lead_name, company_name = frappe.db.get_value( + "Lead", self.party_name, ["lead_name", "company_name"] + ) self.customer_name = company_name or lead_name def update_opportunity(self, status): @@ -70,21 +93,24 @@ class Quotation(SellingController): @frappe.whitelist() def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): if not self.has_sales_order(): - get_lost_reasons = frappe.get_list('Quotation Lost Reason', - fields = ["name"]) - lost_reasons_lst = [reason.get('name') for reason in get_lost_reasons] - frappe.db.set(self, 'status', 'Lost') + get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) + lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] + frappe.db.set(self, "status", "Lost") if detailed_reason: - frappe.db.set(self, 'order_lost_reason', detailed_reason) + frappe.db.set(self, "order_lost_reason", detailed_reason) for reason in lost_reasons_list: - if reason.get('lost_reason') in lost_reasons_lst: - self.append('lost_reasons', reason) + if reason.get("lost_reason") in lost_reasons_lst: + self.append("lost_reasons", reason) else: - frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason')))) + frappe.throw( + _("Invalid lost reason {0}, please create a new lost reason").format( + frappe.bold(reason.get("lost_reason")) + ) + ) - self.update_opportunity('Lost') + self.update_opportunity("Lost") self.update_lead() self.save() @@ -93,11 +119,12 @@ class Quotation(SellingController): def on_submit(self): # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) - #update enquiry status - self.update_opportunity('Quotation') + # update enquiry status + self.update_opportunity("Quotation") self.update_lead() def on_cancel(self): @@ -105,14 +132,14 @@ class Quotation(SellingController): self.lost_reasons = [] super(Quotation, self).on_cancel() - #update enquiry status + # update enquiry status self.set_status(update=True) - self.update_opportunity('Open') + self.update_opportunity("Open") self.update_lead() - def print_other_charges(self,docname): + def print_other_charges(self, docname): print_lst = [] - for d in self.get('taxes'): + for d in self.get("taxes"): lst1 = [] lst1.append(d.description) lst1.append(d.total) @@ -122,25 +149,35 @@ class Quotation(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): self.valid_till = None + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Quotations'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Quotations"), + } + ) return list_context + @frappe.whitelist() def make_sales_order(source_name, target_doc=None): - quotation = frappe.db.get_value("Quotation", source_name, ["transaction_date", "valid_till"], as_dict = 1) - if quotation.valid_till and (quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())): + quotation = frappe.db.get_value( + "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 + ) + if quotation.valid_till and ( + quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) + ): frappe.throw(_("Validity period of this quotation has ended.")) return _make_sales_order(source_name, target_doc) + def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) @@ -149,9 +186,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.customer = customer.name target.customer_name = customer.customer_name if source.referral_sales_partner: - target.sales_partner=source.referral_sales_partner - target.commission_rate=frappe.get_value('Sales Partner', source.referral_sales_partner, 'commission_rate') - target.ignore_pricing_rule = 1 + target.sales_partner = source.referral_sales_partner + target.commission_rate = frappe.get_value( + "Sales Partner", source.referral_sales_partner, "commission_rate" + ) target.flags.ignore_permissions = ignore_permissions target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -164,38 +202,31 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.blanket_order = obj.blanket_order target.blanket_order_rate = obj.blanket_order_rate - doclist = get_mapped_doc("Quotation", source_name, { - "Quotation": { - "doctype": "Sales Order", - "validation": { - "docstatus": ["=", 1] - } - }, + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}}, "Quotation Item": { "doctype": "Sales Order Item", - "field_map": { - "parent": "prevdoc_docname" - }, - "postprocess": update_item + "field_map": {"parent": "prevdoc_docname"}, + "postprocess": update_item, }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - }, - "Payment Schedule": { - "doctype": "Payment Schedule", - "add_if_empty": True - } - }, target_doc, set_missing_values, ignore_permissions=ignore_permissions) + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, + "Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True}, + }, + target_doc, + set_missing_values, + ignore_permissions=ignore_permissions, + ) # postprocess: fetch shipping address, set missing values + doclist.set_onload("ignore_price_list", True) return doclist + def set_expired_status(): # filter out submitted non expired quotations whose validity has been ended cond = "qo.docstatus = 1 and qo.status != 'Expired' and qo.valid_till < %s" @@ -210,15 +241,18 @@ def set_expired_status(): # if not exists any SO, set status as Expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""" - .format(cond=cond, so_against_quo=so_against_quo), - (nowdate()) - ) + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""".format( + cond=cond, so_against_quo=so_against_quo + ), + (nowdate()), + ) + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): return _make_sales_invoice(source_name, target_doc) + def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) @@ -226,7 +260,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if customer: target.customer = customer.name target.customer_name = customer.customer_name - target.ignore_pricing_rule = 1 + target.flags.ignore_permissions = ignore_permissions target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -235,52 +269,52 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.cost_center = None target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) - doclist = get_mapped_doc("Quotation", source_name, { - "Quotation": { - "doctype": "Sales Invoice", - "validation": { - "docstatus": ["=", 1] - } - }, - "Quotation Item": { - "doctype": "Sales Invoice Item", - "postprocess": update_item - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } - }, target_doc, set_missing_values, ignore_permissions=ignore_permissions) + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, + "Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, + }, + target_doc, + set_missing_values, + ignore_permissions=ignore_permissions, + ) + + doclist.set_onload("ignore_price_list", True) return doclist -def _make_customer(source_name, ignore_permissions=False): - quotation = frappe.db.get_value("Quotation", - source_name, ["order_type", "party_name", "customer_name"], as_dict=1) - if quotation and quotation.get('party_name'): +def _make_customer(source_name, ignore_permissions=False): + quotation = frappe.db.get_value( + "Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1 + ) + + if quotation and quotation.get("party_name"): if not frappe.db.exists("Customer", quotation.get("party_name")): lead_name = quotation.get("party_name") - customer_name = frappe.db.get_value("Customer", {"lead_name": lead_name}, - ["name", "customer_name"], as_dict=True) + customer_name = frappe.db.get_value( + "Customer", {"lead_name": lead_name}, ["name", "customer_name"], as_dict=True + ) if not customer_name: from erpnext.crm.doctype.lead.lead import _make_customer + customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions) customer = frappe.get_doc(customer_doclist) customer.flags.ignore_permissions = ignore_permissions if quotation.get("party_name") == "Shopping Cart": - customer.customer_group = frappe.db.get_value("E Commerce Settings", None, - "default_customer_group") + customer.customer_group = frappe.db.get_value( + "E Commerce Settings", None, "default_customer_group" + ) try: customer.insert() return customer except frappe.NameError: - if frappe.defaults.get_global_default('cust_master_name') == "Customer Name": + if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": customer.run_method("autoname") customer.name += "-" + lead_name customer.insert() @@ -288,12 +322,14 @@ def _make_customer(source_name, ignore_permissions=False): else: raise except frappe.MandatoryError as e: - mandatory_fields = e.args[0].split(':')[1].split(',') + mandatory_fields = e.args[0].split(":")[1].split(",") mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] frappe.local.message_log = [] lead_link = frappe.utils.get_link_to_form("Lead", lead_name) - message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
    " + message = ( + _("Could not auto create Customer due to the following missing mandatory field(s):") + "
    " + ) message += "
    • " + "
    • ".join(mandatory_fields) + "
    " message += _("Please create Customer from Lead {0}.").format(lead_link) diff --git a/erpnext/selling/doctype/quotation/quotation_dashboard.py b/erpnext/selling/doctype/quotation/quotation_dashboard.py index 46de292cc4d..7bfa034c532 100644 --- a/erpnext/selling/doctype/quotation/quotation_dashboard.py +++ b/erpnext/selling/doctype/quotation/quotation_dashboard.py @@ -1,21 +1,14 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'prevdoc_docname', - 'non_standard_fieldnames': { - 'Auto Repeat': 'reference_document', + "fieldname": "prevdoc_docname", + "non_standard_fieldnames": { + "Auto Repeat": "reference_document", }, - 'transactions': [ - { - 'label': _('Sales Order'), - 'items': ['Sales Order'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Sales Order"), "items": ["Sales Order"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/selling/doctype/quotation/regional/india.js b/erpnext/selling/doctype/quotation/regional/india.js new file mode 100644 index 00000000000..955083565bc --- /dev/null +++ b/erpnext/selling/doctype/quotation/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Quotation'); diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 4357201d23d..6f0b381fc16 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -2,17 +2,16 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, add_months, flt, getdate, nowdate -from erpnext.tests.utils import ERPNextTestCase - test_dependencies = ["Product Bundle"] -class TestQuotation(ERPNextTestCase): +class TestQuotation(FrappeTestCase): def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) - self.assertFalse(quotation.get('payment_schedule')) + self.assertFalse(quotation.get("payment_schedule")) quotation.insert() @@ -29,7 +28,7 @@ class TestQuotation(ERPNextTestCase): sales_order = make_sales_order(quotation.name) - self.assertTrue(sales_order.get('payment_schedule')) + self.assertTrue(sales_order.get("payment_schedule")) def test_make_sales_order_with_different_currency(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -81,9 +80,7 @@ class TestQuotation(ERPNextTestCase): quotation = frappe.copy_doc(test_records[0]) quotation.transaction_date = nowdate() quotation.valid_till = add_months(quotation.transaction_date, 1) - quotation.update( - {"payment_terms_template": "_Test Payment Term Template"} - ) + quotation.update({"payment_terms_template": "_Test Payment Term Template"}) quotation.insert() self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) @@ -93,7 +90,9 @@ class TestQuotation(ERPNextTestCase): self.assertEqual(quotation.payment_schedule[0].payment_amount, 8906.00) self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date) self.assertEqual(quotation.payment_schedule[1].payment_amount, 8906.00) - self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30)) + self.assertEqual( + quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30) + ) sales_order = make_sales_order(quotation.name) @@ -109,7 +108,7 @@ class TestQuotation(ERPNextTestCase): sales_order.insert() # Remove any unknown taxes if applied - sales_order.set('taxes', []) + sales_order.set("taxes", []) sales_order.save() self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00) @@ -131,6 +130,15 @@ class TestQuotation(ERPNextTestCase): quotation.submit() self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) + def test_shopping_cart_without_website_item(self): + if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}): + frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete() + + quotation = frappe.copy_doc(test_records[0]) + quotation.order_type = "Shopping Cart" + quotation.valid_till = getdate() + self.assertRaises(frappe.ValidationError, quotation.validate) + def test_create_quotation_with_margin(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order from erpnext.selling.doctype.sales_order.sales_order import ( @@ -138,11 +146,11 @@ class TestQuotation(ERPNextTestCase): make_sales_invoice, ) - rate_with_margin = flt((1500*18.75)/100 + 1500) + rate_with_margin = flt((1500 * 18.75) / 100 + 1500) - test_records[0]['items'][0]['price_list_rate'] = 1500 - test_records[0]['items'][0]['margin_type'] = 'Percentage' - test_records[0]['items'][0]['margin_rate_or_amount'] = 18.75 + test_records[0]["items"][0]["price_list_rate"] = 1500 + test_records[0]["items"][0]["margin_type"] = "Percentage" + test_records[0]["items"][0]["margin_rate_or_amount"] = 18.75 quotation = frappe.copy_doc(test_records[0]) quotation.transaction_date = nowdate() @@ -175,11 +183,9 @@ class TestQuotation(ERPNextTestCase): def test_create_two_quotations(self): from erpnext.stock.doctype.item.test_item import make_item - first_item = make_item("_Test Laptop", - {"is_stock_item": 1}) + first_item = make_item("_Test Laptop", {"is_stock_item": 1}) - second_item = make_item("_Test CPU", - {"is_stock_item": 1}) + second_item = make_item("_Test CPU", {"is_stock_item": 1}) qo_item1 = [ { @@ -188,7 +194,7 @@ class TestQuotation(ERPNextTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", } ] @@ -198,7 +204,7 @@ class TestQuotation(ERPNextTestCase): "warehouse": "_Test Warehouse - _TC", "qty": 2, "rate": 300, - "conversion_factor": 1.0 + "conversion_factor": 1.0, } ] @@ -210,17 +216,12 @@ class TestQuotation(ERPNextTestCase): def test_quotation_expiry(self): from erpnext.selling.doctype.quotation.quotation import set_expired_status - quotation_item = [ - { - "item_code": "_Test Item", - "warehouse":"", - "qty": 1, - "rate": 500 - } - ] + quotation_item = [{"item_code": "_Test Item", "warehouse": "", "qty": 1, "rate": 500}] yesterday = add_days(nowdate(), -1) - expired_quotation = make_quotation(item_list=quotation_item, transaction_date=yesterday, do_not_submit=True) + expired_quotation = make_quotation( + item_list=quotation_item, transaction_date=yesterday, do_not_submit=True + ) expired_quotation.valid_till = yesterday expired_quotation.save() expired_quotation.submit() @@ -237,24 +238,49 @@ class TestQuotation(ERPNextTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) quotation = make_quotation(item_code="_Test Product Bundle", qty=1, rate=100) sales_order = make_sales_order(quotation.name) - quotation_item = [quotation.items[0].item_code, quotation.items[0].rate, quotation.items[0].qty, quotation.items[0].amount] - so_item = [sales_order.items[0].item_code, sales_order.items[0].rate, sales_order.items[0].qty, sales_order.items[0].amount] + quotation_item = [ + quotation.items[0].item_code, + quotation.items[0].rate, + quotation.items[0].qty, + quotation.items[0].amount, + ] + so_item = [ + sales_order.items[0].item_code, + sales_order.items[0].rate, + sales_order.items[0].qty, + sales_order.items[0].amount, + ] self.assertEqual(quotation_item, so_item) quotation_packed_items = [ - [quotation.packed_items[0].parent_item, quotation.packed_items[0].item_code, quotation.packed_items[0].qty], - [quotation.packed_items[1].parent_item, quotation.packed_items[1].item_code, quotation.packed_items[1].qty] + [ + quotation.packed_items[0].parent_item, + quotation.packed_items[0].item_code, + quotation.packed_items[0].qty, + ], + [ + quotation.packed_items[1].parent_item, + quotation.packed_items[1].item_code, + quotation.packed_items[1].qty, + ], ] so_packed_items = [ - [sales_order.packed_items[0].parent_item, sales_order.packed_items[0].item_code, sales_order.packed_items[0].qty], - [sales_order.packed_items[1].parent_item, sales_order.packed_items[1].item_code, sales_order.packed_items[1].qty] + [ + sales_order.packed_items[0].parent_item, + sales_order.packed_items[0].item_code, + sales_order.packed_items[0].qty, + ], + [ + sales_order.packed_items[1].parent_item, + sales_order.packed_items[1].item_code, + sales_order.packed_items[1].qty, + ], ] self.assertEqual(quotation_packed_items, so_packed_items) @@ -267,8 +293,7 @@ class TestQuotation(ERPNextTestCase): bundle_item1 = make_item("_Test Bundle Item 1", {"is_stock_item": 1}) bundle_item2 = make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) bundle_item1.valuation_rate = 100 bundle_item1.save() @@ -287,8 +312,7 @@ class TestQuotation(ERPNextTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) enable_calculate_bundle_price() @@ -302,7 +326,9 @@ class TestQuotation(ERPNextTestCase): enable_calculate_bundle_price(enable=0) - def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self): + def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked( + self, + ): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.stock.doctype.item.test_item import make_item @@ -312,10 +338,8 @@ class TestQuotation(ERPNextTestCase): make_item("_Test Bundle Item 2", {"is_stock_item": 1}) make_item("_Test Bundle Item 3", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle 1", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) - make_product_bundle("_Test Product Bundle 2", - ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 1", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", ["_Test Bundle Item 2", "_Test Bundle Item 3"]) enable_calculate_bundle_price() @@ -326,7 +350,7 @@ class TestQuotation(ERPNextTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 2", @@ -334,8 +358,8 @@ class TestQuotation(ERPNextTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] quotation = make_quotation(item_list=item_list, do_not_submit=1) @@ -348,7 +372,7 @@ class TestQuotation(ERPNextTestCase): expected_values = [300, 500] for item in quotation.items: - self.assertEqual(item.amount, expected_values[item.idx-1]) + self.assertEqual(item.amount, expected_values[item.idx - 1]) enable_calculate_bundle_price(enable=0) @@ -363,12 +387,9 @@ class TestQuotation(ERPNextTestCase): make_item("_Test Bundle Item 2", {"is_stock_item": 1}) make_item("_Test Bundle Item 3", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle 1", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) - make_product_bundle("_Test Product Bundle 2", - ["_Test Bundle Item 2", "_Test Bundle Item 3"]) - make_product_bundle("_Test Product Bundle 3", - ["_Test Bundle Item 3", "_Test Bundle Item 1"]) + make_product_bundle("_Test Product Bundle 1", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 3", ["_Test Bundle Item 3", "_Test Bundle Item 1"]) item_list = [ { @@ -377,7 +398,7 @@ class TestQuotation(ERPNextTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 2", @@ -385,7 +406,7 @@ class TestQuotation(ERPNextTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 3", @@ -393,8 +414,8 @@ class TestQuotation(ERPNextTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] quotation = make_quotation(item_list=item_list, do_not_submit=1) @@ -405,29 +426,26 @@ class TestQuotation(ERPNextTestCase): expected_index = id + 1 self.assertEqual(item.idx, expected_index) -test_records = frappe.get_test_records('Quotation') + +test_records = frappe.get_test_records("Quotation") + def enable_calculate_bundle_price(enable=1): selling_settings = frappe.get_doc("Selling Settings") selling_settings.editable_bundle_item_rates = enable selling_settings.save() + def get_quotation_dict(party_name=None, item_code=None): if not party_name: - party_name = '_Test Customer' + party_name = "_Test Customer" if not item_code: - item_code = '_Test Item' + item_code = "_Test Item" return { - 'doctype': 'Quotation', - 'party_name': party_name, - 'items': [ - { - 'item_code': item_code, - 'qty': 1, - 'rate': 100 - } - ] + "doctype": "Quotation", + "party_name": party_name, + "items": [{"item_code": item_code, "qty": 1, "rate": 100}], } @@ -451,13 +469,16 @@ def make_quotation(**args): qo.append("items", item) else: - qo.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse, - "qty": args.qty or 10, - "uom": args.uom or None, - "rate": args.rate or 100 - }) + qo.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse, + "qty": args.qty or 10, + "uom": args.uom or None, + "rate": args.rate or 100, + }, + ) qo.delivery_date = add_days(qo.transaction_date, 10) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 2d5bb2013f2..213909b9b99 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -65,7 +65,11 @@ frappe.ui.form.on("Sales Order", { frm.set_value('transaction_date', frappe.datetime.get_today()) } erpnext.queries.setup_queries(frm, "Warehouse", function() { - return erpnext.queries.warehouse(frm.doc); + return { + filters: [ + ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], + ] + }; }); frm.set_query('project', function(doc, cdt, cdn) { @@ -77,7 +81,19 @@ frappe.ui.form.on("Sales Order", { } }); - erpnext.queries.setup_warehouse_query(frm); + frm.set_query('warehouse', 'items', function(doc, cdt, cdn) { + let row = locals[cdt][cdn]; + let query = { + filters: [ + ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], + ] + }; + if (row.item_code) { + query.query = "erpnext.controllers.queries.warehouse_query"; + query.filters.push(["Bin", "item_code", "=", row.item_code]); + } + return query; + }); frm.ignore_doctypes_on_cancel_all = ['Purchase Order']; }, @@ -152,7 +168,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } - this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) { + this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + } const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1; const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1; @@ -562,6 +580,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( var me = this; var dialog = new frappe.ui.Dialog({ title: __("Select Items"), + size: "large", fields: [ { "fieldtype": "Check", @@ -663,7 +682,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } else { let po_items = []; me.frm.doc.items.forEach(d => { - let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); + let ordered_qty = me.get_ordered_qty(d, me.frm.doc); + let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ "doctype": "Sales Order Item", @@ -689,6 +709,24 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( dialog.show(); }, + get_ordered_qty: function(item, so) { + let ordered_qty = item.ordered_qty; + if (so.packed_items && so.packed_items.length) { + // calculate ordered qty based on packed items in case of product bundle + let packed_items = so.packed_items.filter( + (pi) => pi.parent_detail_docname == item.name + ); + if (packed_items && packed_items.length) { + ordered_qty = packed_items.reduce( + (sum, pi) => sum + flt(pi.ordered_qty), + 0 + ); + ordered_qty = ordered_qty / packed_items.length; + } + } + return ordered_qty; + }, + hold_sales_order: function(){ var me = this; var d = new frappe.ui.Dialog({ diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 7e99a062439..6bcc8f05ac3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -25,6 +25,10 @@ "po_no", "po_date", "tax_id", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "contact_info", "customer_address", "address_display", @@ -113,7 +117,6 @@ "is_internal_customer", "represents_company", "inter_company_order_reference", - "project", "party_account_currency", "column_break_77", "source", @@ -130,6 +133,7 @@ "per_delivered", "column_break_81", "per_billed", + "per_picked", "billing_status", "sales_team_section_break", "sales_partner", @@ -1514,13 +1518,36 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "fieldname": "per_picked", + "fieldtype": "Percent", + "label": "% Picked", + "no_copy": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-10-05 12:16:40.775704", + "modified": "2022-04-26 14:38:18.350207", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1594,8 +1621,9 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "customer_name", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 8336a143617..edd7d26d0bc 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -28,11 +28,12 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + + +class WarehouseRequired(frappe.ValidationError): + pass -class WarehouseRequired(frappe.ValidationError): pass class SalesOrder(SellingController): def __init__(self, *args, **kwargs): @@ -49,20 +50,26 @@ class SalesOrder(SellingController): self.validate_warehouse() self.validate_drop_ship() self.validate_serial_no_based_delivery() - validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_order_reference) + validate_inter_company_party( + self.doctype, self.customer, self.company, self.inter_company_order_reference + ) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(self.coupon_code) from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) self.validate_with_previous_doc() self.set_status() - if not self.billing_status: self.billing_status = 'Not Billed' - if not self.delivery_status: self.delivery_status = 'Not Delivered' + if not self.billing_status: + self.billing_status = "Not Billed" + if not self.delivery_status: + self.delivery_status = "Not Delivered" self.reset_default_field_value("set_warehouse", "items", "warehouse") @@ -71,55 +78,82 @@ class SalesOrder(SellingController): if self.po_date and not self.skip_delivery_note: for d in self.get("items"): if d.delivery_date and getdate(self.po_date) > getdate(d.delivery_date): - frappe.throw(_("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date") - .format(d.idx)) + frappe.throw( + _("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date").format(d.idx) + ) if self.po_no and self.customer and not self.skip_delivery_note: - so = frappe.db.sql("select name from `tabSales Order` \ + so = frappe.db.sql( + "select name from `tabSales Order` \ where ifnull(po_no, '') = %s and name != %s and docstatus < 2\ - and customer = %s", (self.po_no, self.name, self.customer)) - if so and so[0][0] and not cint(frappe.db.get_single_value("Selling Settings", - "allow_against_multiple_purchase_orders")): - frappe.msgprint(_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(so[0][0], self.po_no)) + and customer = %s", + (self.po_no, self.name, self.customer), + ) + if ( + so + and so[0][0] + and not cint( + frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders") + ) + ): + frappe.msgprint( + _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format( + so[0][0], self.po_no + ) + ) def validate_for_items(self): - for d in self.get('items'): + for d in self.get("items"): # used for production plan d.transaction_date = self.transaction_date - tot_avail_qty = frappe.db.sql("select projected_qty from `tabBin` \ - where item_code = %s and warehouse = %s", (d.item_code, d.warehouse)) + tot_avail_qty = frappe.db.sql( + "select projected_qty from `tabBin` \ + where item_code = %s and warehouse = %s", + (d.item_code, d.warehouse), + ) d.projected_qty = tot_avail_qty and flt(tot_avail_qty[0][0]) or 0 def product_bundle_has_stock_item(self, product_bundle): """Returns true if product bundle has stock item""" - ret = len(frappe.db.sql("""select i.name from tabItem i, `tabProduct Bundle Item` pbi - where pbi.parent = %s and pbi.item_code = i.name and i.is_stock_item = 1""", product_bundle)) + ret = len( + frappe.db.sql( + """select i.name from tabItem i, `tabProduct Bundle Item` pbi + where pbi.parent = %s and pbi.item_code = i.name and i.is_stock_item = 1""", + product_bundle, + ) + ) return ret def validate_sales_mntc_quotation(self): - for d in self.get('items'): + for d in self.get("items"): if d.prevdoc_docname: - res = frappe.db.sql("select name from `tabQuotation` where name=%s and order_type = %s", - (d.prevdoc_docname, self.order_type)) + res = frappe.db.sql( + "select name from `tabQuotation` where name=%s and order_type = %s", + (d.prevdoc_docname, self.order_type), + ) if not res: - frappe.msgprint(_("Quotation {0} not of type {1}") - .format(d.prevdoc_docname, self.order_type)) + frappe.msgprint(_("Quotation {0} not of type {1}").format(d.prevdoc_docname, self.order_type)) def validate_delivery_date(self): - if self.order_type == 'Sales' and not self.skip_delivery_note: + if self.order_type == "Sales" and not self.skip_delivery_note: delivery_date_list = [d.delivery_date for d in self.get("items") if d.delivery_date] max_delivery_date = max(delivery_date_list) if delivery_date_list else None - if (max_delivery_date and not self.delivery_date) or (max_delivery_date and getdate(self.delivery_date) != getdate(max_delivery_date)): + if (max_delivery_date and not self.delivery_date) or ( + max_delivery_date and getdate(self.delivery_date) != getdate(max_delivery_date) + ): self.delivery_date = max_delivery_date if self.delivery_date: for d in self.get("items"): if not d.delivery_date: d.delivery_date = self.delivery_date if getdate(self.transaction_date) > getdate(d.delivery_date): - frappe.msgprint(_("Expected Delivery Date should be after Sales Order Date"), - indicator='orange', title=_('Warning')) + frappe.msgprint( + _("Expected Delivery Date should be after Sales Order Date"), + indicator="orange", + title=_("Warning"), + ) else: frappe.throw(_("Please enter Delivery Date")) @@ -127,47 +161,56 @@ class SalesOrder(SellingController): def validate_proj_cust(self): if self.project and self.customer_name: - res = frappe.db.sql("""select name from `tabProject` where name = %s + res = frappe.db.sql( + """select name from `tabProject` where name = %s and (customer = %s or ifnull(customer,'')='')""", - (self.project, self.customer)) + (self.project, self.customer), + ) if not res: - frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) + frappe.throw( + _("Customer {0} does not belong to project {1}").format(self.customer, self.project) + ) def validate_warehouse(self): super(SalesOrder, self).validate_warehouse() for d in self.get("items"): - if (frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 or - (self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code))) \ - and not d.warehouse and not cint(d.delivered_by_supplier): - frappe.throw(_("Delivery warehouse required for stock item {0}").format(d.item_code), - WarehouseRequired) + if ( + ( + frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 + or (self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code)) + ) + and not d.warehouse + and not cint(d.delivered_by_supplier) + ): + frappe.throw( + _("Delivery warehouse required for stock item {0}").format(d.item_code), WarehouseRequired + ) def validate_with_previous_doc(self): - super(SalesOrder, self).validate_with_previous_doc({ - "Quotation": { - "ref_dn_field": "prevdoc_docname", - "compare_fields": [["company", "="]] - } - }) - + super(SalesOrder, self).validate_with_previous_doc( + {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} + ) def update_enquiry_status(self, prevdoc, flag): - enq = frappe.db.sql("select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", prevdoc) + enq = frappe.db.sql( + "select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", + prevdoc, + ) if enq: - frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0])) + frappe.db.sql("update `tabOpportunity` set status = %s where name=%s", (flag, enq[0][0])) def update_prevdoc_status(self, flag=None): for quotation in set(d.prevdoc_docname for d in self.get("items")): if quotation: doc = frappe.get_doc("Quotation", quotation) - if doc.docstatus==2: + if doc.docstatus == 2: frappe.throw(_("Quotation {0} is cancelled").format(quotation)) doc.set_status(update=True) def validate_drop_ship(self): - for d in self.get('items'): + for d in self.get("items"): if d.delivered_by_supplier and not d.supplier: frappe.throw(_("Row #{0}: Set Supplier for item {1}").format(d.idx, d.item_code)) @@ -175,41 +218,47 @@ class SalesOrder(SellingController): self.check_credit_limit() self.update_reserved_qty() - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) self.update_project() - self.update_prevdoc_status('submit') + self.update_prevdoc_status("submit") self.update_blanket_order() update_linked_doc(self.doctype, self.name, self.inter_company_order_reference) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'used') + + update_coupon_code_count(self.coupon_code, "used") def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") super(SalesOrder, self).on_cancel() # Cannot cancel closed SO - if self.status == 'Closed': + if self.status == "Closed": frappe.throw(_("Closed order cannot be cancelled. Unclose to cancel.")) self.check_nextdoc_docstatus() self.update_reserved_qty() self.update_project() - self.update_prevdoc_status('cancel') + self.update_prevdoc_status("cancel") - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") self.update_blanket_order() unlink_inter_company_doc(self.doctype, self.name, self.inter_company_order_reference) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'cancelled') + + update_coupon_code_count(self.coupon_code, "cancelled") def update_project(self): - if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') != "Each Transaction": + if ( + frappe.db.get_single_value("Selling Settings", "sales_update_frequency") != "Each Transaction" + ): return if self.project: @@ -220,71 +269,103 @@ class SalesOrder(SellingController): def check_credit_limit(self): # if bypass credit limit check is set to true (1) at sales order level, # then we need not to check credit limit and vise versa - if not cint(frappe.db.get_value("Customer Credit Limit", - {'parent': self.customer, 'parenttype': 'Customer', 'company': self.company}, - "bypass_credit_limit_check")): + if not cint( + frappe.db.get_value( + "Customer Credit Limit", + {"parent": self.customer, "parenttype": "Customer", "company": self.company}, + "bypass_credit_limit_check", + ) + ): check_credit_limit(self.customer, self.company) def check_nextdoc_docstatus(self): # Checks Delivery Note - submit_dn = frappe.db.sql_list(""" + submit_dn = frappe.db.sql_list( + """ select t1.name from `tabDelivery Note` t1,`tabDelivery Note Item` t2 - where t1.name = t2.parent and t2.against_sales_order = %s and t1.docstatus = 1""", self.name) + where t1.name = t2.parent and t2.against_sales_order = %s and t1.docstatus = 1""", + self.name, + ) if submit_dn: submit_dn = [get_link_to_form("Delivery Note", dn) for dn in submit_dn] - frappe.throw(_("Delivery Notes {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_dn))) + frappe.throw( + _("Delivery Notes {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(submit_dn) + ) + ) # Checks Sales Invoice - submit_rv = frappe.db.sql_list("""select t1.name + submit_rv = frappe.db.sql_list( + """select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""", - self.name) + self.name, + ) if submit_rv: submit_rv = [get_link_to_form("Sales Invoice", si) for si in submit_rv] - frappe.throw(_("Sales Invoice {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_rv))) + frappe.throw( + _("Sales Invoice {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(submit_rv) + ) + ) - #check maintenance schedule - submit_ms = frappe.db.sql_list(""" + # check maintenance schedule + submit_ms = frappe.db.sql_list( + """ select t1.name from `tabMaintenance Schedule` t1, `tabMaintenance Schedule Item` t2 - where t2.parent=t1.name and t2.sales_order = %s and t1.docstatus = 1""", self.name) + where t2.parent=t1.name and t2.sales_order = %s and t1.docstatus = 1""", + self.name, + ) if submit_ms: submit_ms = [get_link_to_form("Maintenance Schedule", ms) for ms in submit_ms] - frappe.throw(_("Maintenance Schedule {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_ms))) + frappe.throw( + _("Maintenance Schedule {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(submit_ms) + ) + ) # check maintenance visit - submit_mv = frappe.db.sql_list(""" + submit_mv = frappe.db.sql_list( + """ select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 - where t2.parent=t1.name and t2.prevdoc_docname = %s and t1.docstatus = 1""",self.name) + where t2.parent=t1.name and t2.prevdoc_docname = %s and t1.docstatus = 1""", + self.name, + ) if submit_mv: submit_mv = [get_link_to_form("Maintenance Visit", mv) for mv in submit_mv] - frappe.throw(_("Maintenance Visit {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_mv))) + frappe.throw( + _("Maintenance Visit {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(submit_mv) + ) + ) # check work order - pro_order = frappe.db.sql_list(""" + pro_order = frappe.db.sql_list( + """ select name from `tabWork Order` - where sales_order = %s and docstatus = 1""", self.name) + where sales_order = %s and docstatus = 1""", + self.name, + ) if pro_order: pro_order = [get_link_to_form("Work Order", po) for po in pro_order] - frappe.throw(_("Work Order {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(pro_order))) + frappe.throw( + _("Work Order {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(pro_order) + ) + ) def check_modified_date(self): mod_db = frappe.db.get_value("Sales Order", self.name, "modified") - date_diff = frappe.db.sql("select TIMEDIFF('%s', '%s')" % - ( mod_db, cstr(self.modified))) + date_diff = frappe.db.sql("select TIMEDIFF('%s', '%s')" % (mod_db, cstr(self.modified))) if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(self.doctype, self.name)) @@ -298,10 +379,15 @@ class SalesOrder(SellingController): def update_reserved_qty(self, so_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] + def _valid_for_reserve(item_code, warehouse): - if item_code and warehouse and [item_code, warehouse] not in item_wh_list \ - and frappe.get_cached_value("Item", item_code, "is_stock_item"): - item_wh_list.append([item_code, warehouse]) + if ( + item_code + and warehouse + and [item_code, warehouse] not in item_wh_list + and frappe.get_cached_value("Item", item_code, "is_stock_item") + ): + item_wh_list.append([item_code, warehouse]) for d in self.get("items"): if (not so_item_rows or d.name in so_item_rows) and not d.delivered_by_supplier: @@ -313,9 +399,7 @@ class SalesOrder(SellingController): _valid_for_reserve(d.item_code, d.warehouse) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, { - "reserved_qty": get_reserved_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"reserved_qty": get_reserved_qty(item_code, warehouse)}) def on_update(self): pass @@ -332,13 +416,18 @@ class SalesOrder(SellingController): for item in self.items: if item.supplier: - supplier = frappe.db.get_value("Sales Order Item", {"parent": self.name, "item_code": item.item_code}, - "supplier") + supplier = frappe.db.get_value( + "Sales Order Item", {"parent": self.name, "item_code": item.item_code}, "supplier" + ) if item.ordered_qty > 0.0 and item.supplier != supplier: - exc_list.append(_("Row #{0}: Not allowed to change Supplier as Purchase Order already exists").format(item.idx)) + exc_list.append( + _("Row #{0}: Not allowed to change Supplier as Purchase Order already exists").format( + item.idx + ) + ) if exc_list: - frappe.throw('\n'.join(exc_list)) + frappe.throw("\n".join(exc_list)) def update_delivery_status(self): """Update delivery status from Purchase Order for drop shipping""" @@ -346,13 +435,16 @@ class SalesOrder(SellingController): for item in self.items: if item.delivered_by_supplier: - item_delivered_qty = frappe.db.sql("""select sum(qty) + item_delivered_qty = frappe.db.sql( + """select sum(qty) from `tabPurchase Order Item` poi, `tabPurchase Order` po where poi.sales_order_item = %s and poi.item_code = %s and poi.parent = po.name and po.docstatus = 1 - and po.status = 'Delivered'""", (item.name, item.item_code)) + and po.status = 'Delivered'""", + (item.name, item.item_code), + ) item_delivered_qty = item_delivered_qty[0][0] if item_delivered_qty else 0 item.db_set("delivered_qty", flt(item_delivered_qty), update_modified=False) @@ -361,9 +453,17 @@ class SalesOrder(SellingController): tot_qty += item.qty if tot_qty != 0: - self.db_set("per_delivered", flt(delivered_qty/tot_qty) * 100, - update_modified=False) + self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False) + def update_picking_status(self): + total_picked_qty = 0.0 + total_qty = 0.0 + for so_item in self.items: + total_picked_qty += flt(so_item.picked_qty) + total_qty += flt(so_item.stock_qty) + per_picked = total_picked_qty / total_qty * 100 + + self.db_set("per_picked", flt(per_picked), update_modified=False) def set_indicator(self): """Set indicator for portal""" @@ -381,49 +481,62 @@ class SalesOrder(SellingController): @frappe.whitelist() def get_work_order_items(self, for_raw_material_request=0): - '''Returns items with BOM that already do not have a linked work order''' + """Returns items with BOM that already do not have a linked work order""" items = [] item_codes = [i.item_code for i in self.items] - product_bundle_parents = [pb.new_item_code for pb in frappe.get_all("Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"])] + product_bundle_parents = [ + pb.new_item_code + for pb in frappe.get_all( + "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] + ) + ] for table in [self.items, self.packed_items]: for i in table: bom = get_default_bom_item(i.item_code) - stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty + stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty if not for_raw_material_request: - total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order` - where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2''', (i.item_code, self.name, i.name))[0][0]) + total_work_order_qty = flt( + frappe.db.sql( + """select sum(qty) from `tabWork Order` + where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""", + (i.item_code, self.name, i.name), + )[0][0] + ) pending_qty = stock_qty - total_work_order_qty else: pending_qty = stock_qty if pending_qty and i.item_code not in product_bundle_parents: if bom: - items.append(dict( - name= i.name, - item_code= i.item_code, - description= i.description, - bom = bom, - warehouse = i.warehouse, - pending_qty = pending_qty, - required_qty = pending_qty if for_raw_material_request else 0, - sales_order_item = i.name - )) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom=bom, + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, + ) + ) else: - items.append(dict( - name= i.name, - item_code= i.item_code, - description= i.description, - bom = '', - warehouse = i.warehouse, - pending_qty = pending_qty, - required_qty = pending_qty if for_raw_material_request else 0, - sales_order_item = i.name - )) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom="", + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, + ) + ) return items def on_recurring(self, reference_doc, auto_repeat_doc): - def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date) @@ -433,15 +546,26 @@ class SalesOrder(SellingController): return delivery_date - self.set("delivery_date", _get_delivery_date(reference_doc.delivery_date, - reference_doc.transaction_date, self.transaction_date )) + self.set( + "delivery_date", + _get_delivery_date( + reference_doc.delivery_date, reference_doc.transaction_date, self.transaction_date + ), + ) for d in self.get("items"): - reference_delivery_date = frappe.db.get_value("Sales Order Item", - {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, "delivery_date") + reference_delivery_date = frappe.db.get_value( + "Sales Order Item", + {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, + "delivery_date", + ) - d.set("delivery_date", _get_delivery_date(reference_delivery_date, - reference_doc.transaction_date, self.transaction_date)) + d.set( + "delivery_date", + _get_delivery_date( + reference_delivery_date, reference_doc.transaction_date, self.transaction_date + ), + ) def validate_serial_no_based_delivery(self): reserved_items = [] @@ -449,32 +573,52 @@ class SalesOrder(SellingController): for item in self.items: if item.ensure_delivery_based_on_produced_serial_no: if item.item_code in normal_items: - frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) + frappe.throw( + _( + "Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No." + ).format(item.item_code) + ) if item.item_code not in reserved_items: if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): - frappe.throw(_("Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No").format(item.item_code)) + frappe.throw( + _( + "Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No" + ).format(item.item_code) + ) if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}): - frappe.throw(_("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format(item.item_code)) + frappe.throw( + _("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format( + item.item_code + ) + ) reserved_items.append(item.item_code) else: normal_items.append(item.item_code) - if not item.ensure_delivery_based_on_produced_serial_no and \ - item.item_code in reserved_items: - frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) + if not item.ensure_delivery_based_on_produced_serial_no and item.item_code in reserved_items: + frappe.throw( + _( + "Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No." + ).format(item.item_code) + ) + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Orders'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Orders"), + } + ) return list_context + @frappe.whitelist() def close_or_unclose_sales_orders(names, status): if not frappe.has_permission("Sales Order", "write"): @@ -485,23 +629,32 @@ def close_or_unclose_sales_orders(names, status): so = frappe.get_doc("Sales Order", name) if so.docstatus == 1: if status == "Closed": - if so.status not in ("Cancelled", "Closed") and (so.per_delivered < 100 or so.per_billed < 100): + if so.status not in ("Cancelled", "Closed") and ( + so.per_delivered < 100 or so.per_billed < 100 + ): so.update_status(status) else: if so.status == "Closed": - so.update_status('Draft') + so.update_status("Draft") so.update_blanket_order() frappe.local.message_log = [] + def get_requested_item_qty(sales_order): - return frappe._dict(frappe.db.sql(""" + return frappe._dict( + frappe.db.sql( + """ select sales_order_item, sum(qty) from `tabMaterial Request Item` where docstatus = 1 and sales_order = %s group by sales_order_item - """, sales_order)) + """, + sales_order, + ) + ) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): @@ -514,71 +667,71 @@ def make_material_request(source_name, target_doc=None): target.qty = qty - requested_item_qty.get(source.name, 0) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Material Request", - "validation": { - "docstatus": ["=", 1] - } - }, - "Packed Item": { - "doctype": "Material Request Item", - "field_map": { - "parent": "sales_order", - "uom": "stock_uom" + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Material Request", "validation": {"docstatus": ["=", 1]}}, + "Packed Item": { + "doctype": "Material Request Item", + "field_map": {"parent": "sales_order", "uom": "stock_uom"}, + "postprocess": update_item, }, - "postprocess": update_item - }, - "Sales Order Item": { - "doctype": "Material Request Item", - "field_map": { - "name": "sales_order_item", - "parent": "sales_order" + "Sales Order Item": { + "doctype": "Material Request Item", + "field_map": {"name": "sales_order_item", "parent": "sales_order"}, + "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) + and doc.stock_qty > requested_item_qty.get(doc.name, 0), + "postprocess": update_item, }, - "condition": lambda doc: not frappe.db.exists('Product Bundle', doc.item_code) and doc.stock_qty > requested_item_qty.get(doc.name, 0), - "postprocess": update_item - } - }, target_doc) + }, + target_doc, + ) return doc + @frappe.whitelist() def make_project(source_name, target_doc=None): def postprocess(source, doc): doc.project_type = "External" doc.project_name = source.name - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Project", - "validation": { - "docstatus": ["=", 1] + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Project", + "validation": {"docstatus": ["=", 1]}, + "field_map": { + "name": "sales_order", + "base_grand_total": "estimated_costing", + }, }, - "field_map":{ - "name" : "sales_order", - "base_grand_total" : "estimated_costing", - } }, - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doc + @frappe.whitelist() def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Delivery Note", 'company_address', target.company_address)) + target.update(get_fetch_values("Delivery Note", "company_address", target.company_address)) def update_item(source, target, source_parent): target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate) @@ -589,34 +742,26 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): item_group = get_item_group_defaults(target.item_code, source_parent.company) if item: - target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center") \ - or item.get("buying_cost_center") \ + target.cost_center = ( + frappe.db.get_value("Project", source_parent.project, "cost_center") + or item.get("buying_cost_center") or item_group.get("buying_cost_center") + ) mapper = { - "Sales Order": { - "doctype": "Delivery Note", - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } + "Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, } if not skip_item_mapping: + def condition(doc): # make_mapped_doc sets js `args` into `frappe.flags.args` if frappe.flags.args and frappe.flags.args.delivery_dates: if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates: return False - return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1 mapper["Sales Order Item"] = { "doctype": "Delivery Note Item", @@ -626,36 +771,38 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): "parent": "against_sales_order", }, "postprocess": update_item, - "condition": condition + "condition": condition, } target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values) + target_doc.set_onload("ignore_price_list", True) + return target_doc + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): def postprocess(source, target): set_missing_values(source, target) - #Get the advance paid Journal Entries in Sales Invoice Advance + # Get the advance paid Journal Entries in Sales Invoice Advance if target.get("allocate_advances_automatically"): target.set_advances() def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.flags.ignore_permissions = True target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) + target.update(get_fetch_values("Sales Invoice", "company_address", target.company_address)) # set the redeem loyalty points if provided via shopping cart if source.loyalty_points and source.order_type == "Shopping Cart": @@ -664,106 +811,117 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): def update_item(source, target, source_parent): target.amount = flt(source.amount) - flt(source.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) - target.qty = target.amount / flt(source.rate) if (source.rate and source.billed_amt) else source.qty - source.returned_qty + target.qty = ( + target.amount / flt(source.rate) + if (source.rate and source.billed_amt) + else source.qty - source.returned_qty + ) if source_parent.project: target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center") if target.item_code: item = get_item_defaults(target.item_code, source_parent.company) item_group = get_item_group_defaults(target.item_code, source_parent.company) - cost_center = item.get("selling_cost_center") \ - or item_group.get("selling_cost_center") + cost_center = item.get("selling_cost_center") or item_group.get("selling_cost_center") if cost_center: target.cost_center = cost_center - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Sales Invoice", - "field_map": { - "party_account_currency": "party_account_currency", - "payment_terms_template": "payment_terms_template" + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Sales Invoice", + "field_map": { + "party_account_currency": "party_account_currency", + "payment_terms_template": "payment_terms_template", + }, + "field_no_map": ["payment_terms_template"], + "validation": {"docstatus": ["=", 1]}, }, - "field_no_map": ["payment_terms_template"], - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Order Item": { - "doctype": "Sales Invoice Item", - "field_map": { - "name": "so_detail", - "parent": "sales_order", + "Sales Order Item": { + "doctype": "Sales Invoice Item", + "field_map": { + "name": "so_detail", + "parent": "sales_order", + }, + "postprocess": update_item, + "condition": lambda doc: doc.qty + and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), }, - "postprocess": update_item, - "condition": lambda doc: doc.qty and (doc.base_amount==0 or abs(doc.billed_amt) < abs(doc.amount)) + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } - }, target_doc, postprocess, ignore_permissions=ignore_permissions) + target_doc, + postprocess, + ignore_permissions=ignore_permissions, + ) - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + automatically_fetch_payment_terms = cint( + frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) if automatically_fetch_payment_terms: doclist.set_payment_schedule() + doclist.set_onload("ignore_price_list", True) + return doclist + @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - maint_schedule = frappe.db.sql("""select t1.name + maint_schedule = frappe.db.sql( + """select t1.name from `tabMaintenance Schedule` t1, `tabMaintenance Schedule Item` t2 - where t2.parent=t1.name and t2.sales_order=%s and t1.docstatus=1""", source_name) + where t2.parent=t1.name and t2.sales_order=%s and t1.docstatus=1""", + source_name, + ) if not maint_schedule: - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Maintenance Schedule", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Maintenance Schedule Item", + "field_map": {"parent": "sales_order"}, + }, }, - "Sales Order Item": { - "doctype": "Maintenance Schedule Item", - "field_map": { - "parent": "sales_order" - } - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None): - visit = frappe.db.sql("""select t1.name + visit = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent=t1.name and t2.prevdoc_docname=%s - and t1.docstatus=1 and t1.completion_status='Fully Completed'""", source_name) + and t1.docstatus=1 and t1.completion_status='Fully Completed'""", + source_name, + ) if not visit: - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Maintenance Visit", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Maintenance Visit", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Maintenance Visit Purpose", + "field_map": {"parent": "prevdoc_docname", "parenttype": "prevdoc_doctype"}, + }, }, - "Sales Order Item": { - "doctype": "Maintenance Visit Purpose", - "field_map": { - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype" - } - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def get_events(start, end, filters=None): """Returns events for Gantt / Calendar view rendering. @@ -773,9 +931,11 @@ def get_events(start, end, filters=None): :param filters: Filters (JSON). """ from frappe.desk.calendar import get_event_conditions + conditions = get_event_conditions("Sales Order", filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ select distinct `tabSales Order`.name, `tabSales Order`.customer_name, `tabSales Order`.status, `tabSales Order`.delivery_status, `tabSales Order`.billing_status, @@ -788,16 +948,21 @@ def get_events(start, end, filters=None): and (`tabSales Order Item`.delivery_date between %(start)s and %(end)s) and `tabSales Order`.docstatus < 2 {conditions} - """.format(conditions=conditions), { - "start": start, - "end": end - }, as_dict=True, update={"allDay": 0}) + """.format( + conditions=conditions + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) return data + @frappe.whitelist() def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): """Creates Purchase Order for each Supplier. Returns a list of doc objects.""" - if not selected_items: return + if not selected_items: + return if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) @@ -813,7 +978,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t if default_price_list: target.buying_price_list = default_price_list - if any( item.delivered_by_supplier==1 for item in source.items): + if any(item.delivered_by_supplier == 1 for item in source.items): if source.shipping_address_name: target.shipping_address = source.shipping_address_name target.shipping_address_display = source.shipping_address @@ -836,59 +1001,67 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t def update_item(source, target, source_parent): target.schedule_date = source.delivery_date target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) - target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) + target.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty) target.project = source_parent.project - suppliers = [item.get('supplier') for item in selected_items if item.get('supplier')] - suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order + suppliers = [item.get("supplier") for item in selected_items if item.get("supplier")] + suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order - items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code')] + items_to_map = [item.get("item_code") for item in selected_items if item.get("item_code")] items_to_map = list(set(items_to_map)) if not suppliers: - frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) + frappe.throw( + _("Please set a Supplier against the Items to be considered in the Purchase Order.") + ) purchase_orders = [] for supplier in suppliers: - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address", - "terms" - ], - "validation": { - "docstatus": ["=", 1] - } + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Purchase Order", + "field_no_map": [ + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "taxes_and_charges", + "shipping_address", + "terms", + ], + "validation": {"docstatus": ["=", 1]}, + }, + "Sales Order Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_item"], + ["parent", "sales_order"], + ["stock_uom", "stock_uom"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["delivery_date", "schedule_date"], + ], + "field_no_map": [ + "rate", + "price_list_rate", + "item_tax_template", + "discount_percentage", + "discount_amount", + "pricing_rules", + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty + and doc.supplier == supplier + and doc.item_code in items_to_map, + }, }, - "Sales Order Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "sales_order_item"], - ["parent", "sales_order"], - ["stock_uom", "stock_uom"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["delivery_date", "schedule_date"] - ], - "field_no_map": [ - "rate", - "price_list_rate", - "item_tax_template", - "discount_percentage", - "discount_amount", - "pricing_rules" - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) doc.insert() frappe.db.commit() @@ -896,14 +1069,20 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t return purchase_orders + @frappe.whitelist() def make_purchase_order(source_name, selected_items=None, target_doc=None): - if not selected_items: return + if not selected_items: + return if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) - items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')] + items_to_map = [ + item.get("item_code") + for item in selected_items + if item.get("item_code") and item.get("item_code") + ] items_to_map = list(set(items_to_map)) def set_missing_values(source, target): @@ -920,81 +1099,89 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): def update_item(source, target, source_parent): target.schedule_date = source.delivery_date target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) - target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) + target.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty) target.project = source_parent.project + def update_item_for_packed_item(source, target, source_parent): + target.qty = flt(source.qty) - flt(source.ordered_qty) + # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address", - "terms" - ], - "validation": { - "docstatus": ["=", 1] - } + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Purchase Order", + "field_no_map": [ + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "taxes_and_charges", + "shipping_address", + "terms", + ], + "validation": {"docstatus": ["=", 1]}, + }, + "Sales Order Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_item"], + ["parent", "sales_order"], + ["stock_uom", "stock_uom"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["delivery_date", "schedule_date"], + ], + "field_no_map": [ + "rate", + "price_list_rate", + "item_tax_template", + "discount_percentage", + "discount_amount", + "supplier", + "pricing_rules", + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty + and doc.item_code in items_to_map + and not is_product_bundle(doc.item_code), + }, + "Packed Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_packed_item"], + ["parent", "sales_order"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["parent_item", "product_bundle"], + ["rate", "rate"], + ], + "field_no_map": [ + "price_list_rate", + "item_tax_template", + "discount_percentage", + "discount_amount", + "supplier", + "pricing_rules", + ], + "postprocess": update_item_for_packed_item, + "condition": lambda doc: doc.parent_item in items_to_map, + }, }, - "Sales Order Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "sales_order_item"], - ["parent", "sales_order"], - ["stock_uom", "stock_uom"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["delivery_date", "schedule_date"] - ], - "field_no_map": [ - "rate", - "price_list_rate", - "item_tax_template", - "discount_percentage", - "discount_amount", - "supplier", - "pricing_rules" - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map and not is_product_bundle(doc.item_code) - }, - "Packed Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["parent", "sales_order"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["parent_item", "product_bundle"], - ["rate", "rate"] - ], - "field_no_map": [ - "price_list_rate", - "item_tax_template", - "discount_percentage", - "discount_amount", - "supplier", - "pricing_rules" - ], - "condition": lambda doc: doc.parent_item in items_to_map - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) set_delivery_date(doc.items, source_name) return doc + def set_delivery_date(items, sales_order): delivery_dates = frappe.get_all( - 'Sales Order Item', - filters = { - 'parent': sales_order - }, - fields = ['delivery_date', 'item_code'] + "Sales Order Item", filters={"parent": sales_order}, fields=["delivery_date", "item_code"] ) delivery_by_item = frappe._dict() @@ -1005,13 +1192,15 @@ def set_delivery_date(items, sales_order): if item.product_bundle: item.schedule_date = delivery_by_item[item.product_bundle] + def is_product_bundle(item_code): - return frappe.db.exists('Product Bundle', item_code) + return frappe.db.exists("Product Bundle", item_code) + @frappe.whitelist() def make_work_orders(items, sales_order, company, project=None): - '''Make Work Orders against the given Sales Order for the given `items`''' - items = json.loads(items).get('items') + """Make Work Orders against the given Sales Order for the given `items`""" + items = json.loads(items).get("items") out = [] for i in items: @@ -1020,18 +1209,20 @@ def make_work_orders(items, sales_order, company, project=None): if not i.get("pending_qty"): frappe.throw(_("Please select Qty against item {0}").format(i.get("item_code"))) - work_order = frappe.get_doc(dict( - doctype='Work Order', - production_item=i['item_code'], - bom_no=i.get('bom'), - qty=i['pending_qty'], - company=company, - sales_order=sales_order, - sales_order_item=i['sales_order_item'], - project=project, - fg_warehouse=i['warehouse'], - description=i['description'] - )).insert() + work_order = frappe.get_doc( + dict( + doctype="Work Order", + production_item=i["item_code"], + bom_no=i.get("bom"), + qty=i["pending_qty"], + company=company, + sales_order=sales_order, + sales_order_item=i["sales_order_item"], + project=project, + fg_warehouse=i["warehouse"], + description=i["description"], + ) + ).insert() work_order.set_work_order_operations() work_order.flags.ignore_mandatory = True work_order.save() @@ -1039,18 +1230,20 @@ def make_work_orders(items, sales_order, company, project=None): return [p.name for p in out] + @frappe.whitelist() def update_status(status, name): so = frappe.get_doc("Sales Order", name) so.update_status(status) + def get_default_bom_item(item_code): - bom = frappe.get_all('BOM', dict(item=item_code, is_active=True), - order_by='is_default desc') + bom = frappe.get_all("BOM", dict(item=item_code, is_active=True), order_by="is_default desc") bom = bom[0].name if bom else None return bom + @frappe.whitelist() def make_raw_material_request(items, company, sales_order, project=None): if not frappe.has_permission("Sales Order", "write"): @@ -1059,43 +1252,49 @@ def make_raw_material_request(items, company, sales_order, project=None): if isinstance(items, string_types): items = frappe._dict(json.loads(items)) - for item in items.get('items'): - item["include_exploded_items"] = items.get('include_exploded_items') - item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty') - item["include_raw_materials_from_sales_order"] = items.get('include_raw_materials_from_sales_order') + for item in items.get("items"): + item["include_exploded_items"] = items.get("include_exploded_items") + item["ignore_existing_ordered_qty"] = items.get("ignore_existing_ordered_qty") + item["include_raw_materials_from_sales_order"] = items.get( + "include_raw_materials_from_sales_order" + ) - items.update({ - 'company': company, - 'sales_order': sales_order - }) + items.update({"company": company, "sales_order": sales_order}) raw_materials = get_items_for_material_requests(items) if not raw_materials: - frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) + frappe.msgprint( + _("Material Request not created, as quantity for Raw Materials already available.") + ) return - material_request = frappe.new_doc('Material Request') - material_request.update(dict( - doctype = 'Material Request', - transaction_date = nowdate(), - company = company, - material_request_type = 'Purchase' - )) + material_request = frappe.new_doc("Material Request") + material_request.update( + dict( + doctype="Material Request", + transaction_date=nowdate(), + company=company, + material_request_type="Purchase", + ) + ) for item in raw_materials: - item_doc = frappe.get_cached_doc('Item', item.get('item_code')) + item_doc = frappe.get_cached_doc("Item", item.get("item_code")) schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) - row = material_request.append('items', { - 'item_code': item.get('item_code'), - 'qty': item.get('quantity'), - 'schedule_date': schedule_date, - 'warehouse': item.get('warehouse'), - 'sales_order': sales_order, - 'project': project - }) + row = material_request.append( + "items", + { + "item_code": item.get("item_code"), + "qty": item.get("quantity"), + "schedule_date": schedule_date, + "warehouse": item.get("warehouse"), + "sales_order": sales_order, + "project": project, + }, + ) if not (strip_html(item.get("description")) and strip_html(item_doc.description)): - row.description = item_doc.item_name or item.get('item_code') + row.description = item_doc.item_name or item.get("item_code") material_request.insert() material_request.flags.ignore_permissions = 1 @@ -1103,53 +1302,86 @@ def make_raw_material_request(items, company, sales_order, project=None): material_request.submit() return material_request + @frappe.whitelist() def make_inter_company_purchase_order(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction + return make_inter_company_transaction("Sales Order", source_name, target_doc) + @frappe.whitelist() def create_pick_list(source_name, target_doc=None): - def update_item_quantity(source, target, source_parent): - target.qty = flt(source.qty) - flt(source.delivered_qty) - target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor) + from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle - doc = get_mapped_doc('Sales Order', source_name, { - 'Sales Order': { - 'doctype': 'Pick List', - 'validation': { - 'docstatus': ['=', 1] - } - }, - 'Sales Order Item': { - 'doctype': 'Pick List Item', - 'field_map': { - 'parent': 'sales_order', - 'name': 'sales_order_item' + def update_item_quantity(source, target, source_parent) -> None: + picked_qty = flt(source.picked_qty) / (flt(source.conversion_factor) or 1) + qty_to_be_picked = flt(source.qty) - max(picked_qty, flt(source.delivered_qty)) + + target.qty = qty_to_be_picked + target.stock_qty = qty_to_be_picked * flt(source.conversion_factor) + + def update_packed_item_qty(source, target, source_parent) -> None: + qty = flt(source.qty) + for item in source_parent.items: + if source.parent_detail_docname == item.name: + picked_qty = flt(item.picked_qty) / (flt(item.conversion_factor) or 1) + pending_percent = (item.qty - max(picked_qty, item.delivered_qty)) / item.qty + target.qty = target.stock_qty = qty * pending_percent + return + + def should_pick_order_item(item) -> bool: + return ( + abs(item.delivered_qty) < abs(item.qty) + and item.delivered_by_supplier != 1 + and not is_product_bundle(item.item_code) + ) + + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Pick List Item", + "field_map": {"parent": "sales_order", "name": "sales_order_item"}, + "postprocess": update_item_quantity, + "condition": should_pick_order_item, + }, + "Packed Item": { + "doctype": "Pick List Item", + "field_map": { + "parent": "sales_order", + "name": "sales_order_item", + "parent_detail_docname": "product_bundle_item", + }, + "field_no_map": ["picked_qty"], + "postprocess": update_packed_item_qty, }, - 'postprocess': update_item_quantity, - 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 }, - }, target_doc) + target_doc, + ) - doc.purpose = 'Delivery' + doc.purpose = "Delivery" doc.set_item_locations() return doc + def update_produced_qty_in_so_item(sales_order, sales_order_item): - #for multiple work orders against same sales order item - linked_wo_with_so_item = frappe.db.get_all('Work Order', ['produced_qty'], { - 'sales_order_item': sales_order_item, - 'sales_order': sales_order, - 'docstatus': 1 - }) + # for multiple work orders against same sales order item + linked_wo_with_so_item = frappe.db.get_all( + "Work Order", + ["produced_qty"], + {"sales_order_item": sales_order_item, "sales_order": sales_order, "docstatus": 1}, + ) total_produced_qty = 0 for wo in linked_wo_with_so_item: - total_produced_qty += flt(wo.get('produced_qty')) + total_produced_qty += flt(wo.get("produced_qty")) - if not total_produced_qty and frappe.flags.in_patch: return + if not total_produced_qty and frappe.flags.in_patch: + return - frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) + frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty) diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index abf507a9e54..ace2e29c2b4 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -1,45 +1,27 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'sales_order', - 'non_standard_fieldnames': { - 'Delivery Note': 'against_sales_order', - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', - 'Payment Request': 'reference_name', - 'Auto Repeat': 'reference_document', - 'Maintenance Visit': 'prevdoc_docname' + "fieldname": "sales_order", + "non_standard_fieldnames": { + "Delivery Note": "against_sales_order", + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", + "Payment Request": "reference_name", + "Auto Repeat": "reference_document", + "Maintenance Visit": "prevdoc_docname", }, - 'internal_links': { - 'Quotation': ['items', 'prevdoc_docname'] - }, - 'transactions': [ + "internal_links": {"Quotation": ["items", "prevdoc_docname"]}, + "transactions": [ { - 'label': _('Fulfillment'), - 'items': ['Sales Invoice', 'Pick List', 'Delivery Note', 'Maintenance Visit'] + "label": _("Fulfillment"), + "items": ["Sales Invoice", "Pick List", "Delivery Note", "Maintenance Visit"], }, - { - 'label': _('Purchasing'), - 'items': ['Material Request', 'Purchase Order'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - }, - { - 'label': _('Manufacturing'), - 'items': ['Work Order'] - }, - { - 'label': _('Reference'), - 'items': ['Quotation', 'Auto Repeat'] - }, - { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Payment Request', 'Journal Entry'] - }, - ] + {"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]}, + {"label": _("Projects"), "items": ["Project"]}, + {"label": _("Manufacturing"), "items": ["Work Order"]}, + {"label": _("Reference"), "items": ["Quotation", "Auto Repeat"]}, + {"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]}, + ], } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 788a8350caa..8edc9394c10 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,6 +6,7 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.controllers.accounts_controller import update_child_qty_rate @@ -21,22 +22,27 @@ from erpnext.selling.doctype.sales_order.sales_order import ( ) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase -class TestSalesOrder(ERPNextTestCase): - +class TestSalesOrder(FrappeTestCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order")) + cls.unlink_setting = int( + frappe.db.get_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order" + ) + ) @classmethod def tearDownClass(cls) -> None: # reset config to previous state - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting) + frappe.db.set_value( + "Accounts Settings", + "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order", + cls.unlink_setting, + ) super().tearDownClass() def tearDown(self): @@ -83,6 +89,7 @@ class TestSalesOrder(ERPNextTestCase): def test_so_billed_amount_against_return_entry(self): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + so = make_sales_order(do_not_submit=True) so.submit() @@ -111,7 +118,7 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(len(si.get("items")), 1) si.insert() - si.set('taxes', []) + si.set("taxes", []) si.save() self.assertEqual(si.payment_schedule[0].payment_amount, 500.0) @@ -179,16 +186,16 @@ class TestSalesOrder(ERPNextTestCase): dn1.items[0].so_detail = so.items[0].name dn1.submit() - si1 = create_sales_invoice(is_return=1, return_against=si2.name, qty=-1, update_stock=1, do_not_submit=True) + si1 = create_sales_invoice( + is_return=1, return_against=si2.name, qty=-1, update_stock=1, do_not_submit=True + ) si1.items[0].sales_order = so.name si1.items[0].so_detail = so.items[0].name si1.submit() - so.load_from_db() self.assertEqual(so.get("items")[0].delivered_qty, 5) - def test_reserved_qty_for_partial_delivery(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) existing_reserved_qty = get_reserved_qty() @@ -206,7 +213,7 @@ class TestSalesOrder(ERPNextTestCase): # unclose so so.load_from_db() - so.update_status('Draft') + so.update_status("Draft") self.assertEqual(get_reserved_qty(), existing_reserved_qty + 5) dn.cancel() @@ -220,7 +227,7 @@ class TestSalesOrder(ERPNextTestCase): def test_reserved_qty_for_over_delivery(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Item", 'over_delivery_receipt_allowance', 50) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50) existing_reserved_qty = get_reserved_qty() @@ -237,8 +244,8 @@ class TestSalesOrder(ERPNextTestCase): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Item", 'over_delivery_receipt_allowance', 50) - frappe.db.set_value('Item', "_Test Item", 'over_billing_allowance', 20) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50) + frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 20) existing_reserved_qty = get_reserved_qty() @@ -266,7 +273,9 @@ class TestSalesOrder(ERPNextTestCase): def test_reserved_qty_for_partial_delivery_with_packing_list(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100) + make_stock_entry( + item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100 + ) existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") @@ -274,14 +283,16 @@ class TestSalesOrder(ERPNextTestCase): so = make_sales_order(item_code="_Test Product Bundle Item") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) dn = create_dn_against_so(so.name) self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 10) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10 + ) # close so so.load_from_db() @@ -292,16 +303,18 @@ class TestSalesOrder(ERPNextTestCase): # unclose so so.load_from_db() - so.update_status('Draft') + so.update_status("Draft") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 10) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10 + ) dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) so.load_from_db() so.cancel() @@ -310,17 +323,19 @@ class TestSalesOrder(ERPNextTestCase): def test_sales_order_on_hold(self): so = make_sales_order(item_code="_Test Product Bundle Item") - so.db_set('Status', "On Hold") + so.db_set("Status", "On Hold") si = make_sales_invoice(so.name) self.assertRaises(frappe.ValidationError, create_dn_against_so, so.name) self.assertRaises(frappe.ValidationError, si.submit) def test_reserved_qty_for_over_delivery_with_packing_list(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100) + make_stock_entry( + item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100 + ) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Product Bundle Item", 'over_delivery_receipt_allowance', 50) + frappe.db.set_value("Item", "_Test Product Bundle Item", "over_delivery_receipt_allowance", 50) existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") @@ -328,22 +343,23 @@ class TestSalesOrder(ERPNextTestCase): so = make_sales_order(item_code="_Test Product Bundle Item") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) dn = create_dn_against_so(so.name, 15) self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2) dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) def test_update_child_adding_new_item(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) create_dn_against_so(so.name, 4) make_sales_invoice(so.name) @@ -354,38 +370,38 @@ class TestSalesOrder(ERPNextTestCase): reserved_qty_for_second_item = get_reserved_qty("_Test Item 2") first_item_of_so = so.get("items")[0] - trans_item = json.dumps([ - {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \ - 'qty' : first_item_of_so.qty, 'docname': first_item_of_so.name}, - {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7} - ]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [ + { + "item_code": first_item_of_so.item_code, + "rate": first_item_of_so.rate, + "qty": first_item_of_so.qty, + "docname": first_item_of_so.name, + }, + {"item_code": "_Test Item 2", "rate": 200, "qty": 7}, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() - self.assertEqual(so.get("items")[-1].item_code, '_Test Item 2') + self.assertEqual(so.get("items")[-1].item_code, "_Test Item 2") self.assertEqual(so.get("items")[-1].rate, 200) self.assertEqual(so.get("items")[-1].qty, 7) self.assertEqual(so.get("items")[-1].amount, 1400) # reserved qty should increase after adding row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 7) + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item + 7) - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(so.status, "To Deliver and Bill") updated_total = so.get("base_total") updated_total_in_words = so.get("base_in_words") - self.assertEqual(updated_total, prev_total+1400) + self.assertEqual(updated_total, prev_total + 1400) self.assertNotEqual(updated_total_in_words, prev_total_in_words) def test_update_child_removing_item(self): - so = make_sales_order(**{ - "item_list": [{ - "item_code": '_Test Item', - "qty": 5, - "rate":1000 - }] - }) + so = make_sales_order(**{"item_list": [{"item_code": "_Test Item", "qty": 5, "rate": 1000}]}) create_dn_against_so(so.name, 2) make_sales_invoice(so.name) @@ -393,64 +409,67 @@ class TestSalesOrder(ERPNextTestCase): reserved_qty_for_second_item = get_reserved_qty("_Test Item 2") # add an item so as to try removing items - trans_item = json.dumps([ - {"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name}, - {"item_code": '_Test Item 2', "qty": 2, "rate":500} - ]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [ + {"item_code": "_Test Item", "qty": 5, "rate": 1000, "docname": so.get("items")[0].name}, + {"item_code": "_Test Item 2", "qty": 2, "rate": 500}, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(len(so.get("items")), 2) # reserved qty should increase after adding row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 2) + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item + 2) # check if delivered items can be removed - trans_item = json.dumps([{ - "item_code": '_Test Item 2', - "qty": 2, - "rate":500, - "docname": so.get("items")[1].name - }]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item 2", "qty": 2, "rate": 500, "docname": so.get("items")[1].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) - #remove last added item - trans_item = json.dumps([{ - "item_code": '_Test Item', - "qty": 5, - "rate":1000, - "docname": so.get("items")[0].name - }]) - update_child_qty_rate('Sales Order', trans_item, so.name) + # remove last added item + trans_item = json.dumps( + [{"item_code": "_Test Item", "qty": 5, "rate": 1000, "docname": so.get("items")[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(len(so.get("items")), 1) # reserved qty should decrease (back to initial) after deleting row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item) - - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item) + self.assertEqual(so.status, "To Deliver and Bill") def test_update_child(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) create_dn_against_so(so.name, 4) make_sales_invoice(so.name) existing_reserved_qty = get_reserved_qty() - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": so.items[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(so.get("items")[0].rate, 200) self.assertEqual(so.get("items")[0].qty, 7) self.assertEqual(so.get("items")[0].amount, 1400) - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(so.status, "To Deliver and Bill") self.assertEqual(get_reserved_qty(), existing_reserved_qty + 3) - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 2, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) def test_update_child_with_precision(self): from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -459,48 +478,60 @@ class TestSalesOrder(ERPNextTestCase): precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate")) make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency") - so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664) + so = make_sales_order(item_code="_Test Item", qty=4, rate=200.34664) - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200.34669, "qty": 4, "docname": so.items[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(so.items[0].rate, 200.34669) make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency") def test_update_child_perm(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) test_user = create_user("test_so_child_perms@example.com", "Accounts User") frappe.set_user(test_user.name) # update qty - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) # add new item - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps([{"item_code": "_Test Item", "rate": 100, "qty": 2}]) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow workflow = make_sales_order_workflow() - so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) - apply_workflow(so, 'Approve') + so = make_sales_order(item_code="_Test Item", qty=1, rate=150, do_not_submit=1) + apply_workflow(so, "Approve") - user = 'test@example.com' - test_user = frappe.get_doc('User', user) + user = "test@example.com" + test_user = frappe.get_doc("User", user) test_user.add_roles("Sales User", "Test Junior Approver") frappe.set_user(user) # user shouldn't be able to edit since grand_total will become > 200 if qty is doubled - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 150, "qty": 2, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) frappe.set_user("Administrator") - user2 = 'test2@example.com' - test_user2 = frappe.get_doc('User', user2) + user2 = "test2@example.com" + test_user2 = frappe.get_doc("User", user2) test_user2.add_roles("Sales User", "Test Approver") frappe.set_user(user2) @@ -519,21 +550,21 @@ class TestSalesOrder(ERPNextTestCase): # test Update Items with product bundle if not frappe.db.exists("Item", "_Product Bundle Item"): bundle_item = make_item("_Product Bundle Item", {"is_stock_item": 0}) - bundle_item.append("item_defaults", { - "company": "_Test Company", - "default_warehouse": "_Test Warehouse - _TC"}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) bundle_item.save(ignore_permissions=True) make_item("_Packed Item", {"is_stock_item": 1}) make_product_bundle("_Product Bundle Item", ["_Packed Item"], 2) - so = make_sales_order(item_code = "_Test Item", warehouse=None) + so = make_sales_order(item_code="_Test Item", warehouse=None) # get reserved qty of packed item existing_reserved_qty = get_reserved_qty("_Packed Item") - added_item = json.dumps([{"item_code" : "_Product Bundle Item", "rate" : 200, 'qty' : 2}]) - update_child_qty_rate('Sales Order', added_item, so.name) + added_item = json.dumps([{"item_code": "_Product Bundle Item", "rate": 200, "qty": 2}]) + update_child_qty_rate("Sales Order", added_item, so.name) so.reload() self.assertEqual(so.packed_items[0].qty, 4) @@ -542,15 +573,19 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 4) # test uom and conversion factor change - update_uom_conv_factor = json.dumps([{ - 'item_code': so.get("items")[0].item_code, - 'rate': so.get("items")[0].rate, - 'qty': so.get("items")[0].qty, - 'uom': "_Test UOM 1", - 'conversion_factor': 2, - 'docname': so.get("items")[0].name - }]) - update_child_qty_rate('Sales Order', update_uom_conv_factor, so.name) + update_uom_conv_factor = json.dumps( + [ + { + "item_code": so.get("items")[0].item_code, + "rate": so.get("items")[0].rate, + "qty": so.get("items")[0].qty, + "uom": "_Test UOM 1", + "conversion_factor": 2, + "docname": so.get("items")[0].name, + } + ] + ) + update_child_qty_rate("Sales Order", update_uom_conv_factor, so.name) so.reload() self.assertEqual(so.packed_items[0].qty, 8) @@ -560,61 +595,67 @@ class TestSalesOrder(ERPNextTestCase): def test_update_child_with_tax_template(self): """ - Test Action: Create a SO with one item having its tax account head already in the SO. - Add the same item + new item with tax template via Update Items. - Expected result: First Item's tax row is updated. New tax row is added for second Item. + Test Action: Create a SO with one item having its tax account head already in the SO. + Add the same item + new item with tax template via Update Items. + Expected result: First Item's tax row is updated. New tax row is added for second Item. """ if not frappe.db.exists("Item", "Test Item with Tax"): - make_item("Test Item with Tax", { - 'is_stock_item': 1, - }) + make_item( + "Test Item with Tax", + { + "is_stock_item": 1, + }, + ) - if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): - frappe.get_doc({ - 'doctype': 'Item Tax Template', - 'title': 'Test Update Items Template', - 'company': '_Test Company', - 'taxes': [ - { - 'tax_type': "_Test Account Service Tax - _TC", - 'tax_rate': 10, - } - ] - }).insert() + if not frappe.db.exists("Item Tax Template", {"title": "Test Update Items Template"}): + frappe.get_doc( + { + "doctype": "Item Tax Template", + "title": "Test Update Items Template", + "company": "_Test Company", + "taxes": [ + { + "tax_type": "_Test Account Service Tax - _TC", + "tax_rate": 10, + } + ], + } + ).insert() new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") - new_item_with_tax.append("taxes", { - "item_tax_template": "Test Update Items Template - _TC", - "valid_from": nowdate() - }) + new_item_with_tax.append( + "taxes", {"item_tax_template": "Test Update Items Template - _TC", "valid_from": nowdate()} + ) new_item_with_tax.save() tax_template = "_Test Account Excise Duty @ 10 - _TC" - item = "_Test Item Home Desktop 100" - if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item = "_Test Item Home Desktop 100" + if not frappe.db.exists("Item Tax", {"parent": item, "item_tax_template": tax_template}): item_doc = frappe.get_doc("Item", item) - item_doc.append("taxes", { - "item_tax_template": tax_template, - "valid_from": nowdate() - }) + item_doc.append("taxes", {"item_tax_template": tax_template, "valid_from": nowdate()}) item_doc.save() else: # update valid from - frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = CURDATE() where parent = %(item)s and item_tax_template = %(tax)s""", - {"item": item, "tax": tax_template}) + {"item": item, "tax": tax_template}, + ) so = make_sales_order(item_code=item, qty=1, do_not_save=1) - so.append("taxes", { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 10 - }) + so.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 10, + }, + ) so.insert() so.submit() @@ -624,12 +665,22 @@ class TestSalesOrder(ERPNextTestCase): old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse") frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC") - items = json.dumps([ - {'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name}, - {'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO - {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO - ]) - update_child_qty_rate('Sales Order', items, so.name) + items = json.dumps( + [ + {"item_code": item, "rate": 100, "qty": 1, "docname": so.items[0].name}, + { + "item_code": item, + "rate": 200, + "qty": 1, + }, # added item whose tax account head already exists in PO + { + "item_code": new_item_with_tax.name, + "rate": 100, + "qty": 1, + }, # added item whose tax account head is missing in PO + ] + ) + update_child_qty_rate("Sales Order", items, so.name) so.reload() self.assertEqual(so.taxes[0].tax_amount, 40) @@ -639,8 +690,11 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(so.taxes[1].total, 480) # teardown - frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL - where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}, + ) so.cancel() so.delete() new_item_with_tax.delete() @@ -660,8 +714,12 @@ class TestSalesOrder(ERPNextTestCase): frappe.set_user(test_user.name) - so = make_sales_order(company="_Test Company 1", customer="_Test Customer 1", - warehouse="_Test Warehouse 2 - _TC1", do_not_save=True) + so = make_sales_order( + company="_Test Company 1", + customer="_Test Customer 1", + warehouse="_Test Warehouse 2 - _TC1", + do_not_save=True, + ) so.conversion_rate = 0.02 so.plc_conversion_rate = 0.02 self.assertRaises(frappe.PermissionError, so.insert) @@ -671,7 +729,9 @@ class TestSalesOrder(ERPNextTestCase): frappe.set_user("Administrator") frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name) - frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name) + frappe.permissions.remove_user_permission( + "Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name + ) frappe.permissions.remove_user_permission("Company", "_Test Company 1", test_user_2.name) def test_block_delivery_note_against_cancelled_sales_order(self): @@ -691,10 +751,12 @@ class TestSalesOrder(ERPNextTestCase): make_item("_Test Service Product Bundle Item 1", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 2", {"is_stock_item": 0}) - make_product_bundle("_Test Service Product Bundle", - ["_Test Service Product Bundle Item 1", "_Test Service Product Bundle Item 2"]) + make_product_bundle( + "_Test Service Product Bundle", + ["_Test Service Product Bundle Item 1", "_Test Service Product Bundle Item 2"], + ) - so = make_sales_order(item_code = "_Test Service Product Bundle", warehouse=None) + so = make_sales_order(item_code="_Test Service Product Bundle", warehouse=None) self.assertTrue("_Test Service Product Bundle Item 1" in [d.item_code for d in so.packed_items]) self.assertTrue("_Test Service Product Bundle Item 2" in [d.item_code for d in so.packed_items]) @@ -704,38 +766,59 @@ class TestSalesOrder(ERPNextTestCase): make_item("_Test Mix Product Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Mix Product Bundle Item 2", {"is_stock_item": 0}) - make_product_bundle("_Test Mix Product Bundle", - ["_Test Mix Product Bundle Item 1", "_Test Mix Product Bundle Item 2"]) + make_product_bundle( + "_Test Mix Product Bundle", + ["_Test Mix Product Bundle Item 1", "_Test Mix Product Bundle Item 2"], + ) - self.assertRaises(WarehouseRequired, make_sales_order, item_code = "_Test Mix Product Bundle", warehouse="") + self.assertRaises( + WarehouseRequired, make_sales_order, item_code="_Test Mix Product Bundle", warehouse="" + ) def test_auto_insert_price(self): make_item("_Test Item for Auto Price List", {"is_stock_item": 0}) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) - item_price = frappe.db.get_value("Item Price", {"price_list": "_Test Price List", - "item_code": "_Test Item for Auto Price List"}) + item_price = frappe.db.get_value( + "Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"} + ) if item_price: frappe.delete_doc("Item Price", item_price) - make_sales_order(item_code = "_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100) - - self.assertEqual(frappe.db.get_value("Item Price", - {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, "price_list_rate"), 100) + make_sales_order( + item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100 + ) + self.assertEqual( + frappe.db.get_value( + "Item Price", + {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, + "price_list_rate", + ), + 100, + ) # do not update price list frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) - item_price = frappe.db.get_value("Item Price", {"price_list": "_Test Price List", - "item_code": "_Test Item for Auto Price List"}) + item_price = frappe.db.get_value( + "Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"} + ) if item_price: frappe.delete_doc("Item Price", item_price) - make_sales_order(item_code = "_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100) + make_sales_order( + item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100 + ) - self.assertEqual(frappe.db.get_value("Item Price", - {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, "price_list_rate"), None) + self.assertEqual( + frappe.db.get_value( + "Item Price", + {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, + "price_list_rate", + ), + None, + ) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) @@ -747,7 +830,9 @@ class TestSalesOrder(ERPNextTestCase): from erpnext.selling.doctype.sales_order.sales_order import update_status as so_update_status # make items - po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1}) + po_item = make_item( + "_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1} + ) dn_item = make_item("_Test Regular Item", {"is_stock_item": 1}) so_items = [ @@ -757,21 +842,21 @@ class TestSalesOrder(ERPNextTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 2, "rate": 300, - "conversion_factor": 1.0 - } + "conversion_factor": 1.0, + }, ] - if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1: + if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item") == 1: make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=2, rate=100) - #create so, po and dn + # create so, po and dn so = make_sales_order(item_list=so_items, do_not_submit=True) so.submit() @@ -784,12 +869,15 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(po.items[0].sales_order, so.name) self.assertEqual(po.items[0].item_code, po_item.item_code) self.assertEqual(dn.items[0].item_code, dn_item.item_code) - #test po_item length + # test po_item length self.assertEqual(len(po.items), 1) # test ordered_qty and reserved_qty for drop ship item - bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) + bin_po_item = frappe.get_all( + "Bin", + filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty", "reserved_qty"], + ) ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 @@ -804,12 +892,15 @@ class TestSalesOrder(ERPNextTestCase): po.load_from_db() # test after closing so - so.db_set('status', "Closed") + so.db_set("status", "Closed") so.update_reserved_qty() # test ordered_qty and reserved_qty for drop ship item after closing so - bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) + bin_po_item = frappe.get_all( + "Bin", + filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty", "reserved_qty"], + ) ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 @@ -832,8 +923,12 @@ class TestSalesOrder(ERPNextTestCase): from erpnext.selling.doctype.sales_order.sales_order import update_status as so_update_status # make items - po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1}) - po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1}) + po_item1 = make_item( + "_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1} + ) + po_item2 = make_item( + "_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1} + ) so_items = [ { @@ -842,7 +937,7 @@ class TestSalesOrder(ERPNextTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": po_item2.item_code, @@ -850,8 +945,8 @@ class TestSalesOrder(ERPNextTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] # create so and po @@ -865,7 +960,7 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(so.customer, po1.customer) self.assertEqual(po1.items[0].sales_order, so.name) self.assertEqual(po1.items[0].item_code, po_item1.item_code) - #test po item length + # test po item length self.assertEqual(len(po1.items), 1) # create po for remaining item @@ -899,7 +994,7 @@ class TestSalesOrder(ERPNextTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Item for Drop Shipping 2", @@ -907,8 +1002,8 @@ class TestSalesOrder(ERPNextTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier 1' - } + "supplier": "_Test Supplier 1", + }, ] # create so and po @@ -918,149 +1013,233 @@ class TestSalesOrder(ERPNextTestCase): purchase_orders = make_purchase_order_for_default_supplier(so.name, selected_items=so_items) self.assertEqual(len(purchase_orders), 2) - self.assertEqual(purchase_orders[0].supplier, '_Test Supplier') - self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1') + self.assertEqual(purchase_orders[0].supplier, "_Test Supplier") + self.assertEqual(purchase_orders[1].supplier, "_Test Supplier 1") + + def test_product_bundles_in_so_are_replaced_with_bundle_items_in_po(self): + """ + Tests if the the Product Bundles in the Items table of Sales Orders are replaced with + their child items(from the Packed Items table) on creating a Purchase Order from it. + """ + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order + + product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + + so_items = [ + { + "item_code": product_bundle.item_code, + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": "_Test Supplier", + } + ] + + so = make_sales_order(item_list=so_items) + + purchase_order = make_purchase_order(so.name, selected_items=so_items) + + self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1") + self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2") + + def test_purchase_order_updates_packed_item_ordered_qty(self): + """ + Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order + """ + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order + + product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + + so_items = [ + { + "item_code": product_bundle.item_code, + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": "_Test Supplier", + } + ] + + so = make_sales_order(item_list=so_items) + + purchase_order = make_purchase_order(so.name, selected_items=so_items) + purchase_order.supplier = "_Test Supplier" + purchase_order.set_warehouse = "_Test Warehouse - _TC" + purchase_order.save() + purchase_order.submit() + + so.reload() + self.assertEqual(so.packed_items[0].ordered_qty, 2) + self.assertEqual(so.packed_items[1].ordered_qty, 2) def test_reserved_qty_for_closing_so(self): - bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, - fields=["reserved_qty"]) + bin = frappe.get_all( + "Bin", + filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + fields=["reserved_qty"], + ) existing_reserved_qty = bin[0].reserved_qty if bin else 0.0 so = make_sales_order(item_code="_Test Item", qty=1) - self.assertEqual(get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), existing_reserved_qty+1) + self.assertEqual( + get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), + existing_reserved_qty + 1, + ) so.update_status("Closed") - self.assertEqual(get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), existing_reserved_qty) + self.assertEqual( + get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), + existing_reserved_qty, + ) def test_create_so_with_margin(self): so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True) so.items[0].price_list_rate = price_list_rate = 100 - so.items[0].margin_type = 'Percentage' + so.items[0].margin_type = "Percentage" so.items[0].margin_rate_or_amount = 25 so.save() new_so = frappe.copy_doc(so) new_so.save(ignore_permissions=True) - self.assertEqual(new_so.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) + self.assertEqual( + new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate) + ) new_so.items[0].margin_rate_or_amount = 25 new_so.payment_schedule = [] new_so.save() new_so.submit() - self.assertEqual(new_so.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) + self.assertEqual( + new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate) + ) def test_terms_auto_added(self): so = make_sales_order(do_not_save=1) - self.assertFalse(so.get('payment_schedule')) + self.assertFalse(so.get("payment_schedule")) so.insert() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) def test_terms_not_copied(self): so = make_sales_order() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) si = make_sales_invoice(so.name) - self.assertFalse(si.get('payment_schedule')) + self.assertFalse(si.get("payment_schedule")) def test_terms_copied(self): so = make_sales_order(do_not_copy=1, do_not_save=1) - so.payment_terms_template = '_Test Payment Term Template' + so.payment_terms_template = "_Test Payment Term Template" so.insert() so.submit() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) si = make_sales_invoice(so.name) si.insert() - self.assertTrue(si.get('payment_schedule')) + self.assertTrue(si.get("payment_schedule")) def test_make_work_order(self): # Make a new Sales Order - so = make_sales_order(**{ - "item_list": [{ - "item_code": "_Test FG Item", - "qty": 10, - "rate":100 - }, - { - "item_code": "_Test FG Item", - "qty": 20, - "rate":200 - }] - }) + so = make_sales_order( + **{ + "item_list": [ + {"item_code": "_Test FG Item", "qty": 10, "rate": 100}, + {"item_code": "_Test FG Item", "qty": 20, "rate": 200}, + ] + } + ) # Raise Work Orders - po_items= [] - so_item_name= {} + po_items = [] + so_item_name = {} for item in so.get_work_order_items(): - po_items.append({ - "warehouse": item.get("warehouse"), - "item_code": item.get("item_code"), - "pending_qty": item.get("pending_qty"), - "sales_order_item": item.get("sales_order_item"), - "bom": item.get("bom"), - "description": item.get("description") - }) - so_item_name[item.get("sales_order_item")]= item.get("pending_qty") - make_work_orders(json.dumps({"items":po_items}), so.name, so.company) + po_items.append( + { + "warehouse": item.get("warehouse"), + "item_code": item.get("item_code"), + "pending_qty": item.get("pending_qty"), + "sales_order_item": item.get("sales_order_item"), + "bom": item.get("bom"), + "description": item.get("description"), + } + ) + so_item_name[item.get("sales_order_item")] = item.get("pending_qty") + make_work_orders(json.dumps({"items": po_items}), so.name, so.company) # Check if Work Orders were raised for item in so_item_name: - wo_qty = frappe.db.sql("select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s", (so.name, item)) + wo_qty = frappe.db.sql( + "select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s", + (so.name, item), + ) self.assertEqual(wo_qty[0][0], so_item_name.get(item)) def test_serial_no_based_delivery(self): frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1) - item = make_item("_Reserved_Serialized_Item", {"is_stock_item": 1, - "maintain_stock": 1, - "has_serial_no": 1, - "serial_no_series": "SI.####", - "valuation_rate": 500, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + item = make_item( + "_Reserved_Serialized_Item", + { + "is_stock_item": 1, + "maintain_stock": 1, + "has_serial_no": 1, + "serial_no_series": "SI.####", + "valuation_rate": 500, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) frappe.db.sql("""delete from `tabSerial No` where item_code=%s""", (item.item_code)) - make_item("_Test Item A", {"maintain_stock": 1, - "valuation_rate": 100, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Item B", {"maintain_stock": 1, - "valuation_rate": 200, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + make_item( + "_Test Item A", + { + "maintain_stock": 1, + "valuation_rate": 100, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Item B", + { + "maintain_stock": 1, + "valuation_rate": 200, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - make_bom(item=item.item_code, rate=1000, - raw_materials = ['_Test Item A', '_Test Item B']) - so = make_sales_order(**{ - "item_list": [{ - "item_code": item.item_code, - "ensure_delivery_based_on_produced_serial_no": 1, - "qty": 1, - "rate":1000 - }] - }) + make_bom(item=item.item_code, rate=1000, raw_materials=["_Test Item A", "_Test Item B"]) + + so = make_sales_order( + **{ + "item_list": [ + { + "item_code": item.item_code, + "ensure_delivery_based_on_produced_serial_no": 1, + "qty": 1, + "rate": 1000, + } + ] + } + ) so.submit() from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - work_order = make_wo_order_test_record(item=item.item_code, - qty=1, do_not_save=True) + + work_order = make_wo_order_test_record(item=item.item_code, qty=1, do_not_save=True) work_order.fg_warehouse = "_Test Warehouse - _TC" work_order.sales_order = so.name work_order.submit() @@ -1069,6 +1248,7 @@ class TestSalesOrder(ERPNextTestCase): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as make_production_stock_entry, ) + se = frappe.get_doc(make_production_stock_entry(work_order.name, "Manufacture", 1)) se.submit() reserved_serial_no = se.get("items")[2].serial_no @@ -1080,7 +1260,7 @@ class TestSalesOrder(ERPNextTestCase): item_line = dn.get("items")[0] item_line.serial_no = item_serial_no.name item_line = dn.get("items")[0] - item_line.serial_no = reserved_serial_no + item_line.serial_no = reserved_serial_no dn.submit() dn.load_from_db() dn.cancel() @@ -1103,6 +1283,7 @@ class TestSalesOrder(ERPNextTestCase): from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( make_delivery_note as make_delivery_note_from_invoice, ) + dn = make_delivery_note_from_invoice(si.name) dn.save() dn.submit() @@ -1117,8 +1298,10 @@ class TestSalesOrder(ERPNextTestCase): def test_advance_payment_entry_unlink_against_sales_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 0) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 + ) so = make_sales_order() @@ -1133,7 +1316,7 @@ class TestSalesOrder(ERPNextTestCase): pe.save(ignore_permissions=True) pe.submit() - so_doc = frappe.get_doc('Sales Order', so.name) + so_doc = frappe.get_doc("Sales Order", so.name) self.assertRaises(frappe.LinkExistsError, so_doc.cancel) @@ -1144,8 +1327,9 @@ class TestSalesOrder(ERPNextTestCase): so = make_sales_order() # disable unlinking of payment entry - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 0) + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 + ) # create a payment entry against sales order pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") @@ -1165,81 +1349,77 @@ class TestSalesOrder(ERPNextTestCase): # Cancel sales order try: - so_doc = frappe.get_doc('Sales Order', so.name) + so_doc = frappe.get_doc("Sales Order", so.name) so_doc.cancel() except Exception: self.fail("Can not cancel sales order with linked cancelled payment entry") def test_request_for_raw_materials(self): - item = make_item("_Test Finished Item", {"is_stock_item": 1, - "maintain_stock": 1, - "valuation_rate": 500, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Raw Item A", {"maintain_stock": 1, - "valuation_rate": 100, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Raw Item B", {"maintain_stock": 1, - "valuation_rate": 200, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + item = make_item( + "_Test Finished Item", + { + "is_stock_item": 1, + "maintain_stock": 1, + "valuation_rate": 500, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Raw Item A", + { + "maintain_stock": 1, + "valuation_rate": 100, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Raw Item B", + { + "maintain_stock": 1, + "valuation_rate": 200, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - make_bom(item=item.item_code, rate=1000, - raw_materials = ['_Test Raw Item A', '_Test Raw Item B']) - so = make_sales_order(**{ - "item_list": [{ - "item_code": item.item_code, - "qty": 1, - "rate":1000 - }] - }) + make_bom(item=item.item_code, rate=1000, raw_materials=["_Test Raw Item A", "_Test Raw Item B"]) + + so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]}) so.submit() mr_dict = frappe._dict() items = so.get_work_order_items(1) - mr_dict['items'] = items - mr_dict['include_exploded_items'] = 0 - mr_dict['ignore_existing_ordered_qty'] = 1 + mr_dict["items"] = items + mr_dict["include_exploded_items"] = 0 + mr_dict["ignore_existing_ordered_qty"] = 1 make_raw_material_request(mr_dict, so.company, so.name) - mr = frappe.db.sql("""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1)[0] - mr_doc = frappe.get_doc('Material Request',mr.get('name')) + mr = frappe.db.sql( + """select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1 + )[0] + mr_doc = frappe.get_doc("Material Request", mr.get("name")) self.assertEqual(mr_doc.items[0].sales_order, so.name) def test_so_optional_blanket_order(self): """ - Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. - Second Sales Order should not add on to Blanket Orders Ordered Quantity. + Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. + Second Sales Order should not add on to Blanket Orders Ordered Quantity. """ - bo = make_blanket_order(blanket_order_type = "Selling", quantity = 10, rate = 10) + bo = make_blanket_order(blanket_order_type="Selling", quantity=10, rate=10) - so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) - so_doc = frappe.get_doc('Sales Order', so.get('name')) + so = make_sales_order(item_code="_Test Item", qty=5, against_blanket_order=1) + so_doc = frappe.get_doc("Sales Order", so.get("name")) # To test if the SO has a Blanket Order self.assertTrue(so_doc.items[0].blanket_order) - so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) - so_doc = frappe.get_doc('Sales Order', so.get('name')) + so = make_sales_order(item_code="_Test Item", qty=5, against_blanket_order=0) + so_doc = frappe.get_doc("Sales Order", so.get("name")) # To test if the SO does NOT have a Blanket Order self.assertEqual(so_doc.items[0].blanket_order, None) def test_so_cancellation_when_si_drafted(self): """ - Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state - Expected result: sales order should not get cancelled + Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state + Expected result: sales order should not get cancelled """ so = make_sales_order() so.submit() @@ -1258,7 +1438,7 @@ class TestSalesOrder(ERPNextTestCase): so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() - so.payment_terms_template = 'Test Receivable Template' + so.payment_terms_template = "Test Receivable Template" so.submit() si = create_sales_invoice(qty=10, do_not_save=1) @@ -1280,10 +1460,10 @@ class TestSalesOrder(ERPNextTestCase): so.submit() self.assertEqual(so.net_total, 0) - self.assertEqual(so.billing_status, 'Not Billed') + self.assertEqual(so.billing_status, "Not Billed") si = create_sales_invoice(qty=10, do_not_save=1) - si.price_list = '_Test Price List' + si.price_list = "_Test Price List" si.items[0].rate = 0 si.items[0].price_list_rate = 0 si.items[0].sales_order = so.name @@ -1293,7 +1473,7 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(si.net_total, 0) so.load_from_db() - self.assertEqual(so.billing_status, 'Fully Billed') + self.assertEqual(so.billing_status, "Fully Billed") def test_so_back_updated_from_wo_via_mr(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." @@ -1302,7 +1482,7 @@ class TestSalesOrder(ERPNextTestCase): ) from erpnext.stock.doctype.material_request.material_request import raise_work_orders - so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) + so = make_sales_order(item_list=[{"item_code": "_Test FG Item", "qty": 2, "rate": 100}]) mr = make_material_request(so.name) mr.material_request_type = "Manufacture" @@ -1319,17 +1499,18 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(wo.sales_order_item, so.items[0].name) wo.submit() - make_stock_entry(item_code="_Test Item", # Stock RM - target="Work In Progress - _TC", - qty=4, basic_rate=100 + make_stock_entry( + item_code="_Test Item", target="Work In Progress - _TC", qty=4, basic_rate=100 # Stock RM ) - make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM + make_stock_entry( + item_code="_Test Item Home Desktop 100", # Stock RM target="Work In Progress - _TC", - qty=4, basic_rate=100 + qty=4, + basic_rate=100, ) se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2)) - se.submit() # Finish WO + se.submit() # Finish WO mr.reload() wo.reload() @@ -1337,29 +1518,57 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) self.assertEqual(mr.status, "Manufactured") + def test_sales_order_with_shipping_rule(self): + from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule + + shipping_rule = create_shipping_rule( + shipping_rule_type="Selling", shipping_rule_name="Shipping Rule - Sales Invoice Test" + ) + sales_order = make_sales_order(do_not_save=True) + sales_order.shipping_rule = shipping_rule.name + + sales_order.items[0].qty = 1 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 50) + + sales_order.items[0].qty = 2 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 100) + + sales_order.items[0].qty = 3 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 200) + + sales_order.items[0].qty = 21 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 0) + + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable accounts_settings.save() + def compare_payment_schedules(doc, doc1, doc2): - for index, schedule in enumerate(doc1.get('payment_schedule')): + for index, schedule in enumerate(doc1.get("payment_schedule")): doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term) doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date) doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion) doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount) + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) if args.transaction_date: so.transaction_date = args.transaction_date - so.set_warehouse = "" # no need to test set_warehouse permission since it only affects the client + so.set_warehouse = "" # no need to test set_warehouse permission since it only affects the client so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" - so.po_no = args.po_no or '12345' + so.po_no = args.po_no or "12345" if args.selling_price_list: so.selling_price_list = args.selling_price_list @@ -1371,14 +1580,17 @@ def make_sales_order(**args): so.append("items", item) else: - so.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse, - "qty": args.qty or 10, - "uom": args.uom or None, - "rate": args.rate or 100, - "against_blanket_order": args.against_blanket_order - }) + so.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse, + "qty": args.qty or 10, + "uom": args.uom or None, + "rate": args.rate or 100, + "against_blanket_order": args.against_blanket_order, + }, + ) so.delivery_date = add_days(so.transaction_date, 10) @@ -1393,6 +1605,7 @@ def make_sales_order(**args): return so + def create_dn_against_so(so, delivered_qty=0): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -1402,41 +1615,63 @@ def create_dn_against_so(so, delivered_qty=0): dn.submit() return dn + def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): - return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - "reserved_qty")) + return flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "reserved_qty") + ) + test_dependencies = ["Currency Exchange"] + def make_sales_order_workflow(): - if frappe.db.exists('Workflow', 'SO Test Workflow'): + if frappe.db.exists("Workflow", "SO Test Workflow"): doc = frappe.get_doc("Workflow", "SO Test Workflow") doc.set("is_active", 1) doc.save() return doc - frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True) - frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) - frappe.cache().hdel('roles', frappe.session.user) + frappe.get_doc(dict(doctype="Role", role_name="Test Junior Approver")).insert( + ignore_if_duplicate=True + ) + frappe.get_doc(dict(doctype="Role", role_name="Test Approver")).insert(ignore_if_duplicate=True) + frappe.cache().hdel("roles", frappe.session.user) - workflow = frappe.get_doc({ - "doctype": "Workflow", - "workflow_name": "SO Test Workflow", - "document_type": "Sales Order", - "workflow_state_field": "workflow_state", - "is_active": 1, - "send_email_alert": 0, - }) - workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) - workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) - workflow.append('transitions', dict( - state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, - condition = 'doc.grand_total < 200' - )) - workflow.append('transitions', dict( - state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1, - condition = 'doc.grand_total > 200' - )) + workflow = frappe.get_doc( + { + "doctype": "Workflow", + "workflow_name": "SO Test Workflow", + "document_type": "Sales Order", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 0, + } + ) + workflow.append("states", dict(state="Pending", allow_edit="All")) + workflow.append("states", dict(state="Approved", allow_edit="Test Approver", doc_status=1)) + workflow.append( + "transitions", + dict( + state="Pending", + action="Approve", + next_state="Approved", + allowed="Test Junior Approver", + allow_self_approval=1, + condition="doc.grand_total < 200", + ), + ) + workflow.append( + "transitions", + dict( + state="Pending", + action="Approve", + next_state="Approved", + allowed="Test Approver", + allow_self_approval=1, + condition="doc.grand_total > 200", + ), + ) workflow.insert(ignore_permissions=True) return workflow diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 7e55499533b..21abb94557c 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -23,6 +23,7 @@ "quantity_and_rate", "qty", "stock_uom", + "picked_qty", "col_break2", "uom", "conversion_factor", @@ -83,8 +84,8 @@ "planned_qty", "column_break_69", "work_order_qty", - "delivered_qty", "produced_qty", + "delivered_qty", "returned_qty", "shopping_cart_section", "additional_notes", @@ -701,10 +702,8 @@ "width": "50px" }, { - "description": "For Production", "fieldname": "produced_qty", "fieldtype": "Float", - "hidden": 1, "label": "Produced Quantity", "oldfieldname": "produced_qty", "oldfieldtype": "Currency", @@ -798,12 +797,19 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "fieldname": "picked_qty", + "fieldtype": "Float", + "label": "Picked Qty (in Stock UOM)", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-02-24 14:41:57.325799", + "modified": "2022-04-27 03:15:34.366563", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 441a6ac9709..83d3f3bc076 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -9,5 +9,6 @@ from frappe.model.document import Document class SalesOrderItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Sales Order Item", ["item_code", "warehouse"]) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index a0c1c85dd52..eabfd7d2e22 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -13,6 +13,7 @@ "territory", "crm_settings_section", "campaign_naming_by", + "contract_naming_by", "default_valid_till", "column_break_9", "close_opportunity_after_days", @@ -29,7 +30,6 @@ "so_required", "dn_required", "sales_update_frequency", - "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", "hide_tax_id" @@ -193,6 +193,12 @@ "fieldname": "sales_transactions_settings_section", "fieldtype": "Section Break", "label": "Transaction Settings" + }, + { + "fieldname": "contract_naming_by", + "fieldtype": "Select", + "label": "Contract Naming By", + "options": "Party Name\nNaming Series" } ], "icon": "fa fa-cog", @@ -200,7 +206,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-14 22:05:06.139820", + "modified": "2022-03-28 12:18:06.768403", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -219,4 +225,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index fb86e614b6c..1d1a76fba5e 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -16,23 +16,46 @@ class SellingSettings(Document): self.toggle_editable_rate_for_bundle_items() def validate(self): - for key in ["cust_master_name", "campaign_naming_by", "customer_group", "territory", - "maintain_same_sales_rate", "editable_price_list_rate", "selling_price_list"]: - frappe.db.set_default(key, self.get(key, "")) + for key in [ + "cust_master_name", + "campaign_naming_by", + "customer_group", + "territory", + "maintain_same_sales_rate", + "editable_price_list_rate", + "selling_price_list", + ]: + frappe.db.set_default(key, self.get(key, "")) from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Customer", "customer_name", - self.get("cust_master_name")=="Naming Series", hide_name_field=False) + + set_by_naming_series( + "Customer", + "customer_name", + self.get("cust_master_name") == "Naming Series", + hide_name_field=False, + ) def toggle_hide_tax_id(self): self.hide_tax_id = cint(self.hide_tax_id) # Make property setters to hide tax_id fields for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"): - make_property_setter(doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False + ) def toggle_editable_rate_for_bundle_items(self): editable_bundle_item_rates = cint(self.editable_bundle_item_rates) - make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False) + make_property_setter( + "Packed Item", + "rate", + "read_only", + not (editable_bundle_item_rates), + "Check", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/selling/doctype/sms_center/sms_center.py b/erpnext/selling/doctype/sms_center/sms_center.py index d192457ee07..cdc7397e1ec 100644 --- a/erpnext/selling/doctype/sms_center/sms_center.py +++ b/erpnext/selling/doctype/sms_center/sms_center.py @@ -12,59 +12,80 @@ from frappe.utils import cstr class SMSCenter(Document): @frappe.whitelist() def create_receiver_list(self): - rec, where_clause = '', '' - if self.send_to == 'All Customer Contact': + rec, where_clause = "", "" + if self.send_to == "All Customer Contact": where_clause = " and dl.link_doctype = 'Customer'" if self.customer: - where_clause += " and dl.link_name = '%s'" % \ - self.customer.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to == 'All Supplier Contact': + where_clause += ( + " and dl.link_name = '%s'" % self.customer.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to == "All Supplier Contact": where_clause = " and dl.link_doctype = 'Supplier'" if self.supplier: - where_clause += " and dl.link_name = '%s'" % \ - self.supplier.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to == 'All Sales Partner Contact': + where_clause += ( + " and dl.link_name = '%s'" % self.supplier.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to == "All Sales Partner Contact": where_clause = " and dl.link_doctype = 'Sales Partner'" if self.sales_partner: - where_clause += "and dl.link_name = '%s'" % \ - self.sales_partner.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to in ['All Contact', 'All Customer Contact', 'All Supplier Contact', 'All Sales Partner Contact']: - rec = frappe.db.sql("""select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')), + where_clause += ( + "and dl.link_name = '%s'" % self.sales_partner.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to in [ + "All Contact", + "All Customer Contact", + "All Supplier Contact", + "All Sales Partner Contact", + ]: + rec = frappe.db.sql( + """select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')), c.mobile_no from `tabContact` c, `tabDynamic Link` dl where ifnull(c.mobile_no,'')!='' and - c.docstatus != 2 and dl.parent = c.name%s""" % where_clause) + c.docstatus != 2 and dl.parent = c.name%s""" + % where_clause + ) - elif self.send_to == 'All Lead (Open)': - rec = frappe.db.sql("""select lead_name, mobile_no from `tabLead` where - ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'""") + elif self.send_to == "All Lead (Open)": + rec = frappe.db.sql( + """select lead_name, mobile_no from `tabLead` where + ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'""" + ) - elif self.send_to == 'All Employee (Active)': - where_clause = self.department and " and department = '%s'" % \ - self.department.replace("'", "\'") or "" - where_clause += self.branch and " and branch = '%s'" % \ - self.branch.replace("'", "\'") or "" + elif self.send_to == "All Employee (Active)": + where_clause = ( + self.department and " and department = '%s'" % self.department.replace("'", "'") or "" + ) + where_clause += self.branch and " and branch = '%s'" % self.branch.replace("'", "'") or "" - rec = frappe.db.sql("""select employee_name, cell_number from + rec = frappe.db.sql( + """select employee_name, cell_number from `tabEmployee` where status = 'Active' and docstatus < 2 and - ifnull(cell_number,'')!='' %s""" % where_clause) + ifnull(cell_number,'')!='' %s""" + % where_clause + ) - elif self.send_to == 'All Sales Person': - rec = frappe.db.sql("""select sales_person_name, + elif self.send_to == "All Sales Person": + rec = frappe.db.sql( + """select sales_person_name, tabEmployee.cell_number from `tabSales Person` left join tabEmployee on `tabSales Person`.employee = tabEmployee.name - where ifnull(tabEmployee.cell_number,'')!=''""") + where ifnull(tabEmployee.cell_number,'')!=''""" + ) - rec_list = '' + rec_list = "" for d in rec: - rec_list += d[0] + ' - ' + d[1] + '\n' + rec_list += d[0] + " - " + d[1] + "\n" self.receiver_list = rec_list def get_receiver_nos(self): receiver_nos = [] if self.receiver_list: - for d in self.receiver_list.split('\n'): + for d in self.receiver_list.split("\n"): receiver_no = d - if '-' in d: - receiver_no = receiver_no.split('-')[1] + if "-" in d: + receiver_no = receiver_no.split("-")[1] if receiver_no.strip(): receiver_nos.append(cstr(receiver_no).strip()) else: 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 216e35a3903..bf629824ad9 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -8,7 +8,7 @@ import frappe from frappe.utils.nestedset import get_root_of from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability -from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups +from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups def search_by_term(search_term, warehouse, price_list): @@ -20,31 +20,46 @@ def search_by_term(search_term, warehouse, price_list): 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_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, is_stock_item = 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"]) or [None, None] + price_list_rate, currency = frappe.db.get_value( + "Item Price", + {"price_list": price_list, "item_code": item_code}, + ["price_list_rate", "currency"], + ) or [None, None] - 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 - }) + 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]} - return {'items': [item_info]} @frappe.whitelist() 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']) + "POS Profile", pos_profile, ["warehouse", "hide_unavailable_items"] + ) result = [] @@ -53,20 +68,23 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te if result: return result - if not frappe.db.exists('Item Group', item_group): - item_group = get_root_of('Item Group') + if not frappe.db.exists("Item Group", item_group): + item_group = get_root_of("Item Group") condition = get_conditions(search_term) condition += get_item_group_condition(pos_profile) - lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) + lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) bin_join_selection, bin_join_condition = "", "" if hide_unavailable_items: bin_join_selection = ", `tabBin` bin" - bin_join_condition = "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" + bin_join_condition = ( + "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" + ) - items_data = frappe.db.sql(""" + items_data = frappe.db.sql( + """ SELECT item.name AS item_code, item.item_name, @@ -87,22 +105,26 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ORDER BY item.name asc LIMIT - {start}, {page_length}""" - .format( + {start}, {page_length}""".format( start=start, page_length=page_length, lft=lft, rgt=rgt, condition=condition, bin_join_selection=bin_join_selection, - bin_join_condition=bin_join_condition - ), {'warehouse': warehouse}, as_dict=1) + bin_join_condition=bin_join_condition, + ), + {"warehouse": warehouse}, + as_dict=1, + ) if 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"], - filters = {'price_list': price_list, 'item_code': ['in', items]}) + item_prices_data = frappe.get_all( + "Item Price", + fields=["item_code", "price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": ["in", items]}, + ) item_prices = {} for d in item_prices_data: @@ -115,163 +137,204 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te row = {} row.update(item) - row.update({ - 'price_list_rate': item_price.get('price_list_rate'), - 'currency': item_price.get('currency'), - 'actual_qty': item_stock_qty, - }) + row.update( + { + "price_list_rate": item_price.get("price_list_rate"), + "currency": item_price.get("currency"), + "actual_qty": item_stock_qty, + } + ) result.append(row) - return {'items': result} + return {"items": result} + @frappe.whitelist() 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) + barcode_data = frappe.db.get_value( + "Item Barcode", {"barcode": search_value}, ["barcode", "parent as item_code"], as_dict=True + ) if barcode_data: return barcode_data # search serial no - serial_no_data = frappe.db.get_value('Serial No', search_value, ['name as serial_no', 'item_code'], as_dict=True) + serial_no_data = frappe.db.get_value( + "Serial No", search_value, ["name as serial_no", "item_code"], as_dict=True + ) if serial_no_data: return serial_no_data # search batch no - batch_no_data = frappe.db.get_value('Batch', search_value, ['name as batch_no', 'item as item_code'], as_dict=True) + batch_no_data = frappe.db.get_value( + "Batch", search_value, ["name as batch_no", "item as item_code"], as_dict=True + ) if batch_no_data: return batch_no_data return {} + def get_conditions(search_term): condition = "(" condition += """item.name like {search_term} - or item.item_name like {search_term}""".format(search_term=frappe.db.escape('%' + 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(search_term): - condition = '' - search_fields = frappe.get_all('POS Search Fields', fields = ['fieldname']) + 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('%' + search_term + '%')) + condition += " or item.`{0}` like {1}".format( + field["fieldname"], frappe.db.escape("%" + search_term + "%") + ) return condition + def get_item_group_condition(pos_profile): cond = "and 1=1" item_groups = get_item_groups(pos_profile) if item_groups: - cond = "and item.item_group in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups))) return cond % tuple(item_groups) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_group_query(doctype, txt, searchfield, start, page_len, filters): item_groups = [] cond = "1=1" - pos_profile= filters.get('pos_profile') + pos_profile = filters.get("pos_profile") if pos_profile: item_groups = get_item_groups(pos_profile) if item_groups: - cond = "name in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups))) cond = cond % tuple(item_groups) - return frappe.db.sql(""" select distinct name from `tabItem Group` - where {condition} and (name like %(txt)s) limit {start}, {page_len}""" - .format(condition = cond, start=start, page_len= page_len), - {'txt': '%%%s%%' % txt}) + return frappe.db.sql( + """ select distinct name from `tabItem Group` + where {condition} and (name like %(txt)s) limit {start}, {page_len}""".format( + condition=cond, start=start, page_len=page_len + ), + {"txt": "%%%s%%" % txt}, + ) + @frappe.whitelist() def check_opening_entry(user): - open_vouchers = frappe.db.get_all("POS Opening Entry", - filters = { - "user": user, - "pos_closing_entry": ["in", ["", None]], - "docstatus": 1 - }, - fields = ["name", "company", "pos_profile", "period_start_date"], - order_by = "period_start_date desc" + open_vouchers = frappe.db.get_all( + "POS Opening Entry", + filters={"user": user, "pos_closing_entry": ["in", ["", None]], "docstatus": 1}, + fields=["name", "company", "pos_profile", "period_start_date"], + order_by="period_start_date desc", ) return open_vouchers + @frappe.whitelist() def create_opening_voucher(pos_profile, company, balance_details): balance_details = json.loads(balance_details) - new_pos_opening = frappe.get_doc({ - 'doctype': 'POS Opening Entry', - "period_start_date": frappe.utils.get_datetime(), - "posting_date": frappe.utils.getdate(), - "user": frappe.session.user, - "pos_profile": pos_profile, - "company": company, - }) + new_pos_opening = frappe.get_doc( + { + "doctype": "POS Opening Entry", + "period_start_date": frappe.utils.get_datetime(), + "posting_date": frappe.utils.getdate(), + "user": frappe.session.user, + "pos_profile": pos_profile, + "company": company, + } + ) new_pos_opening.set("balance_details", balance_details) new_pos_opening.submit() return new_pos_opening.as_dict() + @frappe.whitelist() def get_past_order_list(search_term, status, limit=20): - fields = ['name', 'grand_total', 'currency', 'customer', 'posting_time', 'posting_date'] + fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"] invoice_list = [] if search_term and status: - invoices_by_customer = frappe.db.get_all('POS Invoice', filters={ - 'customer': ['like', '%{}%'.format(search_term)], - 'status': status - }, fields=fields) - invoices_by_name = frappe.db.get_all('POS Invoice', filters={ - 'name': ['like', '%{}%'.format(search_term)], - 'status': status - }, fields=fields) + invoices_by_customer = frappe.db.get_all( + "POS Invoice", + filters={"customer": ["like", "%{}%".format(search_term)], "status": status}, + fields=fields, + ) + invoices_by_name = frappe.db.get_all( + "POS Invoice", + filters={"name": ["like", "%{}%".format(search_term)], "status": status}, + fields=fields, + ) invoice_list = invoices_by_customer + invoices_by_name elif status: - invoice_list = frappe.db.get_all('POS Invoice', filters={ - 'status': status - }, fields=fields) + invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields) return invoice_list + @frappe.whitelist() def set_customer_info(fieldname, customer, value=""): - if fieldname == 'loyalty_program': - frappe.db.set_value('Customer', customer, 'loyalty_program', value) + if fieldname == "loyalty_program": + frappe.db.set_value("Customer", customer, "loyalty_program", value) - contact = frappe.get_cached_value('Customer', customer, 'customer_primary_contact') + contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact") if not contact: - contact = frappe.db.sql(""" + contact = frappe.db.sql( + """ SELECT parent FROM `tabDynamic Link` WHERE parenttype = 'Contact' AND parentfield = 'links' AND link_doctype = 'Customer' AND link_name = %s - """, (customer), as_dict=1) - contact = contact[0].get('parent') if contact else None + """, + (customer), + as_dict=1, + ) + contact = contact[0].get("parent") if contact else None if not contact: - new_contact = frappe.new_doc('Contact') + new_contact = frappe.new_doc("Contact") new_contact.is_primary_contact = 1 new_contact.first_name = customer - new_contact.set('links', [{'link_doctype': 'Customer', 'link_name': customer}]) + new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}]) new_contact.save() contact = new_contact.name - frappe.db.set_value('Customer', customer, 'customer_primary_contact', contact) + frappe.db.set_value("Customer", customer, "customer_primary_contact", contact) - contact_doc = frappe.get_doc('Contact', contact) - if fieldname == 'email_id': - contact_doc.set('email_ids', [{ 'email_id': value, 'is_primary': 1}]) - frappe.db.set_value('Customer', customer, 'email_id', value) - elif fieldname == 'mobile_no': - contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}]) - frappe.db.set_value('Customer', customer, 'mobile_no', value) + contact_doc = frappe.get_doc("Contact", contact) + if fieldname == "email_id": + contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}]) + frappe.db.set_value("Customer", customer, "email_id", value) + elif fieldname == "mobile_no": + contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}]) + frappe.db.set_value("Customer", customer, "mobile_no", value) contact_doc.save() + + +@frappe.whitelist() +def get_pos_profile_data(pos_profile): + pos_profile = frappe.get_doc("POS Profile", pos_profile) + pos_profile = pos_profile.as_dict() + + _customer_groups_with_children = [] + for row in pos_profile.customer_groups: + children = get_child_nodes("Customer Group", row.customer_group) + _customer_groups_with_children.extend(children) + + pos_profile.customer_groups = _customer_groups_with_children + return 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 ea8459f970b..7a6838680f0 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -119,10 +119,15 @@ erpnext.PointOfSale.Controller = class { this.allow_negative_stock = flt(message.allow_negative_stock) || false; }); - frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => { - Object.assign(this.settings, profile); - this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group); - this.make_app(); + frappe.call({ + method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data", + args: { "pos_profile": this.pos_profile }, + callback: (res) => { + const profile = res.message; + Object.assign(this.settings, profile); + this.settings.customer_groups = profile.customer_groups.map(group => group.name); + this.make_app(); + } }); } @@ -338,9 +343,9 @@ erpnext.PointOfSale.Controller = class { toggle_other_sections: (show) => { if (show) { this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : ''; - this.item_selector.$component.css('display', 'none'); + this.item_selector.toggle_component(false); } else { - this.item_selector.$component.css('display', 'flex'); + this.item_selector.toggle_component(true); } }, @@ -555,7 +560,7 @@ erpnext.PointOfSale.Controller = class { if (this.item_details.$component.is(':visible')) this.edit_item_details_of(item_row); - if (this.check_serial_batch_selection_needed(item_row)) + if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible')) this.edit_item_details_of(item_row); } @@ -704,7 +709,7 @@ erpnext.PointOfSale.Controller = class { frappe.dom.freeze(); const { doctype, name, current_item } = this.item_details; - frappe.model.set_value(doctype, name, 'qty', 0) + return frappe.model.set_value(doctype, name, 'qty', 0) .then(() => { frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true); @@ -715,7 +720,17 @@ erpnext.PointOfSale.Controller = class { } async save_and_checkout() { - this.frm.is_dirty() && await this.frm.save(); - this.payment.checkout(); + if (this.frm.is_dirty()) { + let save_error = false; + await this.frm.save(null, null, null, () => save_error = true); + // only move to payment section if save is successful + !save_error && this.payment.checkout(); + // show checkout button on error + save_error && setTimeout(() => { + this.cart.toggle_checkout_btn(true); + }, 300); // wait for save to finish + } else { + this.payment.checkout(); + } } }; 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 4a99f068cd5..eacf480ef8f 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -130,10 +130,10 @@ erpnext.PointOfSale.ItemCart = class { }, cols: 5, keys: [ - [ 1, 2, 3, __('Quantity') ], - [ 4, 5, 6, __('Discount') ], - [ 7, 8, 9, __('Rate') ], - [ '.', 0, __('Delete'), __('Remove') ] + [ 1, 2, 3, 'Quantity' ], + [ 4, 5, 6, 'Discount' ], + [ 7, 8, 9, 'Rate' ], + [ '.', 0, 'Delete', 'Remove' ] ], css_classes: [ [ '', '', '', 'col-span-2' ], 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 a3ad0025943..1d720f7291a 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -60,12 +60,18 @@ erpnext.PointOfSale.ItemDetails = class { return item && item.name == this.current_item.name; } - toggle_item_details_section(item) { + async toggle_item_details_section(item) { const current_item_changed = !this.compare_with_current_item(item); // if item is null or highlighted cart item is clicked twice const hide_item_details = !Boolean(item) || !current_item_changed; + if ((!hide_item_details && current_item_changed) || hide_item_details) { + // if item details is being closed OR if item details is opened but item is changed + // in both cases, if the current item is a serialized item, then validate and remove the item + await this.validate_serial_batch_item(); + } + this.events.toggle_item_selector(!hide_item_details); this.toggle_component(!hide_item_details); @@ -83,7 +89,6 @@ erpnext.PointOfSale.ItemDetails = class { this.render_form(item); this.events.highlight_cart_item(item); } else { - this.validate_serial_batch_item(); this.current_item = {}; } } @@ -103,11 +108,11 @@ erpnext.PointOfSale.ItemDetails = class { (serialized && batched && (no_batch_selected || no_serial_selected))) { frappe.show_alert({ - message: __("Item will be removed since no serial / batch no selected."), + message: __("Item is removed since no serial / batch no selected."), indicator: 'orange' }); frappe.utils.play_sound("cancel"); - this.events.remove_item_from_cart(); + return this.events.remove_item_from_cart(); } } 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 1177615aee9..7a90fb044f3 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -179,6 +179,25 @@ erpnext.PointOfSale.ItemSelector = class { }); this.search_field.toggle_label(false); this.item_group_field.toggle_label(false); + + this.attach_clear_btn(); + } + + attach_clear_btn() { + this.search_field.$wrapper.find('.control-input').append( + ` + + ${frappe.utils.icon('close', 'sm')} + + ` + ); + + this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn'); + + this.$clear_search_btn.on('click', 'a', () => { + this.set_search_value(''); + this.search_field.set_focus(); + }); } set_search_value(value) { @@ -243,7 +262,7 @@ erpnext.PointOfSale.ItemSelector = class { value: "+1", item: { item_code, batch_no, serial_no, uom, rate } }); - me.set_search_value(''); + me.search_field.set_focus(); }); this.search_field.$input.on('input', (e) => { @@ -252,6 +271,16 @@ erpnext.PointOfSale.ItemSelector = class { const search_term = e.target.value; this.filter_items({ search_term }); }, 300); + + this.$clear_search_btn.toggle( + Boolean(this.search_field.$input.val()) + ); + }); + + this.search_field.$input.on('focus', () => { + this.$clear_search_btn.toggle( + Boolean(this.search_field.$input.val()) + ); }); } @@ -284,7 +313,7 @@ erpnext.PointOfSale.ItemSelector = class { if (this.items.length == 1) { this.$items_container.find(".item-wrapper").click(); frappe.utils.play_sound("submit"); - $(this.search_field.$input[0]).val("").trigger("input"); + this.set_search_value(''); } else if (this.items.length == 0 && this.barcode_scanned) { // only show alert of barcode is scanned and enter is pressed frappe.show_alert({ @@ -293,7 +322,7 @@ erpnext.PointOfSale.ItemSelector = class { }); frappe.utils.play_sound("error"); this.barcode_scanned = false; - $(this.search_field.$input[0]).val("").trigger("input"); + this.set_search_value(''); } }); } @@ -328,6 +357,7 @@ erpnext.PointOfSale.ItemSelector = class { add_filtered_item_to_cart() { this.$items_container.find(".item-wrapper").click(); + this.set_search_value(''); } resize_selector(minimize) { @@ -349,6 +379,7 @@ erpnext.PointOfSale.ItemSelector = class { } toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); + this.set_search_value(''); + this.$component.css('display', show ? 'flex': 'none'); } }; diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js index 95293d1dd5c..f27b0d55ef6 100644 --- a/erpnext/selling/page/point_of_sale/pos_number_pad.js +++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js @@ -25,7 +25,7 @@ erpnext.PointOfSale.NumberPad = class { const fieldname = fieldnames && fieldnames[number] ? fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; - return a2 + `
    ${number}
    `; + return a2 + `
    ${__(number)}
    `; }, ''); }, ''); } diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 1e9f6d7d920..b4ece46e6e1 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -170,20 +170,24 @@ erpnext.PointOfSale.Payment = class { }); frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { - if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); - } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { - frappe.show_alert({ - message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), - indicator: "orange" - }); + if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { + if (!frm.doc.ignore_pricing_rule) { + frm.applying_pos_coupon_code = true; + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc), + () => (frm.applying_pos_coupon_code = false) + ]); + } else if (frm.doc.ignore_pricing_rule) { + frappe.show_alert({ + message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), + indicator: "orange" + }); + } } }); diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index a75108e4032..c626f5b05fc 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -16,86 +16,153 @@ def validate_filters(from_date, to_date, company): if not company: frappe.throw(_("Please Select a Company")) + @frappe.whitelist() def get_funnel_data(from_date, to_date, company): validate_filters(from_date, to_date, company) - active_leads = frappe.db.sql("""select count(*) from `tabLead` + active_leads = frappe.db.sql( + """select count(*) from `tabLead` where (date(`creation`) between %s and %s) - and company=%s""", (from_date, to_date, company))[0][0] + and company=%s""", + (from_date, to_date, company), + )[0][0] - opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` + opportunities = frappe.db.sql( + """select count(*) from `tabOpportunity` where (date(`creation`) between %s and %s) - and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0] + and opportunity_from='Lead' and company=%s""", + (from_date, to_date, company), + )[0][0] - quotations = frappe.db.sql("""select count(*) from `tabQuotation` + quotations = frappe.db.sql( + """select count(*) from `tabQuotation` where docstatus = 1 and (date(`creation`) between %s and %s) - and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0] + and (opportunity!="" or quotation_to="Lead") and company=%s""", + (from_date, to_date, company), + )[0][0] - converted = frappe.db.sql("""select count(*) from `tabCustomer` + converted = frappe.db.sql( + """select count(*) from `tabCustomer` JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name WHERE (date(`tabCustomer`.creation) between %s and %s) - and `tabLead`.company=%s""", (from_date, to_date, company))[0][0] - + and `tabLead`.company=%s""", + (from_date, to_date, company), + )[0][0] return [ - { "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" }, - { "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" }, - { "title": _("Quotations"), "value": quotations, "color": "#006685" }, - { "title": _("Converted"), "value": converted, "color": "#00AD65" } + {"title": _("Active Leads"), "value": active_leads, "color": "#B03B46"}, + {"title": _("Opportunities"), "value": opportunities, "color": "#F09C00"}, + {"title": _("Quotations"), "value": quotations, "color": "#006685"}, + {"title": _("Converted"), "value": converted, "color": "#00AD65"}, ] + @frappe.whitelist() def get_opp_by_lead_source(from_date, to_date, company): validate_filters(from_date, to_date, company) - opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability', 'source']) + opportunities = frappe.get_all( + "Opportunity", + filters=[ + ["status", "in", ["Open", "Quotation", "Replied"]], + ["company", "=", company], + ["transaction_date", "Between", [from_date, to_date]], + ], + fields=["currency", "sales_stage", "opportunity_amount", "probability", "source"], + ) if opportunities: - default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency') + default_currency = frappe.get_cached_value("Global Defaults", "None", "default_currency") - cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities] + cp_opportunities = [ + dict( + x, + **{ + "compound_amount": ( + convert(x["opportunity_amount"], x["currency"], default_currency, to_date) + * x["probability"] + / 100 + ) + } + ) + for x in opportunities + ] - df = pd.DataFrame(cp_opportunities).groupby(['source', 'sales_stage'], as_index=False).agg({'compound_amount': 'sum'}) + df = ( + pd.DataFrame(cp_opportunities) + .groupby(["source", "sales_stage"], as_index=False) + .agg({"compound_amount": "sum"}) + ) result = {} - result['labels'] = list(set(df.source.values)) - result['datasets'] = [] + result["labels"] = list(set(df.source.values)) + result["datasets"] = [] for s in set(df.sales_stage.values): - result['datasets'].append({'name': s, 'values': [0]*len(result['labels']), 'chartType': 'bar'}) + result["datasets"].append( + {"name": s, "values": [0] * len(result["labels"]), "chartType": "bar"} + ) for row in df.itertuples(): - source_index = result['labels'].index(row.source) + source_index = result["labels"].index(row.source) - for dataset in result['datasets']: - if dataset['name'] == row.sales_stage: - dataset['values'][source_index] = row.compound_amount + for dataset in result["datasets"]: + if dataset["name"] == row.sales_stage: + dataset["values"][source_index] = row.compound_amount return result else: - return 'empty' + return "empty" + @frappe.whitelist() def get_pipeline_data(from_date, to_date, company): validate_filters(from_date, to_date, company) - opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability']) + opportunities = frappe.get_all( + "Opportunity", + filters=[ + ["status", "in", ["Open", "Quotation", "Replied"]], + ["company", "=", company], + ["transaction_date", "Between", [from_date, to_date]], + ], + fields=["currency", "sales_stage", "opportunity_amount", "probability"], + ) if opportunities: - default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency') + default_currency = frappe.get_cached_value("Global Defaults", "None", "default_currency") - cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities] + cp_opportunities = [ + dict( + x, + **{ + "compound_amount": ( + convert(x["opportunity_amount"], x["currency"], default_currency, to_date) + * x["probability"] + / 100 + ) + } + ) + for x in opportunities + ] - df = pd.DataFrame(cp_opportunities).groupby(['sales_stage'], as_index=True).agg({'compound_amount': 'sum'}).to_dict() + df = ( + pd.DataFrame(cp_opportunities) + .groupby(["sales_stage"], as_index=True) + .agg({"compound_amount": "sum"}) + .to_dict() + ) result = {} - result['labels'] = df['compound_amount'].keys() - result['datasets'] = [] - result['datasets'].append({'name': _("Total Amount"), 'values': df['compound_amount'].values(), 'chartType': 'bar'}) + result["labels"] = df["compound_amount"].keys() + result["datasets"] = [] + result["datasets"].append( + {"name": _("Total Amount"), "values": df["compound_amount"].values(), "chartType": "bar"} + ) return result else: - return 'empty' + return "empty" diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 319e78f4889..5832d445423 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -7,20 +7,30 @@ from six import iteritems from six.moves import range field_map = { - "Contact": [ "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact" ], - "Address": [ "address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address" ] + "Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"], + "Address": [ + "address_line1", + "address_line2", + "city", + "state", + "pincode", + "country", + "is_primary_address", + ], } + def execute(filters=None): columns, data = get_columns(filters), get_data(filters) return columns, data + def get_columns(filters): party_type = filters.get("party_type") party_type_value = get_party_group(party_type) return [ "{party_type}:Link/{party_type}".format(party_type=party_type), - "{party_value_type}::150".format(party_value_type = frappe.unscrub(str(party_type_value))), + "{party_value_type}::150".format(party_value_type=frappe.unscrub(str(party_type_value))), "Address Line 1", "Address Line 2", "City", @@ -33,9 +43,10 @@ def get_columns(filters): "Phone", "Mobile No", "Email Id", - "Is Primary Contact:Check" + "Is Primary Contact:Check", ] + def get_data(filters): party_type = filters.get("party_type") party = filters.get("party_name") @@ -43,6 +54,7 @@ def get_data(filters): return get_party_addresses_and_contact(party_type, party, party_group) + def get_party_addresses_and_contact(party_type, party, party_group): data = [] filters = None @@ -52,9 +64,11 @@ def get_party_addresses_and_contact(party_type, party, party_group): return [] if party: - filters = { "name": party } + filters = {"name": party} - fetch_party_list = frappe.get_list(party_type, filters=filters, fields=["name", party_group], as_list=True) + fetch_party_list = frappe.get_list( + party_type, filters=filters, fields=["name", party_group], as_list=True + ) party_list = [d[0] for d in fetch_party_list] party_groups = {} for d in fetch_party_list: @@ -68,7 +82,7 @@ def get_party_addresses_and_contact(party_type, party, party_group): for party, details in iteritems(party_details): addresses = details.get("address", []) - contacts = details.get("contact", []) + contacts = details.get("contact", []) if not any([addresses, contacts]): result = [party] result.append(party_groups[party]) @@ -91,10 +105,11 @@ def get_party_addresses_and_contact(party_type, party, party_group): data.append(result) return data + def get_party_details(party_type, party_list, doctype, party_details): - filters = [ + filters = [ ["Dynamic Link", "link_doctype", "=", party_type], - ["Dynamic Link", "link_name", "in", party_list] + ["Dynamic Link", "link_name", "in", party_list], ] fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) @@ -105,15 +120,18 @@ def get_party_details(party_type, party_list, doctype, party_details): return party_details + def add_blank_columns_for(doctype): return ["" for field in field_map.get(doctype, [])] + def get_party_group(party_type): - if not party_type: return + if not party_type: + return group = { "Customer": "customer_group", "Supplier": "supplier_group", - "Sales Partner": "partner_type" + "Sales Partner": "partner_type", } return group[party_type] diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py index e702a51d0e7..5e763bb4364 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py @@ -7,7 +7,8 @@ from frappe.utils import flt def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() iwq_map = get_item_warehouse_quantity_map() @@ -20,32 +21,49 @@ def execute(filters=None): for wh, item_qty in warehouse.items(): total += 1 if item_map.get(sbom): - row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, - item_map.get(sbom).stock_uom, wh] + row = [ + sbom, + item_map.get(sbom).item_name, + item_map.get(sbom).description, + item_map.get(sbom).stock_uom, + wh, + ] available_qty = item_qty total_qty += flt(available_qty) row += [available_qty] if available_qty: data.append(row) - if (total == len(warehouse)): + if total == len(warehouse): row = ["", "", "Total", "", "", total_qty] data.append(row) return columns, data + def get_columns(): - columns = ["Item Code:Link/Item:100", "Item Name::100", "Description::120", \ - "UOM:Link/UOM:80", "Warehouse:Link/Warehouse:100", "Quantity::100"] + columns = [ + "Item Code:Link/Item:100", + "Item Name::100", + "Description::120", + "UOM:Link/UOM:80", + "Warehouse:Link/Warehouse:100", + "Quantity::100", + ] return columns + def get_item_details(): item_map = {} - for item in frappe.db.sql("""SELECT name, item_name, description, stock_uom - from `tabItem`""", as_dict=1): + for item in frappe.db.sql( + """SELECT name, item_name, description, stock_uom + from `tabItem`""", + as_dict=1, + ): item_map.setdefault(item.name, item) return item_map + def get_item_warehouse_quantity_map(): query = """SELECT parent, warehouse, MIN(qty) AS qty FROM (SELECT b.parent, bi.item_code, bi.warehouse, diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 2426cbb0b55..33badc37f8a 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -12,178 +12,177 @@ from frappe.utils import cint, cstr, getdate def execute(filters=None): common_columns = [ { - 'label': _('New Customers'), - 'fieldname': 'new_customers', - 'fieldtype': 'Int', - 'default': 0, - 'width': 125 + "label": _("New Customers"), + "fieldname": "new_customers", + "fieldtype": "Int", + "default": 0, + "width": 125, }, { - 'label': _('Repeat Customers'), - 'fieldname': 'repeat_customers', - 'fieldtype': 'Int', - 'default': 0, - 'width': 125 + "label": _("Repeat Customers"), + "fieldname": "repeat_customers", + "fieldtype": "Int", + "default": 0, + "width": 125, + }, + {"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "default": 0, "width": 100}, + { + "label": _("New Customer Revenue"), + "fieldname": "new_customer_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, { - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Int', - 'default': 0, - 'width': 100 + "label": _("Repeat Customer Revenue"), + "fieldname": "repeat_customer_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, { - 'label': _('New Customer Revenue'), - 'fieldname': 'new_customer_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 + "label": _("Total Revenue"), + "fieldname": "total_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, - { - 'label': _('Repeat Customer Revenue'), - 'fieldname': 'repeat_customer_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - }, - { - 'label': _('Total Revenue'), - 'fieldname': 'total_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - } ] - if filters.get('view_type') == 'Monthly': + if filters.get("view_type") == "Monthly": return get_data_by_time(filters, common_columns) else: return get_data_by_territory(filters, common_columns) + def get_data_by_time(filters, common_columns): # key yyyy-mm columns = [ - { - 'label': _('Year'), - 'fieldname': 'year', - 'fieldtype': 'Data', - 'width': 100 - }, - { - 'label': _('Month'), - 'fieldname': 'month', - 'fieldtype': 'Data', - 'width': 100 - }, + {"label": _("Year"), "fieldname": "year", "fieldtype": "Data", "width": 100}, + {"label": _("Month"), "fieldname": "month", "fieldtype": "Data", "width": 100}, ] columns += common_columns customers_in = get_customer_stats(filters) # time series - from_year, from_month, temp = filters.get('from_date').split('-') - to_year, to_month, temp = filters.get('to_date').split('-') + from_year, from_month, temp = filters.get("from_date").split("-") + to_year, to_month, temp = filters.get("to_date").split("-") - from_year, from_month, to_year, to_month = \ - cint(from_year), cint(from_month), cint(to_year), cint(to_month) + from_year, from_month, to_year, to_month = ( + cint(from_year), + cint(from_month), + cint(to_year), + cint(to_month), + ) out = [] - for year in range(from_year, to_year+1): - for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): - key = '{year}-{month:02d}'.format(year=year, month=month) + for year in range(from_year, to_year + 1): + for month in range( + from_month if year == from_year else 1, (to_month + 1) if year == to_year else 13 + ): + key = "{year}-{month:02d}".format(year=year, month=month) data = customers_in.get(key) - new = data['new'] if data else [0, 0.0] - repeat = data['repeat'] if data else [0, 0.0] - out.append({ - 'year': cstr(year), - 'month': calendar.month_name[month], - 'new_customers': new[0], - 'repeat_customers': repeat[0], - 'total': new[0] + repeat[0], - 'new_customer_revenue': new[1], - 'repeat_customer_revenue': repeat[1], - 'total_revenue': new[1] + repeat[1] - }) + new = data["new"] if data else [0, 0.0] + repeat = data["repeat"] if data else [0, 0.0] + out.append( + { + "year": cstr(year), + "month": calendar.month_name[month], + "new_customers": new[0], + "repeat_customers": repeat[0], + "total": new[0] + repeat[0], + "new_customer_revenue": new[1], + "repeat_customer_revenue": repeat[1], + "total_revenue": new[1] + repeat[1], + } + ) return columns, out + def get_data_by_territory(filters, common_columns): - columns = [{ - 'label': 'Territory', - 'fieldname': 'territory', - 'fieldtype': 'Link', - 'options': 'Territory', - 'width': 150 - }] + columns = [ + { + "label": "Territory", + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 150, + } + ] columns += common_columns customers_in = get_customer_stats(filters, tree_view=True) territory_dict = {} - for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1): - territory_dict.update({ - t.name: { - 'parent': t.parent_territory, - 'is_group': t.is_group - } - }) + for t in frappe.db.sql( + """SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft""", as_dict=1 + ): + territory_dict.update({t.name: {"parent": t.parent_territory, "is_group": t.is_group}}) depth_map = frappe._dict() for name, info in territory_dict.items(): - default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 + default = depth_map.get(info["parent"]) + 1 if info["parent"] else 0 depth_map.setdefault(name, default) data = [] for name, indent in depth_map.items(): condition = customers_in.get(name) - new = customers_in[name]['new'] if condition else [0, 0.0] - repeat = customers_in[name]['repeat'] if condition else [0, 0.0] + new = customers_in[name]["new"] if condition else [0, 0.0] + repeat = customers_in[name]["repeat"] if condition else [0, 0.0] temp = { - 'territory': name, - 'parent_territory': territory_dict[name]['parent'], - 'indent': indent, - 'new_customers': new[0], - 'repeat_customers': repeat[0], - 'total': new[0] + repeat[0], - 'new_customer_revenue': new[1], - 'repeat_customer_revenue': repeat[1], - 'total_revenue': new[1] + repeat[1], - 'bold': 0 if indent else 1 + "territory": name, + "parent_territory": territory_dict[name]["parent"], + "indent": indent, + "new_customers": new[0], + "repeat_customers": repeat[0], + "total": new[0] + repeat[0], + "new_customer_revenue": new[1], + "repeat_customer_revenue": repeat[1], + "total_revenue": new[1] + repeat[1], + "bold": 0 if indent else 1, } data.append(temp) - loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) + loop_data = sorted(data, key=lambda k: k["indent"], reverse=True) for ld in loop_data: - if ld['parent_territory']: - parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0] + if ld["parent_territory"]: + parent_data = [x for x in data if x["territory"] == ld["parent_territory"]][0] for key in parent_data.keys(): - if key not in ['indent', 'territory', 'parent_territory', 'bold']: + if key not in ["indent", "territory", "parent_territory", "bold"]: parent_data[key] += ld[key] return columns, data, None, None, None, 1 + def get_customer_stats(filters, tree_view=False): - """ Calculates number of new and repeated customers and revenue. """ - company_condition = '' - if filters.get('company'): - company_condition = ' and company=%(company)s' + """Calculates number of new and repeated customers and revenue.""" + company_condition = "" + if filters.get("company"): + company_condition = " and company=%(company)s" customers = [] customers_in = {} - for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` + for si in frappe.db.sql( + """select territory, posting_date, customer, base_grand_total from `tabSales Invoice` where docstatus=1 and posting_date <= %(to_date)s - {company_condition} order by posting_date'''.format(company_condition=company_condition), - filters, as_dict=1): + {company_condition} order by posting_date""".format( + company_condition=company_condition + ), + filters, + as_dict=1, + ): - key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') - new_or_repeat = 'new' if si.customer not in customers else 'repeat' - customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]}) + key = si.territory if tree_view else si.posting_date.strftime("%Y-%m") + new_or_repeat = "new" if si.customer not in customers else "repeat" + customers_in.setdefault(key, {"new": [0, 0.0], "repeat": [0, 0.0]}) # if filters.from_date <= si.posting_date.strftime('%Y-%m-%d'): if getdate(filters.from_date) <= getdate(si.posting_date): - customers_in[key][new_or_repeat][0] += 1 - customers_in[key][new_or_repeat][1] += si.base_grand_total - if new_or_repeat == 'new': + customers_in[key][new_or_repeat][0] += 1 + customers_in[key][new_or_repeat][1] += si.base_grand_total + if new_or_repeat == "new": customers.append(si.customer) return customers_in diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py index dd49f1355d2..1c10a374b6f 100644 --- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py +++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py @@ -10,8 +10,9 @@ from erpnext.selling.doctype.customer.customer import get_credit_limit, get_cust def execute(filters=None): - if not filters: filters = {} - #Check if customer id is according to naming series or customer name + if not filters: + filters = {} + # Check if customer id is according to naming series or customer name customer_naming_type = frappe.db.get_value("Selling Settings", None, "cust_master_name") columns = get_columns(customer_naming_type) @@ -22,8 +23,9 @@ def execute(filters=None): for d in customer_list: row = [] - outstanding_amt = get_customer_outstanding(d.name, filters.get("company"), - ignore_outstanding_sales_order=d.bypass_credit_limit_check) + outstanding_amt = get_customer_outstanding( + d.name, filters.get("company"), ignore_outstanding_sales_order=d.bypass_credit_limit_check + ) credit_limit = get_credit_limit(d.name, filters.get("company")) @@ -31,15 +33,24 @@ def execute(filters=None): if customer_naming_type == "Naming Series": row = [ - d.name, d.customer_name, credit_limit, - outstanding_amt, bal, d.bypass_credit_limit_check, - d.is_frozen, d.disabled + d.name, + d.customer_name, + credit_limit, + outstanding_amt, + bal, + d.bypass_credit_limit_check, + d.is_frozen, + d.disabled, ] else: row = [ - d.name, credit_limit, outstanding_amt, bal, - d.bypass_credit_limit_check, d.is_frozen, - d.disabled + d.name, + credit_limit, + outstanding_amt, + bal, + d.bypass_credit_limit_check, + d.is_frozen, + d.disabled, ] if credit_limit: @@ -47,6 +58,7 @@ def execute(filters=None): return columns, data + def get_columns(customer_naming_type): columns = [ _("Customer") + ":Link/Customer:120", @@ -63,6 +75,7 @@ def get_columns(customer_naming_type): return columns + def get_details(filters): sql_query = """SELECT diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py index e5f93543209..a58f40362ba 100644 --- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py +++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py @@ -30,32 +30,23 @@ def get_columns(filters=None): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 150 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Selling Rate"), - "fieldname": "selling_rate", - "fieldtype": "Currency" + "width": 150, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Selling Rate"), "fieldname": "selling_rate", "fieldtype": "Currency"}, { "label": _("Available Stock"), "fieldname": "available_stock", "fieldtype": "Float", - "width": 150 + "width": 150, }, { "label": _("Price List"), "fieldname": "price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 - } + "width": 120, + }, ] @@ -64,30 +55,33 @@ def get_data(filters=None): customer_details = get_customer_details(filters) items = get_selling_items(filters) - item_stock_map = frappe.get_all("Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code") + item_stock_map = frappe.get_all( + "Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code" + ) item_stock_map = {item.item_code: item.available for item in item_stock_map} for item in items: price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0 available_stock = item_stock_map.get(item.item_code) - data.append({ - "item_code": item.item_code, - "item_name": item.item_name, - "selling_rate": price_list_rate, - "price_list": customer_details.get("price_list"), - "available_stock": available_stock, - }) + data.append( + { + "item_code": item.item_code, + "item_name": item.item_name, + "selling_rate": price_list_rate, + "price_list": customer_details.get("price_list"), + "available_stock": available_stock, + } + ) return data def get_customer_details(filters): customer_details = get_party_details(party=filters.get("customer"), party_type="Customer") - customer_details.update({ - "company": get_default_company(), - "price_list": customer_details.get("selling_price_list") - }) + customer_details.update( + {"company": get_default_company(), "price_list": customer_details.get("selling_price_list")} + ) return customer_details @@ -98,6 +92,8 @@ def get_selling_items(filters): else: item_filters = {"is_sales_item": 1, "disabled": 0} - items = frappe.get_all("Item", filters=item_filters, fields=["item_code", "item_name"], order_by="item_name") + items = frappe.get_all( + "Item", filters=item_filters, fields=["item_code", "item_name"], order_by="item_name" + ) return items diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index d97e1c6dcb0..1b337fc495e 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -8,7 +8,8 @@ from frappe.utils import cint def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} days_since_last_order = filters.get("days_since_last_order") doctype = filters.get("doctype") @@ -22,10 +23,11 @@ def execute(filters=None): data = [] for cust in customers: if cint(cust[8]) >= cint(days_since_last_order): - cust.insert(7,get_last_sales_amt(cust[0], doctype)) + cust.insert(7, get_last_sales_amt(cust[0], doctype)) data.append(cust) return columns, data + def get_sales_details(doctype): cond = """sum(so.base_net_total) as 'total_order_considered', max(so.posting_date) as 'last_order_date', @@ -37,7 +39,8 @@ def get_sales_details(doctype): max(so.transaction_date) as 'last_order_date', DATEDIFF(CURDATE(), max(so.transaction_date)) as 'days_since_last_order'""" - return frappe.db.sql("""select + return frappe.db.sql( + """select cust.name, cust.customer_name, cust.territory, @@ -47,18 +50,29 @@ def get_sales_details(doctype): from `tabCustomer` cust, `tab{1}` so where cust.name = so.customer and so.docstatus = 1 group by cust.name - order by 'days_since_last_order' desc """.format(cond, doctype), as_list=1) + order by 'days_since_last_order' desc """.format( + cond, doctype + ), + as_list=1, + ) + def get_last_sales_amt(customer, doctype): cond = "posting_date" - if doctype =="Sales Order": + if doctype == "Sales Order": cond = "transaction_date" - res = frappe.db.sql("""select base_net_total from `tab{0}` + res = frappe.db.sql( + """select base_net_total from `tab{0}` where customer = %s and docstatus = 1 order by {1} desc - limit 1""".format(doctype, cond), customer) + limit 1""".format( + doctype, cond + ), + customer, + ) return res and res[0][0] or 0 + def get_columns(): return [ _("Customer") + ":Link/Customer:120", @@ -70,5 +84,5 @@ def get_columns(): _("Total Order Considered") + ":Currency:160", _("Last Order Amount") + ":Currency:160", _("Last Order Date") + ":Date:160", - _("Days Since Last Order") + "::160" + _("Days Since Last Order") + "::160", ] diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 4a245e1f778..12ca7b3ff83 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -20,6 +20,7 @@ def execute(filters=None): return columns, data, None, chart_data + def get_columns(filters): return [ { @@ -27,120 +28,85 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "item_code", "options": "Item", - "width": 120 - }, - { - "label": _("Item Name"), - "fieldtype": "Data", - "fieldname": "item_name", - "width": 140 + "width": 120, }, + {"label": _("Item Name"), "fieldtype": "Data", "fieldname": "item_name", "width": 140}, { "label": _("Item Group"), "fieldtype": "Link", "fieldname": "item_group", "options": "Item Group", - "width": 120 - }, - { - "label": _("Description"), - "fieldtype": "Data", - "fieldname": "description", - "width": 150 - }, - { - "label": _("Quantity"), - "fieldtype": "Float", - "fieldname": "quantity", - "width": 150 - }, - { - "label": _("UOM"), - "fieldtype": "Link", - "fieldname": "uom", - "options": "UOM", - "width": 100 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "options": "Currency", - "width": 120 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "options": "Currency", - "width": 120 + "width": 120, }, + {"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150}, + {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150}, + {"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100}, + {"label": _("Rate"), "fieldname": "rate", "options": "Currency", "width": 120}, + {"label": _("Amount"), "fieldname": "amount", "options": "Currency", "width": 120}, { "label": _("Sales Order"), "fieldtype": "Link", "fieldname": "sales_order", "options": "Sales Order", - "width": 100 + "width": 100, }, { "label": _("Transaction Date"), "fieldtype": "Date", "fieldname": "transaction_date", - "width": 90 + "width": 90, }, { "label": _("Customer"), "fieldtype": "Link", "fieldname": "customer", "options": "Customer", - "width": 100 - }, - { - "label": _("Customer Name"), - "fieldtype": "Data", - "fieldname": "customer_name", - "width": 140 + "width": 100, }, + {"label": _("Customer Name"), "fieldtype": "Data", "fieldname": "customer_name", "width": 140}, { "label": _("Customer Group"), "fieldtype": "Link", "fieldname": "customer_group", "options": "Customer Group", - "width": 120 + "width": 120, }, { "label": _("Territory"), "fieldtype": "Link", "fieldname": "territory", "options": "Territory", - "width": 100 + "width": 100, }, { "label": _("Project"), "fieldtype": "Link", "fieldname": "project", "options": "Project", - "width": 100 + "width": 100, }, { "label": _("Delivered Quantity"), "fieldtype": "Float", "fieldname": "delivered_quantity", - "width": 150 + "width": 150, }, { "label": _("Billed Amount"), "fieldtype": "currency", "fieldname": "billed_amount", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldtype": "Link", "fieldname": "company", "options": "Company", - "width": 100 - } + "width": 100, + }, ] + def get_data(filters): data = [] @@ -156,74 +122,75 @@ def get_data(filters): customer_record = customer_details.get(record.customer) item_record = item_details.get(record.item_code) row = { - "item_code": record.item_code, - "item_name": item_record.item_name, - "item_group": item_record.item_group, - "description": record.description, - "quantity": record.qty, - "uom": record.uom, - "rate": record.base_rate, - "amount": record.base_amount, - "sales_order": record.name, - "transaction_date": record.transaction_date, - "customer": record.customer, - "customer_name": customer_record.customer_name, - "customer_group": customer_record.customer_group, - "territory": record.territory, - "project": record.project, - "delivered_quantity": flt(record.delivered_qty), - "billed_amount": flt(record.billed_amt), - "company": record.company + "item_code": record.get("item_code"), + "item_name": item_record.get("item_name"), + "item_group": item_record.get("item_group"), + "description": record.get("description"), + "quantity": record.get("qty"), + "uom": record.get("uom"), + "rate": record.get("base_rate"), + "amount": record.get("base_amount"), + "sales_order": record.get("name"), + "transaction_date": record.get("transaction_date"), + "customer": record.get("customer"), + "customer_name": customer_record.get("customer_name"), + "customer_group": customer_record.get("customer_group"), + "territory": record.get("territory"), + "project": record.get("project"), + "delivered_quantity": flt(record.get("delivered_qty")), + "billed_amount": flt(record.get("billed_amt")), + "company": record.get("company"), } data.append(row) return data + def get_conditions(filters): - conditions = '' - if filters.get('item_group'): - conditions += "AND so_item.item_group = %s" %frappe.db.escape(filters.item_group) + conditions = "" + if filters.get("item_group"): + conditions += "AND so_item.item_group = %s" % frappe.db.escape(filters.item_group) - if filters.get('from_date'): - conditions += "AND so.transaction_date >= '%s'" %filters.from_date + if filters.get("from_date"): + conditions += "AND so.transaction_date >= '%s'" % filters.from_date - if filters.get('to_date'): - conditions += "AND so.transaction_date <= '%s'" %filters.to_date + if filters.get("to_date"): + conditions += "AND so.transaction_date <= '%s'" % filters.to_date if filters.get("item_code"): - conditions += "AND so_item.item_code = %s" %frappe.db.escape(filters.item_code) + conditions += "AND so_item.item_code = %s" % frappe.db.escape(filters.item_code) if filters.get("customer"): - conditions += "AND so.customer = %s" %frappe.db.escape(filters.customer) + conditions += "AND so.customer = %s" % frappe.db.escape(filters.customer) return conditions + def get_customer_details(): - details = frappe.get_all("Customer", - fields=["name", "customer_name", "customer_group"]) + details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"]) customer_details = {} for d in details: - customer_details.setdefault(d.name, frappe._dict({ - "customer_name": d.customer_name, - "customer_group": d.customer_group - })) + customer_details.setdefault( + d.name, frappe._dict({"customer_name": d.customer_name, "customer_group": d.customer_group}) + ) return customer_details + def get_item_details(): - details = frappe.db.get_all("Item", - fields=["item_code", "item_name", "item_group"]) + details = frappe.db.get_all("Item", fields=["name", "item_name", "item_group"]) item_details = {} for d in details: - item_details.setdefault(d.item_code, frappe._dict({ - "item_name": d.item_name, - "item_group": d.item_group - })) + item_details.setdefault( + d.name, frappe._dict({"item_name": d.item_name, "item_group": d.item_group}) + ) return item_details + def get_sales_order_details(company_list, filters): conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT so_item.item_code, so_item.description, so_item.qty, so_item.uom, so_item.base_rate, so_item.base_amount, @@ -236,7 +203,13 @@ def get_sales_order_details(company_list, filters): so.name = so_item.parent AND so.company in ({0}) AND so.docstatus = 1 {1} - """.format(','.join(["%s"] * len(company_list)), conditions), tuple(company_list), as_dict=1) + """.format( + ",".join(["%s"] * len(company_list)), conditions + ), + tuple(company_list), + as_dict=1, + ) + def get_chart_data(data): item_wise_sales_map = {} @@ -250,21 +223,19 @@ def get_chart_data(data): item_wise_sales_map[item_key] = flt(item_wise_sales_map[item_key]) + flt(row.get("amount")) - item_wise_sales_map = { item: value for item, value in (sorted(item_wise_sales_map.items(), key = lambda i: i[1], reverse=True))} + item_wise_sales_map = { + item: value + for item, value in (sorted(item_wise_sales_map.items(), key=lambda i: i[1], reverse=True)) + } for key in item_wise_sales_map: labels.append(key) datapoints.append(item_wise_sales_map[key]) return { - "data" : { - "labels" : labels[:30], # show max of 30 items in chart - "datasets" : [ - { - "name" : _(" Total Sales Amount"), - "values" : datapoints[:30] - } - ] + "data": { + "labels": labels[:30], # show max of 30 items in chart + "datasets": [{"name": _(" Total Sales Amount"), "values": datapoints[:30]}], }, - "type" : "bar" + "type": "bar", } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 0e36b3fe3d2..c068ae3b5a4 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -27,28 +27,55 @@ function get_filters() { "default": frappe.datetime.get_today() }, { - "fieldname":"sales_order", - "label": __("Sales Order"), - "fieldtype": "MultiSelectList", + "fieldname":"customer_group", + "label": __("Customer Group"), + "fieldtype": "Link", "width": 100, - "options": "Sales Order", - "get_data": function(txt) { - return frappe.db.get_link_options("Sales Order", txt, this.filters()); - }, - "filters": () => { - return { - docstatus: 1, - payment_terms_template: ['not in', ['']], - company: frappe.query_report.get_filter_value("company"), - transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]] + "options": "Customer Group", + }, + { + "fieldname":"customer", + "label": __("Customer"), + "fieldtype": "Link", + "width": 100, + "options": "Customer", + "get_query": () => { + var customer_group = frappe.query_report.get_filter_value('customer_group'); + return{ + "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", + "filters": [ + ['Customer', 'disabled', '=', '0'], + ['Customer Group','name', '=', customer_group] + ] + } + } + }, + { + "fieldname":"item_group", + "label": __("Item Group"), + "fieldtype": "Link", + "width": 100, + "options": "Item Group", + + }, + { + "fieldname":"item", + "label": __("Item"), + "fieldtype": "Link", + "width": 100, + "options": "Item", + "get_query": () => { + var item_group = frappe.query_report.get_filter_value('item_group'); + return{ + "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", + "filters": [ + ['Item', 'disabled', '=', '0'], + ['Item Group','name', '=', item_group] + ] } - }, - on_change: function(){ - frappe.query_report.refresh(); } } ] - return filters; } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index e6a56eea310..cb22fb6a80f 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -3,7 +3,7 @@ import frappe from frappe import _, qb, query_builder -from frappe.query_builder import functions +from frappe.query_builder import Criterion, functions def get_columns(): @@ -14,6 +14,12 @@ def get_columns(): "fieldtype": "Link", "options": "Sales Order", }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + }, { "label": _("Posting Date"), "fieldname": "submitted", @@ -62,16 +68,60 @@ def get_columns(): "fieldname": "status", "fieldtype": "Data", }, - { - "label": _("Currency"), - "fieldname": "currency", - "fieldtype": "Currency", - "hidden": 1 - } + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "hidden": 1}, ] return columns +def get_descendants_of(doctype, group_name): + group_doc = qb.DocType(doctype) + # get lft and rgt of group node + lft, rgt = ( + qb.from_(group_doc).select(group_doc.lft, group_doc.rgt).where(group_doc.name == group_name) + ).run()[0] + + # get all children of group node + query = ( + qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt)) + ) + + child_nodes = [] + for x in query.run(): + child_nodes.append(x[0]) + + return child_nodes + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_customers_or_items(doctype, txt, searchfield, start, page_len, filters): + filter_list = [] + if isinstance(filters, list): + for item in filters: + if item[0] == doctype: + filter_list.append(item) + elif item[0] == "Customer Group": + if item[3] != "": + filter_list.append( + [doctype, "customer_group", "in", get_descendants_of("Customer Group", item[3])] + ) + elif item[0] == "Item Group": + if item[3] != "": + filter_list.append([doctype, "item_group", "in", get_descendants_of("Item Group", item[3])]) + + if searchfield and txt: + filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt]) + + return frappe.desk.reportview.execute( + doctype, + filters=filter_list, + fields=["name", "customer_group"] if doctype == "Customer" else ["name", "item_group"], + limit_start=start, + limit_page_length=page_len, + as_list=True, + ) + + def get_conditions(filters): """ Convert filter options to conditions used in query @@ -84,11 +134,37 @@ def get_conditions(filters): conditions.start_date = filters.period_start_date or frappe.utils.add_months( conditions.end_date, -1 ) - conditions.sales_order = filters.sales_order or [] return conditions +def build_filter_criterions(filters): + filters = frappe._dict(filters) if filters else frappe._dict({}) + qb_criterions = [] + + if filters.customer_group: + qb_criterions.append( + qb.DocType("Sales Order").customer_group.isin( + get_descendants_of("Customer Group", filters.customer_group) + ) + ) + + if filters.customer: + qb_criterions.append(qb.DocType("Sales Order").customer == filters.customer) + + if filters.item_group: + qb_criterions.append( + qb.DocType("Sales Order Item").item_group.isin( + get_descendants_of("Item Group", filters.item_group) + ) + ) + + if filters.item: + qb_criterions.append(qb.DocType("Sales Order Item").item_code == filters.item) + + return qb_criterions + + def get_so_with_invoices(filters): """ Get Sales Order with payment terms template with their associated Invoices @@ -97,16 +173,23 @@ def get_so_with_invoices(filters): so = qb.DocType("Sales Order") ps = qb.DocType("Payment Schedule") + soi = qb.DocType("Sales Order Item") + + conditions = get_conditions(filters) + filter_criterions = build_filter_criterions(filters) + datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"]) ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) - conditions = get_conditions(filters) query_so = ( qb.from_(so) + .join(soi) + .on(soi.parent == so.name) .join(ps) .on(ps.parent == so.name) .select( so.name, + so.customer, so.transaction_date.as_("submitted"), ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), ps.payment_term, @@ -122,12 +205,10 @@ def get_so_with_invoices(filters): & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) ) + .where(Criterion.all(filter_criterions)) .orderby(so.name, so.transaction_date, ps.due_date) ) - if conditions.sales_order != []: - query_so = query_so.where(so.name.isin(conditions.sales_order)) - sorders = query_so.run(as_dict=True) invoices = [] @@ -156,7 +237,7 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): """ for so in sales_orders: - so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + so.currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") so.invoices = "" for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.base_payment_amount - so.paid_amount > 0: @@ -182,8 +263,14 @@ def prepare_chart(s_orders): "data": { "labels": [term.payment_term for term in s_orders], "datasets": [ - {"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],}, - {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, + { + "name": "Payment Amount", + "values": [x.base_payment_amount for x in s_orders], + }, + { + "name": "Paid Amount", + "values": [x.paid_amount for x in s_orders], + }, ], }, "type": "bar", diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index cad41e1dc03..9d542f5079c 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -1,6 +1,7 @@ import datetime import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice @@ -9,12 +10,14 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s execute, ) from erpnext.stock.doctype.item.test_item import create_item -from erpnext.tests.utils import ERPNextTestCase -test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template", "Customer"] -class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): +class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): + def tearDown(self): + frappe.db.rollback() + def create_payment_terms_template(self): # create template for 50-50 payments template = None @@ -48,9 +51,9 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): template.insert() self.template = template - def test_payment_terms_status(self): + def test_01_payment_terms_status(self): self.create_payment_terms_template() - item = create_item(item_code="_Test Excavator", is_stock_item=0) + item = create_item(item_code="_Test Excavator 1", is_stock_item=0) so = make_sales_order( transaction_date="2021-06-15", delivery_date=add_days("2021-06-15", -30), @@ -78,13 +81,14 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "company": "_Test Company", "period_start_date": "2021-06-01", "period_end_date": "2021-06-30", - "sales_order": [so.name], + "item": item.item_code, } ) expected_value = [ { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Completed", "payment_term": None, @@ -94,10 +98,11 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "currency": "INR", "base_payment_amount": 500000.0, "paid_amount": 500000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Partly Paid", "payment_term": None, @@ -107,32 +112,36 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "currency": "INR", "base_payment_amount": 500000.0, "paid_amount": 100000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, ] self.assertEqual(data, expected_value) def create_exchange_rate(self, date): # make an entry in Currency Exchange list. serves as a static exchange rate - if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}): + if frappe.db.exists( + {"doctype": "Currency Exchange", "date": date, "from_currency": "USD", "to_currency": "INR"} + ): return else: - doc = frappe.get_doc({ - 'doctype': "Currency Exchange", - 'date': date, - 'from_currency': 'USD', - 'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'), - 'exchange_rate': 70, - 'for_buying': True, - 'for_selling': True - }) + doc = frappe.get_doc( + { + "doctype": "Currency Exchange", + "date": date, + "from_currency": "USD", + "to_currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), + "exchange_rate": 70, + "for_buying": True, + "for_selling": True, + } + ) doc.insert() - def test_alternate_currency(self): + def test_02_alternate_currency(self): transaction_date = "2021-06-15" self.create_payment_terms_template() self.create_exchange_rate(transaction_date) - item = create_item(item_code="_Test Excavator", is_stock_item=0) + item = create_item(item_code="_Test Excavator 2", is_stock_item=0) so = make_sales_order( transaction_date=transaction_date, currency="USD", @@ -162,7 +171,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "company": "_Test Company", "period_start_date": "2021-06-01", "period_end_date": "2021-06-30", - "sales_order": [so.name], + "item": item.item_code, } ) @@ -170,29 +179,162 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): expected_value = [ { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Completed", "payment_term": None, "description": "_Test 50-50", "due_date": datetime.date(2021, 6, 30), "invoice_portion": 50.0, - "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), "base_payment_amount": 3500000.0, "paid_amount": 3500000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Partly Paid", "payment_term": None, "description": "_Test 50-50", "due_date": datetime.date(2021, 7, 15), "invoice_portion": 50.0, - "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), "base_payment_amount": 3500000.0, "paid_amount": 700000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, ] self.assertEqual(data, expected_value) + + def test_03_group_filters(self): + transaction_date = "2021-06-15" + self.create_payment_terms_template() + item1 = create_item(item_code="_Test Excavator 1", is_stock_item=0) + item1.item_group = "Products" + item1.save() + + so1 = make_sales_order( + transaction_date=transaction_date, + delivery_date=add_days(transaction_date, -30), + item=item1.item_code, + qty=1, + rate=1000000, + do_not_save=True, + ) + so1.po_no = "" + so1.taxes_and_charges = "" + so1.taxes = "" + so1.payment_terms_template = self.template.name + so1.save() + so1.submit() + + item2 = create_item(item_code="_Test Steel", is_stock_item=0) + item2.item_group = "Raw Material" + item2.save() + + so2 = make_sales_order( + customer="_Test Customer 1", + transaction_date=transaction_date, + delivery_date=add_days(transaction_date, -30), + item=item2.item_code, + qty=100, + rate=1000, + do_not_save=True, + ) + so2.po_no = "" + so2.taxes_and_charges = "" + so2.taxes = "" + so2.payment_terms_template = self.template.name + so2.save() + so2.submit() + + base_filters = { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + } + + expected_value_so1 = [ + { + "name": so1.name, + "customer": so1.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 500000.0, + "paid_amount": 0.0, + "invoices": "", + }, + { + "name": so1.name, + "customer": so1.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 500000.0, + "paid_amount": 0.0, + "invoices": "", + }, + ] + + expected_value_so2 = [ + { + "name": so2.name, + "customer": so2.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 50000.0, + "paid_amount": 0.0, + "invoices": "", + }, + { + "name": so2.name, + "customer": so2.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 50000.0, + "paid_amount": 0.0, + "invoices": "", + }, + ] + + group_filters = [ + {"customer_group": "All Customer Groups"}, + {"item_group": "All Item Groups"}, + {"item_group": "Products"}, + {"item_group": "Raw Material"}, + ] + + expected_values_for_group_filters = [ + expected_value_so1 + expected_value_so2, + expected_value_so1 + expected_value_so2, + expected_value_so1, + expected_value_so2, + ] + + for idx, g in enumerate(group_filters, 0): + # build filter + filters = frappe._dict({}).update(base_filters).update(g) + with self.subTest(filters=filters): + columns, data, message, chart = execute(filters) + self.assertEqual(data, expected_values_for_group_filters[idx]) diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py index 01421e8fd0e..cc1055c787d 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py @@ -12,6 +12,7 @@ def execute(filters=None): data = get_data() return columns, data + def get_columns(): columns = [ { @@ -19,80 +20,37 @@ def get_columns(): "options": "Item", "fieldname": "item_code", "fieldtype": "Link", - "width": 200 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 140 + "width": 200, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 140}, { "label": _("S.O. No."), "options": "Sales Order", "fieldname": "sales_order_no", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 140 + "width": 140, }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 140}, { "label": _("Material Request"), "fieldname": "material_request", "fieldtype": "Data", - "width": 140 + "width": 140, }, - { - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("Territory"), - "fieldname": "territory", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("SO Qty"), - "fieldname": "so_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Requested Qty"), - "fieldname": "requested_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Pending Qty"), - "fieldname": "pending_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Data", - "width": 140 - } + {"label": _("Customer"), "fieldname": "customer", "fieldtype": "Data", "width": 140}, + {"label": _("Territory"), "fieldname": "territory", "fieldtype": "Data", "width": 140}, + {"label": _("SO Qty"), "fieldname": "so_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Requested Qty"), "fieldname": "requested_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Pending Qty"), "fieldname": "pending_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Company"), "fieldname": "company", "fieldtype": "Data", "width": 140}, ] return columns + def get_data(): - sales_order_entry = frappe.db.sql(""" + sales_order_entry = frappe.db.sql( + """ SELECT so_item.item_code, so_item.item_name, @@ -110,88 +68,94 @@ def get_data(): and so.status not in ("Closed","Completed","Cancelled") GROUP BY so.name,so_item.item_code - """, as_dict = 1) + """, + as_dict=1, + ) sales_orders = [row.name for row in sales_order_entry] - mr_records = frappe.get_all("Material Request Item", + mr_records = frappe.get_all( + "Material Request Item", {"sales_order": ("in", sales_orders), "docstatus": 1}, - ["parent", "qty", "sales_order", "item_code"]) + ["parent", "qty", "sales_order", "item_code"], + ) bundled_item_map = get_packed_items(sales_orders) - item_with_product_bundle = get_items_with_product_bundle([row.item_code for row in sales_order_entry]) + item_with_product_bundle = get_items_with_product_bundle( + [row.item_code for row in sales_order_entry] + ) materials_request_dict = {} for record in mr_records: key = (record.sales_order, record.item_code) if key not in materials_request_dict: - materials_request_dict.setdefault(key, { - 'qty': 0, - 'material_requests': [record.parent] - }) + materials_request_dict.setdefault(key, {"qty": 0, "material_requests": [record.parent]}) details = materials_request_dict.get(key) - details['qty'] += record.qty + details["qty"] += record.qty - if record.parent not in details.get('material_requests'): - details['material_requests'].append(record.parent) + if record.parent not in details.get("material_requests"): + details["material_requests"].append(record.parent) pending_so = [] for so in sales_order_entry: if so.item_code not in item_with_product_bundle: material_requests_against_so = materials_request_dict.get((so.name, so.item_code)) or {} # check for pending sales order - if flt(so.total_qty) > flt(material_requests_against_so.get('qty')): + if flt(so.total_qty) > flt(material_requests_against_so.get("qty")): so_record = { "item_code": so.item_code, "item_name": so.item_name, "description": so.description, "sales_order_no": so.name, "date": so.transaction_date, - "material_request": ','.join(material_requests_against_so.get('material_requests', [])), + "material_request": ",".join(material_requests_against_so.get("material_requests", [])), "customer": so.customer, "territory": so.territory, "so_qty": so.total_qty, - "requested_qty": material_requests_against_so.get('qty'), - "pending_qty": so.total_qty - flt(material_requests_against_so.get('qty')), - "company": so.company + "requested_qty": material_requests_against_so.get("qty"), + "pending_qty": so.total_qty - flt(material_requests_against_so.get("qty")), + "company": so.company, } pending_so.append(so_record) else: for item in bundled_item_map.get((so.name, so.item_code), []): material_requests_against_so = materials_request_dict.get((so.name, item.item_code)) or {} - if flt(item.qty) > flt(material_requests_against_so.get('qty')): + if flt(item.qty) > flt(material_requests_against_so.get("qty")): so_record = { "item_code": item.item_code, "item_name": item.item_name, "description": item.description, "sales_order_no": so.name, "date": so.transaction_date, - "material_request": ','.join(material_requests_against_so.get('material_requests', [])), + "material_request": ",".join(material_requests_against_so.get("material_requests", [])), "customer": so.customer, "territory": so.territory, "so_qty": item.qty, - "requested_qty": material_requests_against_so.get('qty', 0), - "pending_qty": item.qty - flt(material_requests_against_so.get('qty', 0)), - "company": so.company + "requested_qty": material_requests_against_so.get("qty", 0), + "pending_qty": item.qty - flt(material_requests_against_so.get("qty", 0)), + "company": so.company, } pending_so.append(so_record) - return pending_so + def get_items_with_product_bundle(item_list): - bundled_items = frappe.get_all("Product Bundle", filters = [ - ("new_item_code", "IN", item_list) - ], fields = ["new_item_code"]) + bundled_items = frappe.get_all( + "Product Bundle", filters=[("new_item_code", "IN", item_list)], fields=["new_item_code"] + ) return [d.new_item_code for d in bundled_items] + def get_packed_items(sales_order_list): - packed_items = frappe.get_all("Packed Item", filters = [ - ("parent", "IN", sales_order_list) - ], fields = ["parent_item", "item_code", "qty", "item_name", "description", "parent"]) + packed_items = frappe.get_all( + "Packed Item", + filters=[("parent", "IN", sales_order_list)], + fields=["parent_item", "item_code", "qty", "item_name", "description", "parent"], + ) bundled_item_map = frappe._dict() for d in packed_items: diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py index d62915fc66d..e4ad5c622b8 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py @@ -2,6 +2,7 @@ # For license information, please see license.txt +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months, nowdate from erpnext.selling.doctype.sales_order.sales_order import make_material_request @@ -9,22 +10,21 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import ( execute, ) -from erpnext.tests.utils import ERPNextTestCase -class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase): - def test_result_for_partial_material_request(self): - so = make_sales_order() - mr=make_material_request(so.name) - mr.items[0].qty = 4 - mr.schedule_date = add_months(nowdate(),1) - mr.submit() - report = execute() - l = len(report[1]) - self.assertEqual((so.items[0].qty - mr.items[0].qty), report[1][l-1]['pending_qty']) +class TestPendingSOItemsForPurchaseRequest(FrappeTestCase): + def test_result_for_partial_material_request(self): + so = make_sales_order() + mr = make_material_request(so.name) + mr.items[0].qty = 4 + mr.schedule_date = add_months(nowdate(), 1) + mr.submit() + report = execute() + l = len(report[1]) + self.assertEqual((so.items[0].qty - mr.items[0].qty), report[1][l - 1]["pending_qty"]) - def test_result_for_so_item(self): - so = make_sales_order() - report = execute() - l = len(report[1]) - self.assertEqual(so.items[0].qty, report[1][l-1]['pending_qty']) + def test_result_for_so_item(self): + so = make_sales_order() + report = execute() + l = len(report[1]) + self.assertEqual(so.items[0].qty, report[1][l - 1]["pending_qty"]) diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index 047b09081af..dfcec22cca2 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Quotation") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, conditions, filters): if not (data and conditions): return [] @@ -29,32 +31,27 @@ def get_chart_data(data, conditions, filters): # fetch only periodic columns as labels columns = conditions.get("columns")[start:-2][1::2] - labels = [column.split(':')[0] for column in columns] + labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start-1]: + if not row[start - 1]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[1::2] for i in range(len(row)): datapoints[i] += row[i] return { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : _("{0}").format(filters.get("period")) + _(" Quoted Amount"), - "values" : datapoints - } - ] + "data": { + "labels": labels, + "datasets": [ + {"name": _("{0}").format(filters.get("period")) + _(" Quoted Amount"), "values": datapoints} + ], }, - "type" : "line", - "lineOptions": { - "regionFill": 1 - } + "type": "line", + "lineOptions": {"regionFill": 1}, } diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index a380f842ebf..f1aada3e185 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -13,12 +13,29 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): return Analytics(filters).run() + class Analytics(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) - self.date_field = 'transaction_date' \ - if self.filters.doc_type in ['Sales Order', 'Purchase Order'] else 'posting_date' - self.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + self.date_field = ( + "transaction_date" + if self.filters.doc_type in ["Sales Order", "Purchase Order"] + else "posting_date" + ) + self.months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] self.get_period_date_ranges() def run(self): @@ -35,52 +52,52 @@ class Analytics(object): return self.columns, self.data, None, self.chart, None, skip_total_row def get_columns(self): - self.columns = [{ + self.columns = [ + { "label": _(self.filters.tree_type), "options": self.filters.tree_type if self.filters.tree_type != "Order Type" else "", "fieldname": "entity", "fieldtype": "Link" if self.filters.tree_type != "Order Type" else "Data", - "width": 140 if self.filters.tree_type != "Order Type" else 200 - }] + "width": 140 if self.filters.tree_type != "Order Type" else 200, + } + ] if self.filters.tree_type in ["Customer", "Supplier", "Item"]: - self.columns.append({ - "label": _(self.filters.tree_type + " Name"), - "fieldname": "entity_name", - "fieldtype": "Data", - "width": 140 - }) + self.columns.append( + { + "label": _(self.filters.tree_type + " Name"), + "fieldname": "entity_name", + "fieldtype": "Data", + "width": 140, + } + ) if self.filters.tree_type == "Item": - self.columns.append({ - "label": _("UOM"), - "fieldname": 'stock_uom', - "fieldtype": "Link", - "options": "UOM", - "width": 100 - }) + self.columns.append( + { + "label": _("UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, + } + ) for end_date in self.periodic_daterange: period = self.get_period(end_date) - self.columns.append({ - "label": _(period), - "fieldname": scrub(period), - "fieldtype": "Float", - "width": 120 - }) + self.columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} + ) - self.columns.append({ - "label": _("Total"), - "fieldname": "total", - "fieldtype": "Float", - "width": 120 - }) + self.columns.append( + {"label": _("Total"), "fieldname": "total", "fieldtype": "Float", "width": 120} + ) def get_data(self): if self.filters.tree_type in ["Customer", "Supplier"]: self.get_sales_transactions_based_on_customers_or_suppliers() self.get_rows() - elif self.filters.tree_type == 'Item': + elif self.filters.tree_type == "Item": self.get_sales_transactions_based_on_items() self.get_rows() @@ -88,7 +105,7 @@ class Analytics(object): self.get_sales_transactions_based_on_customer_or_territory_group() self.get_rows_by_group() - elif self.filters.tree_type == 'Item Group': + elif self.filters.tree_type == "Item Group": self.get_sales_transactions_based_on_item_group() self.get_rows_by_group() @@ -104,40 +121,45 @@ class Analytics(object): self.get_rows() def get_sales_transactions_based_on_order_type(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total" else: value_field = "total_qty" - self.entries = frappe.db.sql(""" select s.order_type as entity, s.{value_field} as value_field, s.{date_field} + self.entries = frappe.db.sql( + """ select s.order_type as entity, s.{value_field} as value_field, s.{date_field} from `tab{doctype}` s where s.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s and ifnull(s.order_type, '') != '' order by s.order_type - """ - .format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.get_teams() def get_sales_transactions_based_on_customers_or_suppliers(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" - if self.filters.tree_type == 'Customer': + if self.filters.tree_type == "Customer": entity = "customer as entity" entity_name = "customer_name as entity_name" else: entity = "supplier as entity" entity_name = "supplier_name as entity_name" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity, entity_name, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) self.entity_names = {} @@ -146,80 +168,91 @@ class Analytics(object): def get_sales_transactions_based_on_items(self): - if self.filters["value_quantity"] == 'Value': - value_field = 'base_amount' + if self.filters["value_quantity"] == "Value": + value_field = "base_amount" else: - value_field = 'stock_qty' + value_field = "stock_qty" - self.entries = frappe.db.sql(""" + self.entries = frappe.db.sql( + """ select i.item_code as entity, i.item_name as entity_name, i.stock_uom, i.{value_field} as value_field, s.{date_field} from `tab{doctype} Item` i , `tab{doctype}` s where s.name = i.parent and i.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s - """ - .format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.entity_names = {} for d in self.entries: self.entity_names.setdefault(d.entity, d.entity_name) def get_sales_transactions_based_on_customer_or_territory_group(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" - if self.filters.tree_type == 'Customer Group': - entity_field = 'customer_group as entity' - elif self.filters.tree_type == 'Supplier Group': + if self.filters.tree_type == "Customer Group": + entity_field = "customer_group as entity" + elif self.filters.tree_type == "Supplier Group": entity_field = "supplier as entity" self.get_supplier_parent_child_map() else: entity_field = "territory as entity" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity_field, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) self.get_groups() def get_sales_transactions_based_on_item_group(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_amount" else: value_field = "qty" - self.entries = frappe.db.sql(""" + self.entries = frappe.db.sql( + """ select i.item_group as entity, i.{value_field} as value_field, s.{date_field} from `tab{doctype} Item` i , `tab{doctype}` s where s.name = i.parent and i.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s - """.format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.get_groups() def get_sales_transactions_based_on_project(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" entity = "project as entity" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, "project": ["!=", ""], - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) def get_rows(self): @@ -229,7 +262,7 @@ class Analytics(object): for entity, period_data in iteritems(self.entity_periodic_data): row = { "entity": entity, - "entity_name": self.entity_names.get(entity) if hasattr(self, 'entity_names') else None + "entity_name": self.entity_names.get(entity) if hasattr(self, "entity_names") else None, } total = 0 for end_date in self.periodic_daterange: @@ -250,10 +283,7 @@ class Analytics(object): out = [] for d in reversed(self.group_entries): - row = { - "entity": d.name, - "indent": self.depth_map.get(d.name) - } + row = {"entity": d.name, "indent": self.depth_map.get(d.name)} total = 0 for end_date in self.periodic_daterange: period = self.get_period(end_date) @@ -280,14 +310,14 @@ class Analytics(object): self.entity_periodic_data[d.entity][period] += flt(d.value_field) if self.filters.tree_type == "Item": - self.entity_periodic_data[d.entity]['stock_uom'] = d.stock_uom + self.entity_periodic_data[d.entity]["stock_uom"] = d.stock_uom def get_period(self, posting_date): - if self.filters.range == 'Weekly': + if self.filters.range == "Weekly": period = "Week " + str(posting_date.isocalendar()[1]) + " " + str(posting_date.year) - elif self.filters.range == 'Monthly': + elif self.filters.range == "Monthly": period = str(self.months[posting_date.month - 1]) + " " + str(posting_date.year) - elif self.filters.range == 'Quarterly': + elif self.filters.range == "Quarterly": period = "Quarter " + str(((posting_date.month - 1) // 3) + 1) + " " + str(posting_date.year) else: year = get_fiscal_year(posting_date, company=self.filters.company) @@ -296,16 +326,14 @@ class Analytics(object): def get_period_date_ranges(self): from dateutil.relativedelta import MO, relativedelta + from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) - increment = { - "Monthly": 1, - "Quarterly": 3, - "Half-Yearly": 6, - "Yearly": 12 - }.get(self.filters.range, 1) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( + self.filters.range, 1 + ) - if self.filters.range in ['Monthly', 'Quarterly']: + if self.filters.range in ["Monthly", "Quarterly"]: from_date = from_date.replace(day=1) elif self.filters.range == "Yearly": from_date = get_fiscal_year(from_date)[1] @@ -330,19 +358,23 @@ class Analytics(object): def get_groups(self): if self.filters.tree_type == "Territory": - parent = 'parent_territory' + parent = "parent_territory" if self.filters.tree_type == "Customer Group": - parent = 'parent_customer_group' + parent = "parent_customer_group" if self.filters.tree_type == "Item Group": - parent = 'parent_item_group' + parent = "parent_item_group" if self.filters.tree_type == "Supplier Group": - parent = 'parent_supplier_group' + parent = "parent_supplier_group" self.depth_map = frappe._dict() - self.group_entries = frappe.db.sql("""select name, lft, rgt , {parent} as parent - from `tab{tree}` order by lft""" - .format(tree=self.filters.tree_type, parent=parent), as_dict=1) + self.group_entries = frappe.db.sql( + """select name, lft, rgt , {parent} as parent + from `tab{tree}` order by lft""".format( + tree=self.filters.tree_type, parent=parent + ), + as_dict=1, + ) for d in self.group_entries: if d.parent: @@ -353,11 +385,15 @@ class Analytics(object): def get_teams(self): self.depth_map = frappe._dict() - self.group_entries = frappe.db.sql(""" select * from (select "Order Types" as name, 0 as lft, + self.group_entries = frappe.db.sql( + """ select * from (select "Order Types" as name, 0 as lft, 2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent from `tab{doctype}` where ifnull(order_type, '') != '') as b order by lft, name - """ - .format(doctype=self.filters.doc_type), as_dict=1) + """.format( + doctype=self.filters.doc_type + ), + as_dict=1, + ) for d in self.group_entries: if d.parent: @@ -366,21 +402,17 @@ class Analytics(object): self.depth_map.setdefault(d.name, 0) def get_supplier_parent_child_map(self): - self.parent_child_map = frappe._dict(frappe.db.sql(""" select name, supplier_group from `tabSupplier`""")) + self.parent_child_map = frappe._dict( + frappe.db.sql(""" select name, supplier_group from `tabSupplier`""") + ) def get_chart_data(self): length = len(self.columns) if self.filters.tree_type in ["Customer", "Supplier"]: - labels = [d.get("label") for d in self.columns[2:length - 1]] + labels = [d.get("label") for d in self.columns[2 : length - 1]] elif self.filters.tree_type == "Item": - labels = [d.get("label") for d in self.columns[3:length - 1]] + labels = [d.get("label") for d in self.columns[3 : length - 1]] else: - labels = [d.get("label") for d in self.columns[1:length - 1]] - self.chart = { - "data": { - 'labels': labels, - 'datasets': [] - }, - "type": "line" - } + labels = [d.get("label") for d in self.columns[1 : length - 1]] + self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index f56cce2dfdc..15f06d9c9b8 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -3,13 +3,13 @@ import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.sales_analytics.sales_analytics import execute -from erpnext.tests.utils import ERPNextTestCase -class TestAnalytics(ERPNextTestCase): +class TestAnalytics(FrappeTestCase): def test_sales_analytics(self): frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") @@ -19,16 +19,15 @@ class TestAnalytics(ERPNextTestCase): self.compare_result_for_customer_group() self.compare_result_for_customer_based_on_quantity() - def compare_result_for_customer(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Value' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Value", } report = execute(filters) @@ -49,7 +48,7 @@ class TestAnalytics(ERPNextTestCase): "jan_2018": 0.0, "feb_2018": 2000.0, "mar_2018": 0.0, - "total":2000.0 + "total": 2000.0, }, { "entity": "_Test Customer 2", @@ -66,7 +65,7 @@ class TestAnalytics(ERPNextTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total":2500.0 + "total": 2500.0, }, { "entity": "_Test Customer 3", @@ -83,21 +82,21 @@ class TestAnalytics(ERPNextTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total": 3000.0 - } + "total": 3000.0, + }, ] - result = sorted(report[1], key=lambda k: k['entity']) + result = sorted(report[1], key=lambda k: k["entity"]) self.assertEqual(expected_data, result) def compare_result_for_customer_group(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer Group', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Value' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer Group", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Value", } report = execute(filters) @@ -117,19 +116,19 @@ class TestAnalytics(ERPNextTestCase): "jan_2018": 0.0, "feb_2018": 2000.0, "mar_2018": 0.0, - "total":7500.0 + "total": 7500.0, } self.assertEqual(expected_first_row, report[1][0]) def compare_result_for_customer_based_on_quantity(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Quantity' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Quantity", } report = execute(filters) @@ -150,7 +149,7 @@ class TestAnalytics(ERPNextTestCase): "jan_2018": 0.0, "feb_2018": 20.0, "mar_2018": 0.0, - "total":20.0 + "total": 20.0, }, { "entity": "_Test Customer 2", @@ -167,7 +166,7 @@ class TestAnalytics(ERPNextTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total":25.0 + "total": 25.0, }, { "entity": "_Test Customer 3", @@ -184,47 +183,66 @@ class TestAnalytics(ERPNextTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total": 30.0 - } + "total": 30.0, + }, ] - result = sorted(report[1], key=lambda k: k['entity']) + result = sorted(report[1], key=lambda k: k["entity"]) self.assertEqual(expected_data, result) + def create_sales_orders(): frappe.set_user("Administrator") - make_sales_order(company="_Test Company 2", qty=10, - customer = "_Test Customer 1", - transaction_date = '2018-02-10', - warehouse = 'Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 1", + transaction_date="2018-02-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=10, customer = "_Test Customer 1", - transaction_date = '2018-02-15', - warehouse = 'Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 1", + transaction_date="2018-02-15", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company = "_Test Company 2", - qty=10, customer = "_Test Customer 2", - transaction_date = '2017-10-10', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 2", + transaction_date="2017-10-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=15, customer = "_Test Customer 2", - transaction_date='2017-09-23', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=15, + customer="_Test Customer 2", + transaction_date="2017-09-23", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=20, customer = "_Test Customer 3", - transaction_date='2017-06-15', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=20, + customer="_Test Customer 3", + transaction_date="2017-06-15", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=10, customer = "_Test Customer 3", - transaction_date='2017-07-10', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 3", + transaction_date="2017-07-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 001095588ba..609fe26d869 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -26,6 +26,7 @@ def execute(filters=None): return columns, data, None, chart_data + def validate_filters(filters): from_date, to_date = filters.get("from_date"), filters.get("to_date") @@ -34,6 +35,7 @@ def validate_filters(filters): elif date_diff(to_date, from_date) < 0: frappe.throw(_("To Date cannot be before From Date.")) + def get_conditions(filters): conditions = "" if filters.get("from_date") and filters.get("to_date"): @@ -50,8 +52,11 @@ def get_conditions(filters): return conditions + def get_data(conditions, filters): - data = frappe.db.sql(""" + # nosemgrep + data = frappe.db.sql( + """ SELECT so.transaction_date as date, soi.delivery_date as delivery_date, @@ -61,6 +66,7 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, + IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver, IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, @@ -71,9 +77,13 @@ def get_data(conditions, filters): soi.description as description FROM `tabSales Order` so, - `tabSales Order Item` soi + (`tabSales Order Item` soi LEFT JOIN `tabSales Invoice Item` sii - ON sii.so_detail = soi.name and sii.docstatus = 1 + ON sii.so_detail = soi.name and sii.docstatus = 1) + LEFT JOIN `tabDelivery Note Item` dni + on dni.so_detail = soi.name + LEFT JOIN `tabDelivery Note` dn + on dni.parent = dn.name and dn.docstatus = 1 WHERE soi.parent = so.name and so.status not in ('Stopped', 'Closed', 'On Hold') @@ -81,10 +91,16 @@ def get_data(conditions, filters): {conditions} GROUP BY soi.name ORDER BY so.transaction_date ASC, soi.item_code ASC - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) return data + def prepare_data(data, filters): completed, pending = 0, 0 @@ -114,8 +130,17 @@ def prepare_data(data, filters): so_row["delay"] = min(so_row["delay"], row["delay"]) # sum numeric columns - fields = ["qty", "delivered_qty", "pending_qty", "billed_qty", "qty_to_bill", "amount", - "delivered_qty_amount", "billed_amount", "pending_amount"] + fields = [ + "qty", + "delivered_qty", + "pending_qty", + "billed_qty", + "qty_to_bill", + "amount", + "delivered_qty_amount", + "billed_amount", + "pending_amount", + ] for field in fields: so_row[field] = flt(row[field]) + flt(so_row[field]) @@ -129,160 +154,148 @@ def prepare_data(data, filters): return data, chart_data + def prepare_chart_data(pending, completed): labels = ["Amount to Bill", "Billed Amount"] return { - "data" : { - "labels": labels, - "datasets": [ - {"values": [pending, completed]} - ] - }, - "type": 'donut', - "height": 300 + "data": {"labels": labels, "datasets": [{"values": [pending, completed]}]}, + "type": "donut", + "height": 300, } + def get_columns(filters): columns = [ - { - "label":_("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 90 - }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 90}, { "label": _("Sales Order"), "fieldname": "sales_order", "fieldtype": "Link", "options": "Sales Order", - "width": 160 - }, - { - "label":_("Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 130 + "width": 160, }, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 130}, { "label": _("Customer"), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", - "width": 130 - }] - - if not filters.get("group_by_so"): - columns.append({ - "label":_("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100 - }) - columns.append({ - "label":_("Description"), - "fieldname": "description", - "fieldtype": "Small Text", - "width": 100 - }) - - columns.extend([ - { - "label": _("Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Delivered Qty"), - "fieldname": "delivered_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Qty to Deliver"), - "fieldname": "pending_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Billed Qty"), - "fieldname": "billed_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Qty to Bill"), - "fieldname": "qty_to_bill", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Billed Amount"), - "fieldname": "billed_amount", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Pending Amount"), - "fieldname": "pending_amount", - "fieldtype": "Currency", "width": 130, - "options": "Company:company:default_currency", - "convertible": "rate" }, - { - "label": _("Amount Delivered"), - "fieldname": "delivered_qty_amount", - "fieldtype": "Currency", - "width": 100, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label":_("Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 120 - }, - { - "label": _("Delay (in Days)"), - "fieldname": "delay", - "fieldtype": "Data", - "width": 100 - } - ]) + ] + if not filters.get("group_by_so"): - columns.append({ - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100 - }) - columns.append({ + columns.append( + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + } + ) + columns.append( + {"label": _("Description"), "fieldname": "description", "fieldtype": "Small Text", "width": 100} + ) + + columns.extend( + [ + { + "label": _("Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Delivered Qty"), + "fieldname": "delivered_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Qty to Deliver"), + "fieldname": "pending_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Billed Qty"), + "fieldname": "billed_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Qty to Bill"), + "fieldname": "qty_to_bill", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Billed Amount"), + "fieldname": "billed_amount", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Pending Amount"), + "fieldname": "pending_amount", + "fieldtype": "Currency", + "width": 130, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Amount Delivered"), + "fieldname": "delivered_qty_amount", + "fieldtype": "Currency", + "width": 100, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + {"label": _("Delivery Date"), "fieldname": "delivery_date", "fieldtype": "Date", "width": 120}, + {"label": _("Delay (in Days)"), "fieldname": "delay", "fieldtype": "Data", "width": 100}, + { + "label": _("Time Taken to Deliver"), + "fieldname": "time_taken_to_deliver", + "fieldtype": "Duration", + "width": 100, + }, + ] + ) + if not filters.get("group_by_so"): + columns.append( + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + } + ) + columns.append( + { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 100 - }) - + "width": 100, + } + ) return columns diff --git a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py new file mode 100644 index 00000000000..25cbb734499 --- /dev/null +++ b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py @@ -0,0 +1,166 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days + +from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.sales_order_analysis.sales_order_analysis import execute +from erpnext.stock.doctype.item.test_item import create_item + +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"] + + +class TestSalesOrderAnalysis(FrappeTestCase): + def create_sales_order(self, transaction_date): + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date=transaction_date, + item=item.item_code, + qty=10, + rate=100000, + do_not_save=True, + ) + so.po_no = "" + so.taxes_and_charges = "" + so.taxes = "" + so.items[0].delivery_date = add_days(transaction_date, 15) + so.save() + so.submit() + return item, so + + def create_sales_invoice(self, so): + sinv = make_sales_invoice(so.name) + sinv.posting_date = so.transaction_date + sinv.taxes_and_charges = "" + sinv.taxes = "" + sinv.insert() + sinv.submit() + return sinv + + def create_delivery_note(self, so): + dn = make_delivery_note(so.name) + dn.set_posting_time = True + dn.posting_date = add_days(so.transaction_date, 1) + dn.save() + dn.submit() + return dn + + def test_01_so_to_deliver_and_bill(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "status": ["To Deliver and Bill"], + } + ) + expected_value = { + "status": "To Deliver and Bill", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 0, + "pending_qty": 10, + "qty_to_bill": 10, + "time_taken_to_deliver": 0, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_02_so_to_deliver(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + self.create_sales_invoice(so) + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "status": ["To Deliver"], + } + ) + expected_value = { + "status": "To Deliver", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 0, + "pending_qty": 10, + "qty_to_bill": 0, + "time_taken_to_deliver": 0, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_03_so_to_bill(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + self.create_delivery_note(so) + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "status": ["To Bill"], + } + ) + expected_value = { + "status": "To Bill", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 10, + "pending_qty": 0, + "qty_to_bill": 10, + "time_taken_to_deliver": 86400, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_04_so_completed(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + self.create_sales_invoice(so) + self.create_delivery_note(so) + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "status": ["Completed"], + } + ) + expected_value = { + "status": "Completed", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 10, + "pending_qty": 0, + "qty_to_bill": 0, + "billed_qty": 10, + "time_taken_to_deliver": 86400, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_05_all_so_status(self): + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + } + ) + # SO's from first 4 test cases should be in output + self.assertEqual(len(data), 4) diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py index 5a711712620..93707bd46d9 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Sales Order") data = get_data(filters, conditions) @@ -16,6 +17,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, conditions, filters): if not (data and conditions): return [] @@ -28,32 +30,27 @@ def get_chart_data(data, conditions, filters): # fetch only periodic columns as labels columns = conditions.get("columns")[start:-2][1::2] - labels = [column.split(':')[0] for column in columns] + labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start-1]: + if not row[start - 1]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[1::2] for i in range(len(row)): datapoints[i] += row[i] return { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : _("{0}").format(filters.get("period")) + _(" Sales Value"), - "values" : datapoints - } - ] + "data": { + "labels": labels, + "datasets": [ + {"name": _("{0}").format(filters.get("period")) + _(" Sales Value"), "values": datapoints} + ], }, - "type" : "line", - "lineOptions": { - "regionFill": 1 - } + "type": "line", + "lineOptions": {"regionFill": 1}, } diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py index b775907bd53..cf9ea219c1b 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py @@ -7,80 +7,73 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) data = get_entries(filters) return columns, data + def get_columns(filters): if not filters.get("doctype"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doctype"]), "options": filters["doctype"], "fieldname": "name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Partner"), "options": "Sales Partner", "fieldname": "sales_partner", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": _("Total Commission"), "fieldname": "total_commission", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] return columns + def get_entries(filters): - date_field = ("transaction_date" if filters.get('doctype') == "Sales Order" - else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" conditions = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT name, customer, territory, {0} as posting_date, base_net_total as amount, sales_partner, commission_rate, total_commission @@ -89,10 +82,16 @@ def get_entries(filters): WHERE {2} and docstatus = 1 and sales_partner is not null and sales_partner != '' order by name desc, sales_partner - """.format(date_field, filters.get('doctype'), conditions), filters, as_dict=1) + """.format( + date_field, filters.get("doctype"), conditions + ), + filters, + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = "1=1" diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index a647eb4fea2..f34f3e34e2c 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -14,8 +14,15 @@ from erpnext.accounts.utils import get_fiscal_year def get_data_column(filters, partner_doctype): data = [] - period_list = get_period_list(filters.fiscal_year, filters.fiscal_year, '', '', - 'Fiscal Year', filters.period, company=filters.company) + period_list = get_period_list( + filters.fiscal_year, + filters.fiscal_year, + "", + "", + "Fiscal Year", + filters.period, + company=filters.company, + ) rows = get_data(filters, period_list, partner_doctype) columns = get_columns(filters, period_list, partner_doctype) @@ -24,20 +31,19 @@ def get_data_column(filters, partner_doctype): return columns, data for key, value in rows.items(): - value.update({ - frappe.scrub(partner_doctype): key[0], - 'item_group': key[1] - }) + value.update({frappe.scrub(partner_doctype): key[0], "item_group": key[1]}) data.append(value) return columns, data + def get_data(filters, period_list, partner_doctype): sales_field = frappe.scrub(partner_doctype) sales_users_data = get_parents_data(filters, partner_doctype) - if not sales_users_data: return + if not sales_users_data: + return sales_users, item_groups = [], [] for d in sales_users_data: @@ -47,99 +53,110 @@ def get_data(filters, period_list, partner_doctype): if d.item_group not in item_groups: item_groups.append(d.item_group) - date_field = ("transaction_date" - if filters.get('doctype') == "Sales Order" else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" actual_data = get_actual_data(filters, item_groups, sales_users, date_field, sales_field) - return prepare_data(filters, sales_users_data, - actual_data, date_field, period_list, sales_field) + return prepare_data(filters, sales_users_data, actual_data, date_field, period_list, sales_field) + def get_columns(filters, period_list, partner_doctype): fieldtype, options = "Currency", "currency" - if filters.get("target_on") == 'Quantity': + if filters.get("target_on") == "Quantity": fieldtype, options = "Float", "" - columns = [{ - "fieldname": frappe.scrub(partner_doctype), - "label": _(partner_doctype), - "fieldtype": "Link", - "options": partner_doctype, - "width": 150 - }, { - "fieldname": "item_group", - "label": _("Item Group"), - "fieldtype": "Link", - "options": "Item Group", - "width": 150 - }] + columns = [ + { + "fieldname": frappe.scrub(partner_doctype), + "label": _(partner_doctype), + "fieldtype": "Link", + "options": partner_doctype, + "width": 150, + }, + { + "fieldname": "item_group", + "label": _("Item Group"), + "fieldtype": "Link", + "options": "Item Group", + "width": 150, + }, + ] for period in period_list: - target_key = 'target_{}'.format(period.key) - variance_key = 'variance_{}'.format(period.key) + target_key = "target_{}".format(period.key) + variance_key = "variance_{}".format(period.key) - columns.extend([{ - "fieldname": target_key, - "label": _("Target ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": period.key, - "label": _("Achieved ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": variance_key, - "label": _("Variance ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }]) + columns.extend( + [ + { + "fieldname": target_key, + "label": _("Target ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": period.key, + "label": _("Achieved ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": variance_key, + "label": _("Variance ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + ] + ) - columns.extend([{ - "fieldname": "total_target", - "label": _("Total Target"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": "total_achieved", - "label": _("Total Achieved"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": "total_variance", - "label": _("Total Variance"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }]) + columns.extend( + [ + { + "fieldname": "total_target", + "label": _("Total Target"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": "total_achieved", + "label": _("Total Achieved"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": "total_variance", + "label": _("Total Variance"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + ] + ) return columns + def prepare_data(filters, sales_users_data, actual_data, date_field, period_list, sales_field): rows = {} - target_qty_amt_field = ("target_qty" - if filters.get("target_on") == 'Quantity' else "target_amount") + target_qty_amt_field = "target_qty" if filters.get("target_on") == "Quantity" else "target_amount" - qty_or_amount_field = ("stock_qty" - if filters.get("target_on") == 'Quantity' else "base_net_amount") + qty_or_amount_field = "stock_qty" if filters.get("target_on") == "Quantity" else "base_net_amount" for d in sales_users_data: key = (d.parent, d.item_group) - dist_data = get_periodwise_distribution_data(d.distribution_id, period_list, filters.get("period")) + dist_data = get_periodwise_distribution_data( + d.distribution_id, period_list, filters.get("period") + ) if key not in rows: - rows.setdefault(key,{ - 'total_target': 0, - 'total_achieved': 0, - 'total_variance': 0 - }) + rows.setdefault(key, {"total_target": 0, "total_achieved": 0, "total_variance": 0}) details = rows[key] for period in period_list: @@ -147,15 +164,19 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list if p_key not in details: details[p_key] = 0 - target_key = 'target_{}'.format(p_key) - variance_key = 'variance_{}'.format(p_key) + target_key = "target_{}".format(p_key) + variance_key = "variance_{}".format(p_key) details[target_key] = (d.get(target_qty_amt_field) * dist_data.get(p_key)) / 100 details[variance_key] = 0 details["total_target"] += details[target_key] for r in actual_data: - if (r.get(sales_field) == d.parent and r.item_group == d.item_group and - period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date): + if ( + r.get(sales_field) == d.parent + and r.item_group == d.item_group + and period.from_date <= r.get(date_field) + and r.get(date_field) <= period.to_date + ): details[p_key] += r.get(qty_or_amount_field, 0) details[variance_key] = details.get(p_key) - details.get(target_key) @@ -164,24 +185,28 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list return rows + def get_actual_data(filters, item_groups, sales_users_or_territory_data, date_field, sales_field): fiscal_year = get_fiscal_year(fiscal_year=filters.get("fiscal_year"), as_dict=1) dates = [fiscal_year.year_start_date, fiscal_year.year_end_date] select_field = "`tab{0}`.{1}".format(filters.get("doctype"), sales_field) - child_table = "`tab{0}`".format(filters.get("doctype") + ' Item') + child_table = "`tab{0}`".format(filters.get("doctype") + " Item") - if sales_field == 'sales_person': + if sales_field == "sales_person": select_field = "`tabSales Team`.sales_person" - child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + ' Item') + child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + " Item") cond = """`tabSales Team`.parent = `tab{0}`.name and - `tabSales Team`.sales_person in ({1}) """.format(filters.get("doctype"), - ','.join(['%s'] * len(sales_users_or_territory_data))) + `tabSales Team`.sales_person in ({1}) """.format( + filters.get("doctype"), ",".join(["%s"] * len(sales_users_or_territory_data)) + ) else: - cond = "`tab{0}`.{1} in ({2})".format(filters.get("doctype"), sales_field, - ','.join(['%s'] * len(sales_users_or_territory_data))) + cond = "`tab{0}`.{1} in ({2})".format( + filters.get("doctype"), sales_field, ",".join(["%s"] * len(sales_users_or_territory_data)) + ) - return frappe.db.sql(""" SELECT `tab{child_doc}`.item_group, + return frappe.db.sql( + """ SELECT `tab{child_doc}`.item_group, `tab{child_doc}`.stock_qty, `tab{child_doc}`.base_net_amount, {select_field}, `tab{parent_doc}`.{date_field} FROM `tab{parent_doc}`, {child_table} @@ -189,26 +214,30 @@ def get_actual_data(filters, item_groups, sales_users_or_territory_data, date_fi `tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.docstatus = 1 and {cond} and `tab{child_doc}`.item_group in ({item_groups}) - and `tab{parent_doc}`.{date_field} between %s and %s""" - .format( - cond = cond, - date_field = date_field, - select_field = select_field, - child_table = child_table, - parent_doc = filters.get("doctype"), - child_doc = filters.get("doctype") + ' Item', - item_groups = ','.join(['%s'] * len(item_groups)) - ), tuple(sales_users_or_territory_data + item_groups + dates), as_dict=1) + and `tab{parent_doc}`.{date_field} between %s and %s""".format( + cond=cond, + date_field=date_field, + select_field=select_field, + child_table=child_table, + parent_doc=filters.get("doctype"), + child_doc=filters.get("doctype") + " Item", + item_groups=",".join(["%s"] * len(item_groups)), + ), + tuple(sales_users_or_territory_data + item_groups + dates), + as_dict=1, + ) + def get_parents_data(filters, partner_doctype): - filters_dict = {'parenttype': partner_doctype} + filters_dict = {"parenttype": partner_doctype} - target_qty_amt_field = ("target_qty" - if filters.get("target_on") == 'Quantity' else "target_amount") + target_qty_amt_field = "target_qty" if filters.get("target_on") == "Quantity" else "target_amount" if filters.get("fiscal_year"): filters_dict["fiscal_year"] = filters.get("fiscal_year") - return frappe.get_all('Target Detail', - filters = filters_dict, - fields = ["parent", "item_group", target_qty_amt_field, "fiscal_year", "distribution_id"]) + return frappe.get_all( + "Target Detail", + filters=filters_dict, + fields=["parent", "item_group", target_qty_amt_field, "fiscal_year", "distribution_id"], + ) diff --git a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py index c64555bf2d3..2049520eadc 100644 --- a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py +++ b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py @@ -7,120 +7,98 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) data = get_entries(filters) return columns, data + def get_columns(filters): if not filters.get("doctype"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doctype"]), "options": filters["doctype"], "fieldname": "name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 100 + "width": 100, }, { "label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 100, }, { "label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", - "width": 100 - }, - { - "label": _("Quantity"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "fieldtype": "Currency", - "width": 120 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120}, + {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Partner"), "options": "Sales Partner", "fieldname": "sales_partner", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Commission"), - "fieldname": "commission", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120}, { "label": _("Currency"), "fieldname": "currency", "fieldtype": "Link", "options": "Currency", - "width": 120 - } + "width": 120, + }, ] return columns + def get_entries(filters): - date_field = ("transaction_date" if filters.get('doctype') == "Sales Order" - else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" conditions = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency, dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount, @@ -132,11 +110,16 @@ def get_entries(filters): {cond} and dt.name = dt_item.parent and dt.docstatus = 1 and dt.sales_partner is not null and dt.sales_partner != '' order by dt.name desc, dt.sales_partner - """.format(date_field=date_field, doctype=filters.get('doctype'), - cond=conditions), filters, as_dict=1) + """.format( + date_field=date_field, doctype=filters.get("doctype"), cond=conditions + ), + filters, + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = "1=1" @@ -150,18 +133,19 @@ def get_conditions(filters, date_field): if filters.get("to_date"): conditions += " and dt.{0} <= %(to_date)s".format(date_field) - if not filters.get('show_return_entries'): + if not filters.get("show_return_entries"): conditions += " and dt_item.qty > 0.0" - if filters.get('brand'): + if filters.get("brand"): conditions += " and dt_item.brand = %(brand)s" - if filters.get('item_group'): - lft, rgt = frappe.get_cached_value('Item Group', - filters.get('item_group'), ['lft', 'rgt']) + if filters.get("item_group"): + lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"]) conditions += """ and dt_item.item_group in (select name from - `tabItem Group` where lft >= %s and rgt <= %s)""" % (lft, rgt) - + `tabItem Group` where lft >= %s and rgt <= %s)""" % ( + lft, + rgt, + ) return conditions diff --git a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py index 1542e31feff..a8df5308036 100644 --- a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py +++ b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py @@ -7,102 +7,101 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) entries = get_entries(filters) data = [] for d in entries: - data.append([ - d.name, d.customer, d.territory, d.posting_date, - d.base_net_amount, d.sales_person, d.allocated_percentage, d.commission_rate, d.allocated_amount,d.incentives - ]) + data.append( + [ + d.name, + d.customer, + d.territory, + d.posting_date, + d.base_net_amount, + d.sales_person, + d.allocated_percentage, + d.commission_rate, + d.allocated_amount, + d.incentives, + ] + ) if data: - total_row = [""]*len(data[0]) + total_row = [""] * len(data[0]) data.append(total_row) return columns, data + def get_columns(filters): if not filters.get("doc_type"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doc_type"]), "options": filters["doc_type"], - "fieldname": filters['doc_type'], + "fieldname": filters["doc_type"], "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Person"), "options": "Sales Person", "fieldname": "sales_person", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Contribution %"), "fieldname": "contribution_percentage", "fieldtype": "Data", - "width": 110 + "width": 110, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": _("Contribution Amount"), "fieldname": "contribution_amount", "fieldtype": "Currency", - "width": 120 + "width": 120, }, - { - "label": _("Incentives"), - "fieldname": "incentives", - "fieldtype": "Currency", - "width": 120 - } + {"label": _("Incentives"), "fieldname": "incentives", "fieldtype": "Currency", "width": 120}, ] return columns + def get_entries(filters): date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" conditions, values = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ select dt.name, dt.customer, dt.territory, dt.%s as posting_date,dt.base_net_total as base_net_amount, st.commission_rate,st.sales_person, st.allocated_percentage, st.allocated_amount, st.incentives @@ -111,11 +110,15 @@ def get_entries(filters): where st.parent = dt.name and st.parenttype = %s and dt.docstatus = 1 %s order by dt.name desc,st.sales_person - """ %(date_field, filters["doc_type"], '%s', conditions), - tuple([filters["doc_type"]] + values), as_dict=1) + """ + % (date_field, filters["doc_type"], "%s", conditions), + tuple([filters["doc_type"]] + values), + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = [""] values = [] diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index c621be88295..cb6e8a1102f 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -9,7 +9,8 @@ from erpnext import get_company_currency def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) entries = get_entries(filters) @@ -19,19 +20,33 @@ def execute(filters=None): company_currency = get_company_currency(filters.get("company")) for d in entries: - if d.stock_qty > 0 or filters.get('show_return_entries', 0): - data.append([ - d.name, d.customer, d.territory, d.warehouse, d.posting_date, d.item_code, - item_details.get(d.item_code, {}).get("item_group"), item_details.get(d.item_code, {}).get("brand"), - d.stock_qty, d.base_net_amount, d.sales_person, d.allocated_percentage, d.contribution_amt, company_currency - ]) + if d.stock_qty > 0 or filters.get("show_return_entries", 0): + data.append( + [ + d.name, + d.customer, + d.territory, + d.warehouse, + d.posting_date, + d.item_code, + item_details.get(d.item_code, {}).get("item_group"), + item_details.get(d.item_code, {}).get("brand"), + d.stock_qty, + d.base_net_amount, + d.sales_person, + d.allocated_percentage, + d.contribution_amt, + company_currency, + ] + ) if data: - total_row = [""]*len(data[0]) + total_row = [""] * len(data[0]) data.append(total_row) return columns, data + def get_columns(filters): if not filters.get("doc_type"): msgprint(_("Please select the document type first"), raise_exception=1) @@ -40,102 +55,88 @@ def get_columns(filters): { "label": _(filters["doc_type"]), "options": filters["doc_type"], - "fieldname": frappe.scrub(filters['doc_type']), + "fieldname": frappe.scrub(filters["doc_type"]), "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Warehouse"), "options": "Warehouse", "fieldname": "warehouse", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 140 + "width": 140, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 140}, { "label": _("Item Code"), "options": "Item", "fieldname": "item_code", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Item Group"), "options": "Item Group", "fieldname": "item_group", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Brand"), "options": "Brand", "fieldname": "brand", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 140 + "width": 140, }, + {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140}, { "label": _("Amount"), "options": "currency", "fieldname": "amount", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Sales Person"), "options": "Sales Person", "fieldname": "sales_person", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Contribution %"), - "fieldname": "contribution", - "fieldtype": "Float", - "width": 140 + "width": 140, }, + {"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140}, { "label": _("Contribution Amount"), "options": "currency", "fieldname": "contribution_amt", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { - "label":_("Currency"), + "label": _("Currency"), "options": "Currency", - "fieldname":"currency", - "fieldtype":"Link", - "hidden" : 1 - } + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + }, ] return columns + def get_entries(filters): date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" if filters["doc_type"] == "Sales Order": @@ -144,7 +145,8 @@ def get_entries(filters): qty_field = "qty" conditions, values = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT dt.name, dt.customer, dt.territory, dt.%s as posting_date, dt_item.item_code, st.sales_person, st.allocated_percentage, dt_item.warehouse, @@ -165,11 +167,24 @@ def get_entries(filters): WHERE st.parent = dt.name and dt.name = dt_item.parent and st.parenttype = %s and dt.docstatus = 1 %s order by st.sales_person, dt.name desc - """ %(date_field, qty_field, qty_field, qty_field, filters["doc_type"], filters["doc_type"], '%s', conditions), - tuple([filters["doc_type"]] + values), as_dict=1) + """ + % ( + date_field, + qty_field, + qty_field, + qty_field, + filters["doc_type"], + filters["doc_type"], + "%s", + conditions, + ), + tuple([filters["doc_type"]] + values), + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = [""] values = [] @@ -181,7 +196,11 @@ def get_conditions(filters, date_field): if filters.get("sales_person"): lft, rgt = frappe.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"]) - conditions.append("exists(select name from `tabSales Person` where lft >= {0} and rgt <= {1} and name=st.sales_person)".format(lft, rgt)) + conditions.append( + "exists(select name from `tabSales Person` where lft >= {0} and rgt <= {1} and name=st.sales_person)".format( + lft, rgt + ) + ) if filters.get("from_date"): conditions.append("dt.{0}>=%s".format(date_field)) @@ -193,23 +212,29 @@ def get_conditions(filters, date_field): items = get_items(filters) if items: - conditions.append("dt_item.item_code in (%s)" % ', '.join(['%s']*len(items))) + conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items))) values += items return " and ".join(conditions), values + def get_items(filters): - if filters.get("item_group"): key = "item_group" - elif filters.get("brand"): key = "brand" - else: key = "" + if filters.get("item_group"): + key = "item_group" + elif filters.get("brand"): + key = "brand" + else: + key = "" items = [] if key: - items = frappe.db.sql_list("""select name from tabItem where %s = %s""" % - (key, '%s'), (filters[key])) + items = frappe.db.sql_list( + """select name from tabItem where %s = %s""" % (key, "%s"), (filters[key]) + ) return items + def get_item_details(): item_details = {} for d in frappe.db.sql("""SELECT `name`, `item_group`, `brand` FROM `tabItem`""", as_dict=1): diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index b7b4d3aa4ca..5dfc1db0976 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -23,38 +23,39 @@ def get_columns(): "fieldname": "territory", "fieldtype": "Link", "options": "Territory", - "width": 150 + "width": 150, }, { "label": _("Opportunity Amount"), "fieldname": "opportunity_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Quotation Amount"), "fieldname": "quotation_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Order Amount"), "fieldname": "order_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Billing Amount"), "fieldname": "billing_amount", "fieldtype": "Currency", "options": currency, - "width": 150 - } + "width": 150, + }, ] + def get_data(filters=None): data = [] @@ -84,26 +85,32 @@ def get_data(filters=None): if territory_orders: t_order_names = [t.name for t in territory_orders] - territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else [] + territory_invoices = ( + list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) + if t_order_names and sales_invoices + else [] + ) territory_data = { "territory": territory.name, "opportunity_amount": _get_total(territory_opportunities, "opportunity_amount"), "quotation_amount": _get_total(territory_quotations), "order_amount": _get_total(territory_orders), - "billing_amount": _get_total(territory_invoices) + "billing_amount": _get_total(territory_invoices), } data.append(territory_data) return data + def get_opportunities(filters): conditions = "" - if filters.get('transaction_date'): + if filters.get("transaction_date"): conditions = " WHERE transaction_date between {0} and {1}".format( - frappe.db.escape(filters['transaction_date'][0]), - frappe.db.escape(filters['transaction_date'][1])) + frappe.db.escape(filters["transaction_date"][0]), + frappe.db.escape(filters["transaction_date"][1]), + ) if filters.company: if conditions: @@ -112,11 +119,17 @@ def get_opportunities(filters): conditions += " WHERE" conditions += " company = %(company)s" - - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT name, territory, opportunity_amount FROM `tabOpportunity` {0} - """.format(conditions), filters, as_dict=1) #nosec + """.format( + conditions + ), + filters, + as_dict=1, + ) # nosec + def get_quotations(opportunities): if not opportunities: @@ -124,11 +137,18 @@ def get_quotations(opportunities): opportunity_names = [o.name for o in opportunities] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `name`,`base_grand_total`, `opportunity` FROM `tabQuotation` WHERE docstatus=1 AND opportunity in ({0}) - """.format(', '.join(["%s"]*len(opportunity_names))), tuple(opportunity_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(opportunity_names)) + ), + tuple(opportunity_names), + as_dict=1, + ) # nosec + def get_sales_orders(quotations): if not quotations: @@ -136,11 +156,18 @@ def get_sales_orders(quotations): quotation_names = [q.name for q in quotations] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT so.`name`, so.`base_grand_total`, soi.prevdoc_docname as quotation FROM `tabSales Order` so, `tabSales Order Item` soi WHERE so.docstatus=1 AND so.name = soi.parent AND soi.prevdoc_docname in ({0}) - """.format(', '.join(["%s"]*len(quotation_names))), tuple(quotation_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(quotation_names)) + ), + tuple(quotation_names), + as_dict=1, + ) # nosec + def get_sales_invoice(sales_orders): if not sales_orders: @@ -148,11 +175,18 @@ def get_sales_invoice(sales_orders): so_names = [so.name for so in sales_orders] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT si.name, si.base_grand_total, sii.sales_order FROM `tabSales Invoice` si, `tabSales Invoice Item` sii WHERE si.docstatus=1 AND si.name = sii.parent AND sii.sales_order in ({0}) - """.format(', '.join(["%s"]*len(so_names))), tuple(so_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(so_names)) + ), + tuple(so_names), + as_dict=1, + ) # nosec + def _get_total(doclist, amount_field="base_grand_total"): if not doclist: diff --git a/erpnext/setup/default_energy_point_rules.py b/erpnext/setup/default_energy_point_rules.py index cfff75e525c..b7fe19758c1 100644 --- a/erpnext/setup/default_energy_point_rules.py +++ b/erpnext/setup/default_energy_point_rules.py @@ -1,57 +1,48 @@ - from frappe import _ doctype_rule_map = { - 'Item': { - 'points': 5, - 'for_doc_event': 'New' + "Item": {"points": 5, "for_doc_event": "New"}, + "Customer": {"points": 5, "for_doc_event": "New"}, + "Supplier": {"points": 5, "for_doc_event": "New"}, + "Lead": {"points": 2, "for_doc_event": "New"}, + "Opportunity": { + "points": 10, + "for_doc_event": "Custom", + "condition": 'doc.status=="Converted"', + "rule_name": _("On Converting Opportunity"), + "user_field": "converted_by", }, - 'Customer': { - 'points': 5, - 'for_doc_event': 'New' + "Sales Order": { + "points": 10, + "for_doc_event": "Submit", + "rule_name": _("On Sales Order Submission"), + "user_field": "modified_by", }, - 'Supplier': { - 'points': 5, - 'for_doc_event': 'New' + "Purchase Order": { + "points": 10, + "for_doc_event": "Submit", + "rule_name": _("On Purchase Order Submission"), + "user_field": "modified_by", }, - 'Lead': { - 'points': 2, - 'for_doc_event': 'New' + "Task": { + "points": 5, + "condition": 'doc.status == "Completed"', + "rule_name": _("On Task Completion"), + "user_field": "completed_by", }, - 'Opportunity': { - 'points': 10, - 'for_doc_event': 'Custom', - 'condition': 'doc.status=="Converted"', - 'rule_name': _('On Converting Opportunity'), - 'user_field': 'converted_by' - }, - 'Sales Order': { - 'points': 10, - 'for_doc_event': 'Submit', - 'rule_name': _('On Sales Order Submission'), - 'user_field': 'modified_by' - }, - 'Purchase Order': { - 'points': 10, - 'for_doc_event': 'Submit', - 'rule_name': _('On Purchase Order Submission'), - 'user_field': 'modified_by' - }, - 'Task': { - 'points': 5, - 'condition': 'doc.status == "Completed"', - 'rule_name': _('On Task Completion'), - 'user_field': 'completed_by' - } } + def get_default_energy_point_rules(): - return [{ - 'doctype': 'Energy Point Rule', - 'reference_doctype': doctype, - 'for_doc_event': rule.get('for_doc_event') or 'Custom', - 'condition': rule.get('condition'), - 'rule_name': rule.get('rule_name') or _('On {0} Creation').format(doctype), - 'points': rule.get('points'), - 'user_field': rule.get('user_field') or 'owner' - } for doctype, rule in doctype_rule_map.items()] + return [ + { + "doctype": "Energy Point Rule", + "reference_doctype": doctype, + "for_doc_event": rule.get("for_doc_event") or "Custom", + "condition": rule.get("condition"), + "rule_name": rule.get("rule_name") or _("On {0} Creation").format(doctype), + "points": rule.get("points"), + "user_field": rule.get("user_field") or "owner", + } + for doctype, rule in doctype_rule_map.items() + ] diff --git a/erpnext/setup/default_success_action.py b/erpnext/setup/default_success_action.py index 338fb43f249..2b9e75c3265 100644 --- a/erpnext/setup/default_success_action.py +++ b/erpnext/setup/default_success_action.py @@ -1,26 +1,31 @@ - from frappe import _ doctype_list = [ - 'Purchase Receipt', - 'Purchase Invoice', - 'Quotation', - 'Sales Order', - 'Delivery Note', - 'Sales Invoice' + "Purchase Receipt", + "Purchase Invoice", + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", ] + def get_message(doctype): return _("{0} has been submitted successfully").format(_(doctype)) + def get_first_success_message(doctype): return get_message(doctype) + def get_default_success_action(): - return [{ - 'doctype': 'Success Action', - 'ref_doctype': doctype, - 'message': get_message(doctype), - 'first_success_message': get_first_success_message(doctype), - 'next_actions': 'new\nprint\nemail' - } for doctype in doctype_list] + return [ + { + "doctype": "Success Action", + "ref_doctype": doctype, + "message": get_message(doctype), + "first_success_message": get_first_success_message(doctype), + "next_actions": "new\nprint\nemail", + } + for doctype in doctype_list + ] diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 2a0d785520a..309658d2601 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -12,90 +12,121 @@ from erpnext.utilities.transaction_base import TransactionBase class AuthorizationControl(TransactionBase): def get_appr_user_role(self, det, doctype_name, total, based_on, condition, item, company): amt_list, appr_users, appr_roles = [], [], [] - users, roles = '','' + users, roles = "", "" if det: for x in det: amt_list.append(flt(x[0])) max_amount = max(amt_list) - app_dtl = frappe.db.sql("""select approving_user, approving_role from `tabAuthorization Rule` + app_dtl = frappe.db.sql( + """select approving_user, approving_role from `tabAuthorization Rule` where transaction = %s and (value = %s or value > %s) - and docstatus != 2 and based_on = %s and company = %s %s""" % - ('%s', '%s', '%s', '%s', '%s', condition), - (doctype_name, flt(max_amount), total, based_on, company)) + and docstatus != 2 and based_on = %s and company = %s %s""" + % ("%s", "%s", "%s", "%s", "%s", condition), + (doctype_name, flt(max_amount), total, based_on, company), + ) if not app_dtl: - app_dtl = frappe.db.sql("""select approving_user, approving_role from `tabAuthorization Rule` + app_dtl = frappe.db.sql( + """select approving_user, approving_role from `tabAuthorization Rule` where transaction = %s and (value = %s or value > %s) and docstatus != 2 - and based_on = %s and ifnull(company,'') = '' %s""" % - ('%s', '%s', '%s', '%s', condition), (doctype_name, flt(max_amount), total, based_on)) + and based_on = %s and ifnull(company,'') = '' %s""" + % ("%s", "%s", "%s", "%s", condition), + (doctype_name, flt(max_amount), total, based_on), + ) for d in app_dtl: - if(d[0]): appr_users.append(d[0]) - if(d[1]): appr_roles.append(d[1]) + if d[0]: + appr_users.append(d[0]) + if d[1]: + appr_roles.append(d[1]) - if not has_common(appr_roles, frappe.get_roles()) and not has_common(appr_users, [session['user']]): + if not has_common(appr_roles, frappe.get_roles()) and not has_common( + appr_users, [session["user"]] + ): frappe.msgprint(_("Not authroized since {0} exceeds limits").format(_(based_on))) frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users))) - def validate_auth_rule(self, doctype_name, total, based_on, cond, company, item = ''): + def validate_auth_rule(self, doctype_name, total, based_on, cond, company, item=""): chk = 1 - add_cond1,add_cond2 = '','' - if based_on == 'Itemwise Discount': + add_cond1, add_cond2 = "", "" + if based_on == "Itemwise Discount": add_cond1 += " and master_name = " + frappe.db.escape(cstr(item)) - itemwise_exists = frappe.db.sql("""select value from `tabAuthorization Rule` + itemwise_exists = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s - and based_on = %s and company = %s and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', '%s', cond, add_cond1), (doctype_name, total, based_on, company)) + and based_on = %s and company = %s and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", "%s", cond, add_cond1), + (doctype_name, total, based_on, company), + ) if not itemwise_exists: - itemwise_exists = frappe.db.sql("""select value from `tabAuthorization Rule` + itemwise_exists = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and ifnull(company,'') = '' and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', cond, add_cond1), (doctype_name, total, based_on)) + and ifnull(company,'') = '' and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", cond, add_cond1), + (doctype_name, total, based_on), + ) if itemwise_exists: - self.get_appr_user_role(itemwise_exists, doctype_name, total, based_on, cond+add_cond1, item,company) + self.get_appr_user_role( + itemwise_exists, doctype_name, total, based_on, cond + add_cond1, item, company + ) chk = 0 if chk == 1: - if based_on == 'Itemwise Discount': + if based_on == "Itemwise Discount": add_cond2 += " and ifnull(master_name,'') = ''" - appr = frappe.db.sql("""select value from `tabAuthorization Rule` + appr = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and company = %s and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', '%s', cond, add_cond2), (doctype_name, total, based_on, company)) + and company = %s and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", "%s", cond, add_cond2), + (doctype_name, total, based_on, company), + ) if not appr: - appr = frappe.db.sql("""select value from `tabAuthorization Rule` + appr = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and ifnull(company,'') = '' and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', cond, add_cond2), (doctype_name, total, based_on)) + and ifnull(company,'') = '' and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", cond, add_cond2), + (doctype_name, total, based_on), + ) - self.get_appr_user_role(appr, doctype_name, total, based_on, cond+add_cond2, item, company) + self.get_appr_user_role(appr, doctype_name, total, based_on, cond + add_cond2, item, company) def bifurcate_based_on_type(self, doctype_name, total, av_dis, based_on, doc_obj, val, company): - add_cond = '' + add_cond = "" auth_value = av_dis - if val == 1: add_cond += " and system_user = {}".format(frappe.db.escape(session['user'])) - elif val == 2: add_cond += " and system_role IN %s" % ("('"+"','".join(frappe.get_roles())+"')") - else: add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''" + if val == 1: + add_cond += " and system_user = {}".format(frappe.db.escape(session["user"])) + elif val == 2: + add_cond += " and system_role IN %s" % ("('" + "','".join(frappe.get_roles()) + "')") + else: + add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''" - if based_on == 'Grand Total': auth_value = total - elif based_on == 'Customerwise Discount': + if based_on == "Grand Total": + auth_value = total + elif based_on == "Customerwise Discount": if doc_obj: - if doc_obj.doctype == 'Sales Invoice': customer = doc_obj.customer - else: customer = doc_obj.customer_name + if doc_obj.doctype == "Sales Invoice": + customer = doc_obj.customer + else: + customer = doc_obj.customer_name add_cond = " and master_name = {}".format(frappe.db.escape(customer)) - if based_on == 'Itemwise Discount': + if based_on == "Itemwise Discount": if doc_obj: for t in doc_obj.get("items"): - self.validate_auth_rule(doctype_name, t.discount_percentage, based_on, add_cond, company,t.item_code ) + self.validate_auth_rule( + doctype_name, t.discount_percentage, based_on, add_cond, company, t.item_code + ) else: self.validate_auth_rule(doctype_name, auth_value, based_on, add_cond, company) - def validate_approving_authority(self, doctype_name,company, total, doc_obj = ''): + def validate_approving_authority(self, doctype_name, company, total, doc_obj=""): if not frappe.db.count("Authorization Rule"): return @@ -109,56 +140,85 @@ class AuthorizationControl(TransactionBase): if doc_obj.get("discount_amount"): base_rate -= flt(doc_obj.discount_amount) - if price_list_rate: av_dis = 100 - flt(base_rate * 100 / price_list_rate) + if price_list_rate: + av_dis = 100 - flt(base_rate * 100 / price_list_rate) - final_based_on = ['Grand Total','Average Discount','Customerwise Discount','Itemwise Discount'] + final_based_on = [ + "Grand Total", + "Average Discount", + "Customerwise Discount", + "Itemwise Discount", + ] # Check for authorization set for individual user - based_on = [x[0] for x in frappe.db.sql("""select distinct based_on from `tabAuthorization Rule` + based_on = [ + x[0] + for x in frappe.db.sql( + """select distinct based_on from `tabAuthorization Rule` where transaction = %s and system_user = %s and (company = %s or ifnull(company,'')='') and docstatus != 2""", - (doctype_name, session['user'], company))] + (doctype_name, session["user"], company), + ) + ] for d in based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, d, doc_obj, 1, company) # Remove user specific rules from global authorization rules for r in based_on: - if r in final_based_on and r != 'Itemwise Discount': final_based_on.remove(r) + if r in final_based_on and r != "Itemwise Discount": + final_based_on.remove(r) # Check for authorization set on particular roles - based_on = [x[0] for x in frappe.db.sql("""select based_on + based_on = [ + x[0] + for x in frappe.db.sql( + """select based_on from `tabAuthorization Rule` where transaction = %s and system_role IN (%s) and based_on IN (%s) and (company = %s or ifnull(company,'')='') and docstatus != 2 - """ % ('%s', "'"+"','".join(frappe.get_roles())+"'", "'"+"','".join(final_based_on)+"'", '%s'), (doctype_name, company))] + """ + % ( + "%s", + "'" + "','".join(frappe.get_roles()) + "'", + "'" + "','".join(final_based_on) + "'", + "%s", + ), + (doctype_name, company), + ) + ] for d in based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, d, doc_obj, 2, company) # Remove role specific rules from global authorization rules for r in based_on: - if r in final_based_on and r != 'Itemwise Discount': final_based_on.remove(r) + if r in final_based_on and r != "Itemwise Discount": + final_based_on.remove(r) # Check for global authorization for g in final_based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, g, doc_obj, 0, company) - def get_value_based_rule(self,doctype_name,employee,total_claimed_amount,company): - val_lst =[] - val = frappe.db.sql("""select value from `tabAuthorization Rule` + def get_value_based_rule(self, doctype_name, employee, total_claimed_amount, company): + val_lst = [] + val = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)< %s and company = %s and docstatus!=2""", - (doctype_name,employee,employee,total_claimed_amount,company)) + (doctype_name, employee, employee, total_claimed_amount, company), + ) if not val: - val = frappe.db.sql("""select value from `tabAuthorization Rule` + val = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)< %s and ifnull(company,'') = '' and docstatus!=2""", - (doctype_name, employee, employee, total_claimed_amount)) + (doctype_name, employee, employee, total_claimed_amount), + ) if val: val_lst = [y[0] for y in val] @@ -166,64 +226,83 @@ class AuthorizationControl(TransactionBase): val_lst.append(0) max_val = max(val_lst) - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and company = %s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)= %s and docstatus!=2""", - (doctype_name,company,employee,employee,flt(max_val)), as_dict=1) + (doctype_name, company, employee, employee, flt(max_val)), + as_dict=1, + ) if not rule: - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and ifnull(company,'') = '' and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)= %s and docstatus!=2""", - (doctype_name,employee,employee,flt(max_val)), as_dict=1) + (doctype_name, employee, employee, flt(max_val)), + as_dict=1, + ) return rule # related to payroll module only - def get_approver_name(self, doctype_name, total, doc_obj=''): - app_user=[] - app_specific_user =[] - rule ={} + def get_approver_name(self, doctype_name, total, doc_obj=""): + app_user = [] + app_specific_user = [] + rule = {} if doc_obj: - if doctype_name == 'Expense Claim': - rule = self.get_value_based_rule(doctype_name, doc_obj.employee, - doc_obj.total_claimed_amount, doc_obj.company) - elif doctype_name == 'Appraisal': - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + if doctype_name == "Expense Claim": + rule = self.get_value_based_rule( + doctype_name, doc_obj.employee, doc_obj.total_claimed_amount, doc_obj.company + ) + elif doctype_name == "Appraisal": + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and company = %s and docstatus!=2""", - (doctype_name,doc_obj.employee, doc_obj.employee, doc_obj.company),as_dict=1) + (doctype_name, doc_obj.employee, doc_obj.employee, doc_obj.company), + as_dict=1, + ) if not rule: - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(company,'') = '' and docstatus!=2""", - (doctype_name,doc_obj.employee, doc_obj.employee), as_dict=1) + (doctype_name, doc_obj.employee, doc_obj.employee), + as_dict=1, + ) if rule: for m in rule: - if m['to_emp'] or m['to_designation']: - if m['approving_user']: - app_specific_user.append(m['approving_user']) - elif m['approving_role']: - user_lst = [z[0] for z in frappe.db.sql("""select distinct t1.name + if m["to_emp"] or m["to_designation"]: + if m["approving_user"]: + app_specific_user.append(m["approving_user"]) + elif m["approving_role"]: + user_lst = [ + z[0] + for z in frappe.db.sql( + """select distinct t1.name from `tabUser` t1, `tabHas Role` t2 where t2.role=%s and t2.parent=t1.name and t1.name !='Administrator' - and t1.name != 'Guest' and t1.docstatus !=2""", m['approving_role'])] + and t1.name != 'Guest' and t1.docstatus !=2""", + m["approving_role"], + ) + ] for x in user_lst: if not x in app_user: app_user.append(x) - if len(app_specific_user) >0: + if len(app_specific_user) > 0: return app_specific_user else: return app_user diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.py b/erpnext/setup/doctype/authorization_rule/authorization_rule.py index e07de3b2934..faecd5ae069 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.py @@ -10,40 +10,58 @@ from frappe.utils import cstr, flt class AuthorizationRule(Document): def check_duplicate_entry(self): - exists = frappe.db.sql("""select name, docstatus from `tabAuthorization Rule` + exists = frappe.db.sql( + """select name, docstatus from `tabAuthorization Rule` where transaction = %s and based_on = %s and system_user = %s and system_role = %s and approving_user = %s and approving_role = %s and to_emp =%s and to_designation=%s and name != %s""", - (self.transaction, self.based_on, cstr(self.system_user), - cstr(self.system_role), cstr(self.approving_user), - cstr(self.approving_role), cstr(self.to_emp), - cstr(self.to_designation), self.name)) - auth_exists = exists and exists[0][0] or '' + ( + self.transaction, + self.based_on, + cstr(self.system_user), + cstr(self.system_role), + cstr(self.approving_user), + cstr(self.approving_role), + cstr(self.to_emp), + cstr(self.to_designation), + self.name, + ), + ) + auth_exists = exists and exists[0][0] or "" if auth_exists: frappe.throw(_("Duplicate Entry. Please check Authorization Rule {0}").format(auth_exists)) - def validate_rule(self): - if self.transaction != 'Appraisal': + if self.transaction != "Appraisal": if not self.approving_role and not self.approving_user: frappe.throw(_("Please enter Approving Role or Approving User")) elif self.system_user and self.system_user == self.approving_user: frappe.throw(_("Approving User cannot be same as user the rule is Applicable To")) elif self.system_role and self.system_role == self.approving_role: frappe.throw(_("Approving Role cannot be same as role the rule is Applicable To")) - elif self.transaction in ['Purchase Order', 'Purchase Receipt', \ - 'Purchase Invoice', 'Stock Entry'] and self.based_on \ - in ['Average Discount', 'Customerwise Discount', 'Itemwise Discount']: - frappe.throw(_("Cannot set authorization on basis of Discount for {0}").format(self.transaction)) - elif self.based_on == 'Average Discount' and flt(self.value) > 100.00: + elif self.transaction in [ + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + "Stock Entry", + ] and self.based_on in [ + "Average Discount", + "Customerwise Discount", + "Itemwise Discount", + ]: + frappe.throw( + _("Cannot set authorization on basis of Discount for {0}").format(self.transaction) + ) + elif self.based_on == "Average Discount" and flt(self.value) > 100.00: frappe.throw(_("Discount must be less than 100")) - elif self.based_on == 'Customerwise Discount' and not self.master_name: + elif self.based_on == "Customerwise Discount" and not self.master_name: frappe.throw(_("Customer required for 'Customerwise Discount'")) else: - if self.transaction == 'Appraisal': + if self.transaction == "Appraisal": self.based_on = "Not Applicable" def validate(self): self.check_duplicate_entry() self.validate_rule() - if not self.value: self.value = 0.0 + if not self.value: + self.value = 0.0 diff --git a/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py b/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py index 7d3d5d4c4d3..55c1bbb79b1 100644 --- a/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Authorization Rule') + class TestAuthorizationRule(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/brand/brand.py b/erpnext/setup/doctype/brand/brand.py index 9b91b456c34..1bb6fc9f16c 100644 --- a/erpnext/setup/doctype/brand/brand.py +++ b/erpnext/setup/doctype/brand/brand.py @@ -11,6 +11,7 @@ from frappe.model.document import Document class Brand(Document): pass + def get_brand_defaults(item, company): item = frappe.get_cached_doc("Item", item) if item.brand: diff --git a/erpnext/setup/doctype/brand/test_brand.py b/erpnext/setup/doctype/brand/test_brand.py index 1c71448cb8d..2e030b09a31 100644 --- a/erpnext/setup/doctype/brand/test_brand.py +++ b/erpnext/setup/doctype/brand/test_brand.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Brand') +test_records = frappe.get_test_records("Brand") diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index dd185fc6636..0de5b2d5a32 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -233,7 +233,8 @@ erpnext.company.setup_queries = function(frm) { ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], - ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}] + ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}], + ["default_provisional_account", {"root_type": ["in", ["Liability", "Asset"]]}] ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 3347935234c..ee39d3a4ac5 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -21,7 +21,7 @@ from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_ch class Company(NestedSet): - nsm_parent_field = 'parent_company' + nsm_parent_field = "parent_company" def onload(self): load_address_and_contact(self, "company") @@ -29,12 +29,24 @@ class Company(NestedSet): @frappe.whitelist() def check_if_transactions_exist(self): exists = False - for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation", - "Purchase Invoice", "Purchase Receipt", "Purchase Order", "Supplier Quotation"]: - if frappe.db.sql("""select name from `tab%s` where company=%s and docstatus=1 - limit 1""" % (doctype, "%s"), self.name): - exists = True - break + for doctype in [ + "Sales Invoice", + "Delivery Note", + "Sales Order", + "Quotation", + "Purchase Invoice", + "Purchase Receipt", + "Purchase Order", + "Supplier Quotation", + ]: + if frappe.db.sql( + """select name from `tab%s` where company=%s and docstatus=1 + limit 1""" + % (doctype, "%s"), + self.name, + ): + exists = True + break return exists @@ -56,7 +68,7 @@ class Company(NestedSet): def validate_abbr(self): if not self.abbr: - self.abbr = ''.join(c[0] for c in self.company_name.split()).upper() + self.abbr = "".join(c[0] for c in self.company_name.split()).upper() self.abbr = self.abbr.strip() @@ -66,7 +78,9 @@ class Company(NestedSet): if not self.abbr.strip(): frappe.throw(_("Abbreviation is mandatory")) - if frappe.db.sql("select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr)): + if frappe.db.sql( + "select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr) + ): frappe.throw(_("Abbreviation already used for another company")) @frappe.whitelist() @@ -75,37 +89,57 @@ class Company(NestedSet): def validate_default_accounts(self): accounts = [ - ["Default Bank Account", "default_bank_account"], ["Default Cash Account", "default_cash_account"], - ["Default Receivable Account", "default_receivable_account"], ["Default Payable Account", "default_payable_account"], - ["Default Expense Account", "default_expense_account"], ["Default Income Account", "default_income_account"], - ["Stock Received But Not Billed Account", "stock_received_but_not_billed"], ["Stock Adjustment Account", "stock_adjustment_account"], - ["Expense Included In Valuation Account", "expenses_included_in_valuation"], ["Default Payroll Payable Account", "default_payroll_payable_account"] + ["Default Bank Account", "default_bank_account"], + ["Default Cash Account", "default_cash_account"], + ["Default Receivable Account", "default_receivable_account"], + ["Default Payable Account", "default_payable_account"], + ["Default Expense Account", "default_expense_account"], + ["Default Income Account", "default_income_account"], + ["Stock Received But Not Billed Account", "stock_received_but_not_billed"], + ["Stock Adjustment Account", "stock_adjustment_account"], + ["Expense Included In Valuation Account", "expenses_included_in_valuation"], + ["Default Payroll Payable Account", "default_payroll_payable_account"], ] for account in accounts: if self.get(account[1]): for_company = frappe.db.get_value("Account", self.get(account[1]), "company") if for_company != self.name: - frappe.throw(_("Account {0} does not belong to company: {1}").format(self.get(account[1]), self.name)) + frappe.throw( + _("Account {0} does not belong to company: {1}").format(self.get(account[1]), self.name) + ) if get_account_currency(self.get(account[1])) != self.default_currency: - error_message = _("{0} currency must be same as company's default currency. Please select another account.") \ - .format(frappe.bold(account[0])) + error_message = _( + "{0} currency must be same as company's default currency. Please select another account." + ).format(frappe.bold(account[0])) frappe.throw(error_message) def validate_currency(self): if self.is_new(): return - self.previous_default_currency = frappe.get_cached_value('Company', self.name, "default_currency") - if self.default_currency and self.previous_default_currency and \ - self.default_currency != self.previous_default_currency and \ - self.check_if_transactions_exist(): - frappe.throw(_("Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency.")) + self.previous_default_currency = frappe.get_cached_value( + "Company", self.name, "default_currency" + ) + if ( + self.default_currency + and self.previous_default_currency + and self.default_currency != self.previous_default_currency + and self.check_if_transactions_exist() + ): + frappe.throw( + _( + "Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency." + ) + ) def on_update(self): NestedSet.on_update(self) - if not frappe.db.sql("""select name from tabAccount - where company=%s and docstatus<2 limit 1""", self.name): + if not frappe.db.sql( + """select name from tabAccount + where company=%s and docstatus<2 limit 1""", + self.name, + ): if not frappe.local.flags.ignore_chart_of_accounts: frappe.flags.country_change = True self.create_default_accounts() @@ -120,7 +154,8 @@ class Company(NestedSet): if not frappe.db.get_value("Department", {"company": self.name}): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures - install_post_company_fixtures(frappe._dict({'company_name': self.name})) + + install_post_company_fixtures(frappe._dict({"company_name": self.name})) if not frappe.local.flags.ignore_chart_of_accounts: self.set_default_accounts() @@ -130,12 +165,15 @@ class Company(NestedSet): if self.default_currency: frappe.db.set_value("Currency", self.default_currency, "enabled", 1) - if hasattr(frappe.local, 'enable_perpetual_inventory') and \ - self.name in frappe.local.enable_perpetual_inventory: + if ( + hasattr(frappe.local, "enable_perpetual_inventory") + and self.name in frappe.local.enable_perpetual_inventory + ): frappe.local.enable_perpetual_inventory[self.name] = self.enable_perpetual_inventory if frappe.flags.parent_company_changed: from frappe.utils.nestedset import rebuild_tree + rebuild_tree("Company", "parent_company") frappe.clear_cache() @@ -146,31 +184,48 @@ class Company(NestedSet): {"warehouse_name": _("Stores"), "is_group": 0}, {"warehouse_name": _("Work In Progress"), "is_group": 0}, {"warehouse_name": _("Finished Goods"), "is_group": 0}, - {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}]: + {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}, + ]: - if not frappe.db.exists("Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr)): - warehouse = frappe.get_doc({ - "doctype":"Warehouse", - "warehouse_name": wh_detail["warehouse_name"], - "is_group": wh_detail["is_group"], - "company": self.name, - "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) \ - if not wh_detail["is_group"] else "", - "warehouse_type" : wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None - }) + if not frappe.db.exists( + "Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr) + ): + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": wh_detail["warehouse_name"], + "is_group": wh_detail["is_group"], + "company": self.name, + "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) + if not wh_detail["is_group"] + else "", + "warehouse_type": wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None, + } + ) warehouse.flags.ignore_permissions = True warehouse.flags.ignore_mandatory = True warehouse.insert() def create_default_accounts(self): from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts + frappe.local.flags.ignore_root_company_validation = True create_charts(self.name, self.chart_of_accounts, self.existing_company) - frappe.db.set(self, "default_receivable_account", frappe.db.get_value("Account", - {"company": self.name, "account_type": "Receivable", "is_group": 0})) - frappe.db.set(self, "default_payable_account", frappe.db.get_value("Account", - {"company": self.name, "account_type": "Payable", "is_group": 0})) + frappe.db.set( + self, + "default_receivable_account", + frappe.db.get_value( + "Account", {"company": self.name, "account_type": "Receivable", "is_group": 0} + ), + ) + frappe.db.set( + self, + "default_payable_account", + frappe.db.get_value( + "Account", {"company": self.name, "account_type": "Payable", "is_group": 0} + ), + ) def validate_coa_input(self): if self.create_chart_of_accounts_based_on == "Existing Company": @@ -187,34 +242,46 @@ class Company(NestedSet): def validate_perpetual_inventory(self): if not self.get("__islocal"): if cint(self.enable_perpetual_inventory) == 1 and not self.default_inventory_account: - frappe.msgprint(_("Set default inventory account for perpetual inventory"), - alert=True, indicator='orange') + frappe.msgprint( + _("Set default inventory account for perpetual inventory"), alert=True, indicator="orange" + ) def validate_provisional_account_for_non_stock_items(self): if not self.get("__islocal"): - if cint(self.enable_provisional_accounting_for_non_stock_items) == 1 and not self.default_provisional_account: - frappe.throw(_("Set default {0} account for non stock items").format( - frappe.bold('Provisional Account'))) + if ( + cint(self.enable_provisional_accounting_for_non_stock_items) == 1 + and not self.default_provisional_account + ): + frappe.throw( + _("Set default {0} account for non stock items").format(frappe.bold("Provisional Account")) + ) - make_property_setter("Purchase Receipt", "provisional_expense_account", "hidden", - not self.enable_provisional_accounting_for_non_stock_items, "Check", validate_fields_for_doctype=False) + make_property_setter( + "Purchase Receipt", + "provisional_expense_account", + "hidden", + not self.enable_provisional_accounting_for_non_stock_items, + "Check", + validate_fields_for_doctype=False, + ) def check_country_change(self): frappe.flags.country_change = False - if not self.is_new() and \ - self.country != frappe.get_cached_value('Company', self.name, 'country'): + if not self.is_new() and self.country != frappe.get_cached_value( + "Company", self.name, "country" + ): frappe.flags.country_change = True def set_chart_of_accounts(self): - ''' If parent company is set, chart of accounts will be based on that company ''' + """If parent company is set, chart of accounts will be based on that company""" if self.parent_company: self.create_chart_of_accounts_based_on = "Existing Company" self.existing_company = self.parent_company def validate_parent_company(self): if self.parent_company: - is_group = frappe.get_value('Company', self.parent_company, 'is_group') + is_group = frappe.get_value("Company", self.parent_company, "is_group") if not is_group: frappe.throw(_("Parent Company must be a group company")) @@ -228,29 +295,33 @@ class Company(NestedSet): "depreciation_expense_account": "Depreciation", "capital_work_in_progress_account": "Capital Work in Progress", "asset_received_but_not_billed": "Asset Received But Not Billed", - "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation" + "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation", } if self.enable_perpetual_inventory: - default_accounts.update({ - "stock_received_but_not_billed": "Stock Received But Not Billed", - "default_inventory_account": "Stock", - "stock_adjustment_account": "Stock Adjustment", - "expenses_included_in_valuation": "Expenses Included In Valuation", - "default_expense_account": "Cost of Goods Sold" - }) + default_accounts.update( + { + "stock_received_but_not_billed": "Stock Received But Not Billed", + "default_inventory_account": "Stock", + "stock_adjustment_account": "Stock Adjustment", + "expenses_included_in_valuation": "Expenses Included In Valuation", + "default_expense_account": "Cost of Goods Sold", + } + ) if self.update_default_account: for default_account in default_accounts: self._set_default_account(default_account, default_accounts.get(default_account)) if not self.default_income_account: - income_account = frappe.db.get_value("Account", - {"account_name": _("Sales"), "company": self.name, "is_group": 0}) + income_account = frappe.db.get_value( + "Account", {"account_name": _("Sales"), "company": self.name, "is_group": 0} + ) if not income_account: - income_account = frappe.db.get_value("Account", - {"account_name": _("Sales Account"), "company": self.name}) + income_account = frappe.db.get_value( + "Account", {"account_name": _("Sales Account"), "company": self.name} + ) self.db_set("default_income_account", income_account) @@ -258,32 +329,38 @@ class Company(NestedSet): self.db_set("default_payable_account", self.default_payable_account) if not self.default_payroll_payable_account: - payroll_payable_account = frappe.db.get_value("Account", - {"account_name": _("Payroll Payable"), "company": self.name, "is_group": 0}) + payroll_payable_account = frappe.db.get_value( + "Account", {"account_name": _("Payroll Payable"), "company": self.name, "is_group": 0} + ) self.db_set("default_payroll_payable_account", payroll_payable_account) if not self.default_employee_advance_account: - employe_advance_account = frappe.db.get_value("Account", - {"account_name": _("Employee Advances"), "company": self.name, "is_group": 0}) + employe_advance_account = frappe.db.get_value( + "Account", {"account_name": _("Employee Advances"), "company": self.name, "is_group": 0} + ) self.db_set("default_employee_advance_account", employe_advance_account) if not self.write_off_account: - write_off_acct = frappe.db.get_value("Account", - {"account_name": _("Write Off"), "company": self.name, "is_group": 0}) + write_off_acct = frappe.db.get_value( + "Account", {"account_name": _("Write Off"), "company": self.name, "is_group": 0} + ) self.db_set("write_off_account", write_off_acct) if not self.exchange_gain_loss_account: - exchange_gain_loss_acct = frappe.db.get_value("Account", - {"account_name": _("Exchange Gain/Loss"), "company": self.name, "is_group": 0}) + exchange_gain_loss_acct = frappe.db.get_value( + "Account", {"account_name": _("Exchange Gain/Loss"), "company": self.name, "is_group": 0} + ) self.db_set("exchange_gain_loss_account", exchange_gain_loss_acct) if not self.disposal_account: - disposal_acct = frappe.db.get_value("Account", - {"account_name": _("Gain/Loss on Asset Disposal"), "company": self.name, "is_group": 0}) + disposal_acct = frappe.db.get_value( + "Account", + {"account_name": _("Gain/Loss on Asset Disposal"), "company": self.name, "is_group": 0}, + ) self.db_set("disposal_account", disposal_acct) @@ -291,35 +368,39 @@ class Company(NestedSet): if self.get(fieldname): return - account = frappe.db.get_value("Account", {"account_type": account_type, "is_group": 0, "company": self.name}) + account = frappe.db.get_value( + "Account", {"account_type": account_type, "is_group": 0, "company": self.name} + ) if account: self.db_set(fieldname, account) def set_mode_of_payment_account(self): - cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name') - if cash and self.default_cash_account \ - and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}): - mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True) - mode_of_payment.append('accounts', { - 'company': self.name, - 'default_account': self.default_cash_account - }) + cash = frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name") + if ( + cash + and self.default_cash_account + and not frappe.db.get_value("Mode of Payment Account", {"company": self.name, "parent": cash}) + ): + mode_of_payment = frappe.get_doc("Mode of Payment", cash, for_update=True) + mode_of_payment.append( + "accounts", {"company": self.name, "default_account": self.default_cash_account} + ) mode_of_payment.save(ignore_permissions=True) def create_default_cost_center(self): cc_list = [ { - 'cost_center_name': self.name, - 'company':self.name, - 'is_group': 1, - 'parent_cost_center':None + "cost_center_name": self.name, + "company": self.name, + "is_group": 1, + "parent_cost_center": None, }, { - 'cost_center_name':_('Main'), - 'company':self.name, - 'is_group':0, - 'parent_cost_center':self.name + ' - ' + self.abbr + "cost_center_name": _("Main"), + "company": self.name, + "is_group": 0, + "parent_cost_center": self.name + " - " + self.abbr, }, ] for cc in cc_list: @@ -338,26 +419,32 @@ class Company(NestedSet): def after_rename(self, olddn, newdn, merge=False): frappe.db.set(self, "company_name", newdn) - frappe.db.sql("""update `tabDefaultValue` set defvalue=%s - where defkey='Company' and defvalue=%s""", (newdn, olddn)) + frappe.db.sql( + """update `tabDefaultValue` set defvalue=%s + where defkey='Company' and defvalue=%s""", + (newdn, olddn), + ) clear_defaults_cache() def abbreviate(self): - self.abbr = ''.join(c[0].upper() for c in self.company_name.split()) + self.abbr = "".join(c[0].upper() for c in self.company_name.split()) def on_trash(self): """ - Trash accounts and cost centers for this company if no gl entry exists + Trash accounts and cost centers for this company if no gl entry exists """ NestedSet.validate_if_child_exists(self) frappe.utils.nestedset.update_nsm(self) rec = frappe.db.sql("SELECT name from `tabGL Entry` where company = %s", self.name) if not rec: - frappe.db.sql("""delete from `tabBudget Account` + frappe.db.sql( + """delete from `tabBudget Account` where exists(select name from tabBudget - where name=`tabBudget Account`.parent and company = %s)""", self.name) + where name=`tabBudget Account`.parent and company = %s)""", + self.name, + ) for doctype in ["Account", "Cost Center", "Budget", "Party Account"]: frappe.db.sql("delete from `tab{0}` where company = %s".format(doctype), self.name) @@ -372,26 +459,37 @@ class Company(NestedSet): # clear default accounts, warehouses from item warehouses = frappe.db.sql_list("select name from tabWarehouse where company=%s", self.name) if warehouses: - frappe.db.sql("""delete from `tabItem Reorder` where warehouse in (%s)""" - % ', '.join(['%s']*len(warehouses)), tuple(warehouses)) + frappe.db.sql( + """delete from `tabItem Reorder` where warehouse in (%s)""" + % ", ".join(["%s"] * len(warehouses)), + tuple(warehouses), + ) # reset default company - frappe.db.sql("""update `tabSingles` set value="" + frappe.db.sql( + """update `tabSingles` set value="" where doctype='Global Defaults' and field='default_company' - and value=%s""", self.name) + and value=%s""", + self.name, + ) # reset default company - frappe.db.sql("""update `tabSingles` set value="" + frappe.db.sql( + """update `tabSingles` set value="" where doctype='Chart of Accounts Importer' and field='company' - and value=%s""", self.name) + and value=%s""", + self.name, + ) # delete BOMs boms = frappe.db.sql_list("select name from tabBOM where company=%s", self.name) if boms: frappe.db.sql("delete from tabBOM where company=%s", self.name) for dt in ("BOM Operation", "BOM Item", "BOM Scrap Item", "BOM Explosion Item"): - frappe.db.sql("delete from `tab%s` where parent in (%s)""" - % (dt, ', '.join(['%s']*len(boms))), tuple(boms)) + frappe.db.sql( + "delete from `tab%s` where parent in (%s)" "" % (dt, ", ".join(["%s"] * len(boms))), + tuple(boms), + ) frappe.db.sql("delete from tabEmployee where company=%s", self.name) frappe.db.sql("delete from tabDepartment where company=%s", self.name) @@ -404,18 +502,20 @@ class Company(NestedSet): frappe.db.sql("delete from `tabItem Tax Template` where company=%s", self.name) # delete Process Deferred Accounts if no GL Entry found - if not frappe.db.get_value('GL Entry', {'company': self.name}): + if not frappe.db.get_value("GL Entry", {"company": self.name}): frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name) def check_parent_changed(self): frappe.flags.parent_company_changed = False - if not self.is_new() and \ - self.parent_company != frappe.db.get_value("Company", self.name, "parent_company"): + if not self.is_new() and self.parent_company != frappe.db.get_value( + "Company", self.name, "parent_company" + ): frappe.flags.parent_company_changed = True + def get_name_with_abbr(name, company): - company_abbr = frappe.get_cached_value('Company', company, "abbr") + company_abbr = frappe.get_cached_value("Company", company, "abbr") parts = name.split(" - ") if parts[-1].lower() != company_abbr.lower(): @@ -423,21 +523,27 @@ def get_name_with_abbr(name, company): return " - ".join(parts) + def install_country_fixtures(company, country): - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) + path = frappe.get_app_path("erpnext", "regional", frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country)) frappe.get_attr(module_name)(company, False) except Exception as e: frappe.log_error() - frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country))) + frappe.throw( + _("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format( + frappe.bold(country) + ) + ) def update_company_current_month_sales(company): current_month_year = formatdate(today(), "MM-yyyy") - results = frappe.db.sql(''' + results = frappe.db.sql( + """ SELECT SUM(base_grand_total) AS total, DATE_FORMAT(`posting_date`, '%m-%Y') AS month_year @@ -449,44 +555,58 @@ def update_company_current_month_sales(company): AND company = {company} GROUP BY month_year - '''.format(current_month_year=current_month_year, company=frappe.db.escape(company)), - as_dict = True) + """.format( + current_month_year=current_month_year, company=frappe.db.escape(company) + ), + as_dict=True, + ) - monthly_total = results[0]['total'] if len(results) > 0 else 0 + monthly_total = results[0]["total"] if len(results) > 0 else 0 frappe.db.set_value("Company", company, "total_monthly_sales", monthly_total) + def update_company_monthly_sales(company): - '''Cache past year monthly sales of every company based on sales invoices''' + """Cache past year monthly sales of every company based on sales invoices""" import json from frappe.utils.goal import get_monthly_results - filter_str = "company = {0} and status != 'Draft' and docstatus=1".format(frappe.db.escape(company)) - month_to_value_dict = get_monthly_results("Sales Invoice", "base_grand_total", - "posting_date", filter_str, "sum") + + filter_str = "company = {0} and status != 'Draft' and docstatus=1".format( + frappe.db.escape(company) + ) + month_to_value_dict = get_monthly_results( + "Sales Invoice", "base_grand_total", "posting_date", filter_str, "sum" + ) frappe.db.set_value("Company", company, "sales_monthly_history", json.dumps(month_to_value_dict)) + def update_transactions_annual_history(company, commit=False): transactions_history = get_all_transactions_annual_history(company) - frappe.db.set_value("Company", company, "transactions_annual_history", json.dumps(transactions_history)) + frappe.db.set_value( + "Company", company, "transactions_annual_history", json.dumps(transactions_history) + ) if commit: frappe.db.commit() + def cache_companies_monthly_sales_history(): - companies = [d['name'] for d in frappe.get_list("Company")] + companies = [d["name"] for d in frappe.get_list("Company")] for company in companies: update_company_monthly_sales(company) update_transactions_annual_history(company) frappe.db.commit() + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): if parent == None or parent == "All Companies": parent = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name as value, is_group as expandable @@ -495,25 +615,30 @@ def get_children(doctype, parent=None, company=None, is_root=False): where ifnull(parent_company, "")={parent} """.format( - doctype = doctype, - parent=frappe.db.escape(parent) - ), as_dict=1) + doctype=doctype, parent=frappe.db.escape(parent) + ), + as_dict=1, + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = frappe.form_dict args = make_tree_args(**args) - if args.parent_company == 'All Companies': + if args.parent_company == "All Companies": args.parent_company = None frappe.get_doc(args).insert() + def get_all_transactions_annual_history(company): out = {} - items = frappe.db.sql(''' + items = frappe.db.sql( + """ select transaction_date, count(*) as count from ( @@ -553,61 +678,68 @@ def get_all_transactions_annual_history(company): group by transaction_date - ''', (company), as_dict=True) + """, + (company), + as_dict=True, + ) for d in items: timestamp = get_timestamp(d["transaction_date"]) - out.update({ timestamp: d["count"] }) + out.update({timestamp: d["count"]}) return out + def get_timeline_data(doctype, name): - '''returns timeline data based on linked records in dashboard''' + """returns timeline data based on linked records in dashboard""" out = {} date_to_value_dict = {} - history = frappe.get_cached_value('Company', name, "transactions_annual_history") + history = frappe.get_cached_value("Company", name, "transactions_annual_history") try: - date_to_value_dict = json.loads(history) if history and '{' in history else None + date_to_value_dict = json.loads(history) if history and "{" in history else None except ValueError: date_to_value_dict = None if date_to_value_dict is None: update_transactions_annual_history(name, True) - history = frappe.get_cached_value('Company', name, "transactions_annual_history") - return json.loads(history) if history and '{' in history else {} + history = frappe.get_cached_value("Company", name, "transactions_annual_history") + return json.loads(history) if history and "{" in history else {} return date_to_value_dict + @frappe.whitelist() -def get_default_company_address(name, sort_key='is_primary_address', existing_address=None): - if sort_key not in ['is_shipping_address', 'is_primary_address']: +def get_default_company_address(name, sort_key="is_primary_address", existing_address=None): + if sort_key not in ["is_shipping_address", "is_primary_address"]: return None - out = frappe.db.sql(""" SELECT + out = frappe.db.sql( + """ SELECT addr.name, addr.%s FROM `tabAddress` addr, `tabDynamic Link` dl WHERE dl.parent = addr.name and dl.link_doctype = 'Company' and dl.link_name = %s and ifnull(addr.disabled, 0) = 0 - """ %(sort_key, '%s'), (name)) #nosec + """ + % (sort_key, "%s"), + (name), + ) # nosec if existing_address: if existing_address in [d[0] for d in out]: return existing_address if out: - return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0] + return sorted(out, key=functools.cmp_to_key(lambda x, y: cmp(y[1], x[1])))[0][0] else: return None + @frappe.whitelist() def create_transaction_deletion_request(company): - tdr = frappe.get_doc({ - 'doctype': 'Transaction Deletion Record', - 'company': company - }) + tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() tdr.submit() diff --git a/erpnext/setup/doctype/company/company_dashboard.py b/erpnext/setup/doctype/company/company_dashboard.py index b63c05dbd11..ff1e7f1103b 100644 --- a/erpnext/setup/doctype/company/company_dashboard.py +++ b/erpnext/setup/doctype/company/company_dashboard.py @@ -1,41 +1,27 @@ - from frappe import _ def get_data(): return { - 'graph': True, - 'graph_method': "frappe.utils.goal.get_monthly_goal_graph_data", - 'graph_method_args': { - 'title': _('Sales'), - 'goal_value_field': 'monthly_sales_target', - 'goal_total_field': 'total_monthly_sales', - 'goal_history_field': 'sales_monthly_history', - 'goal_doctype': 'Sales Invoice', - 'goal_doctype_link': 'company', - 'goal_field': 'base_grand_total', - 'date_field': 'posting_date', - 'filter_str': "docstatus = 1 and is_opening != 'Yes'", - 'aggregation': 'sum' + "graph": True, + "graph_method": "frappe.utils.goal.get_monthly_goal_graph_data", + "graph_method_args": { + "title": _("Sales"), + "goal_value_field": "monthly_sales_target", + "goal_total_field": "total_monthly_sales", + "goal_history_field": "sales_monthly_history", + "goal_doctype": "Sales Invoice", + "goal_doctype_link": "company", + "goal_field": "base_grand_total", + "date_field": "posting_date", + "filter_str": "docstatus = 1 and is_opening != 'Yes'", + "aggregation": "sum", }, - - 'fieldname': 'company', - 'transactions': [ - { - 'label': _('Pre Sales'), - 'items': ['Quotation'] - }, - { - 'label': _('Orders'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Support'), - 'items': ['Issue'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - } - ] + "fieldname": "company", + "transactions": [ + {"label": _("Pre Sales"), "items": ["Quotation"]}, + {"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Support"), "items": ["Issue"]}, + {"label": _("Projects"), "items": ["Project"]}, + ], } diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index e175c5435aa..29e056e34f0 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -14,7 +14,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"] test_dependencies = ["Fiscal Year"] -test_records = frappe.get_test_records('Company') +test_records = frappe.get_test_records("Company") + class TestCompany(unittest.TestCase): def test_coa_based_on_existing_company(self): @@ -37,8 +38,8 @@ class TestCompany(unittest.TestCase): "account_type": "Cash", "is_group": 0, "root_type": "Asset", - "parent_account": "Cash In Hand - CFEC" - } + "parent_account": "Cash In Hand - CFEC", + }, } for account, acc_property in expected_results.items(): @@ -69,15 +70,22 @@ class TestCompany(unittest.TestCase): company.chart_of_accounts = template company.save() - account_types = ["Cost of Goods Sold", "Depreciation", - "Expenses Included In Valuation", "Fixed Asset", "Payable", "Receivable", - "Stock Adjustment", "Stock Received But Not Billed", "Bank", "Cash", "Stock"] + account_types = [ + "Cost of Goods Sold", + "Depreciation", + "Expenses Included In Valuation", + "Fixed Asset", + "Payable", + "Receivable", + "Stock Adjustment", + "Stock Received But Not Billed", + "Bank", + "Cash", + "Stock", + ] for account_type in account_types: - filters = { - "company": template, - "account_type": account_type - } + filters = {"company": template, "account_type": account_type} if account_type in ["Bank", "Cash"]: filters["is_group"] = 1 @@ -90,8 +98,11 @@ class TestCompany(unittest.TestCase): frappe.delete_doc("Company", template) def delete_mode_of_payment(self, company): - frappe.db.sql(""" delete from `tabMode of Payment Account` - where company =%s """, (company)) + frappe.db.sql( + """ delete from `tabMode of Payment Account` + where company =%s """, + (company), + ) def test_basic_tree(self, records=None): min_lft = 1 @@ -101,12 +112,12 @@ class TestCompany(unittest.TestCase): records = test_records[2:] for company in records: - lft, rgt, parent_company = frappe.db.get_value("Company", company["company_name"], - ["lft", "rgt", "parent_company"]) + lft, rgt, parent_company = frappe.db.get_value( + "Company", company["company_name"], ["lft", "rgt", "parent_company"] + ) if parent_company: - parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company, - ["lft", "rgt"]) + parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company, ["lft", "rgt"]) else: # root parent_lft = min_lft - 1 @@ -125,8 +136,11 @@ class TestCompany(unittest.TestCase): def get_no_of_children(companies, no_of_children): children = [] for company in companies: - children += frappe.db.sql_list("""select name from `tabCompany` - where ifnull(parent_company, '')=%s""", company or '') + children += frappe.db.sql_list( + """select name from `tabCompany` + where ifnull(parent_company, '')=%s""", + company or "", + ) if len(children): return get_no_of_children(children, no_of_children + len(children)) @@ -148,40 +162,45 @@ class TestCompany(unittest.TestCase): child_company.save() self.test_basic_tree() + def create_company_communication(doctype, docname): - comm = frappe.get_doc({ + comm = frappe.get_doc( + { "doctype": "Communication", "communication_type": "Communication", "content": "Deduplication of Links", "communication_medium": "Email", - "reference_doctype":doctype, - "reference_name":docname - }) + "reference_doctype": doctype, + "reference_name": docname, + } + ) comm.insert() + def create_child_company(): child_company = frappe.db.exists("Company", "Test Company") if not child_company: - child_company = frappe.get_doc({ - "doctype":"Company", - "company_name":"Test Company", - "abbr":"test_company", - "default_currency":"INR" - }) + child_company = frappe.get_doc( + { + "doctype": "Company", + "company_name": "Test Company", + "abbr": "test_company", + "default_currency": "INR", + } + ) child_company.insert() else: child_company = frappe.get_doc("Company", child_company) return child_company.name + def create_test_lead_in_company(company): lead = frappe.db.exists("Lead", "Test Lead in new company") if not lead: - lead = frappe.get_doc({ - "doctype": "Lead", - "lead_name": "Test Lead in new company", - "scompany": company - }) + lead = frappe.get_doc( + {"doctype": "Lead", "lead_name": "Test Lead in new company", "scompany": company} + ) lead.insert() else: lead = frappe.get_doc("Lead", lead) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index 89be607d047..19b6ef27ac3 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -8,7 +8,8 @@ "domain": "Manufacturing", "chart_of_accounts": "Standard", "default_holiday_list": "_Test Holiday List", - "enable_perpetual_inventory": 0 + "enable_perpetual_inventory": 0, + "allow_account_creation_against_child_company": 1 }, { "abbr": "_TC1", diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.py b/erpnext/setup/doctype/currency_exchange/currency_exchange.py index 4191935742f..f9f3b3a7dcb 100644 --- a/erpnext/setup/doctype/currency_exchange/currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.py @@ -17,13 +17,17 @@ class CurrencyExchange(Document): # If both selling and buying enabled purpose = "Selling-Buying" - if cint(self.for_buying)==0 and cint(self.for_selling)==1: + if cint(self.for_buying) == 0 and cint(self.for_selling) == 1: purpose = "Selling" - if cint(self.for_buying)==1 and cint(self.for_selling)==0: + if cint(self.for_buying) == 1 and cint(self.for_selling) == 0: purpose = "Buying" - self.name = '{0}-{1}-{2}{3}'.format(formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), - self.from_currency, self.to_currency, ("-" + purpose) if purpose else "") + self.name = "{0}-{1}-{2}{3}".format( + formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), + self.from_currency, + self.to_currency, + ("-" + purpose) if purpose else "", + ) def validate(self): self.validate_value("exchange_rate", ">", 0) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index c8d137c4ca2..dcd06607c3e 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -8,7 +8,7 @@ from frappe.utils import cint, flt from erpnext.setup.utils import get_exchange_rate -test_records = frappe.get_test_records('Currency Exchange') +test_records = frappe.get_test_records("Currency Exchange") def save_new_records(test_records): @@ -16,13 +16,19 @@ def save_new_records(test_records): # If both selling and buying enabled purpose = "Selling-Buying" - if cint(record.get("for_buying"))==0 and cint(record.get("for_selling"))==1: + if cint(record.get("for_buying")) == 0 and cint(record.get("for_selling")) == 1: purpose = "Selling" - if cint(record.get("for_buying"))==1 and cint(record.get("for_selling"))==0: + if cint(record.get("for_buying")) == 1 and cint(record.get("for_selling")) == 0: purpose = "Buying" kwargs = dict( doctype=record.get("doctype"), - docname=record.get("date") + '-' + record.get("from_currency") + '-' + record.get("to_currency") + '-' + purpose, + docname=record.get("date") + + "-" + + record.get("from_currency") + + "-" + + record.get("to_currency") + + "-" + + purpose, fieldname="exchange_rate", value=record.get("exchange_rate"), ) diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 5b917265d99..246cc195e12 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -8,7 +8,8 @@ from frappe.utils.nestedset import NestedSet, get_root_of class CustomerGroup(NestedSet): - nsm_parent_field = 'parent_customer_group' + nsm_parent_field = "parent_customer_group" + def validate(self): if not self.parent_customer_group: self.parent_customer_group = get_root_of("Customer Group") @@ -22,12 +23,18 @@ class CustomerGroup(NestedSet): if frappe.db.exists("Customer", self.name): frappe.msgprint(_("A customer with the same name already exists"), raise_exception=1) -def get_parent_customer_groups(customer_group): - lft, rgt = frappe.db.get_value("Customer Group", customer_group, ['lft', 'rgt']) - return frappe.db.sql("""select name from `tabCustomer Group` +def get_parent_customer_groups(customer_group): + lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"]) + + return frappe.db.sql( + """select name from `tabCustomer Group` where lft <= %s and rgt >= %s - order by lft asc""", (lft, rgt), as_dict=True) + order by lft asc""", + (lft, rgt), + as_dict=True, + ) + def on_doctype_update(): frappe.db.add_index("Customer Group", ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/customer_group/test_customer_group.py b/erpnext/setup/doctype/customer_group/test_customer_group.py index f02ae097928..88762701f59 100644 --- a/erpnext/setup/doctype/customer_group/test_customer_group.py +++ b/erpnext/setup/doctype/customer_group/test_customer_group.py @@ -4,7 +4,6 @@ test_ignore = ["Price List"] - import frappe -test_records = frappe.get_test_records('Customer Group') +test_records = frappe.get_test_records("Customer Group") diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 02f9156e196..cdfea7764f1 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -36,16 +36,22 @@ class EmailDigest(Document): self.from_date, self.to_date = self.get_from_to_date() self.set_dates() self._accounts = {} - self.currency = frappe.db.get_value('Company', self.company, "default_currency") + self.currency = frappe.db.get_value("Company", self.company, "default_currency") @frappe.whitelist() def get_users(self): """get list of users""" - user_list = frappe.db.sql(""" + user_list = frappe.db.sql( + """ select name, enabled from tabUser where name not in ({}) and user_type != "Website User" - order by enabled desc, name asc""".format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS, as_dict=1) + order by enabled desc, name asc""".format( + ", ".join(["%s"] * len(STANDARD_USERS)) + ), + STANDARD_USERS, + as_dict=1, + ) if self.recipient_list: recipient_list = self.recipient_list.split("\n") @@ -54,13 +60,18 @@ class EmailDigest(Document): for p in user_list: p["checked"] = p["name"] in recipient_list and 1 or 0 - frappe.response['user_list'] = user_list + frappe.response["user_list"] = user_list @frappe.whitelist() def send(self): # send email only to enabled users - valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser` - where enabled=1""")] + valid_users = [ + p[0] + for p in frappe.db.sql( + """select name from `tabUser` + where enabled=1""" + ) + ] if self.recipients: for row in self.recipients: @@ -70,9 +81,10 @@ class EmailDigest(Document): recipients=row.recipient, subject=_("{0} Digest").format(self.frequency), message=msg_for_this_recipient, - reference_doctype = self.doctype, - reference_name = self.name, - unsubscribe_message = _("Unsubscribe from this Email Digest")) + reference_doctype=self.doctype, + reference_name=self.name, + unsubscribe_message=_("Unsubscribe from this Email Digest"), + ) def get_msg_html(self): """Build email digest content""" @@ -104,7 +116,10 @@ class EmailDigest(Document): context.quote = {"text": quote[0], "author": quote[1]} if self.get("purchase_orders_items_overdue"): - context.purchase_order_list, context.purchase_orders_items_overdue_list = self.get_purchase_orders_items_overdue_list() + ( + context.purchase_order_list, + context.purchase_orders_items_overdue_list, + ) = self.get_purchase_orders_items_overdue_list() if not context.purchase_order_list: frappe.throw(_("No items to be received are overdue")) @@ -114,49 +129,54 @@ class EmailDigest(Document): frappe.flags.ignore_account_permission = False # style - return frappe.render_template("erpnext/setup/doctype/email_digest/templates/default.html", - context, is_path=True) + return frappe.render_template( + "erpnext/setup/doctype/email_digest/templates/default.html", context, is_path=True + ) def set_title(self, context): """Set digest title""" - if self.frequency=="Daily": + if self.frequency == "Daily": context.title = _("Daily Reminders") context.subtitle = _("Pending activities for today") - elif self.frequency=="Weekly": + elif self.frequency == "Weekly": context.title = _("This Week's Summary") context.subtitle = _("Summary for this week and pending activities") - elif self.frequency=="Monthly": + elif self.frequency == "Monthly": context.title = _("This Month's Summary") context.subtitle = _("Summary for this month and pending activities") def set_style(self, context): """Set standard digest style""" - context.text_muted = '#8D99A6' - context.text_color = '#36414C' - context.h1 = 'margin-bottom: 30px; margin-top: 40px; font-weight: 400; font-size: 30px;' - context.h2 = 'margin-bottom: 30px; margin-top: -20px; font-weight: 400; font-size: 20px;' - context.label_css = '''display: inline-block; color: {text_muted}; - padding: 3px 7px; margin-right: 7px;'''.format(text_muted = context.text_muted) - context.section_head = 'margin-top: 60px; font-size: 16px;' - context.line_item = 'padding: 5px 0px; margin: 0; border-bottom: 1px solid #d1d8dd;' - context.link_css = 'color: {text_color}; text-decoration: none;'.format(text_color = context.text_color) - + context.text_muted = "#8D99A6" + context.text_color = "#36414C" + context.h1 = "margin-bottom: 30px; margin-top: 40px; font-weight: 400; font-size: 30px;" + context.h2 = "margin-bottom: 30px; margin-top: -20px; font-weight: 400; font-size: 20px;" + context.label_css = """display: inline-block; color: {text_muted}; + padding: 3px 7px; margin-right: 7px;""".format( + text_muted=context.text_muted + ) + context.section_head = "margin-top: 60px; font-size: 16px;" + context.line_item = "padding: 5px 0px; margin: 0; border-bottom: 1px solid #d1d8dd;" + context.link_css = "color: {text_color}; text-decoration: none;".format( + text_color=context.text_color + ) def get_notifications(self): """Get notifications for user""" notifications = frappe.desk.notifications.get_notifications() - notifications = sorted(notifications.get("open_count_doctype", {}).items(), - key=lambda a: a[1]) + notifications = sorted(notifications.get("open_count_doctype", {}).items(), key=lambda a: a[1]) - notifications = [{"key": n[0], "value": n[1], - "link": get_url_to_list(n[0])} for n in notifications if n[1]] + notifications = [ + {"key": n[0], "value": n[1], "link": get_url_to_list(n[0])} for n in notifications if n[1] + ] return notifications def get_calendar_events(self): """Get calendar events for given user""" from frappe.desk.doctype.event.event import get_events + from_date, to_date = get_future_date_for_calendaer_event(self.frequency) events = get_events(from_date, to_date) @@ -176,10 +196,13 @@ class EmailDigest(Document): if not user_id: user_id = frappe.session.user - todo_list = frappe.db.sql("""select * + todo_list = frappe.db.sql( + """select * from `tabToDo` where (owner=%s or assigned_by=%s) and status="Open" order by field(priority, 'High', 'Medium', 'Low') asc, date asc limit 20""", - (user_id, user_id), as_dict=True) + (user_id, user_id), + as_dict=True, + ) for t in todo_list: t.link = get_url_to_form("ToDo", t.name) @@ -191,9 +214,11 @@ class EmailDigest(Document): if not user_id: user_id = frappe.session.user - return frappe.db.sql("""select count(*) from `tabToDo` + return frappe.db.sql( + """select count(*) from `tabToDo` where status='Open' and (owner=%s or assigned_by=%s)""", - (user_id, user_id))[0][0] + (user_id, user_id), + )[0][0] def get_issue_list(self, user_id=None): """Get issue list""" @@ -205,9 +230,12 @@ class EmailDigest(Document): if not role_permissions.get("read"): return None - issue_list = frappe.db.sql("""select * + issue_list = frappe.db.sql( + """select * from `tabIssue` where status in ("Replied","Open") - order by modified asc limit 10""", as_dict=True) + order by modified asc limit 10""", + as_dict=True, + ) for t in issue_list: t.link = get_url_to_form("Issue", t.name) @@ -216,17 +244,22 @@ class EmailDigest(Document): def get_issue_count(self): """Get count of Issue""" - return frappe.db.sql("""select count(*) from `tabIssue` - where status in ('Open','Replied') """)[0][0] + return frappe.db.sql( + """select count(*) from `tabIssue` + where status in ('Open','Replied') """ + )[0][0] def get_project_list(self, user_id=None): """Get project list""" if not user_id: user_id = frappe.session.user - project_list = frappe.db.sql("""select * + project_list = frappe.db.sql( + """select * from `tabProject` where status='Open' and project_type='External' - order by modified asc limit 10""", as_dict=True) + order by modified asc limit 10""", + as_dict=True, + ) for t in project_list: t.link = get_url_to_form("Issue", t.name) @@ -235,22 +268,41 @@ class EmailDigest(Document): def get_project_count(self): """Get count of Project""" - return frappe.db.sql("""select count(*) from `tabProject` - where status='Open' and project_type='External'""")[0][0] + return frappe.db.sql( + """select count(*) from `tabProject` + where status='Open' and project_type='External'""" + )[0][0] def set_accounting_cards(self, context): """Create accounting cards if checked""" cache = frappe.cache() context.cards = [] - for key in ("income", "expenses_booked", "income_year_to_date", "expense_year_to_date", - "bank_balance", "credit_balance", "invoiced_amount", "payables", - "sales_orders_to_bill", "purchase_orders_to_bill", "sales_order", "purchase_order", - "sales_orders_to_deliver", "purchase_orders_to_receive", "sales_invoice", "purchase_invoice", - "new_quotations", "pending_quotations"): + for key in ( + "income", + "expenses_booked", + "income_year_to_date", + "expense_year_to_date", + "bank_balance", + "credit_balance", + "invoiced_amount", + "payables", + "sales_orders_to_bill", + "purchase_orders_to_bill", + "sales_order", + "purchase_order", + "sales_orders_to_deliver", + "purchase_orders_to_receive", + "sales_invoice", + "purchase_invoice", + "new_quotations", + "pending_quotations", + ): if self.get(key): - cache_key = "email_digest:card:{0}:{1}:{2}:{3}".format(self.company, self.frequency, key, self.from_date) + cache_key = "email_digest:card:{0}:{1}:{2}:{3}".format( + self.company, self.frequency, key, self.from_date + ) card = cache.get(cache_key) if card: @@ -271,8 +323,9 @@ class EmailDigest(Document): if key == "credit_balance": card.last_value = card.last_value * -1 - card.last_value = self.fmt_money(card.last_value,False if key in ("bank_balance", "credit_balance") else True) - + card.last_value = self.fmt_money( + card.last_value, False if key in ("bank_balance", "credit_balance") else True + ) if card.billed_value: card.billed = int(flt(card.billed_value) / card.value * 100) @@ -285,9 +338,11 @@ class EmailDigest(Document): else: card.delivered = "% Received " + str(card.delivered) - if key =="credit_balance": - card.value = card.value *-1 - card.value = self.fmt_money(card.value,False if key in ("bank_balance", "credit_balance") else True) + if key == "credit_balance": + card.value = card.value * -1 + card.value = self.fmt_money( + card.value, False if key in ("bank_balance", "credit_balance") else True + ) cache.set_value(cache_key, card, expires_in_sec=24 * 60 * 60) @@ -295,30 +350,25 @@ class EmailDigest(Document): def get_income(self): """Get income for given period""" - income, past_income, count = self.get_period_amounts(self.get_roots("income"),'income') + income, past_income, count = self.get_period_amounts(self.get_roots("income"), "income") - income_account = frappe.db.get_all('Account', + income_account = frappe.db.get_all( + "Account", fields=["name"], - filters={ - "root_type":"Income", - "parent_account":'', - "company": self.company - }) + filters={"root_type": "Income", "parent_account": "", "company": self.company}, + ) - label = get_link_to_report("General Ledger",self.meta.get_label("income"), + label = get_link_to_report( + "General Ledger", + self.meta.get_label("income"), filters={ "from_date": self.future_from_date, "to_date": self.future_to_date, "account": income_account[0].name, - "company": self.company - } + "company": self.company, + }, ) - return { - "label": label, - "value": income, - "last_value": past_income, - "count": count - } + return {"label": label, "value": income, "last_value": past_income, "count": count} def get_income_year_to_date(self): """Get income to date""" @@ -326,7 +376,7 @@ class EmailDigest(Document): def get_expense_year_to_date(self): """Get income to date""" - return self.get_year_to_date_balance("expense","expenses_booked") + return self.get_year_to_date_balance("expense", "expenses_booked") def get_year_to_date_balance(self, root_type, fieldname): """Get income to date""" @@ -334,67 +384,63 @@ class EmailDigest(Document): count = 0 for account in self.get_root_type_accounts(root_type): - balance += get_balance_on(account, date = self.future_to_date) - count += get_count_on(account, fieldname, date = self.future_to_date) + balance += get_balance_on(account, date=self.future_to_date) + count += get_count_on(account, fieldname, date=self.future_to_date) - if fieldname == 'income': - filters = { - "currency": self.currency - } - label = get_link_to_report('Profit and Loss Statement', label=self.meta.get_label(root_type + "_year_to_date"), filters=filters) + if fieldname == "income": + filters = {"currency": self.currency} + label = get_link_to_report( + "Profit and Loss Statement", + label=self.meta.get_label(root_type + "_year_to_date"), + filters=filters, + ) - elif fieldname == 'expenses_booked': - filters = { - "currency": self.currency - } - label = get_link_to_report('Profit and Loss Statement', label=self.meta.get_label(root_type + "_year_to_date"), filters=filters) + elif fieldname == "expenses_booked": + filters = {"currency": self.currency} + label = get_link_to_report( + "Profit and Loss Statement", + label=self.meta.get_label(root_type + "_year_to_date"), + filters=filters, + ) - return { - "label": label, - "value": balance, - "count": count - } + return {"label": label, "value": balance, "count": count} def get_bank_balance(self): # account is of type "Bank" and root_type is Asset - return self.get_type_balance('bank_balance', 'Bank', root_type='Asset') + return self.get_type_balance("bank_balance", "Bank", root_type="Asset") def get_credit_balance(self): # account is of type "Bank" and root_type is Liability - return self.get_type_balance('credit_balance', 'Bank', root_type='Liability') + return self.get_type_balance("credit_balance", "Bank", root_type="Liability") def get_payables(self): - return self.get_type_balance('payables', 'Payable') + return self.get_type_balance("payables", "Payable") def get_invoiced_amount(self): - return self.get_type_balance('invoiced_amount', 'Receivable') + return self.get_type_balance("invoiced_amount", "Receivable") def get_expenses_booked(self): - expenses, past_expenses, count = self.get_period_amounts(self.get_roots("expense"), 'expenses_booked') - - expense_account = frappe.db.get_all('Account', - fields=["name"], - filters={ - "root_type": "Expense", - "parent_account": '', - "company": self.company - } - ) - - label = get_link_to_report("General Ledger",self.meta.get_label("expenses_booked"), - filters={ - "company":self.company, - "from_date":self.future_from_date, - "to_date":self.future_to_date, - "account": expense_account[0].name - } + expenses, past_expenses, count = self.get_period_amounts( + self.get_roots("expense"), "expenses_booked" ) - return { - "label": label, - "value": expenses, - "last_value": past_expenses, - "count": count - } + + expense_account = frappe.db.get_all( + "Account", + fields=["name"], + filters={"root_type": "Expense", "parent_account": "", "company": self.company}, + ) + + label = get_link_to_report( + "General Ledger", + self.meta.get_label("expenses_booked"), + filters={ + "company": self.company, + "from_date": self.future_from_date, + "to_date": self.future_to_date, + "account": expense_account[0].name, + }, + ) + return {"label": label, "value": expenses, "last_value": past_expenses, "count": count} def get_period_amounts(self, accounts, fieldname): """Get amounts for current and past periods""" @@ -410,113 +456,129 @@ class EmailDigest(Document): def get_sales_orders_to_bill(self): """Get value not billed""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), count(*) from `tabSales Order` where (transaction_date <= %(to_date)s) and billing_status != "Fully Billed" and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Sales Order', label=self.meta.get_label("sales_orders_to_bill"), + label = get_link_to_report( + "Sales Order", + label=self.meta.get_label("sales_orders_to_bill"), report_type="Report Builder", doctype="Sales Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"]], - "per_billed": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"]], + "per_billed": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_sales_orders_to_deliver(self): """Get value not delivered""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_delivered/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_delivered/100)),0), count(*) from `tabSales Order` where (transaction_date <= %(to_date)s) and delivery_status != "Fully Delivered" and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Sales Order', label=self.meta.get_label("sales_orders_to_deliver"), + label = get_link_to_report( + "Sales Order", + label=self.meta.get_label("sales_orders_to_deliver"), report_type="Report Builder", doctype="Sales Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "delivery_status": [['!=', "Fully Delivered"]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "delivery_status": [["!=", "Fully Delivered"]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_purchase_orders_to_receive(self): """Get value not received""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total))-(sum(grand_total*per_received/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total))-(sum(grand_total*per_received/100)),0), count(*) from `tabPurchase Order` where (transaction_date <= %(to_date)s) and per_received < 100 and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Purchase Order', label=self.meta.get_label("purchase_orders_to_receive"), + label = get_link_to_report( + "Purchase Order", + label=self.meta.get_label("purchase_orders_to_receive"), report_type="Report Builder", doctype="Purchase Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_purchase_orders_to_bill(self): """Get purchase not billed""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), count(*) from `tabPurchase Order` where (transaction_date <= %(to_date)s) and per_billed < 100 and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Purchase Order', label=self.meta.get_label("purchase_orders_to_bill"), + label = get_link_to_report( + "Purchase Order", + label=self.meta.get_label("purchase_orders_to_bill"), report_type="Report Builder", doctype="Purchase Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_type_balance(self, fieldname, account_type, root_type=None): if root_type: - accounts = [d.name for d in \ - frappe.db.get_all("Account", filters={"account_type": account_type, - "company": self.company, "is_group": 0, "root_type": root_type})] + accounts = [ + d.name + for d in frappe.db.get_all( + "Account", + filters={ + "account_type": account_type, + "company": self.company, + "is_group": 0, + "root_type": root_type, + }, + ) + ] else: - accounts = [d.name for d in \ - frappe.db.get_all("Account", filters={"account_type": account_type, - "company": self.company, "is_group": 0})] + accounts = [ + d.name + for d in frappe.db.get_all( + "Account", filters={"account_type": account_type, "company": self.company, "is_group": 0} + ) + ] balance = prev_balance = 0.0 count = 0 @@ -525,92 +587,99 @@ class EmailDigest(Document): count += get_count_on(account, fieldname, date=self.future_to_date) prev_balance += get_balance_on(account, date=self.past_to_date, in_account_currency=False) - if fieldname in ("bank_balance","credit_balance"): + if fieldname in ("bank_balance", "credit_balance"): label = "" if fieldname == "bank_balance": filters = { "root_type": "Asset", "account_type": "Bank", "report_date": self.future_to_date, - "company": self.company + "company": self.company, } - label = get_link_to_report('Account Balance', label=self.meta.get_label(fieldname), filters=filters) + label = get_link_to_report( + "Account Balance", label=self.meta.get_label(fieldname), filters=filters + ) else: filters = { "root_type": "Liability", "account_type": "Bank", "report_date": self.future_to_date, - "company": self.company + "company": self.company, } - label = get_link_to_report('Account Balance', label=self.meta.get_label(fieldname), filters=filters) + label = get_link_to_report( + "Account Balance", label=self.meta.get_label(fieldname), filters=filters + ) - return { - 'label': label, - 'value': balance, - 'last_value': prev_balance - } + return {"label": label, "value": balance, "last_value": prev_balance} else: - if account_type == 'Payable': - label = get_link_to_report('Accounts Payable', label=self.meta.get_label(fieldname), - filters={ - "report_date": self.future_to_date, - "company": self.company - } ) - elif account_type == 'Receivable': - label = get_link_to_report('Accounts Receivable', label=self.meta.get_label(fieldname), - filters={ - "report_date": self.future_to_date, - "company": self.company - }) + if account_type == "Payable": + label = get_link_to_report( + "Accounts Payable", + label=self.meta.get_label(fieldname), + filters={"report_date": self.future_to_date, "company": self.company}, + ) + elif account_type == "Receivable": + label = get_link_to_report( + "Accounts Receivable", + label=self.meta.get_label(fieldname), + filters={"report_date": self.future_to_date, "company": self.company}, + ) else: label = self.meta.get_label(fieldname) - return { - 'label': label, - 'value': balance, - 'last_value': prev_balance, - 'count': count - } + return {"label": label, "value": balance, "last_value": prev_balance, "count": count} def get_roots(self, root_type): - return [d.name for d in frappe.db.get_all("Account", - filters={"root_type": root_type.title(), "company": self.company, - "is_group": 1, "parent_account": ["in", ("", None)]})] + return [ + d.name + for d in frappe.db.get_all( + "Account", + filters={ + "root_type": root_type.title(), + "company": self.company, + "is_group": 1, + "parent_account": ["in", ("", None)], + }, + ) + ] def get_root_type_accounts(self, root_type): if not root_type in self._accounts: - self._accounts[root_type] = [d.name for d in \ - frappe.db.get_all("Account", filters={"root_type": root_type.title(), - "company": self.company, "is_group": 0})] + self._accounts[root_type] = [ + d.name + for d in frappe.db.get_all( + "Account", filters={"root_type": root_type.title(), "company": self.company, "is_group": 0} + ) + ] return self._accounts[root_type] def get_purchase_order(self): - return self.get_summary_of_doc("Purchase Order","purchase_order") + return self.get_summary_of_doc("Purchase Order", "purchase_order") def get_sales_order(self): - return self.get_summary_of_doc("Sales Order","sales_order") + return self.get_summary_of_doc("Sales Order", "sales_order") def get_pending_purchase_orders(self): - return self.get_summary_of_pending("Purchase Order","pending_purchase_orders","per_received") + return self.get_summary_of_pending("Purchase Order", "pending_purchase_orders", "per_received") def get_pending_sales_orders(self): - return self.get_summary_of_pending("Sales Order","pending_sales_orders","per_delivered") + return self.get_summary_of_pending("Sales Order", "pending_sales_orders", "per_delivered") def get_sales_invoice(self): - return self.get_summary_of_doc("Sales Invoice","sales_invoice") + return self.get_summary_of_doc("Sales Invoice", "sales_invoice") def get_purchase_invoice(self): - return self.get_summary_of_doc("Purchase Invoice","purchase_invoice") + return self.get_summary_of_doc("Purchase Invoice", "purchase_invoice") def get_new_quotations(self): - return self.get_summary_of_doc("Quotation","new_quotations") + return self.get_summary_of_doc("Quotation", "new_quotations") def get_pending_quotations(self): @@ -618,89 +687,104 @@ class EmailDigest(Document): def get_summary_of_pending(self, doc_type, fieldname, getfield): - value, count, billed_value, delivered_value = frappe.db.sql("""select ifnull(sum(grand_total),0), count(*), + value, count, billed_value, delivered_value = frappe.db.sql( + """select ifnull(sum(grand_total),0), count(*), ifnull(sum(grand_total*per_billed/100),0), ifnull(sum(grand_total*{0}/100),0) from `tab{1}` where (transaction_date <= %(to_date)s) and status not in ('Closed','Cancelled', 'Completed') - and company = %(company)s """.format(getfield, doc_type), - {"to_date": self.future_to_date, "company": self.company})[0] + and company = %(company)s """.format( + getfield, doc_type + ), + {"to_date": self.future_to_date, "company": self.company}, + )[0] return { "label": self.meta.get_label(fieldname), "value": value, "billed_value": billed_value, "delivered_value": delivered_value, - "count": count + "count": count, } def get_summary_of_pending_quotations(self, fieldname): - value, count = frappe.db.sql("""select ifnull(sum(grand_total),0), count(*) from `tabQuotation` + value, count = frappe.db.sql( + """select ifnull(sum(grand_total),0), count(*) from `tabQuotation` where (transaction_date <= %(to_date)s) and company = %(company)s - and status not in ('Ordered','Cancelled', 'Lost') """,{"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Ordered','Cancelled', 'Lost') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - last_value = frappe.db.sql("""select ifnull(sum(grand_total),0) from `tabQuotation` + last_value = frappe.db.sql( + """select ifnull(sum(grand_total),0) from `tabQuotation` where (transaction_date <= %(to_date)s) and company = %(company)s - and status not in ('Ordered','Cancelled', 'Lost') """,{"to_date": self.past_to_date, "company": self.company})[0][0] + and status not in ('Ordered','Cancelled', 'Lost') """, + {"to_date": self.past_to_date, "company": self.company}, + )[0][0] - label = get_link_to_report('Quotation', label=self.meta.get_label(fieldname), + label = get_link_to_report( + "Quotation", + label=self.meta.get_label(fieldname), report_type="Report Builder", doctype="Quotation", - filters = { - "status": [['!=', "Ordered"], ['!=', "Cancelled"], ['!=', "Lost"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Ordered"], ["!=", "Cancelled"], ["!=", "Lost"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "last_value": last_value, - "count": count - } + return {"label": label, "value": value, "last_value": last_value, "count": count} def get_summary_of_doc(self, doc_type, fieldname): - date_field = 'posting_date' if doc_type in ['Sales Invoice', 'Purchase Invoice'] \ - else 'transaction_date' + date_field = ( + "posting_date" if doc_type in ["Sales Invoice", "Purchase Invoice"] else "transaction_date" + ) - value = flt(self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].grand_total) + value = flt( + self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].grand_total + ) count = self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].count - last_value = flt(self.get_total_on(doc_type, self.past_from_date, self.past_to_date)[0].grand_total) + last_value = flt( + self.get_total_on(doc_type, self.past_from_date, self.past_to_date)[0].grand_total + ) filters = { - date_field: [['>=', self.future_from_date], ['<=', self.future_to_date]], - "status": [['!=','Cancelled']], - "company": self.company + date_field: [[">=", self.future_from_date], ["<=", self.future_to_date]], + "status": [["!=", "Cancelled"]], + "company": self.company, } - label = get_link_to_report(doc_type,label=self.meta.get_label(fieldname), - report_type="Report Builder", filters=filters, doctype=doc_type) + label = get_link_to_report( + doc_type, + label=self.meta.get_label(fieldname), + report_type="Report Builder", + filters=filters, + doctype=doc_type, + ) - return { - "label": label, - "value": value, - "last_value": last_value, - "count": count - } + return {"label": label, "value": value, "last_value": last_value, "count": count} def get_total_on(self, doc_type, from_date, to_date): - date_field = 'posting_date' if doc_type in ['Sales Invoice', 'Purchase Invoice'] \ - else 'transaction_date' + date_field = ( + "posting_date" if doc_type in ["Sales Invoice", "Purchase Invoice"] else "transaction_date" + ) - return frappe.get_all(doc_type, + return frappe.get_all( + doc_type, filters={ - date_field: ['between', (from_date, to_date)], - 'status': ['not in', ('Cancelled')], - 'company': self.company + date_field: ["between", (from_date, to_date)], + "status": ["not in", ("Cancelled")], + "company": self.company, }, - fields=['count(*) as count', 'sum(grand_total) as grand_total']) + fields=["count(*) as count", "sum(grand_total) as grand_total"], + ) def get_from_to_date(self): today = now_datetime().date() @@ -717,7 +801,7 @@ class EmailDigest(Document): to_date = from_date + timedelta(days=6) else: # from date is the 1st day of the previous month - from_date = today - relativedelta(days=today.day-1, months=1) + from_date = today - relativedelta(days=today.day - 1, months=1) # to date is the last day of the previous month to_date = today - relativedelta(days=today.day) @@ -728,7 +812,7 @@ class EmailDigest(Document): # decide from date based on email digest frequency if self.frequency == "Daily": - self.past_from_date = self.past_to_date = self.future_from_date - relativedelta(days = 1) + self.past_from_date = self.past_to_date = self.future_from_date - relativedelta(days=1) elif self.frequency == "Weekly": self.past_from_date = self.future_from_date - relativedelta(weeks=1) @@ -755,27 +839,33 @@ class EmailDigest(Document): def onload(self): self.get_next_sending() - def fmt_money(self, value,absol=True): + def fmt_money(self, value, absol=True): if absol: - return fmt_money(abs(value), currency = self.currency) + return fmt_money(abs(value), currency=self.currency) else: return fmt_money(value, currency=self.currency) def get_purchase_orders_items_overdue_list(self): fields_po = "distinct `tabPurchase Order Item`.parent as po" - fields_poi = "`tabPurchase Order Item`.parent, `tabPurchase Order Item`.schedule_date, item_code," \ - "received_qty, qty - received_qty as missing_qty, rate, amount" + fields_poi = ( + "`tabPurchase Order Item`.parent, `tabPurchase Order Item`.schedule_date, item_code," + "received_qty, qty - received_qty as missing_qty, rate, amount" + ) sql_po = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date and received_qty < qty order by `tabPurchase Order Item`.parent DESC, - `tabPurchase Order Item`.schedule_date DESC""".format(fields=fields_po) + `tabPurchase Order Item`.schedule_date DESC""".format( + fields=fields_po + ) sql_poi = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date - and received_qty < qty order by `tabPurchase Order Item`.idx""".format(fields=fields_poi) + and received_qty < qty order by `tabPurchase Order Item`.idx""".format( + fields=fields_poi + ) purchase_order_list = frappe.db.sql(sql_po, as_dict=True) purchase_order_items_overdue_list = frappe.db.sql(sql_poi, as_dict=True) @@ -785,37 +875,44 @@ class EmailDigest(Document): t.amount = fmt_money(t.amount, 2, t.currency) return purchase_order_list, purchase_order_items_overdue_list + def send(): now_date = now_datetime().date() - for ed in frappe.db.sql("""select name from `tabEmail Digest` - where enabled=1 and docstatus<2""", as_list=1): - ed_obj = frappe.get_doc('Email Digest', ed[0]) - if (now_date == ed_obj.get_next_sending()): + for ed in frappe.db.sql( + """select name from `tabEmail Digest` + where enabled=1 and docstatus<2""", + as_list=1, + ): + ed_obj = frappe.get_doc("Email Digest", ed[0]) + if now_date == ed_obj.get_next_sending(): ed_obj.send() + @frappe.whitelist() def get_digest_msg(name): return frappe.get_doc("Email Digest", name).get_msg_html() + def get_incomes_expenses_for_period(account, from_date, to_date): - """Get amounts for current and past periods""" + """Get amounts for current and past periods""" - val = 0.0 - balance_on_to_date = get_balance_on(account, date = to_date) - balance_before_from_date = get_balance_on(account, date = from_date - timedelta(days=1)) + val = 0.0 + balance_on_to_date = get_balance_on(account, date=to_date) + balance_before_from_date = get_balance_on(account, date=from_date - timedelta(days=1)) - fy_start_date = get_fiscal_year(to_date)[1] + fy_start_date = get_fiscal_year(to_date)[1] - if from_date == fy_start_date: - val = balance_on_to_date - elif from_date > fy_start_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)) - val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date) + if from_date == fy_start_date: + val = balance_on_to_date + elif from_date > fy_start_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)) + val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date) + + return val - return val def get_count_for_period(account, fieldname, from_date, to_date): count = 0.0 @@ -833,6 +930,7 @@ def get_count_for_period(account, fieldname, from_date, to_date): return count + def get_future_date_for_calendaer_event(frequency): from_date = to_date = today() diff --git a/erpnext/setup/doctype/email_digest/quotes.py b/erpnext/setup/doctype/email_digest/quotes.py index 0fbadd98cd5..8c077a524c2 100644 --- a/erpnext/setup/doctype/email_digest/quotes.py +++ b/erpnext/setup/doctype/email_digest/quotes.py @@ -1,34 +1,65 @@ - import random def get_random_quote(): quotes = [ - ("Start by doing what's necessary; then do what's possible; and suddenly you are doing the impossible.", "Francis of Assisi"), - ("The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart.", "Hellen Keller"), - ("I can't change the direction of the wind, but I can adjust my sails to always reach my destination.", "Jimmy Dean"), + ( + "Start by doing what's necessary; then do what's possible; and suddenly you are doing the impossible.", + "Francis of Assisi", + ), + ( + "The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart.", + "Hellen Keller", + ), + ( + "I can't change the direction of the wind, but I can adjust my sails to always reach my destination.", + "Jimmy Dean", + ), ("We know what we are, but know not what we may be.", "William Shakespeare"), - ("There are only two mistakes one can make along the road to truth; not going all the way, and not starting.", "Buddha"), + ( + "There are only two mistakes one can make along the road to truth; not going all the way, and not starting.", + "Buddha", + ), ("Always remember that you are absolutely unique. Just like everyone else.", "Margaret Mead"), - ("You have to learn the rules of the game. And then you have to play better than anyone else.", "Albert Einstein"), + ( + "You have to learn the rules of the game. And then you have to play better than anyone else.", + "Albert Einstein", + ), ("Once we accept our limits, we go beyond them.", "Albert Einstein"), ("Quality is not an act, it is a habit.", "Aristotle"), - ("The more that you read, the more things you will know. The more that you learn, the more places you'll go.", "Dr. Seuss"), + ( + "The more that you read, the more things you will know. The more that you learn, the more places you'll go.", + "Dr. Seuss", + ), ("From there to here, and here to there, funny things are everywhere.", "Dr. Seuss"), ("The secret of getting ahead is getting started.", "Mark Twain"), ("All generalizations are false, including this one.", "Mark Twain"), ("Don't let schooling interfere with your education.", "Mark Twain"), ("Cauliflower is nothing but cabbage with a college education.", "Mark Twain"), - ("It's not the size of the dog in the fight, it's the size of the fight in the dog.", "Mark Twain"), + ( + "It's not the size of the dog in the fight, it's the size of the fight in the dog.", + "Mark Twain", + ), ("Climate is what we expect, weather is what we get.", "Mark Twain"), ("There are lies, damned lies and statistics.", "Mark Twain"), - ("Happiness is when what you think, what you say, and what you do are in harmony.", "Mahatma Gandhi"), - ("First they ignore you, then they laugh at you, then they fight you, then you win.", "Mahatma Gandhi"), + ( + "Happiness is when what you think, what you say, and what you do are in harmony.", + "Mahatma Gandhi", + ), + ( + "First they ignore you, then they laugh at you, then they fight you, then you win.", + "Mahatma Gandhi", + ), ("There is more to life than increasing its speed.", "Mahatma Gandhi"), - ("A small body of determined spirits fired by an unquenchable faith in their mission can alter the course of history.", "Mahatma Gandhi"), + ( + "A small body of determined spirits fired by an unquenchable faith in their mission can alter the course of history.", + "Mahatma Gandhi", + ), ("If two wrongs don't make a right, try three.", "Laurence J. Peter"), ("Inspiration exists, but it has to find you working.", "Pablo Picasso"), - ("The world’s first speeding ticket was given to a man going 4 times the speed limit! Walter Arnold was traveling at a breakneck 8 miles an hour in a 2mph zone, and was caught by a policeman on bicycle and fined one shilling!"), + ( + "The world’s first speeding ticket was given to a man going 4 times the speed limit! Walter Arnold was traveling at a breakneck 8 miles an hour in a 2mph zone, and was caught by a policeman on bicycle and fined one shilling!" + ), ] return random.choice(quotes) diff --git a/erpnext/setup/doctype/email_digest/test_email_digest.py b/erpnext/setup/doctype/email_digest/test_email_digest.py index 3fdf168a65e..dae28b81b5e 100644 --- a/erpnext/setup/doctype/email_digest/test_email_digest.py +++ b/erpnext/setup/doctype/email_digest/test_email_digest.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Email Digest') + class TestEmailDigest(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index f0b720a42e1..984bab47294 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -11,35 +11,37 @@ from frappe.utils import cint keydict = { # "key in defaults": "key in Global Defaults" "fiscal_year": "current_fiscal_year", - 'company': 'default_company', - 'currency': 'default_currency', + "company": "default_company", + "currency": "default_currency", "country": "country", - 'hide_currency_symbol':'hide_currency_symbol', - 'account_url':'account_url', - 'disable_rounded_total': 'disable_rounded_total', - 'disable_in_words': 'disable_in_words', + "hide_currency_symbol": "hide_currency_symbol", + "account_url": "account_url", + "disable_rounded_total": "disable_rounded_total", + "disable_in_words": "disable_in_words", } from frappe.model.document import Document class GlobalDefaults(Document): - def on_update(self): """update defaults""" for key in keydict: - frappe.db.set_default(key, self.get(keydict[key], '')) + frappe.db.set_default(key, self.get(keydict[key], "")) # update year start date and year end date from fiscal_year - year_start_end_date = frappe.db.sql("""select year_start_date, year_end_date - from `tabFiscal Year` where name=%s""", self.current_fiscal_year) + year_start_end_date = frappe.db.sql( + """select year_start_date, year_end_date + from `tabFiscal Year` where name=%s""", + self.current_fiscal_year, + ) if year_start_end_date: - ysd = year_start_end_date[0][0] or '' - yed = year_start_end_date[0][1] or '' + ysd = year_start_end_date[0][0] or "" + yed = year_start_end_date[0][1] or "" if ysd and yed: - frappe.db.set_default('year_start_date', ysd.strftime('%Y-%m-%d')) - frappe.db.set_default('year_end_date', yed.strftime('%Y-%m-%d')) + frappe.db.set_default("year_start_date", ysd.strftime("%Y-%m-%d")) + frappe.db.set_default("year_end_date", yed.strftime("%Y-%m-%d")) # enable default currency if self.default_currency: @@ -59,21 +61,81 @@ class GlobalDefaults(Document): self.disable_rounded_total = cint(self.disable_rounded_total) # Make property setters to hide rounded total fields - for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note", - "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"): - make_property_setter(doctype, "base_rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False) + for doctype in ( + "Quotation", + "Sales Order", + "Sales Invoice", + "Delivery Note", + "Supplier Quotation", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + ): + make_property_setter( + doctype, + "base_rounded_total", + "hidden", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False + ) - make_property_setter(doctype, "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "rounded_total", + "hidden", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, + "rounded_total", + "print_hide", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) - make_property_setter(doctype, "disable_rounded_total", "default", cint(self.disable_rounded_total), "Text", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "disable_rounded_total", + "default", + cint(self.disable_rounded_total), + "Text", + validate_fields_for_doctype=False, + ) def toggle_in_words(self): self.disable_in_words = cint(self.disable_in_words) # Make property setters to hide in words fields - for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note", - "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"): - make_property_setter(doctype, "in_words", "hidden", self.disable_in_words, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "in_words", "print_hide", self.disable_in_words, "Check", validate_fields_for_doctype=False) + for doctype in ( + "Quotation", + "Sales Order", + "Sales Invoice", + "Delivery Note", + "Supplier Quotation", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + ): + make_property_setter( + doctype, + "in_words", + "hidden", + self.disable_in_words, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, + "in_words", + "print_hide", + self.disable_in_words, + "Check", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js index 885d874720d..cf96dc1a7d6 100644 --- a/erpnext/setup/doctype/item_group/item_group.js +++ b/erpnext/setup/doctype/item_group/item_group.js @@ -14,6 +14,16 @@ frappe.ui.form.on("Item Group", { ] } } + frm.fields_dict['item_group_defaults'].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': row.company, + "is_group": 0 + } + }; + } frm.fields_dict["item_group_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) { const row = locals[cdt][cdn]; return { @@ -62,17 +72,17 @@ frappe.ui.form.on("Item Group", { }); } - frappe.model.with_doctype('Item', () => { - const item_meta = frappe.get_meta('Item'); + frappe.model.with_doctype('Website Item', () => { + const web_item_meta = frappe.get_meta('Website Item'); - const valid_fields = item_meta.fields.filter( + const valid_fields = web_item_meta.fields.filter( df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden ).map(df => ({ label: df.label, value: df.fieldname })); - frm.fields_dict.filter_fields.grid.update_docfield_property( + frm.get_field("filter_fields").grid.update_docfield_property( 'fieldname', 'fieldtype', 'Select' ); - frm.fields_dict.filter_fields.grid.update_docfield_property( + frm.get_field("filter_fields").grid.update_docfield_property( 'fieldname', 'options', valid_fields ); }); diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 3e0680f4f51..50f923d87e0 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -20,12 +20,14 @@ "sec_break_taxes", "taxes", "sb9", - "show_in_website", "route", - "weightage", - "slideshow", "website_title", "description", + "show_in_website", + "include_descendants", + "column_break_16", + "weightage", + "slideshow", "website_specifications", "website_filters_section", "filter_fields", @@ -111,7 +113,7 @@ }, { "default": "0", - "description": "Check this if you want to show in website", + "description": "Make Item Group visible in website", "fieldname": "show_in_website", "fieldtype": "Check", "label": "Show in Website" @@ -124,6 +126,7 @@ "unique": 1 }, { + "depends_on": "show_in_website", "fieldname": "weightage", "fieldtype": "Int", "label": "Weightage" @@ -186,6 +189,8 @@ "report_hide": 1 }, { + "collapsible": 1, + "depends_on": "show_in_website", "fieldname": "website_filters_section", "fieldtype": "Section Break", "label": "Website Filters" @@ -203,9 +208,22 @@ "options": "Website Attribute" }, { + "depends_on": "show_in_website", "fieldname": "website_title", "fieldtype": "Data", "label": "Title" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "show_in_website", + "description": "Include Website Items belonging to child Item Groups", + "fieldname": "include_descendants", + "fieldtype": "Check", + "label": "Include Descendants" } ], "icon": "fa fa-sitemap", @@ -214,11 +232,12 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2021-02-18 13:40:30.049650", + "modified": "2022-03-09 12:27:11.055782", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", "name_case": "Title Case", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_item_group", "owner": "Administrator", "permissions": [ @@ -285,5 +304,6 @@ "search_fields": "parent_item_group", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 7695affde60..769b2d88085 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -12,16 +12,17 @@ from frappe.website.render import clear_cache from frappe.website.website_generator import WebsiteGenerator from six.moves.urllib.parse import quote +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ECommerceSettings from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder class ItemGroup(NestedSet, WebsiteGenerator): - nsm_parent_field = 'parent_item_group' + nsm_parent_field = "parent_item_group" website = frappe._dict( - condition_field = "show_in_website", - template = "templates/generators/item_group.html", - no_cache = 1, - no_breadcrumbs = 1 + condition_field="show_in_website", + template="templates/generators/item_group.html", + no_cache=1, + no_breadcrumbs=1, ) def autoname(self): @@ -31,11 +32,12 @@ class ItemGroup(NestedSet, WebsiteGenerator): super(ItemGroup, self).validate() if not self.parent_item_group and not frappe.flags.in_test: - if frappe.db.exists("Item Group", _('All Item Groups')): - self.parent_item_group = _('All Item Groups') + if frappe.db.exists("Item Group", _("All Item Groups")): + self.parent_item_group = _("All Item Groups") self.make_route() self.validate_item_group_defaults() + ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True) def on_update(self): NestedSet.on_update(self) @@ -44,15 +46,15 @@ class ItemGroup(NestedSet, WebsiteGenerator): self.delete_child_item_groups_key() def make_route(self): - '''Make website route''' + """Make website route""" if not self.route: - self.route = '' + self.route = "" if self.parent_item_group: - parent_item_group = frappe.get_doc('Item Group', self.parent_item_group) + parent_item_group = frappe.get_doc("Item Group", self.parent_item_group) # make parent route only if not root if parent_item_group.parent_item_group and parent_item_group.route: - self.route = parent_item_group.route + '/' + self.route = parent_item_group.route + "/" self.route += self.scrub(self.item_group_name) @@ -66,28 +68,22 @@ class ItemGroup(NestedSet, WebsiteGenerator): def get_context(self, context): context.show_search = True context.body_class = "product-page" - context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page')) or 6 - context.search_link = '/product_search' + context.page_length = ( + cint(frappe.db.get_single_value("E Commerce Settings", "products_per_page")) or 6 + ) + context.search_link = "/product_search" filter_engine = ProductFiltersBuilder(self.name) context.field_filters = filter_engine.get_field_filters() context.attribute_filters = filter_engine.get_attribute_filters() - context.update({ - "parents": get_parent_item_groups(self.parent_item_group), - "title": self.name - }) + context.update({"parents": get_parent_item_groups(self.parent_item_group), "title": self.name}) if self.slideshow: - values = { - 'show_indicators': 1, - 'show_controls': 0, - 'rounded': 1, - 'slider_name': self.slideshow - } + values = {"show_indicators": 1, "show_controls": 0, "rounded": 1, "slider_name": self.slideshow} slideshow = frappe.get_doc("Website Slideshow", self.slideshow) - slides = slideshow.get({"doctype":"Website Slideshow Item"}) + slides = slideshow.get({"doctype": "Website Slideshow Item"}) for index, slide in enumerate(slides): values[f"slide_{index + 1}_image"] = slide.image values[f"slide_{index + 1}_title"] = slide.heading @@ -110,61 +106,64 @@ class ItemGroup(NestedSet, WebsiteGenerator): def validate_item_group_defaults(self): from erpnext.stock.doctype.item.item import validate_item_default_company_links + validate_item_default_company_links(self.item_group_defaults) -def get_child_groups_for_website(item_group_name, immediate=False): + +def get_child_groups_for_website(item_group_name, immediate=False, include_self=False): """Returns child item groups *excluding* passed group.""" item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - filters = { - "lft": [">", item_group.lft], - "rgt": ["<", item_group.rgt], - "show_in_website": 1 - } + filters = {"lft": [">", item_group.lft], "rgt": ["<", item_group.rgt], "show_in_website": 1} if immediate: filters["parent_item_group"] = item_group_name - return frappe.get_all( - "Item Group", - filters=filters, - fields=["name", "route"] - ) + if include_self: + filters.update({"lft": [">=", item_group.lft], "rgt": ["<=", item_group.rgt]}) + + return frappe.get_all("Item Group", filters=filters, fields=["name", "route"], order_by="name") + def get_child_item_groups(item_group_name): - item_group = frappe.get_cached_value("Item Group", - item_group_name, ["lft", "rgt"], as_dict=1) + item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - child_item_groups = [d.name for d in frappe.get_all('Item Group', - filters= {'lft': ('>=', item_group.lft),'rgt': ('<=', item_group.rgt)})] + child_item_groups = [ + d.name + for d in frappe.get_all( + "Item Group", filters={"lft": (">=", item_group.lft), "rgt": ("<=", item_group.rgt)} + ) + ] return child_item_groups or {} + def get_item_for_list_in_html(context): # add missing absolute link in files # user may forget it during upload if (context.get("website_image") or "").startswith("files/"): context["website_image"] = "/" + quote(context["website_image"]) - context["show_availability_status"] = cint(frappe.db.get_single_value('E Commerce Settings', - 'show_availability_status')) + context["show_availability_status"] = cint( + frappe.db.get_single_value("E Commerce Settings", "show_availability_status") + ) - products_template = 'templates/includes/products_as_list.html' + products_template = "templates/includes/products_as_list.html" return frappe.get_template(products_template).render(context) def get_parent_item_groups(item_group_name, from_item=False): - base_nav_page = {"name": _("Shop by Category"), "route":"/shop-by-category"} + base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} if from_item and frappe.request.environ.get("HTTP_REFERER"): # base page after 'Home' will vary on Item page - last_page = frappe.request.environ["HTTP_REFERER"].split('/')[-1] + last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1] if last_page and last_page in ("shop-by-category", "all-products"): base_nav_page_title = " ".join(last_page.split("-")).title() - base_nav_page = {"name": _(base_nav_page_title), "route":"/"+last_page} + base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} base_parents = [ - {"name": _("Home"), "route":"/"}, + {"name": _("Home"), "route": "/"}, base_nav_page, ] @@ -172,21 +171,27 @@ def get_parent_item_groups(item_group_name, from_item=False): return base_parents item_group = frappe.db.get_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - parent_groups = frappe.db.sql("""select name, route from `tabItem Group` + parent_groups = frappe.db.sql( + """select name, route from `tabItem Group` where lft <= %s and rgt >= %s and show_in_website=1 - order by lft asc""", (item_group.lft, item_group.rgt), as_dict=True) + order by lft asc""", + (item_group.lft, item_group.rgt), + as_dict=True, + ) return base_parents + parent_groups + def invalidate_cache_for(doc, item_group=None): if not item_group: item_group = doc.name for d in get_parent_item_groups(item_group): - item_group_name = frappe.db.get_value("Item Group", d.get('name')) + item_group_name = frappe.db.get_value("Item Group", d.get("name")) if item_group_name: - clear_cache(frappe.db.get_value('Item Group', item_group_name, 'route')) + clear_cache(frappe.db.get_value("Item Group", item_group_name, "route")) + def get_item_group_defaults(item, company): item = frappe.get_cached_doc("Item", item) diff --git a/erpnext/setup/doctype/item_group/test_item_group.py b/erpnext/setup/doctype/item_group/test_item_group.py index f6e9ed4ce59..11bc9b92c12 100644 --- a/erpnext/setup/doctype/item_group/test_item_group.py +++ b/erpnext/setup/doctype/item_group/test_item_group.py @@ -14,7 +14,8 @@ from frappe.utils.nestedset import ( rebuild_tree, ) -test_records = frappe.get_test_records('Item Group') +test_records = frappe.get_test_records("Item Group") + class TestItem(unittest.TestCase): def test_basic_tree(self, records=None): @@ -25,12 +26,12 @@ class TestItem(unittest.TestCase): records = test_records[2:] for item_group in records: - lft, rgt, parent_item_group = frappe.db.get_value("Item Group", item_group["item_group_name"], - ["lft", "rgt", "parent_item_group"]) + lft, rgt, parent_item_group = frappe.db.get_value( + "Item Group", item_group["item_group_name"], ["lft", "rgt", "parent_item_group"] + ) if parent_item_group: - parent_lft, parent_rgt = frappe.db.get_value("Item Group", parent_item_group, - ["lft", "rgt"]) + parent_lft, parent_rgt = frappe.db.get_value("Item Group", parent_item_group, ["lft", "rgt"]) else: # root parent_lft = min_lft - 1 @@ -55,8 +56,11 @@ class TestItem(unittest.TestCase): def get_no_of_children(item_groups, no_of_children): children = [] for ig in item_groups: - children += frappe.db.sql_list("""select name from `tabItem Group` - where ifnull(parent_item_group, '')=%s""", ig or '') + children += frappe.db.sql_list( + """select name from `tabItem Group` + where ifnull(parent_item_group, '')=%s""", + ig or "", + ) if len(children): return get_no_of_children(children, no_of_children + len(children)) @@ -119,7 +123,10 @@ class TestItem(unittest.TestCase): def print_tree(self): import json - print(json.dumps(frappe.db.sql("select name, lft, rgt from `tabItem Group` order by lft"), indent=1)) + + print( + json.dumps(frappe.db.sql("select name, lft, rgt from `tabItem Group` order by lft"), indent=1) + ) def test_move_leaf_into_another_group(self): # before move @@ -149,12 +156,20 @@ class TestItem(unittest.TestCase): def test_delete_leaf(self): # for checking later - parent_item_group = frappe.db.get_value("Item Group", "_Test Item Group B - 3", "parent_item_group") + parent_item_group = frappe.db.get_value( + "Item Group", "_Test Item Group B - 3", "parent_item_group" + ) rgt = frappe.db.get_value("Item Group", parent_item_group, "rgt") ancestors = get_ancestors_of("Item Group", "_Test Item Group B - 3") - ancestors = frappe.db.sql("""select name, rgt from `tabItem Group` - where name in ({})""".format(", ".join(["%s"]*len(ancestors))), tuple(ancestors), as_dict=True) + ancestors = frappe.db.sql( + """select name, rgt from `tabItem Group` + where name in ({})""".format( + ", ".join(["%s"] * len(ancestors)) + ), + tuple(ancestors), + as_dict=True, + ) frappe.delete_doc("Item Group", "_Test Item Group B - 3") records_to_test = test_records[2:] @@ -173,7 +188,9 @@ class TestItem(unittest.TestCase): def test_delete_group(self): # cannot delete group with child, but can delete leaf - self.assertRaises(NestedSetChildExistsError, frappe.delete_doc, "Item Group", "_Test Item Group B") + self.assertRaises( + NestedSetChildExistsError, frappe.delete_doc, "Item Group", "_Test Item Group B" + ) def test_merge_groups(self): frappe.rename_doc("Item Group", "_Test Item Group B", "_Test Item Group C", merge=True) @@ -186,8 +203,10 @@ class TestItem(unittest.TestCase): self.test_basic_tree() # move its children back - for name in frappe.db.sql_list("""select name from `tabItem Group` - where parent_item_group='_Test Item Group C'"""): + for name in frappe.db.sql_list( + """select name from `tabItem Group` + where parent_item_group='_Test Item Group C'""" + ): doc = frappe.get_doc("Item Group", name) doc.parent_item_group = "_Test Item Group B" @@ -206,9 +225,21 @@ class TestItem(unittest.TestCase): self.test_basic_tree() def test_merge_leaf_into_group(self): - self.assertRaises(NestedSetInvalidMergeError, frappe.rename_doc, "Item Group", "_Test Item Group B - 3", - "_Test Item Group B", merge=True) + self.assertRaises( + NestedSetInvalidMergeError, + frappe.rename_doc, + "Item Group", + "_Test Item Group B - 3", + "_Test Item Group B", + merge=True, + ) def test_merge_group_into_leaf(self): - self.assertRaises(NestedSetInvalidMergeError, frappe.rename_doc, "Item Group", "_Test Item Group B", - "_Test Item Group B - 3", merge=True) + self.assertRaises( + NestedSetInvalidMergeError, + frappe.rename_doc, + "Item Group", + "_Test Item Group B", + "_Test Item Group B - 3", + merge=True, + ) diff --git a/erpnext/setup/doctype/naming_series/naming_series.js b/erpnext/setup/doctype/naming_series/naming_series.js index 7c76d9ca4ba..861b2b39835 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.js +++ b/erpnext/setup/doctype/naming_series/naming_series.js @@ -4,10 +4,13 @@ frappe.ui.form.on("Naming Series", { onload: function(frm) { - frm.disable_save(); frm.events.get_doc_and_prefix(frm); }, + refresh: function(frm) { + frm.disable_save(); + }, + get_doc_and_prefix: function(frm) { frappe.call({ method: "get_transactions", diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index 986b4e87ff0..4fba776cb55 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -11,15 +11,25 @@ from frappe.permissions import get_doctypes_with_read from frappe.utils import cint, cstr -class NamingSeriesNotSetError(frappe.ValidationError): pass +class NamingSeriesNotSetError(frappe.ValidationError): + pass + class NamingSeries(Document): @frappe.whitelist() def get_transactions(self, arg=None): - doctypes = list(set(frappe.db.sql_list("""select parent - from `tabDocField` df where fieldname='naming_series'""") - + frappe.db.sql_list("""select dt from `tabCustom Field` - where fieldname='naming_series'"""))) + doctypes = list( + set( + frappe.db.sql_list( + """select parent + from `tabDocField` df where fieldname='naming_series'""" + ) + + frappe.db.sql_list( + """select dt from `tabCustom Field` + where fieldname='naming_series'""" + ) + ) + ) doctypes = list(set(get_doctypes_with_read()).intersection(set(doctypes))) prefixes = "" @@ -28,8 +38,8 @@ class NamingSeries(Document): try: options = self.get_options(d) except frappe.DoesNotExistError: - frappe.msgprint(_('Unable to find DocType {0}').format(d)) - #frappe.pass_does_not_exist_error() + frappe.msgprint(_("Unable to find DocType {0}").format(d)) + # frappe.pass_does_not_exist_error() continue if options: @@ -37,17 +47,21 @@ class NamingSeries(Document): prefixes.replace("\n\n", "\n") prefixes = prefixes.split("\n") - custom_prefixes = frappe.get_all('DocType', fields=["autoname"], - filters={"name": ('not in', doctypes), "autoname":('like', '%.#%'), 'module': ('not in', ['Core'])}) + custom_prefixes = frappe.get_all( + "DocType", + fields=["autoname"], + filters={ + "name": ("not in", doctypes), + "autoname": ("like", "%.#%"), + "module": ("not in", ["Core"]), + }, + ) if custom_prefixes: - prefixes = prefixes + [d.autoname.rsplit('.', 1)[0] for d in custom_prefixes] + prefixes = prefixes + [d.autoname.rsplit(".", 1)[0] for d in custom_prefixes] prefixes = "\n".join(sorted(prefixes)) - return { - "transactions": "\n".join([''] + sorted(doctypes)), - "prefixes": prefixes - } + return {"transactions": "\n".join([""] + sorted(doctypes)), "prefixes": prefixes} def scrub_options_list(self, ol): options = list(filter(lambda x: x, [cstr(n).strip() for n in ol])) @@ -64,7 +78,7 @@ class NamingSeries(Document): self.set_series_for(self.select_doc_for_series, series_list) # create series - map(self.insert_series, [d.split('.')[0] for d in series_list if d.strip()]) + map(self.insert_series, [d.split(".")[0] for d in series_list if d.strip()]) msgprint(_("Series Updated")) @@ -82,32 +96,35 @@ class NamingSeries(Document): self.validate_series_name(i) if options and self.user_must_always_select: - options = [''] + options + options = [""] + options - default = options[0] if options else '' + default = options[0] if options else "" # update in property setter - prop_dict = {'options': "\n".join(options), 'default': default} + prop_dict = {"options": "\n".join(options), "default": default} for prop in prop_dict: - ps_exists = frappe.db.get_value("Property Setter", - {"field_name": 'naming_series', 'doc_type': doctype, 'property': prop}) + ps_exists = frappe.db.get_value( + "Property Setter", {"field_name": "naming_series", "doc_type": doctype, "property": prop} + ) if ps_exists: - ps = frappe.get_doc('Property Setter', ps_exists) + ps = frappe.get_doc("Property Setter", ps_exists) ps.value = prop_dict[prop] ps.save() else: - ps = frappe.get_doc({ - 'doctype': 'Property Setter', - 'doctype_or_field': 'DocField', - 'doc_type': doctype, - 'field_name': 'naming_series', - 'property': prop, - 'value': prop_dict[prop], - 'property_type': 'Text', - '__islocal': 1 - }) + ps = frappe.get_doc( + { + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "doc_type": doctype, + "field_name": "naming_series", + "property": prop, + "value": prop_dict[prop], + "property_type": "Text", + "__islocal": 1, + } + ) ps.save() self.set_options = "\n".join(options) @@ -115,16 +132,22 @@ class NamingSeries(Document): frappe.clear_cache(doctype=doctype) def check_duplicate(self): - parent = list(set( - frappe.db.sql_list("""select dt.name + parent = list( + set( + frappe.db.sql_list( + """select dt.name from `tabDocField` df, `tabDocType` dt where dt.name = df.parent and df.fieldname='naming_series' and dt.name != %s""", - self.select_doc_for_series) - + frappe.db.sql_list("""select dt.name + self.select_doc_for_series, + ) + + frappe.db.sql_list( + """select dt.name from `tabCustom Field` df, `tabDocType` dt where dt.name = df.dt and df.fieldname='naming_series' and dt.name != %s""", - self.select_doc_for_series) - )) + self.select_doc_for_series, + ) + ) + ) sr = [[frappe.get_meta(p).get_field("naming_series").options, p] for p in parent] dt = frappe.get_doc("DocType", self.select_doc_for_series) options = self.scrub_options_list(self.set_options.split("\n")) @@ -132,14 +155,17 @@ class NamingSeries(Document): validate_series(dt, series) for i in sr: if i[0]: - existing_series = [d.split('.')[0] for d in i[0].split("\n")] + existing_series = [d.split(".")[0] for d in i[0].split("\n")] if series.split(".")[0] in existing_series: - frappe.throw(_("Series {0} already used in {1}").format(series,i[1])) + frappe.throw(_("Series {0} already used in {1}").format(series, i[1])) def validate_series_name(self, n): import re + if not re.match(r"^[\w\- \/.#{}]+$", n, re.UNICODE): - throw(_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series')) + throw( + _('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series') + ) @frappe.whitelist() def get_options(self, arg=None): @@ -151,12 +177,11 @@ class NamingSeries(Document): """get series current""" if self.prefix: prefix = self.parse_naming_series() - self.current_value = frappe.db.get_value("Series", - prefix, "current", order_by = "name") + self.current_value = frappe.db.get_value("Series", prefix, "current", order_by="name") def insert_series(self, series): """insert series if missing""" - if frappe.db.get_value('Series', series, 'name', order_by="name") == None: + if frappe.db.get_value("Series", series, "name", order_by="name") == None: frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series)) @frappe.whitelist() @@ -164,14 +189,15 @@ class NamingSeries(Document): if self.prefix: prefix = self.parse_naming_series() self.insert_series(prefix) - frappe.db.sql("update `tabSeries` set current = %s where name = %s", - (cint(self.current_value), prefix)) + frappe.db.sql( + "update `tabSeries` set current = %s where name = %s", (cint(self.current_value), prefix) + ) msgprint(_("Series Updated Successfully")) else: msgprint(_("Please select prefix first")) def parse_naming_series(self): - parts = self.prefix.split('.') + parts = self.prefix.split(".") # Remove ### from the end of series if parts[-1] == "#" * len(parts[-1]): @@ -180,34 +206,59 @@ class NamingSeries(Document): prefix = parse_naming_series(parts) return prefix -def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1): + +def set_by_naming_series( + doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1 +): from frappe.custom.doctype.property_setter.property_setter import make_property_setter + if naming_series: - make_property_setter(doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False + ) # set values for mandatory try: - frappe.db.sql("""update `tab{doctype}` set naming_series={s} where - ifnull(naming_series, '')=''""".format(doctype=doctype, s="%s"), - get_default_naming_series(doctype)) + frappe.db.sql( + """update `tab{doctype}` set naming_series={s} where + ifnull(naming_series, '')=''""".format( + doctype=doctype, s="%s" + ), + get_default_naming_series(doctype), + ) except NamingSeriesNotSetError: pass if hide_name_field: make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False + ) else: - make_property_setter(doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False + ) if hide_name_field: - make_property_setter(doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False + ) make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False) # set values for mandatory - frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=`name` where - ifnull({fieldname}, '')=''""".format(doctype=doctype, fieldname=fieldname)) + frappe.db.sql( + """update `tab{doctype}` set `{fieldname}`=`name` where + ifnull({fieldname}, '')=''""".format( + doctype=doctype, fieldname=fieldname + ) + ) + def get_default_naming_series(doctype): naming_series = frappe.get_meta(doctype).get_field("naming_series").options or "" @@ -215,7 +266,9 @@ def get_default_naming_series(doctype): out = naming_series[0] or (naming_series[1] if len(naming_series) > 1 else None) if not out: - frappe.throw(_("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype), - NamingSeriesNotSetError) + frappe.throw( + _("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype), + NamingSeriesNotSetError, + ) else: return out diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index d0d2946e94a..d07ab084500 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -9,18 +9,20 @@ from frappe.model.document import Document class PartyType(Document): pass + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_party_type(doctype, txt, searchfield, start, page_len, filters): - cond = '' - if filters and filters.get('account'): - account_type = frappe.db.get_value('Account', filters.get('account'), 'account_type') + cond = "" + if filters and filters.get("account"): + account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") cond = "and account_type = '%s'" % account_type - return frappe.db.sql("""select name from `tabParty Type` + return frappe.db.sql( + """select name from `tabParty Type` where `{key}` LIKE %(txt)s {cond} - order by name limit %(start)s, %(page_len)s""" - .format(key=searchfield, cond=cond), { - 'txt': '%' + txt + '%', - 'start': start, 'page_len': page_len - }) + order by name limit %(start)s, %(page_len)s""".format( + key=searchfield, cond=cond + ), + {"txt": "%" + txt + "%", "start": start, "page_len": page_len}, + ) diff --git a/erpnext/setup/doctype/party_type/test_party_type.py b/erpnext/setup/doctype/party_type/test_party_type.py index a9a3db8777e..ab92ee15fcb 100644 --- a/erpnext/setup/doctype/party_type/test_party_type.py +++ b/erpnext/setup/doctype/party_type/test_party_type.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Party Type') + class TestPartyType(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/print_heading/test_print_heading.py b/erpnext/setup/doctype/print_heading/test_print_heading.py index 04de08d2697..f0e4c763c4b 100644 --- a/erpnext/setup/doctype/print_heading/test_print_heading.py +++ b/erpnext/setup/doctype/print_heading/test_print_heading.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Print Heading') +test_records = frappe.get_test_records("Print Heading") diff --git a/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py b/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py index 9330ba85870..891864a69ea 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py +++ b/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Quotation Lost Reason') +test_records = frappe.get_test_records("Quotation Lost Reason") diff --git a/erpnext/setup/doctype/sales_partner/sales_partner.py b/erpnext/setup/doctype/sales_partner/sales_partner.py index d2ec49dd6c3..c3136715fe5 100644 --- a/erpnext/setup/doctype/sales_partner/sales_partner.py +++ b/erpnext/setup/doctype/sales_partner/sales_partner.py @@ -10,9 +10,9 @@ from frappe.website.website_generator import WebsiteGenerator class SalesPartner(WebsiteGenerator): website = frappe._dict( - page_title_field = "partner_name", - condition_field = "show_in_website", - template = "templates/generators/sales_partner.html" + page_title_field="partner_name", + condition_field="show_in_website", + template="templates/generators/sales_partner.html", ) def onload(self): @@ -30,18 +30,25 @@ class SalesPartner(WebsiteGenerator): self.partner_website = "http://" + self.partner_website def get_context(self, context): - address = frappe.db.get_value("Address", - {"sales_partner": self.name, "is_primary_address": 1}, - "*", as_dict=True) + address = frappe.db.get_value( + "Address", {"sales_partner": self.name, "is_primary_address": 1}, "*", as_dict=True + ) if address: city_state = ", ".join(filter(None, [address.city, address.state])) - address_rows = [address.address_line1, address.address_line2, - city_state, address.pincode, address.country] + address_rows = [ + address.address_line1, + address.address_line2, + city_state, + address.pincode, + address.country, + ] - context.update({ - "email": address.email_id, - "partner_address": filter_strip_join(address_rows, "\n
    "), - "phone": filter_strip_join(cstr(address.phone).split(","), "\n
    ") - }) + context.update( + { + "email": address.email_id, + "partner_address": filter_strip_join(address_rows, "\n
    "), + "phone": filter_strip_join(cstr(address.phone).split(","), "\n
    "), + } + ) return context diff --git a/erpnext/setup/doctype/sales_partner/test_sales_partner.py b/erpnext/setup/doctype/sales_partner/test_sales_partner.py index 80ef3680147..933f68da5b0 100644 --- a/erpnext/setup/doctype/sales_partner/test_sales_partner.py +++ b/erpnext/setup/doctype/sales_partner/test_sales_partner.py @@ -3,6 +3,6 @@ import frappe -test_records = frappe.get_test_records('Sales Partner') +test_records = frappe.get_test_records("Sales Partner") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index b79a566578d..cd3a6e1f596 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -11,13 +11,13 @@ from erpnext import get_default_currency class SalesPerson(NestedSet): - nsm_parent_field = 'parent_sales_person' + nsm_parent_field = "parent_sales_person" def validate(self): if not self.parent_sales_person: self.parent_sales_person = get_root_of("Sales Person") - for d in self.get('targets') or []: + for d in self.get("targets") or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory.")) self.validate_employee_id() @@ -28,17 +28,20 @@ class SalesPerson(NestedSet): def load_dashboard_info(self): company_default_currency = get_default_currency() - allocated_amount = frappe.db.sql(""" + allocated_amount = frappe.db.sql( + """ select sum(allocated_amount) from `tabSales Team` where sales_person = %s and docstatus=1 and parenttype = 'Sales Order' - """,(self.sales_person_name)) + """, + (self.sales_person_name), + ) info = {} info["allocated_amount"] = flt(allocated_amount[0][0]) if allocated_amount else 0 info["currency"] = company_default_currency - self.set_onload('dashboard_info', info) + self.set_onload("dashboard_info", info) def on_update(self): super(SalesPerson, self).on_update() @@ -57,30 +60,46 @@ class SalesPerson(NestedSet): sales_person = frappe.db.get_value("Sales Person", {"employee": self.employee}) if sales_person and sales_person != self.name: - frappe.throw(_("Another Sales Person {0} exists with the same Employee id").format(sales_person)) + frappe.throw( + _("Another Sales Person {0} exists with the same Employee id").format(sales_person) + ) + def on_doctype_update(): frappe.db.add_index("Sales Person", ["lft", "rgt"]) + def get_timeline_data(doctype, name): out = {} - out.update(dict(frappe.db.sql('''select + out.update( + dict( + frappe.db.sql( + """select unix_timestamp(dt.transaction_date), count(st.parenttype) from `tabSales Order` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.transaction_date > date_sub(curdate(), interval 1 year) - group by dt.transaction_date ''', name))) + group by dt.transaction_date """, + name, + ) + ) + ) - sales_invoice = dict(frappe.db.sql('''select + sales_invoice = dict( + frappe.db.sql( + """select unix_timestamp(dt.posting_date), count(st.parenttype) from `tabSales Invoice` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year) - group by dt.posting_date ''', name)) + group by dt.posting_date """, + name, + ) + ) for key in sales_invoice: if out.get(key): @@ -88,13 +107,18 @@ def get_timeline_data(doctype, name): else: out[key] = sales_invoice[key] - delivery_note = dict(frappe.db.sql('''select + delivery_note = dict( + frappe.db.sql( + """select unix_timestamp(dt.posting_date), count(st.parenttype) from `tabDelivery Note` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year) - group by dt.posting_date ''', name)) + group by dt.posting_date """, + name, + ) + ) for key in delivery_note: if out.get(key): diff --git a/erpnext/setup/doctype/sales_person/sales_person_dashboard.py b/erpnext/setup/doctype/sales_person/sales_person_dashboard.py index d0a5dd99dfc..2ec2002d3d7 100644 --- a/erpnext/setup/doctype/sales_person/sales_person_dashboard.py +++ b/erpnext/setup/doctype/sales_person/sales_person_dashboard.py @@ -1,16 +1,14 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Sales Person. See timeline below for details'), - 'fieldname': 'sales_person', - 'transactions': [ - { - 'label': _('Sales'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - ] + "heatmap": True, + "heatmap_message": _( + "This is based on transactions against this Sales Person. See timeline below for details" + ), + "fieldname": "sales_person", + "transactions": [ + {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + ], } diff --git a/erpnext/setup/doctype/sales_person/test_sales_person.py b/erpnext/setup/doctype/sales_person/test_sales_person.py index 786d2cac4da..6ff1888230e 100644 --- a/erpnext/setup/doctype/sales_person/test_sales_person.py +++ b/erpnext/setup/doctype/sales_person/test_sales_person.py @@ -5,6 +5,6 @@ test_dependencies = ["Employee"] import frappe -test_records = frappe.get_test_records('Sales Person') +test_records = frappe.get_test_records("Sales Person") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.py b/erpnext/setup/doctype/supplier_group/supplier_group.py index 381e1250c82..9d2b733b743 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.py +++ b/erpnext/setup/doctype/supplier_group/supplier_group.py @@ -7,7 +7,7 @@ from frappe.utils.nestedset import NestedSet, get_root_of class SupplierGroup(NestedSet): - nsm_parent_field = 'parent_supplier_group' + nsm_parent_field = "parent_supplier_group" def validate(self): if not self.parent_supplier_group: diff --git a/erpnext/setup/doctype/supplier_group/test_supplier_group.py b/erpnext/setup/doctype/supplier_group/test_supplier_group.py index 283b3bfec3c..97ba705a502 100644 --- a/erpnext/setup/doctype/supplier_group/test_supplier_group.py +++ b/erpnext/setup/doctype/supplier_group/test_supplier_group.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Supplier Group') +test_records = frappe.get_test_records("Supplier Group") diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py index 8e6421eba8a..cafb3eca7f5 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py @@ -16,9 +16,15 @@ class TermsandConditions(Document): def validate(self): if self.terms: validate_template(self.terms) - if not cint(self.buying) and not cint(self.selling) and not cint(self.hr) and not cint(self.disabled): + if ( + not cint(self.buying) + and not cint(self.selling) + and not cint(self.hr) + and not cint(self.disabled) + ): throw(_("At least one of the Applicable Modules should be selected")) + @frappe.whitelist() def get_terms_and_conditions(template_name, doc): if isinstance(doc, string_types): diff --git a/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py b/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py index ca9e6c1aef8..171840af98f 100644 --- a/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py +++ b/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Terms and Conditions') +test_records = frappe.get_test_records("Terms and Conditions") diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 4c47d829e98..9bb5569de5b 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -9,13 +9,13 @@ from frappe.utils.nestedset import NestedSet, get_root_of class Territory(NestedSet): - nsm_parent_field = 'parent_territory' + nsm_parent_field = "parent_territory" def validate(self): if not self.parent_territory: self.parent_territory = get_root_of("Territory") - for d in self.get('targets') or []: + for d in self.get("targets") or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory")) @@ -23,5 +23,6 @@ class Territory(NestedSet): super(Territory, self).on_update() self.validate_one_root() + def on_doctype_update(): frappe.db.add_index("Territory", ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/territory/test_territory.py b/erpnext/setup/doctype/territory/test_territory.py index a18b7bf70ef..4ec695d385b 100644 --- a/erpnext/setup/doctype/territory/test_territory.py +++ b/erpnext/setup/doctype/territory/test_territory.py @@ -3,6 +3,6 @@ import frappe -test_records = frappe.get_test_records('Territory') +test_records = frappe.get_test_records("Territory") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py index 095c3d0b6fb..319d435ca69 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py @@ -8,61 +8,51 @@ import frappe class TestTransactionDeletionRecord(unittest.TestCase): def setUp(self): - create_company('Dunder Mifflin Paper Co') + create_company("Dunder Mifflin Paper Co") def tearDown(self): frappe.db.rollback() def test_doctypes_contain_company_field(self): - tdr = create_transaction_deletion_request('Dunder Mifflin Paper Co') + tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") for doctype in tdr.doctypes: contains_company = False - doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()['fields'] + doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"] for doctype_field in doctype_fields: - if doctype_field['fieldtype'] == 'Link' and doctype_field['options'] == 'Company': + if doctype_field["fieldtype"] == "Link" and doctype_field["options"] == "Company": contains_company = True break self.assertTrue(contains_company) def test_no_of_docs_is_correct(self): for i in range(5): - create_task('Dunder Mifflin Paper Co') - tdr = create_transaction_deletion_request('Dunder Mifflin Paper Co') + create_task("Dunder Mifflin Paper Co") + tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") for doctype in tdr.doctypes: - if doctype.doctype_name == 'Task': + if doctype.doctype_name == "Task": self.assertEqual(doctype.no_of_docs, 5) def test_deletion_is_successful(self): - create_task('Dunder Mifflin Paper Co') - create_transaction_deletion_request('Dunder Mifflin Paper Co') - tasks_containing_company = frappe.get_all('Task', - filters = { - 'company' : 'Dunder Mifflin Paper Co' - }) + create_task("Dunder Mifflin Paper Co") + create_transaction_deletion_request("Dunder Mifflin Paper Co") + tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"}) self.assertEqual(tasks_containing_company, []) + def create_company(company_name): - company = frappe.get_doc({ - 'doctype': 'Company', - 'company_name': company_name, - 'default_currency': 'INR' - }) - company.insert(ignore_if_duplicate = True) + company = frappe.get_doc( + {"doctype": "Company", "company_name": company_name, "default_currency": "INR"} + ) + company.insert(ignore_if_duplicate=True) + def create_transaction_deletion_request(company): - tdr = frappe.get_doc({ - 'doctype': 'Transaction Deletion Record', - 'company': company - }) + tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() tdr.submit() return tdr def create_task(company): - task = frappe.get_doc({ - 'doctype': 'Task', - 'company': company, - 'subject': 'Delete' - }) + task = frappe.get_doc({"doctype": "Task", "company": company, "subject": "Delete"}) task.insert() 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 83ce042cde0..78b3939012d 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -11,15 +11,19 @@ from frappe.utils import cint class TransactionDeletionRecord(Document): def validate(self): - frappe.only_for('System Manager') + frappe.only_for("System Manager") self.validate_doctypes_to_be_ignored() def validate_doctypes_to_be_ignored(self): 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: - frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."), - title=_("Not Allowed")) + frappe.throw( + _( + "DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it." + ), + title=_("Not Allowed"), + ) def before_submit(self): if not self.doctypes_to_be_ignored: @@ -34,38 +38,55 @@ class TransactionDeletionRecord(Document): def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in doctypes_to_be_ignored_list: - self.append('doctypes_to_be_ignored', { - 'doctype_name' : doctype - }) + self.append("doctypes_to_be_ignored", {"doctype_name": doctype}) def delete_bins(self): - frappe.db.sql("""delete from tabBin where warehouse in - (select name from tabWarehouse where company=%s)""", self.company) + frappe.db.sql( + """delete from tabBin where warehouse in + (select name from tabWarehouse where company=%s)""", + self.company, + ) def delete_lead_addresses(self): """Delete addresses to which leads are linked""" - leads = frappe.get_all('Lead', filters={'company': self.company}) + leads = frappe.get_all("Lead", filters={"company": self.company}) leads = ["'%s'" % row.get("name") for row in leads] addresses = [] if leads: - addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name - in ({leads})""".format(leads=",".join(leads))) + addresses = frappe.db.sql_list( + """select parent from `tabDynamic Link` where link_name + in ({leads})""".format( + leads=",".join(leads) + ) + ) if addresses: addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] - frappe.db.sql("""delete from tabAddress where name in ({addresses}) and + frappe.db.sql( + """delete from tabAddress where name in ({addresses}) and name not in (select distinct dl1.parent from `tabDynamic Link` dl1 inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent - and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))) + and dl1.link_doctype<>dl2.link_doctype)""".format( + addresses=",".join(addresses) + ) + ) - frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead' - and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))) + frappe.db.sql( + """delete from `tabDynamic Link` where link_doctype='Lead' + and parenttype='Address' and link_name in ({leads})""".format( + leads=",".join(leads) + ) + ) - frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) + frappe.db.sql( + """update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format( + leads=",".join(leads) + ) + ) def reset_company_values(self): - company_obj = frappe.get_doc('Company', self.company) + company_obj = frappe.get_doc("Company", self.company) company_obj.total_monthly_sales = 0 company_obj.sales_monthly_history = None company_obj.save() @@ -76,24 +97,26 @@ class TransactionDeletionRecord(Document): tables = self.get_all_child_doctypes() for docfield in docfields: - if docfield['parent'] != self.doctype: - no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + if docfield["parent"] != self.doctype: + no_of_docs = self.get_number_of_docs_linked_with_specified_company( + docfield["parent"], docfield["fieldname"] + ) if no_of_docs > 0: - self.delete_version_log(docfield['parent'], docfield['fieldname']) - self.delete_communications(docfield['parent'], docfield['fieldname']) - self.populate_doctypes_table(tables, docfield['parent'], no_of_docs) + self.delete_version_log(docfield["parent"], docfield["fieldname"]) + self.delete_communications(docfield["parent"], docfield["fieldname"]) + self.populate_doctypes_table(tables, docfield["parent"], no_of_docs) - self.delete_child_tables(docfield['parent'], docfield['fieldname']) - self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + self.delete_child_tables(docfield["parent"], docfield["fieldname"]) + self.delete_docs_linked_with_specified_company(docfield["parent"], docfield["fieldname"]) - naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') + naming_series = frappe.db.get_value("DocType", docfield["parent"], "autoname") if naming_series: - if '#' in naming_series: - self.update_naming_series(naming_series, docfield['parent']) + if "#" in naming_series: + self.update_naming_series(naming_series, docfield["parent"]) def get_doctypes_to_be_ignored_list(self): - singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') + singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") doctypes_to_be_ignored_list = singles for doctype in self.doctypes_to_be_ignored: doctypes_to_be_ignored_list.append(doctype.doctype_name) @@ -101,81 +124,104 @@ class TransactionDeletionRecord(Document): return doctypes_to_be_ignored_list def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list): - docfields = frappe.get_all('DocField', - filters = { - 'fieldtype': 'Link', - 'options': 'Company', - 'parent': ['not in', doctypes_to_be_ignored_list]}, - fields=['parent', 'fieldname']) + docfields = frappe.get_all( + "DocField", + filters={ + "fieldtype": "Link", + "options": "Company", + "parent": ["not in", doctypes_to_be_ignored_list], + }, + fields=["parent", "fieldname"], + ) return docfields def get_all_child_doctypes(self): - return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name') + return frappe.get_all("DocType", filters={"istable": 1}, pluck="name") def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname): - return frappe.db.count(doctype, {company_fieldname : self.company}) + return frappe.db.count(doctype, {company_fieldname: self.company}) def populate_doctypes_table(self, tables, doctype, no_of_docs): if doctype not in tables: - self.append('doctypes', { - 'doctype_name' : doctype, - 'no_of_docs' : no_of_docs - }) + self.append("doctypes", {"doctype_name": doctype, "no_of_docs": no_of_docs}) def delete_child_tables(self, doctype, company_fieldname): - parent_docs_to_be_deleted = frappe.get_all(doctype, { - company_fieldname : self.company - }, pluck = 'name') + parent_docs_to_be_deleted = frappe.get_all( + doctype, {company_fieldname: self.company}, pluck="name" + ) - child_tables = frappe.get_all('DocField', filters = { - 'fieldtype': 'Table', - 'parent': doctype - }, pluck = 'options') + child_tables = frappe.get_all( + "DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options" + ) for table in child_tables: - frappe.db.delete(table, { - 'parent': ['in', parent_docs_to_be_deleted] - }) + frappe.db.delete(table, {"parent": ["in", parent_docs_to_be_deleted]}) def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): - frappe.db.delete(doctype, { - company_fieldname : self.company - }) + frappe.db.delete(doctype, {company_fieldname: self.company}) def update_naming_series(self, naming_series, doctype_name): - if '.' in naming_series: - prefix, hashes = naming_series.rsplit('.', 1) + if "." in naming_series: + prefix, hashes = naming_series.rsplit(".", 1) else: - prefix, hashes = naming_series.rsplit('{', 1) - last = frappe.db.sql("""select max(name) from `tab{0}` - where name like %s""".format(doctype_name), prefix + '%') + prefix, hashes = naming_series.rsplit("{", 1) + last = frappe.db.sql( + """select max(name) from `tab{0}` + where name like %s""".format( + doctype_name + ), + prefix + "%", + ) if last and last[0][0]: - last = cint(last[0][0].replace(prefix, '')) + last = cint(last[0][0].replace(prefix, "")) else: last = 0 frappe.db.sql("""update tabSeries set current = %s where name=%s""", (last, prefix)) def delete_version_log(self, doctype, company_fieldname): - frappe.db.sql("""delete from `tabVersion` where ref_doctype=%s and docname in - (select name from `tab{0}` where `{1}`=%s)""".format(doctype, - company_fieldname), (doctype, self.company)) + frappe.db.sql( + """delete from `tabVersion` where ref_doctype=%s and docname in + (select name from `tab{0}` where `{1}`=%s)""".format( + doctype, company_fieldname + ), + (doctype, self.company), + ) def delete_communications(self, doctype, company_fieldname): - reference_docs = frappe.get_all(doctype, filters={company_fieldname:self.company}) + reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company}) reference_doc_names = [r.name for r in reference_docs] - communications = frappe.get_all('Communication', filters={'reference_doctype':doctype,'reference_name':['in', reference_doc_names]}) + communications = frappe.get_all( + "Communication", + filters={"reference_doctype": doctype, "reference_name": ["in", reference_doc_names]}, + ) communication_names = [c.name for c in communications] - frappe.delete_doc('Communication', communication_names, ignore_permissions=True) + frappe.delete_doc("Communication", communication_names, ignore_permissions=True) + @frappe.whitelist() def get_doctypes_to_be_ignored(): - doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget', - 'Party Account', 'Employee', 'Sales Taxes and Charges Template', - 'Purchase Taxes and Charges Template', 'POS Profile', 'BOM', - 'Company', 'Bank Account', 'Item Tax Template', 'Mode of Payment', - 'Item Default', 'Customer', 'Supplier', 'GST Account'] + doctypes_to_be_ignored_list = [ + "Account", + "Cost Center", + "Warehouse", + "Budget", + "Party Account", + "Employee", + "Sales Taxes and Charges Template", + "Purchase Taxes and Charges Template", + "POS Profile", + "BOM", + "Company", + "Bank Account", + "Item Tax Template", + "Mode of Payment", + "Item Default", + "Customer", + "Supplier", + "GST Account", + ] return doctypes_to_be_ignored_list diff --git a/erpnext/setup/doctype/uom/test_uom.py b/erpnext/setup/doctype/uom/test_uom.py index feb43293079..3278d4eab88 100644 --- a/erpnext/setup/doctype/uom/test_uom.py +++ b/erpnext/setup/doctype/uom/test_uom.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('UOM') +test_records = frappe.get_test_records("UOM") diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 67d11438714..20ba74b8cde 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -20,7 +20,7 @@ default_mail_footer = """
    {1}".format(fileurl, args.get("company_name"))) + frappe.db.set_value( + "Website Settings", + "Website Settings", + "brand_html", + " {1}".format( + fileurl, args.get("company_name") + ), + ) + def create_website(args): website_maker(args) + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/default_website.py b/erpnext/setup/setup_wizard/operations/default_website.py index c11910b584c..40b02b35dfd 100644 --- a/erpnext/setup/setup_wizard/operations/default_website.py +++ b/erpnext/setup/setup_wizard/operations/default_website.py @@ -12,14 +12,14 @@ class website_maker(object): self.args = args self.company = args.company_name self.tagline = args.company_tagline - self.user = args.get('email') + self.user = args.get("email") self.make_web_page() self.make_website_settings() self.make_blog() def make_web_page(self): # home page - homepage = frappe.get_doc('Homepage', 'Homepage') + homepage = frappe.get_doc("Homepage", "Homepage") homepage.company = self.company homepage.tag_line = self.tagline homepage.setup_items() @@ -28,34 +28,25 @@ class website_maker(object): def make_website_settings(self): # update in home page in settings website_settings = frappe.get_doc("Website Settings", "Website Settings") - website_settings.home_page = 'home' + website_settings.home_page = "home" website_settings.brand_html = self.company website_settings.copyright = self.company website_settings.top_bar_items = [] - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label":"Contact", - "url": "/contact" - }) - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label":"Blog", - "url": "/blog" - }) - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label": _("Products"), - "url": "/all-products" - }) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"} + ) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"} + ) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"} + ) website_settings.save() def make_blog(self): - blog_category = frappe.get_doc({ - "doctype": "Blog Category", - "category_name": "general", - "published": 1, - "title": _("General") - }).insert() + blog_category = frappe.get_doc( + {"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")} + ).insert() if not self.user: # Admin setup @@ -69,21 +60,30 @@ class website_maker(object): blogger.avatar = user.user_image blogger.insert() - frappe.get_doc({ - "doctype": "Blog Post", - "title": "Welcome", - "published": 1, - "published_on": nowdate(), - "blogger": blogger.name, - "blog_category": blog_category.name, - "blog_intro": "My First Blog", - "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(), - }).insert() + frappe.get_doc( + { + "doctype": "Blog Post", + "title": "Welcome", + "published": 1, + "published_on": nowdate(), + "blogger": blogger.name, + "blog_category": blog_category.name, + "blog_intro": "My First Blog", + "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(), + } + ).insert() + def test(): frappe.delete_doc("Web Page", "test-company") frappe.delete_doc("Blog Post", "welcome") frappe.delete_doc("Blogger", "administrator") frappe.delete_doc("Blog Category", "general") - website_maker({'company':"Test Company", 'company_tagline': "Better Tools for Everyone", 'name': "Administrator"}) + website_maker( + { + "company": "Test Company", + "company_tagline": "Better Tools for Everyone", + "name": "Administrator", + } + ) frappe.db.commit() diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index a54c7b680ce..e079abe5f57 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -12,12 +12,14 @@ def set_default_settings(args): frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") - global_defaults.update({ - 'current_fiscal_year': get_fy_details(args.get('fy_start_date'), args.get('fy_end_date')), - 'default_currency': args.get('currency'), - 'default_company':args.get('company_name') , - "country": args.get("country"), - }) + global_defaults.update( + { + "current_fiscal_year": get_fy_details(args.get("fy_start_date"), args.get("fy_end_date")), + "default_currency": args.get("currency"), + "default_company": args.get("company_name"), + "country": args.get("country"), + } + ) global_defaults.save() @@ -25,13 +27,15 @@ def set_default_settings(args): system_settings.email_footer_address = args.get("company_name") system_settings.save() - domain_settings = frappe.get_single('Domain Settings') - domain_settings.set_active_domains(args.get('domains')) + domain_settings = frappe.get_single("Domain Settings") + domain_settings.set_active_domains(args.get("domains")) stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')}) + stock_settings.default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores")} + ) stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 @@ -72,61 +76,74 @@ def set_default_settings(args): hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") hr_settings.save() + def set_no_copy_fields_in_variant_settings(): # set no copy fields of an item doctype to item variant settings - doc = frappe.get_doc('Item Variant Settings') + doc = frappe.get_doc("Item Variant Settings") doc.set_default_fields() doc.save() + def create_price_lists(args): for pl_type, pl_name in (("Selling", _("Standard Selling")), ("Buying", _("Standard Buying"))): - frappe.get_doc({ - "doctype": "Price List", - "price_list_name": pl_name, - "enabled": 1, - "buying": 1 if pl_type == "Buying" else 0, - "selling": 1 if pl_type == "Selling" else 0, - "currency": args["currency"] - }).insert() + frappe.get_doc( + { + "doctype": "Price List", + "price_list_name": pl_name, + "enabled": 1, + "buying": 1 if pl_type == "Buying" else 0, + "selling": 1 if pl_type == "Selling" else 0, + "currency": args["currency"], + } + ).insert() + def create_employee_for_self(args): - if frappe.session.user == 'Administrator': + if frappe.session.user == "Administrator": return # create employee for self - emp = frappe.get_doc({ - "doctype": "Employee", - "employee_name": " ".join(filter(None, [args.get("first_name"), args.get("last_name")])), - "user_id": frappe.session.user, - "status": "Active", - "company": args.get("company_name") - }) + emp = frappe.get_doc( + { + "doctype": "Employee", + "employee_name": " ".join(filter(None, [args.get("first_name"), args.get("last_name")])), + "user_id": frappe.session.user, + "status": "Active", + "company": args.get("company_name"), + } + ) emp.flags.ignore_mandatory = True - emp.insert(ignore_permissions = True) + emp.insert(ignore_permissions=True) + def create_territories(): """create two default territories, one for home country and one named Rest of the World""" from frappe.utils.nestedset import get_root_of + country = frappe.db.get_default("country") root_territory = get_root_of("Territory") for name in (country, _("Rest Of The World")): if name and not frappe.db.exists("Territory", name): - frappe.get_doc({ - "doctype": "Territory", - "territory_name": name.replace("'", ""), - "parent_territory": root_territory, - "is_group": "No" - }).insert() + frappe.get_doc( + { + "doctype": "Territory", + "territory_name": name.replace("'", ""), + "parent_territory": root_territory, + "is_group": "No", + } + ).insert() + def create_feed_and_todo(): """update Activity feed and create todo for creation of item, customer, vendor""" return + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index d76d97663cd..e3d5f610d55 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -17,199 +17,376 @@ from frappe.utils.nestedset import rebuild_tree from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -default_lead_sources = ["Existing Customer", "Reference", "Advertisement", - "Cold Calling", "Exhibition", "Supplier Reference", "Mass Mailing", - "Customer's Vendor", "Campaign", "Walk In"] +default_lead_sources = [ + "Existing Customer", + "Reference", + "Advertisement", + "Cold Calling", + "Exhibition", + "Supplier Reference", + "Mass Mailing", + "Customer's Vendor", + "Campaign", + "Walk In", +] + +default_sales_partner_type = [ + "Channel Partner", + "Distributor", + "Dealer", + "Agent", + "Retailer", + "Implementation Partner", + "Reseller", +] -default_sales_partner_type = ["Channel Partner", "Distributor", "Dealer", "Agent", - "Retailer", "Implementation Partner", "Reseller"] def install(country=None): records = [ # domains - { 'doctype': 'Domain', 'domain': 'Distribution'}, - { 'doctype': 'Domain', 'domain': 'Manufacturing'}, - { 'doctype': 'Domain', 'domain': 'Retail'}, - { 'doctype': 'Domain', 'domain': 'Services'}, - { 'doctype': 'Domain', 'domain': 'Education'}, - { 'doctype': 'Domain', 'domain': 'Healthcare'}, - { 'doctype': 'Domain', 'domain': 'Agriculture'}, - { 'doctype': 'Domain', 'domain': 'Non Profit'}, - + {"doctype": "Domain", "domain": "Distribution"}, + {"doctype": "Domain", "domain": "Manufacturing"}, + {"doctype": "Domain", "domain": "Retail"}, + {"doctype": "Domain", "domain": "Services"}, + {"doctype": "Domain", "domain": "Education"}, + {"doctype": "Domain", "domain": "Healthcare"}, + {"doctype": "Domain", "domain": "Agriculture"}, + {"doctype": "Domain", "domain": "Non Profit"}, # ensure at least an empty Address Template exists for this Country - {'doctype':"Address Template", "country": country}, - + {"doctype": "Address Template", "country": country}, # item group - {'doctype': 'Item Group', 'item_group_name': _('All Item Groups'), - 'is_group': 1, 'parent_item_group': ''}, - {'doctype': 'Item Group', 'item_group_name': _('Products'), - 'is_group': 0, 'parent_item_group': _('All Item Groups'), "show_in_website": 1 }, - {'doctype': 'Item Group', 'item_group_name': _('Raw Material'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Services'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Sub Assemblies'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Consumable'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - + { + "doctype": "Item Group", + "item_group_name": _("All Item Groups"), + "is_group": 1, + "parent_item_group": "", + }, + { + "doctype": "Item Group", + "item_group_name": _("Products"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + "show_in_website": 1, + }, + { + "doctype": "Item Group", + "item_group_name": _("Raw Material"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Services"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Sub Assemblies"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Consumable"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, # salary component - {'doctype': 'Salary Component', 'salary_component': _('Income Tax'), 'description': _('Income Tax'), 'type': 'Deduction', 'is_income_tax_component': 1}, - {'doctype': 'Salary Component', 'salary_component': _('Basic'), 'description': _('Basic'), 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': _('Arrear'), 'description': _('Arrear'), 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': _('Leave Encashment'), 'description': _('Leave Encashment'), 'type': 'Earning'}, - - + { + "doctype": "Salary Component", + "salary_component": _("Income Tax"), + "description": _("Income Tax"), + "type": "Deduction", + "is_income_tax_component": 1, + }, + { + "doctype": "Salary Component", + "salary_component": _("Basic"), + "description": _("Basic"), + "type": "Earning", + }, + { + "doctype": "Salary Component", + "salary_component": _("Arrear"), + "description": _("Arrear"), + "type": "Earning", + }, + { + "doctype": "Salary Component", + "salary_component": _("Leave Encashment"), + "description": _("Leave Encashment"), + "type": "Earning", + }, # expense claim type - {'doctype': 'Expense Claim Type', 'name': _('Calls'), 'expense_type': _('Calls')}, - {'doctype': 'Expense Claim Type', 'name': _('Food'), 'expense_type': _('Food')}, - {'doctype': 'Expense Claim Type', 'name': _('Medical'), 'expense_type': _('Medical')}, - {'doctype': 'Expense Claim Type', 'name': _('Others'), 'expense_type': _('Others')}, - {'doctype': 'Expense Claim Type', 'name': _('Travel'), 'expense_type': _('Travel')}, - + {"doctype": "Expense Claim Type", "name": _("Calls"), "expense_type": _("Calls")}, + {"doctype": "Expense Claim Type", "name": _("Food"), "expense_type": _("Food")}, + {"doctype": "Expense Claim Type", "name": _("Medical"), "expense_type": _("Medical")}, + {"doctype": "Expense Claim Type", "name": _("Others"), "expense_type": _("Others")}, + {"doctype": "Expense Claim Type", "name": _("Travel"), "expense_type": _("Travel")}, # leave type - {'doctype': 'Leave Type', 'leave_type_name': _('Casual Leave'), 'name': _('Casual Leave'), - 'allow_encashment': 1, 'is_carry_forward': 1, 'max_continuous_days_allowed': '3', 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Compensatory Off'), 'name': _('Compensatory Off'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1, 'is_compensatory':1 }, - {'doctype': 'Leave Type', 'leave_type_name': _('Sick Leave'), 'name': _('Sick Leave'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Privilege Leave'), 'name': _('Privilege Leave'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Leave Without Pay'), 'name': _('Leave Without Pay'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'is_lwp':1, 'include_holiday': 1}, - + { + "doctype": "Leave Type", + "leave_type_name": _("Casual Leave"), + "name": _("Casual Leave"), + "allow_encashment": 1, + "is_carry_forward": 1, + "max_continuous_days_allowed": "3", + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Compensatory Off"), + "name": _("Compensatory Off"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + "is_compensatory": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Sick Leave"), + "name": _("Sick Leave"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Privilege Leave"), + "name": _("Privilege Leave"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Leave Without Pay"), + "name": _("Leave Without Pay"), + "allow_encashment": 0, + "is_carry_forward": 0, + "is_lwp": 1, + "include_holiday": 1, + }, # Employment Type - {'doctype': 'Employment Type', 'employee_type_name': _('Full-time')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Part-time')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Probation')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Contract')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Commission')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Piecework')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Intern')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Apprentice')}, - - + {"doctype": "Employment Type", "employee_type_name": _("Full-time")}, + {"doctype": "Employment Type", "employee_type_name": _("Part-time")}, + {"doctype": "Employment Type", "employee_type_name": _("Probation")}, + {"doctype": "Employment Type", "employee_type_name": _("Contract")}, + {"doctype": "Employment Type", "employee_type_name": _("Commission")}, + {"doctype": "Employment Type", "employee_type_name": _("Piecework")}, + {"doctype": "Employment Type", "employee_type_name": _("Intern")}, + {"doctype": "Employment Type", "employee_type_name": _("Apprentice")}, # Stock Entry Type - {'doctype': 'Stock Entry Type', 'name': 'Material Issue', 'purpose': 'Material Issue'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Receipt', 'purpose': 'Material Receipt'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Transfer', 'purpose': 'Material Transfer'}, - {'doctype': 'Stock Entry Type', 'name': 'Manufacture', 'purpose': 'Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Repack', 'purpose': 'Repack'}, - {'doctype': 'Stock Entry Type', 'name': 'Send to Subcontractor', 'purpose': 'Send to Subcontractor'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Transfer for Manufacture', 'purpose': 'Material Transfer for Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Consumption for Manufacture', 'purpose': 'Material Consumption for Manufacture'}, - + {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, + {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, + {"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, + {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, + {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, + { + "doctype": "Stock Entry Type", + "name": "Send to Subcontractor", + "purpose": "Send to Subcontractor", + }, + { + "doctype": "Stock Entry Type", + "name": "Material Transfer for Manufacture", + "purpose": "Material Transfer for Manufacture", + }, + { + "doctype": "Stock Entry Type", + "name": "Material Consumption for Manufacture", + "purpose": "Material Consumption for Manufacture", + }, # Designation - {'doctype': 'Designation', 'designation_name': _('CEO')}, - {'doctype': 'Designation', 'designation_name': _('Manager')}, - {'doctype': 'Designation', 'designation_name': _('Analyst')}, - {'doctype': 'Designation', 'designation_name': _('Engineer')}, - {'doctype': 'Designation', 'designation_name': _('Accountant')}, - {'doctype': 'Designation', 'designation_name': _('Secretary')}, - {'doctype': 'Designation', 'designation_name': _('Associate')}, - {'doctype': 'Designation', 'designation_name': _('Administrative Officer')}, - {'doctype': 'Designation', 'designation_name': _('Business Development Manager')}, - {'doctype': 'Designation', 'designation_name': _('HR Manager')}, - {'doctype': 'Designation', 'designation_name': _('Project Manager')}, - {'doctype': 'Designation', 'designation_name': _('Head of Marketing and Sales')}, - {'doctype': 'Designation', 'designation_name': _('Software Developer')}, - {'doctype': 'Designation', 'designation_name': _('Designer')}, - {'doctype': 'Designation', 'designation_name': _('Researcher')}, - + {"doctype": "Designation", "designation_name": _("CEO")}, + {"doctype": "Designation", "designation_name": _("Manager")}, + {"doctype": "Designation", "designation_name": _("Analyst")}, + {"doctype": "Designation", "designation_name": _("Engineer")}, + {"doctype": "Designation", "designation_name": _("Accountant")}, + {"doctype": "Designation", "designation_name": _("Secretary")}, + {"doctype": "Designation", "designation_name": _("Associate")}, + {"doctype": "Designation", "designation_name": _("Administrative Officer")}, + {"doctype": "Designation", "designation_name": _("Business Development Manager")}, + {"doctype": "Designation", "designation_name": _("HR Manager")}, + {"doctype": "Designation", "designation_name": _("Project Manager")}, + {"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")}, + {"doctype": "Designation", "designation_name": _("Software Developer")}, + {"doctype": "Designation", "designation_name": _("Designer")}, + {"doctype": "Designation", "designation_name": _("Researcher")}, # territory: with two default territories, one for home country and one named Rest of the World - {'doctype': 'Territory', 'territory_name': _('All Territories'), 'is_group': 1, 'name': _('All Territories'), 'parent_territory': ''}, - {'doctype': 'Territory', 'territory_name': country.replace("'", ""), 'is_group': 0, 'parent_territory': _('All Territories')}, - {'doctype': 'Territory', 'territory_name': _("Rest Of The World"), 'is_group': 0, 'parent_territory': _('All Territories')}, - + { + "doctype": "Territory", + "territory_name": _("All Territories"), + "is_group": 1, + "name": _("All Territories"), + "parent_territory": "", + }, + { + "doctype": "Territory", + "territory_name": country.replace("'", ""), + "is_group": 0, + "parent_territory": _("All Territories"), + }, + { + "doctype": "Territory", + "territory_name": _("Rest Of The World"), + "is_group": 0, + "parent_territory": _("All Territories"), + }, # customer group - {'doctype': 'Customer Group', 'customer_group_name': _('All Customer Groups'), 'is_group': 1, 'name': _('All Customer Groups'), 'parent_customer_group': ''}, - {'doctype': 'Customer Group', 'customer_group_name': _('Individual'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Commercial'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Non Profit'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Government'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - + { + "doctype": "Customer Group", + "customer_group_name": _("All Customer Groups"), + "is_group": 1, + "name": _("All Customer Groups"), + "parent_customer_group": "", + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Individual"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Commercial"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Non Profit"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Government"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, # supplier group - {'doctype': 'Supplier Group', 'supplier_group_name': _('All Supplier Groups'), 'is_group': 1, 'name': _('All Supplier Groups'), 'parent_supplier_group': ''}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Services'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Local'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Raw Material'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Electrical'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Hardware'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Pharmaceutical'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Distributor'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - + { + "doctype": "Supplier Group", + "supplier_group_name": _("All Supplier Groups"), + "is_group": 1, + "name": _("All Supplier Groups"), + "parent_supplier_group": "", + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Services"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Local"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Raw Material"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Electrical"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Hardware"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Pharmaceutical"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Distributor"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, # Sales Person - {'doctype': 'Sales Person', 'sales_person_name': _('Sales Team'), 'is_group': 1, "parent_sales_person": ""}, - + { + "doctype": "Sales Person", + "sales_person_name": _("Sales Team"), + "is_group": 1, + "parent_sales_person": "", + }, # Mode of Payment - {'doctype': 'Mode of Payment', - 'mode_of_payment': 'Check' if country=="United States" else _('Cheque'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Cash'), - 'type': 'Cash'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Credit Card'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Wire Transfer'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Bank Draft'), - 'type': 'Bank'}, - + { + "doctype": "Mode of Payment", + "mode_of_payment": "Check" if country == "United States" else _("Cheque"), + "type": "Bank", + }, + {"doctype": "Mode of Payment", "mode_of_payment": _("Cash"), "type": "Cash"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Credit Card"), "type": "Bank"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Wire Transfer"), "type": "Bank"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Bank Draft"), "type": "Bank"}, # Activity Type - {'doctype': 'Activity Type', 'activity_type': _('Planning')}, - {'doctype': 'Activity Type', 'activity_type': _('Research')}, - {'doctype': 'Activity Type', 'activity_type': _('Proposal Writing')}, - {'doctype': 'Activity Type', 'activity_type': _('Execution')}, - {'doctype': 'Activity Type', 'activity_type': _('Communication')}, - - {'doctype': "Item Attribute", "attribute_name": _("Size"), "item_attribute_values": [ - {"attribute_value": _("Extra Small"), "abbr": "XS"}, - {"attribute_value": _("Small"), "abbr": "S"}, - {"attribute_value": _("Medium"), "abbr": "M"}, - {"attribute_value": _("Large"), "abbr": "L"}, - {"attribute_value": _("Extra Large"), "abbr": "XL"} - ]}, - - {'doctype': "Item Attribute", "attribute_name": _("Colour"), "item_attribute_values": [ - {"attribute_value": _("Red"), "abbr": "RED"}, - {"attribute_value": _("Green"), "abbr": "GRE"}, - {"attribute_value": _("Blue"), "abbr": "BLU"}, - {"attribute_value": _("Black"), "abbr": "BLA"}, - {"attribute_value": _("White"), "abbr": "WHI"} - ]}, - + {"doctype": "Activity Type", "activity_type": _("Planning")}, + {"doctype": "Activity Type", "activity_type": _("Research")}, + {"doctype": "Activity Type", "activity_type": _("Proposal Writing")}, + {"doctype": "Activity Type", "activity_type": _("Execution")}, + {"doctype": "Activity Type", "activity_type": _("Communication")}, + { + "doctype": "Item Attribute", + "attribute_name": _("Size"), + "item_attribute_values": [ + {"attribute_value": _("Extra Small"), "abbr": "XS"}, + {"attribute_value": _("Small"), "abbr": "S"}, + {"attribute_value": _("Medium"), "abbr": "M"}, + {"attribute_value": _("Large"), "abbr": "L"}, + {"attribute_value": _("Extra Large"), "abbr": "XL"}, + ], + }, + { + "doctype": "Item Attribute", + "attribute_name": _("Colour"), + "item_attribute_values": [ + {"attribute_value": _("Red"), "abbr": "RED"}, + {"attribute_value": _("Green"), "abbr": "GRE"}, + {"attribute_value": _("Blue"), "abbr": "BLU"}, + {"attribute_value": _("Black"), "abbr": "BLA"}, + {"attribute_value": _("White"), "abbr": "WHI"}, + ], + }, # Issue Priority - {'doctype': 'Issue Priority', 'name': _('Low')}, - {'doctype': 'Issue Priority', 'name': _('Medium')}, - {'doctype': 'Issue Priority', 'name': _('High')}, - - #Job Applicant Source - {'doctype': 'Job Applicant Source', 'source_name': _('Website Listing')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Walk In')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Employee Referral')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Campaign')}, - - {'doctype': "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, - {'doctype': "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, - {'doctype': "Email Account", "email_id": "jobs@example.com", "append_to": "Job Applicant"}, - - {'doctype': "Party Type", "party_type": "Customer", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Supplier", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Employee", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"}, - - {'doctype': "Opportunity Type", "name": "Hub"}, - {'doctype': "Opportunity Type", "name": _("Sales")}, - {'doctype': "Opportunity Type", "name": _("Support")}, - {'doctype': "Opportunity Type", "name": _("Maintenance")}, - - {'doctype': "Project Type", "project_type": "Internal"}, - {'doctype': "Project Type", "project_type": "External"}, - {'doctype': "Project Type", "project_type": "Other"}, - + {"doctype": "Issue Priority", "name": _("Low")}, + {"doctype": "Issue Priority", "name": _("Medium")}, + {"doctype": "Issue Priority", "name": _("High")}, + # Job Applicant Source + {"doctype": "Job Applicant Source", "source_name": _("Website Listing")}, + {"doctype": "Job Applicant Source", "source_name": _("Walk In")}, + {"doctype": "Job Applicant Source", "source_name": _("Employee Referral")}, + {"doctype": "Job Applicant Source", "source_name": _("Campaign")}, + {"doctype": "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, + {"doctype": "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, + {"doctype": "Email Account", "email_id": "jobs@example.com", "append_to": "Job Applicant"}, + {"doctype": "Party Type", "party_type": "Customer", "account_type": "Receivable"}, + {"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Member", "account_type": "Receivable"}, + {"doctype": "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Student", "account_type": "Receivable"}, + {"doctype": "Party Type", "party_type": "Donor", "account_type": "Receivable"}, + {"doctype": "Opportunity Type", "name": "Hub"}, + {"doctype": "Opportunity Type", "name": _("Sales")}, + {"doctype": "Opportunity Type", "name": _("Support")}, + {"doctype": "Opportunity Type", "name": _("Maintenance")}, + {"doctype": "Project Type", "project_type": "Internal"}, + {"doctype": "Project Type", "project_type": "External"}, + {"doctype": "Project Type", "project_type": "Other"}, {"doctype": "Offer Term", "offer_term": _("Date of Joining")}, {"doctype": "Offer Term", "offer_term": _("Annual Salary")}, {"doctype": "Offer Term", "offer_term": _("Probationary Period")}, @@ -222,23 +399,22 @@ def install(country=None): {"doctype": "Offer Term", "offer_term": _("Leaves per Year")}, {"doctype": "Offer Term", "offer_term": _("Notice Period")}, {"doctype": "Offer Term", "offer_term": _("Incentives")}, - - {'doctype': "Print Heading", 'print_heading': _("Credit Note")}, - {'doctype': "Print Heading", 'print_heading': _("Debit Note")}, - + {"doctype": "Print Heading", "print_heading": _("Credit Note")}, + {"doctype": "Print Heading", "print_heading": _("Debit Note")}, # Assessment Group - {'doctype': 'Assessment Group', 'assessment_group_name': _('All Assessment Groups'), - 'is_group': 1, 'parent_assessment_group': ''}, - + { + "doctype": "Assessment Group", + "assessment_group_name": _("All Assessment Groups"), + "is_group": 1, + "parent_assessment_group": "", + }, # Share Management {"doctype": "Share Type", "title": _("Equity")}, {"doctype": "Share Type", "title": _("Preference")}, - # Market Segments {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")}, - # Sales Stages {"doctype": "Sales Stage", "stage_name": _("Prospecting")}, {"doctype": "Sales Stage", "stage_name": _("Qualification")}, @@ -248,42 +424,87 @@ def install(country=None): {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, - # Warehouse Type - {'doctype': 'Warehouse Type', 'name': 'Transit'}, + {"doctype": "Warehouse Type", "name": "Transit"}, ] from erpnext.setup.setup_wizard.data.industry_type import get_industry_types - records += [{"doctype":"Industry Type", "industry": d} for d in get_industry_types()] - # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] - records += [{'doctype': 'Lead Source', 'source_name': _(d)} for d in default_lead_sources] - records += [{'doctype': 'Sales Partner Type', 'sales_partner_type': _(d)} for d in default_sales_partner_type] + records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] + # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] + records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] + + records += [ + {"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type + ] base_path = frappe.get_app_path("erpnext", "hr", "doctype") - response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html")) + response = frappe.read_file( + os.path.join(base_path, "leave_application/leave_application_email_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response, - 'subject': _("Leave Approval Notification"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Leave Approval Notification"), + "response": response, + "subject": _("Leave Approval Notification"), + "owner": frappe.session.user, + } + ] - records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response, - 'subject': _("Leave Status Notification"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Leave Status Notification"), + "response": response, + "subject": _("Leave Status Notification"), + "owner": frappe.session.user, + } + ] - response = frappe.read_file(os.path.join(base_path, "interview/interview_reminder_notification_template.html")) + response = frappe.read_file( + os.path.join(base_path, "interview/interview_reminder_notification_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _('Interview Reminder'), 'response': response, - 'subject': _('Interview Reminder'), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Interview Reminder"), + "response": response, + "subject": _("Interview Reminder"), + "owner": frappe.session.user, + } + ] - response = frappe.read_file(os.path.join(base_path, "interview/interview_feedback_reminder_template.html")) + response = frappe.read_file( + os.path.join(base_path, "interview/interview_feedback_reminder_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response, - 'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Interview Feedback Reminder"), + "response": response, + "subject": _("Interview Feedback Reminder"), + "owner": frappe.session.user, + } + ] base_path = frappe.get_app_path("erpnext", "stock", "doctype") - response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) + response = frappe.read_file( + os.path.join(base_path, "delivery_trip/dispatch_notification_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response, - 'subject': _("Your order is out for delivery!"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Dispatch Notification"), + "response": response, + "subject": _("Your order is out for delivery!"), + "owner": frappe.session.user, + } + ] # Records for the Supplier Scorecard from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import make_default_records @@ -294,6 +515,7 @@ def install(country=None): set_more_defaults() update_global_search_doctypes() + def set_more_defaults(): # Do more setup stuff that can be done here with no dependencies update_selling_defaults() @@ -302,6 +524,7 @@ def set_more_defaults(): add_uom_data() update_item_variant_settings() + def update_selling_defaults(): selling_settings = frappe.get_doc("Selling Settings") selling_settings.cust_master_name = "Customer Name" @@ -311,6 +534,7 @@ def update_selling_defaults(): selling_settings.sales_update_frequency = "Each Transaction" selling_settings.save() + def update_buying_defaults(): buying_settings = frappe.get_doc("Buying Settings") buying_settings.supp_master_name = "Supplier Name" @@ -320,6 +544,7 @@ def update_buying_defaults(): buying_settings.allow_multiple_items = 1 buying_settings.save() + def update_hr_defaults(): hr_settings = frappe.get_doc("HR Settings") hr_settings.emp_created_by = "Naming Series" @@ -335,53 +560,66 @@ def update_hr_defaults(): hr_settings.save() + def update_item_variant_settings(): # set no copy fields of an item doctype to item variant settings - doc = frappe.get_doc('Item Variant Settings') + doc = frappe.get_doc("Item Variant Settings") doc.set_default_fields() doc.save() + def add_uom_data(): # add UOMs - uoms = json.loads(open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_data.json")).read()) + uoms = json.loads( + open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_data.json")).read() + ) for d in uoms: - if not frappe.db.exists('UOM', _(d.get("uom_name"))): - uom_doc = frappe.get_doc({ - "doctype": "UOM", - "uom_name": _(d.get("uom_name")), - "name": _(d.get("uom_name")), - "must_be_whole_number": d.get("must_be_whole_number"), - "enabled": 1, - }).db_insert() + if not frappe.db.exists("UOM", _(d.get("uom_name"))): + uom_doc = frappe.get_doc( + { + "doctype": "UOM", + "uom_name": _(d.get("uom_name")), + "name": _(d.get("uom_name")), + "must_be_whole_number": d.get("must_be_whole_number"), + "enabled": 1, + } + ).db_insert() # bootstrap uom conversion factors - uom_conversions = json.loads(open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_conversion_data.json")).read()) + uom_conversions = json.loads( + open( + frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_conversion_data.json") + ).read() + ) for d in uom_conversions: if not frappe.db.exists("UOM Category", _(d.get("category"))): - frappe.get_doc({ - "doctype": "UOM Category", - "category_name": _(d.get("category")) - }).db_insert() + frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() + + if not frappe.db.exists( + "UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} + ): + uom_conversion = frappe.get_doc( + { + "doctype": "UOM Conversion Factor", + "category": _(d.get("category")), + "from_uom": _(d.get("from_uom")), + "to_uom": _(d.get("to_uom")), + "value": d.get("value"), + } + ).insert(ignore_permissions=True) - if not frappe.db.exists("UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}): - uom_conversion = frappe.get_doc({ - "doctype": "UOM Conversion Factor", - "category": _(d.get("category")), - "from_uom": _(d.get("from_uom")), - "to_uom": _(d.get("to_uom")), - "value": d.get("value") - }).insert(ignore_permissions=True) def add_market_segments(): records = [ # Market Segments {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, - {"doctype": "Market Segment", "market_segment": _("Upper Income")} + {"doctype": "Market Segment", "market_segment": _("Upper Income")}, ] make_records(records) + def add_sale_stages(): # Sale Stages records = [ @@ -392,33 +630,33 @@ def add_sale_stages(): {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")} + {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, ] for sales_stage in records: frappe.get_doc(sales_stage).db_insert() + def install_company(args): records = [ # Fiscal Year { - 'doctype': "Fiscal Year", - 'year': get_fy_details(args.fy_start_date, args.fy_end_date), - 'year_start_date': args.fy_start_date, - 'year_end_date': args.fy_end_date + "doctype": "Fiscal Year", + "year": get_fy_details(args.fy_start_date, args.fy_end_date), + "year_start_date": args.fy_start_date, + "year_end_date": args.fy_end_date, }, - # Company { - "doctype":"Company", - 'company_name': args.company_name, - 'enable_perpetual_inventory': 1, - 'abbr': args.company_abbr, - 'default_currency': args.currency, - 'country': args.country, - 'create_chart_of_accounts_based_on': 'Standard Template', - 'chart_of_accounts': args.chart_of_accounts, - 'domain': args.domain - } + "doctype": "Company", + "company_name": args.company_name, + "enable_perpetual_inventory": 1, + "abbr": args.company_abbr, + "default_currency": args.currency, + "country": args.country, + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": args.chart_of_accounts, + "domain": args.domain, + }, ] make_records(records) @@ -427,20 +665,90 @@ def install_company(args): def install_post_company_fixtures(args=None): records = [ # Department - {'doctype': 'Department', 'department_name': _('All Departments'), 'is_group': 1, 'parent_department': ''}, - {'doctype': 'Department', 'department_name': _('Accounts'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Marketing'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Sales'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Purchase'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Operations'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Production'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Dispatch'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Customer Service'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Human Resources'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Management'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Quality Management'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Research & Development'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Legal'), 'parent_department': _('All Departments'), 'company': args.company_name}, + { + "doctype": "Department", + "department_name": _("All Departments"), + "is_group": 1, + "parent_department": "", + }, + { + "doctype": "Department", + "department_name": _("Accounts"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Marketing"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Sales"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Purchase"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Operations"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Production"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Dispatch"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Customer Service"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Human Resources"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Management"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Quality Management"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Research & Development"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Legal"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, ] # Make root department with NSM updation @@ -449,15 +757,28 @@ def install_post_company_fixtures(args=None): frappe.local.flags.ignore_update_nsm = True make_records(records[1:]) frappe.local.flags.ignore_update_nsm = False - rebuild_tree("Department", "parent_department") def install_defaults(args=None): records = [ # Price Lists - { "doctype": "Price List", "price_list_name": _("Standard Buying"), "enabled": 1, "buying": 1, "selling": 0, "currency": args.currency }, - { "doctype": "Price List", "price_list_name": _("Standard Selling"), "enabled": 1, "buying": 0, "selling": 1, "currency": args.currency }, + { + "doctype": "Price List", + "price_list_name": _("Standard Buying"), + "enabled": 1, + "buying": 1, + "selling": 0, + "currency": args.currency, + }, + { + "doctype": "Price List", + "price_list_name": _("Standard Selling"), + "enabled": 1, + "buying": 0, + "selling": 1, + "currency": args.currency, + }, ] make_records(records) @@ -474,27 +795,34 @@ def install_defaults(args=None): args.update({"set_default": 1}) create_bank_account(args) + def set_global_defaults(args): global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") current_fiscal_year = frappe.get_all("Fiscal Year")[0] - global_defaults.update({ - 'current_fiscal_year': current_fiscal_year.name, - 'default_currency': args.get('currency'), - 'default_company':args.get('company_name') , - "country": args.get("country"), - }) + global_defaults.update( + { + "current_fiscal_year": current_fiscal_year.name, + "default_currency": args.get("currency"), + "default_company": args.get("company_name"), + "country": args.get("country"), + } + ) global_defaults.save() + def set_active_domains(args): - frappe.get_single('Domain Settings').set_active_domains(args.get('domains')) + frappe.get_single("Domain Settings").set_active_domains(args.get("domains")) + def update_stock_settings(): stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')}) + stock_settings.default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores")} + ) stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 @@ -502,52 +830,65 @@ def update_stock_settings(): stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.save() + def create_bank_account(args): - if not args.get('bank_account'): + if not args.get("bank_account"): return - company_name = args.get('company_name') - bank_account_group = frappe.db.get_value("Account", - {"account_type": "Bank", "is_group": 1, "root_type": "Asset", - "company": company_name}) + company_name = args.get("company_name") + bank_account_group = frappe.db.get_value( + "Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} + ) if bank_account_group: - bank_account = frappe.get_doc({ - "doctype": "Account", - 'account_name': args.get('bank_account'), - 'parent_account': bank_account_group, - 'is_group':0, - 'company': company_name, - "account_type": "Bank", - }) + bank_account = frappe.get_doc( + { + "doctype": "Account", + "account_name": args.get("bank_account"), + "parent_account": bank_account_group, + "is_group": 0, + "company": company_name, + "account_type": "Bank", + } + ) try: doc = bank_account.insert() - if args.get('set_default'): - frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) + if args.get("set_default"): + frappe.db.set_value( + "Company", + args.get("company_name"), + "default_bank_account", + bank_account.name, + update_modified=False, + ) return doc except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account'))) + frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account"))) except frappe.DuplicateEntryError: # bank account same as a CoA entry pass + def update_shopping_cart_settings(args): shopping_cart = frappe.get_doc("E Commerce Settings") - shopping_cart.update({ - "enabled": 1, - 'company': args.company_name, - 'price_list': frappe.db.get_value("Price List", {"selling": 1}), - 'default_customer_group': _("Individual"), - 'quotation_series': "QTN-", - }) + shopping_cart.update( + { + "enabled": 1, + "company": args.company_name, + "price_list": frappe.db.get_value("Price List", {"selling": 1}), + "default_customer_group": _("Individual"), + "quotation_series": "QTN-", + } + ) shopping_cart.update_single(shopping_cart.get_valid_dict()) + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/sample_data.py b/erpnext/setup/setup_wizard/operations/sample_data.py index 16859947741..7fdd5688f2b 100644 --- a/erpnext/setup/setup_wizard/operations/sample_data.py +++ b/erpnext/setup/setup_wizard/operations/sample_data.py @@ -12,12 +12,12 @@ from frappe import _ from frappe.utils.make_random import add_random_children -def make_sample_data(domains, make_dependent = False): +def make_sample_data(domains, make_dependent=False): """Create a few opportunities, quotes, material requests, issues, todos, projects to help the user get started""" if make_dependent: - items = frappe.get_all("Item", {'is_sales_item': 1}) + items = frappe.get_all("Item", {"is_sales_item": 1}) customers = frappe.get_all("Customer") warehouses = frappe.get_all("Warehouse") @@ -33,91 +33,109 @@ def make_sample_data(domains, make_dependent = False): make_projects(domains) import_notification() -def make_opportunity(items, customer): - b = frappe.get_doc({ - "doctype": "Opportunity", - "opportunity_from": "Customer", - "customer": customer, - "opportunity_type": _("Sales"), - "with_items": 1 - }) - add_random_children(b, "items", rows=len(items), randomize = { - "qty": (1, 5), - "item_code": ["Item"] - }, unique="item_code") +def make_opportunity(items, customer): + b = frappe.get_doc( + { + "doctype": "Opportunity", + "opportunity_from": "Customer", + "customer": customer, + "opportunity_type": _("Sales"), + "with_items": 1, + } + ) + + add_random_children( + b, "items", rows=len(items), randomize={"qty": (1, 5), "item_code": ["Item"]}, unique="item_code" + ) b.insert(ignore_permissions=True) - b.add_comment('Comment', text="This is a dummy record") + b.add_comment("Comment", text="This is a dummy record") + def make_quote(items, customer): - qtn = frappe.get_doc({ - "doctype": "Quotation", - "quotation_to": "Customer", - "party_name": customer, - "order_type": "Sales" - }) + qtn = frappe.get_doc( + { + "doctype": "Quotation", + "quotation_to": "Customer", + "party_name": customer, + "order_type": "Sales", + } + ) - add_random_children(qtn, "items", rows=len(items), randomize = { - "qty": (1, 5), - "item_code": ["Item"] - }, unique="item_code") + add_random_children( + qtn, + "items", + rows=len(items), + randomize={"qty": (1, 5), "item_code": ["Item"]}, + unique="item_code", + ) qtn.insert(ignore_permissions=True) - qtn.add_comment('Comment', text="This is a dummy record") + qtn.add_comment("Comment", text="This is a dummy record") + def make_material_request(items): for i in items: - mr = frappe.get_doc({ - "doctype": "Material Request", - "material_request_type": "Purchase", - "schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7), - "items": [{ + mr = frappe.get_doc( + { + "doctype": "Material Request", + "material_request_type": "Purchase", "schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7), - "item_code": i.name, - "qty": 10 - }] - }) + "items": [ + { + "schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7), + "item_code": i.name, + "qty": 10, + } + ], + } + ) mr.insert() mr.submit() - mr.add_comment('Comment', text="This is a dummy record") + mr.add_comment("Comment", text="This is a dummy record") def make_issue(): pass + def make_projects(domains): current_date = frappe.utils.nowdate() - project = frappe.get_doc({ - "doctype": "Project", - "project_name": "ERPNext Implementation", - }) + project = frappe.get_doc( + { + "doctype": "Project", + "project_name": "ERPNext Implementation", + } + ) tasks = [ { "title": "Explore ERPNext", "start_date": current_date, "end_date": current_date, - "file": "explore.md" - }] + "file": "explore.md", + } + ] - if 'Education' in domains: + if "Education" in domains: tasks += [ { "title": _("Setup your Institute in ERPNext"), "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 1), - "file": "education_masters.md" + "file": "education_masters.md", }, { "title": "Setup Master Data", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 1), - "file": "education_masters.md" - }] + "file": "education_masters.md", + }, + ] else: tasks += [ @@ -125,55 +143,59 @@ def make_projects(domains): "title": "Setup Your Company", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 1), - "file": "masters.md" + "file": "masters.md", }, { "title": "Start Tracking your Sales", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 2), - "file": "sales.md" + "file": "sales.md", }, { "title": "Start Managing Purchases", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 3), - "file": "purchase.md" + "file": "purchase.md", }, { "title": "Import Data", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 4), - "file": "import_data.md" + "file": "import_data.md", }, { "title": "Go Live!", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 5), - "file": "go_live.md" - }] + "file": "go_live.md", + }, + ] for t in tasks: - with open (os.path.join(os.path.dirname(__file__), "tasks", t['file'])) as f: - t['description'] = frappe.utils.md_to_html(f.read()) - del t['file'] + with open(os.path.join(os.path.dirname(__file__), "tasks", t["file"])) as f: + t["description"] = frappe.utils.md_to_html(f.read()) + del t["file"] - project.append('tasks', t) + project.append("tasks", t) project.insert(ignore_permissions=True) + def import_notification(): - '''Import notification for task start''' - with open (os.path.join(os.path.dirname(__file__), "tasks/task_alert.json")) as f: + """Import notification for task start""" + with open(os.path.join(os.path.dirname(__file__), "tasks/task_alert.json")) as f: notification = frappe.get_doc(json.loads(f.read())[0]) notification.insert() # trigger the first message! from frappe.email.doctype.notification.notification import trigger_daily_alerts + trigger_daily_alerts() + def test_sample(): - frappe.db.sql('delete from `tabNotification`') - frappe.db.sql('delete from tabProject') - frappe.db.sql('delete from tabTask') - make_projects('Education') + frappe.db.sql("delete from `tabNotification`") + frappe.db.sql("delete from tabProject") + frappe.db.sql("delete from tabTask") + make_projects("Education") import_notification() diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index b126cc9e6a7..686a0109589 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -10,11 +10,11 @@ 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)) + 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: + 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) country_wise_tax = tax_data.get(country) @@ -22,7 +22,7 @@ def setup_taxes_and_charges(company_name: str, country: str): if not country_wise_tax: return - if 'chart_of_accounts' not in country_wise_tax: + 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) @@ -36,39 +36,44 @@ def simple_to_detailed(templates): Example input: { - "France VAT 20%": { - "account_name": "VAT 20%", - "tax_rate": 20, - "default": 1 - }, - "France VAT 10%": { - "account_name": "VAT 10%", - "tax_rate": 10 - } + "France VAT 20%": { + "account_name": "VAT 20%", + "tax_rate": 20, + "default": 1 + }, + "France VAT 10%": { + "account_name": "VAT 10%", + "tax_rate": 10 + } } """ return { - 'chart_of_accounts': { - '*': { - 'item_tax_templates': [{ - 'title': title, - 'taxes': [{ - 'tax_type': { - 'account_name': data.get('account_name'), - 'tax_rate': data.get('tax_rate') - } - }] - } for title, data in templates.items()], - '*': [{ - 'title': title, - 'is_default': data.get('default', 0), - 'taxes': [{ - 'account_head': { - 'account_name': data.get('account_name'), - 'tax_rate': data.get('tax_rate') - } - }] - } for title, data in templates.items()] + "chart_of_accounts": { + "*": { + "item_tax_templates": [ + { + "title": title, + "taxes": [ + {"tax_type": {"account_name": data.get("account_name"), "tax_rate": data.get("tax_rate")}} + ], + } + for title, data in templates.items() + ], + "*": [ + { + "title": title, + "is_default": data.get("default", 0), + "taxes": [ + { + "account_head": { + "account_name": data.get("account_name"), + "tax_rate": data.get("tax_rate"), + } + } + ], + } + for title, data in templates.items() + ], } } } @@ -76,13 +81,13 @@ 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') - 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('*', {}) + coa_name = frappe.db.get_value("Company", company_name, "chart_of_accounts") + 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: @@ -90,11 +95,11 @@ def from_detailed_data(company_name, data): if sales_tax_templates: for template in sales_tax_templates: - make_taxes_and_charges_template(company_name, 'Sales Taxes and Charges Template', template) + make_taxes_and_charges_template(company_name, "Sales Taxes and Charges Template", template) if purchase_tax_templates: for template in purchase_tax_templates: - make_taxes_and_charges_template(company_name, 'Purchase Taxes and Charges Template', template) + make_taxes_and_charges_template(company_name, "Purchase Taxes and Charges Template", template) if item_tax_templates: for template in item_tax_templates: @@ -102,40 +107,45 @@ def from_detailed_data(company_name, data): def update_regional_tax_settings(country, company): - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) + path = frappe.get_app_path("erpnext", "regional", frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: - module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format(frappe.scrub(country)) + module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format( + frappe.scrub(country) + ) frappe.get_attr(module_name)(country, company) except Exception as e: # Log error and ignore if failed to setup regional tax settings frappe.log_error() pass -def make_taxes_and_charges_template(company_name, doctype, template): - template['company'] = company_name - template['doctype'] = doctype - if frappe.db.exists(doctype, {'title': template.get('title'), 'company': company_name}): +def make_taxes_and_charges_template(company_name, doctype, template): + template["company"] = company_name + template["doctype"] = doctype + + if frappe.db.exists(doctype, {"title": template.get("title"), "company": company_name}): return - for tax_row in template.get('taxes'): - account_data = tax_row.get('account_head') + for tax_row in template.get("taxes"): + account_data = tax_row.get("account_head") tax_row_defaults = { - 'category': 'Total', - 'charge_type': 'On Net Total', - 'cost_center': frappe.db.get_value('Company', company_name, 'cost_center') + "category": "Total", + "charge_type": "On Net Total", + "cost_center": frappe.db.get_value("Company", company_name, "cost_center"), } - if doctype == 'Purchase Taxes and Charges Template': - tax_row_defaults['add_deduct_tax'] = 'Add' + 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')) - tax_row_defaults['rate'] = account_data.get('tax_rate') + tax_row_defaults["description"] = "{0} @ {1}".format( + account_data.get("account_name"), account_data.get("tax_rate") + ) + tax_row_defaults["rate"] = account_data.get("tax_rate") account = get_or_create_account(company_name, account_data) - tax_row['account_head'] = account.name + tax_row["account_head"] = account.name # use the default value if nothing other is specified for fieldname, default_value in tax_row_defaults.items(): @@ -151,28 +161,29 @@ def make_taxes_and_charges_template(company_name, doctype, template): doc.insert(ignore_permissions=True) return doc + def make_item_tax_template(company_name, template): """Create an Item Tax Template. This requires a separate method because Item Tax Template is structured differently from Sales and Purchase Tax Templates. """ - doctype = 'Item Tax Template' - template['company'] = company_name - template['doctype'] = doctype + doctype = "Item Tax Template" + template["company"] = company_name + template["doctype"] = doctype - if frappe.db.exists(doctype, {'title': template.get('title'), 'company': company_name}): + if frappe.db.exists(doctype, {"title": template.get("title"), "company": company_name}): return - for tax_row in template.get('taxes'): - account_data = tax_row.get('tax_type') + for tax_row in template.get("taxes"): + account_data = tax_row.get("tax_type") # if tax_type is a dict, search or create the account and get it's name if isinstance(account_data, dict): account = get_or_create_account(company_name, account_data) - tax_row['tax_type'] = account.name - if 'tax_rate' not in tax_row: - tax_row['tax_rate'] = account_data.get('tax_rate') + tax_row["tax_type"] = account.name + if "tax_rate" not in tax_row: + tax_row["tax_rate"] = account_data.get("tax_rate") doc = frappe.get_doc(template) @@ -183,46 +194,47 @@ def make_item_tax_template(company_name, template): doc.insert(ignore_permissions=True) return doc + def make_tax_category(tax_category): - """ Make tax category based on title if not already created """ - doctype = 'Tax Category' - if not frappe.db.exists(doctype, tax_category['title']): - tax_category['doctype'] = doctype + """Make tax category based on title if not already created""" + doctype = "Tax Category" + if not frappe.db.exists(doctype, tax_category["title"]): + tax_category["doctype"] = doctype doc = frappe.get_doc(tax_category) doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True) + def get_or_create_account(company_name, account): """ Check if account already exists. If not, create it. Return a tax account or None. """ - default_root_type = 'Liability' - root_type = account.get('root_type', default_root_type) + default_root_type = "Liability" + root_type = account.get("root_type", default_root_type) - existing_accounts = frappe.get_all('Account', - filters={ - 'company': company_name, - 'root_type': root_type - }, + existing_accounts = frappe.get_all( + "Account", + filters={"company": company_name, "root_type": root_type}, or_filters={ - 'account_name': account.get('account_name'), - 'account_number': account.get('account_number') - }) + "account_name": account.get("account_name"), + "account_number": account.get("account_number"), + }, + ) if existing_accounts: - return frappe.get_doc('Account', existing_accounts[0].name) + return frappe.get_doc("Account", existing_accounts[0].name) tax_group = get_or_create_tax_group(company_name, root_type) - account['doctype'] = 'Account' - account['company'] = company_name - account['parent_account'] = tax_group - account['report_type'] = 'Balance Sheet' - account['account_type'] = 'Tax' - account['root_type'] = root_type - account['is_group'] = 0 + account["doctype"] = "Account" + account["company"] = company_name + account["parent_account"] = tax_group + account["report_type"] = "Balance Sheet" + account["account_type"] = "Tax" + account["root_type"] = root_type + account["is_group"] = 0 doc = frappe.get_doc(account) doc.flags.ignore_links = True @@ -230,50 +242,53 @@ def get_or_create_account(company_name, account): doc.insert(ignore_permissions=True, ignore_mandatory=True) return doc + def get_or_create_tax_group(company_name, root_type): # Look for a group account of type 'Tax' - tax_group_name = frappe.db.get_value('Account', { - 'is_group': 1, - 'root_type': root_type, - 'account_type': 'Tax', - 'company': company_name - }) + tax_group_name = frappe.db.get_value( + "Account", + {"is_group": 1, "root_type": root_type, "account_type": "Tax", "company": company_name}, + ) if tax_group_name: return tax_group_name # Look for a group account named 'Duties and Taxes' or 'Tax Assets' - account_name = _('Duties and Taxes') if root_type == 'Liability' else _('Tax Assets') - tax_group_name = frappe.db.get_value('Account', { - 'is_group': 1, - 'root_type': root_type, - 'account_name': account_name, - 'company': company_name - }) + account_name = _("Duties and Taxes") if root_type == "Liability" else _("Tax Assets") + tax_group_name = frappe.db.get_value( + "Account", + {"is_group": 1, "root_type": root_type, "account_name": account_name, "company": company_name}, + ) if tax_group_name: return tax_group_name # Create a new group account named 'Duties and Taxes' or 'Tax Assets' just # below the root account - root_account = frappe.get_all('Account', { - 'is_group': 1, - 'root_type': root_type, - 'company': company_name, - 'report_type': 'Balance Sheet', - 'parent_account': ('is', 'not set') - }, limit=1)[0] + root_account = frappe.get_all( + "Account", + { + "is_group": 1, + "root_type": root_type, + "company": company_name, + "report_type": "Balance Sheet", + "parent_account": ("is", "not set"), + }, + limit=1, + )[0] - tax_group_account = frappe.get_doc({ - 'doctype': 'Account', - 'company': company_name, - 'is_group': 1, - 'report_type': 'Balance Sheet', - 'root_type': root_type, - 'account_type': 'Tax', - 'account_name': account_name, - 'parent_account': root_account.name - }) + tax_group_account = frappe.get_doc( + { + "doctype": "Account", + "company": company_name, + "is_group": 1, + "report_type": "Balance Sheet", + "root_type": root_type, + "account_type": "Tax", + "account_name": account_name, + "parent_account": root_account.name, + } + ) tax_group_account.flags.ignore_links = True tax_group_account.flags.ignore_validate = True @@ -285,11 +300,11 @@ def get_or_create_tax_group(company_name, root_type): def make_tax_catgory(tax_category): - doctype = 'Tax Category' + doctype = "Tax Category" if isinstance(tax_category, str): - tax_category = {'title': tax_category} + tax_category = {"title": tax_category} - tax_category['doctype'] = doctype - if not frappe.db.exists(doctype, tax_category['title']): + 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/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index c9ed184e04e..f588ae2fd02 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -14,96 +14,66 @@ def get_setup_stages(args=None): if frappe.db.sql("select name from tabCompany"): stages = [ { - 'status': _('Wrapping up'), - 'fail_msg': _('Failed to login'), - 'tasks': [ - { - 'fn': fin, - 'args': args, - 'fail_msg': _("Failed to login") - } - ] + "status": _("Wrapping up"), + "fail_msg": _("Failed to login"), + "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], } ] else: stages = [ { - 'status': _('Installing presets'), - 'fail_msg': _('Failed to install presets'), - 'tasks': [ - { - 'fn': stage_fixtures, - 'args': args, - 'fail_msg': _("Failed to install presets") - } - ] + "status": _("Installing presets"), + "fail_msg": _("Failed to install presets"), + "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], }, { - 'status': _('Setting up company'), - 'fail_msg': _('Failed to setup company'), - 'tasks': [ - { - 'fn': setup_company, - 'args': args, - 'fail_msg': _("Failed to setup company") - } - ] + "status": _("Setting up company"), + "fail_msg": _("Failed to setup company"), + "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], }, { - 'status': _('Setting defaults'), - 'fail_msg': 'Failed to set defaults', - 'tasks': [ - { - 'fn': setup_defaults, - 'args': args, - 'fail_msg': _("Failed to setup defaults") - }, - { - 'fn': stage_four, - 'args': args, - 'fail_msg': _("Failed to create website") - }, - { - 'fn': set_active_domains, - 'args': args, - 'fail_msg': _("Failed to add Domain") - }, - ] + "status": _("Setting defaults"), + "fail_msg": "Failed to set defaults", + "tasks": [ + {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, + {"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")}, + {"fn": set_active_domains, "args": args, "fail_msg": _("Failed to add Domain")}, + ], }, { - 'status': _('Wrapping up'), - 'fail_msg': _('Failed to login'), - 'tasks': [ - { - 'fn': fin, - 'args': args, - 'fail_msg': _("Failed to login") - } - ] - } + "status": _("Wrapping up"), + "fail_msg": _("Failed to login"), + "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], + }, ] return stages + def stage_fixtures(args): - fixtures.install(args.get('country')) + fixtures.install(args.get("country")) + def setup_company(args): fixtures.install_company(args) + def setup_defaults(args): fixtures.install_defaults(frappe._dict(args)) + def stage_four(args): company_setup.create_website(args) company_setup.create_email_digest() company_setup.create_logo(args) + def fin(args): frappe.local.message_log = [] login_as_first_user(args) - make_sample_data(args.get('domains')) + make_sample_data(args.get("domains")) + def make_sample_data(domains): try: @@ -114,6 +84,7 @@ def make_sample_data(domains): frappe.message_log.pop() pass + def login_as_first_user(args): if args.get("email") and hasattr(frappe.local, "login_manager"): frappe.local.login_manager.login_as(args.get("email")) @@ -127,6 +98,7 @@ def setup_complete(args=None): stage_four(args) fin(args) + def set_active_domains(args): - domain_settings = frappe.get_single('Domain Settings') - domain_settings.set_active_domains(args.get('domains')) + domain_settings = frappe.get_single("Domain Settings") + domain_settings.set_active_domains(args.get("domains")) diff --git a/erpnext/setup/setup_wizard/utils.py b/erpnext/setup/setup_wizard/utils.py index afab7e712bb..53f80d68b18 100644 --- a/erpnext/setup/setup_wizard/utils.py +++ b/erpnext/setup/setup_wizard/utils.py @@ -1,4 +1,3 @@ - import json import os @@ -6,8 +5,7 @@ from frappe.desk.page.setup_wizard.setup_wizard import setup_complete def complete(): - with open(os.path.join(os.path.dirname(__file__), - 'data', 'test_mfg.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "data", "test_mfg.json"), "r") as f: data = json.loads(f.read()) setup_complete(data) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 93b6e8d3af3..5a019c68c9d 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -11,40 +11,51 @@ from erpnext import get_default_company def get_root_of(doctype): """Get root element of a DocType with a tree structure""" - result = frappe.db.sql_list("""select name from `tab%s` - where lft=1 and rgt=(select max(rgt) from `tab%s` where docstatus < 2)""" % - (doctype, doctype)) + result = frappe.db.sql_list( + """select name from `tab%s` + where lft=1 and rgt=(select max(rgt) from `tab%s` where docstatus < 2)""" + % (doctype, doctype) + ) return result[0] if result else None + def get_ancestors_of(doctype, name): """Get ancestor elements of a DocType with a tree structure""" lft, rgt = frappe.db.get_value(doctype, name, ["lft", "rgt"]) - result = frappe.db.sql_list("""select name from `tab%s` - where lft<%s and rgt>%s order by lft desc""" % (doctype, "%s", "%s"), (lft, rgt)) + result = frappe.db.sql_list( + """select name from `tab%s` + where lft<%s and rgt>%s order by lft desc""" + % (doctype, "%s", "%s"), + (lft, rgt), + ) return result or [] + def before_tests(): frappe.clear_cache() # complete setup if missing from frappe.desk.page.setup_wizard.setup_wizard import setup_complete + if not frappe.get_list("Company"): - setup_complete({ - "currency" :"USD", - "full_name" :"Test User", - "company_name" :"Wind Power LLC", - "timezone" :"America/New_York", - "company_abbr" :"WP", - "industry" :"Manufacturing", - "country" :"United States", - "fy_start_date" :"2011-01-01", - "fy_end_date" :"2011-12-31", - "language" :"english", - "company_tagline" :"Testing", - "email" :"test@erpnext.com", - "password" :"test", - "chart_of_accounts" : "Standard", - "domains" : ["Manufacturing"], - }) + setup_complete( + { + "currency": "USD", + "full_name": "Test User", + "company_name": "Wind Power LLC", + "timezone": "America/New_York", + "company_abbr": "WP", + "industry": "Manufacturing", + "country": "United States", + "fy_start_date": "2011-01-01", + "fy_end_date": "2011-12-31", + "language": "english", + "company_tagline": "Testing", + "email": "test@erpnext.com", + "password": "test", + "chart_of_accounts": "Standard", + "domains": ["Manufacturing"], + } + ) frappe.db.sql("delete from `tabLeave Allocation`") frappe.db.sql("delete from `tabLeave Application`") @@ -57,6 +68,7 @@ def before_tests(): frappe.db.commit() + @frappe.whitelist() def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=None): if not (from_currency and to_currency): @@ -73,7 +85,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No filters = [ ["date", "<=", get_datetime_str(transaction_date)], ["from_currency", "=", from_currency], - ["to_currency", "=", to_currency] + ["to_currency", "=", to_currency], ] if args == "for_buying": @@ -88,8 +100,8 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No # cksgb 19/09/2016: get last entry in Currency Exchange with from_currency and to_currency. entries = frappe.get_all( - "Currency Exchange", fields=["exchange_rate"], filters=filters, order_by="date desc", - limit=1) + "Currency Exchange", fields=["exchange_rate"], filters=filters, order_by="date desc", limit=1 + ) if entries: return flt(entries[0].exchange_rate) @@ -100,12 +112,11 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if not value: import requests + api_url = "https://api.exchangerate.host/convert" - response = requests.get(api_url, params={ - "date": transaction_date, - "from": from_currency, - "to": to_currency - }) + response = requests.get( + api_url, params={"date": transaction_date, "from": from_currency, "to": to_currency} + ) # expire in 6 hours response.raise_for_status() value = response.json()["result"] @@ -113,20 +124,26 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No return flt(value) except Exception: frappe.log_error(title="Get Exchange Rate") - frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date)) + frappe.msgprint( + _( + "Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually" + ).format(from_currency, to_currency, transaction_date) + ) return 0.0 + def enable_all_roles_and_domains(): - """ enable all roles and domain for testing """ + """enable all roles and domain for testing""" # add all roles to users domains = frappe.get_all("Domain") if not domains: return from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to - frappe.get_single('Domain Settings').set_active_domains(\ - [d.name for d in domains]) - add_all_roles_to('Administrator') + + frappe.get_single("Domain Settings").set_active_domains([d.name for d in domains]) + add_all_roles_to("Administrator") + def set_defaults_for_tests(): from frappe.utils.nestedset import get_root_of @@ -145,12 +162,13 @@ def insert_record(records): doc.insert(ignore_permissions=True) except frappe.DuplicateEntryError as e: # pass DuplicateEntryError and continue - if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: + if e.args and e.args[0] == doc.doctype and e.args[1] == doc.name: # make sure DuplicateEntryError is for the exact same doc and not a related doc pass else: raise + def welcome_email(): site_name = get_default_company() or "ERPNext" title = _("Welcome to {0}").format(site_name) diff --git a/erpnext/startup/__init__.py b/erpnext/startup/__init__.py index d1933d23969..489e24499ca 100644 --- a/erpnext/startup/__init__.py +++ b/erpnext/startup/__init__.py @@ -1,4 +1,3 @@ - # ERPNext - web based ERP (http://erpnext.com) # Copyright (C) 2012 Frappe Technologies Pvt Ltd # @@ -18,7 +17,4 @@ # default settings that can be made for a user. product_name = "ERPNext" -user_defaults = { - "Company": "company", - "Territory": "territory" -} +user_defaults = {"Company": "company", "Territory": "territory"} diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index ed8c878ad4a..52bd979e884 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt" - import frappe from frappe.utils import cint @@ -10,69 +9,72 @@ from frappe.utils import cint def boot_session(bootinfo): """boot session - send website info if guest""" - bootinfo.custom_css = frappe.db.get_value('Style Settings', None, 'custom_css') or '' + bootinfo.custom_css = frappe.db.get_value("Style Settings", None, "custom_css") or "" - if frappe.session['user']!='Guest': + if frappe.session["user"] != "Guest": update_page_info(bootinfo) load_country_and_currency(bootinfo) - bootinfo.sysdefaults.territory = frappe.db.get_single_value('Selling Settings', - 'territory') - bootinfo.sysdefaults.customer_group = frappe.db.get_single_value('Selling Settings', - 'customer_group') - bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings', - 'allow_stale')) - bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('Selling Settings', - 'default_valid_till')) + bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory") + bootinfo.sysdefaults.customer_group = frappe.db.get_single_value( + "Selling Settings", "customer_group" + ) + bootinfo.sysdefaults.allow_stale = cint( + frappe.db.get_single_value("Accounts Settings", "allow_stale") + ) + bootinfo.sysdefaults.quotation_valid_till = cint( + frappe.db.get_single_value("Selling Settings", "default_valid_till") + ) # if no company, show a dialog box to create a new company bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] if not bootinfo.customer_count: - bootinfo.setup_complete = frappe.db.sql("""SELECT `name` + bootinfo.setup_complete = ( + frappe.db.sql( + """SELECT `name` FROM `tabCompany` - LIMIT 1""") and 'Yes' or 'No' + LIMIT 1""" + ) + and "Yes" + or "No" + ) - bootinfo.docs += frappe.db.sql("""select name, default_currency, cost_center, default_selling_terms, default_buying_terms, + bootinfo.docs += frappe.db.sql( + """select name, default_currency, cost_center, default_selling_terms, default_buying_terms, default_letter_head, default_bank_account, enable_perpetual_inventory, country from `tabCompany`""", - as_dict=1, update={"doctype":":Company"}) + as_dict=1, + update={"doctype": ":Company"}, + ) - party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""") + party_account_types = frappe.db.sql( + """ select name, ifnull(account_type, '') from `tabParty Type`""" + ) bootinfo.party_account_types = frappe._dict(party_account_types) + def load_country_and_currency(bootinfo): country = frappe.db.get_default("country") if country and frappe.db.exists("Country", country): bootinfo.docs += [frappe.get_doc("Country", country)] - bootinfo.docs += frappe.db.sql("""select name, fraction, fraction_units, + bootinfo.docs += frappe.db.sql( + """select name, fraction, fraction_units, number_format, smallest_currency_fraction_value, symbol from tabCurrency - where enabled=1""", as_dict=1, update={"doctype":":Currency"}) + where enabled=1""", + as_dict=1, + update={"doctype": ":Currency"}, + ) + def update_page_info(bootinfo): - bootinfo.page_info.update({ - "Chart of Accounts": { - "title": "Chart of Accounts", - "route": "Tree/Account" - }, - "Chart of Cost Centers": { - "title": "Chart of Cost Centers", - "route": "Tree/Cost Center" - }, - "Item Group Tree": { - "title": "Item Group Tree", - "route": "Tree/Item Group" - }, - "Customer Group Tree": { - "title": "Customer Group Tree", - "route": "Tree/Customer Group" - }, - "Territory Tree": { - "title": "Territory Tree", - "route": "Tree/Territory" - }, - "Sales Person Tree": { - "title": "Sales Person Tree", - "route": "Tree/Sales Person" + bootinfo.page_info.update( + { + "Chart of Accounts": {"title": "Chart of Accounts", "route": "Tree/Account"}, + "Chart of Cost Centers": {"title": "Chart of Cost Centers", "route": "Tree/Cost Center"}, + "Item Group Tree": {"title": "Item Group Tree", "route": "Tree/Item Group"}, + "Customer Group Tree": {"title": "Customer Group Tree", "route": "Tree/Customer Group"}, + "Territory Tree": {"title": "Territory Tree", "route": "Tree/Territory"}, + "Sales Person Tree": {"title": "Sales Person Tree", "route": "Tree/Sales Person"}, } - }) + ) diff --git a/erpnext/startup/filters.py b/erpnext/startup/filters.py index c0ccf54d5f6..4fd64312f56 100644 --- a/erpnext/startup/filters.py +++ b/erpnext/startup/filters.py @@ -1,6 +1,3 @@ - - - def get_filters_config(): filters_config = { "fiscal year": { diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index a92abf11130..da7edbf8144 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -1,5 +1,3 @@ - - import frappe from frappe.utils import cint @@ -8,66 +6,66 @@ def get_leaderboards(): leaderboards = { "Customer": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - 'total_qty_sold', - {'fieldname': 'outstanding_amount', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + "total_qty_sold", + {"fieldname": "outstanding_amount", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_customers", - "icon": "customer" + "icon": "customer", }, "Item": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - 'total_qty_sold', - {'fieldname': 'total_purchase_amount', 'fieldtype': 'Currency'}, - 'total_qty_purchased', - 'available_stock_qty', - {'fieldname': 'available_stock_value', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + "total_qty_sold", + {"fieldname": "total_purchase_amount", "fieldtype": "Currency"}, + "total_qty_purchased", + "available_stock_qty", + {"fieldname": "available_stock_value", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_items", - "icon": "stock" + "icon": "stock", }, "Supplier": { "fields": [ - {'fieldname': 'total_purchase_amount', 'fieldtype': 'Currency'}, - 'total_qty_purchased', - {'fieldname': 'outstanding_amount', 'fieldtype': 'Currency'} + {"fieldname": "total_purchase_amount", "fieldtype": "Currency"}, + "total_qty_purchased", + {"fieldname": "outstanding_amount", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_suppliers", - "icon": "buying" + "icon": "buying", }, "Sales Partner": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - {'fieldname': 'total_commission', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + {"fieldname": "total_commission", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_sales_partner", - "icon": "hr" + "icon": "hr", }, "Sales Person": { - "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'} - ], + "fields": [{"fieldname": "total_sales_amount", "fieldtype": "Currency"}], "method": "erpnext.startup.leaderboard.get_all_sales_person", - "icon": "customer" - } + "icon": "customer", + }, } return leaderboards + @frappe.whitelist() -def get_all_customers(date_range, company, field, limit = None): +def get_all_customers(date_range, company, field, limit=None): if field == "outstanding_amount": - filters = [['docstatus', '=', '1'], ['company', '=', company]] + filters = [["docstatus", "=", "1"], ["company", "=", company]] if date_range: date_range = frappe.parse_json(date_range) - filters.append(['posting_date', '>=', 'between', [date_range[0], date_range[1]]]) - return frappe.db.get_all('Sales Invoice', - fields = ['customer as name', 'sum(outstanding_amount) as value'], - filters = filters, - group_by = 'customer', - order_by = 'value desc', - limit = limit + filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]]) + return frappe.db.get_all( + "Sales Invoice", + fields=["customer as name", "sum(outstanding_amount) as value"], + filters=filters, + group_by="customer", + order_by="value desc", + limit=limit, ) else: if field == "total_sales_amount": @@ -75,9 +73,10 @@ def get_all_customers(date_range, company, field, limit = None): elif field == "total_qty_sold": select_field = "sum(so_item.stock_qty)" - date_condition = get_date_condition(date_range, 'so.transaction_date') + date_condition = get_date_condition(date_range, "so.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select so.customer as name, {0} as value FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item ON so.name = so_item.parent @@ -85,17 +84,24 @@ def get_all_customers(date_range, company, field, limit = None): group by so.customer order by value DESC limit %s - """.format(select_field, date_condition), (company, cint(limit)), as_dict=1) + """.format( + select_field, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) + @frappe.whitelist() -def get_all_items(date_range, company, field, limit = None): +def get_all_items(date_range, company, field, limit=None): if field in ("available_stock_qty", "available_stock_value"): - select_field = "sum(actual_qty)" if field=="available_stock_qty" else "sum(stock_value)" - return frappe.db.get_all('Bin', - fields = ['item_code as name', '{0} as value'.format(select_field)], - group_by = 'item_code', - order_by = 'value desc', - limit = limit + select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)" + return frappe.db.get_all( + "Bin", + fields=["item_code as name", "{0} as value".format(select_field)], + group_by="item_code", + order_by="value desc", + limit=limit, ) else: if field == "total_sales_amount": @@ -111,9 +117,10 @@ def get_all_items(date_range, company, field, limit = None): select_field = "sum(order_item.stock_qty)" select_doctype = "Purchase Order" - date_condition = get_date_condition(date_range, 'sales_order.transaction_date') + date_condition = get_date_condition(date_range, "sales_order.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select order_item.item_code as name, {0} as value from `tab{1}` sales_order join `tab{1} Item` as order_item on sales_order.name = order_item.parent @@ -122,21 +129,28 @@ def get_all_items(date_range, company, field, limit = None): group by order_item.item_code order by value desc limit %s - """.format(select_field, select_doctype, date_condition), (company, cint(limit)), as_dict=1) #nosec + """.format( + select_field, select_doctype, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) # nosec + @frappe.whitelist() -def get_all_suppliers(date_range, company, field, limit = None): +def get_all_suppliers(date_range, company, field, limit=None): if field == "outstanding_amount": - filters = [['docstatus', '=', '1'], ['company', '=', company]] + filters = [["docstatus", "=", "1"], ["company", "=", company]] if date_range: date_range = frappe.parse_json(date_range) - filters.append(['posting_date', 'between', [date_range[0], date_range[1]]]) - return frappe.db.get_all('Purchase Invoice', - fields = ['supplier as name', 'sum(outstanding_amount) as value'], - filters = filters, - group_by = 'supplier', - order_by = 'value desc', - limit = limit + filters.append(["posting_date", "between", [date_range[0], date_range[1]]]) + return frappe.db.get_all( + "Purchase Invoice", + fields=["supplier as name", "sum(outstanding_amount) as value"], + filters=filters, + group_by="supplier", + order_by="value desc", + limit=limit, ) else: if field == "total_purchase_amount": @@ -144,9 +158,10 @@ def get_all_suppliers(date_range, company, field, limit = None): elif field == "total_qty_purchased": select_field = "sum(purchase_order_item.stock_qty)" - date_condition = get_date_condition(date_range, 'purchase_order.modified') + date_condition = get_date_condition(date_range, "purchase_order.modified") - return frappe.db.sql(""" + return frappe.db.sql( + """ select purchase_order.supplier as name, {0} as value FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item` as purchase_order_item ON purchase_order.name = purchase_order_item.parent @@ -156,34 +171,45 @@ def get_all_suppliers(date_range, company, field, limit = None): and purchase_order.company = %s group by purchase_order.supplier order by value DESC - limit %s""".format(select_field, date_condition), (company, cint(limit)), as_dict=1) #nosec + limit %s""".format( + select_field, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) # nosec + @frappe.whitelist() -def get_all_sales_partner(date_range, company, field, limit = None): +def get_all_sales_partner(date_range, company, field, limit=None): if field == "total_sales_amount": select_field = "sum(`base_net_total`)" elif field == "total_commission": select_field = "sum(`total_commission`)" - filters = { - 'sales_partner': ['!=', ''], - 'docstatus': 1, - 'company': company - } + filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company} if date_range: date_range = frappe.parse_json(date_range) - filters['transaction_date'] = ['between', [date_range[0], date_range[1]]] + filters["transaction_date"] = ["between", [date_range[0], date_range[1]]] + + return frappe.get_list( + "Sales Order", + fields=[ + "`sales_partner` as name", + "{} as value".format(select_field), + ], + filters=filters, + group_by="sales_partner", + order_by="value DESC", + limit=limit, + ) - return frappe.get_list('Sales Order', fields=[ - '`sales_partner` as name', - '{} as value'.format(select_field), - ], filters=filters, group_by='sales_partner', order_by='value DESC', limit=limit) @frappe.whitelist() -def get_all_sales_person(date_range, company, field = None, limit = 0): - date_condition = get_date_condition(date_range, 'sales_order.transaction_date') +def get_all_sales_person(date_range, company, field=None, limit=0): + date_condition = get_date_condition(date_range, "sales_order.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select sales_team.sales_person as name, sum(sales_order.base_net_total) as value from `tabSales Order` as sales_order join `tabSales Team` as sales_team on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order' @@ -193,10 +219,16 @@ def get_all_sales_person(date_range, company, field = None, limit = 0): group by sales_team.sales_person order by value DESC limit %s - """.format(date_condition=date_condition), (company, cint(limit)), as_dict=1) + """.format( + date_condition=date_condition + ), + (company, cint(limit)), + as_dict=1, + ) + def get_date_condition(date_range, field): - date_condition = '' + date_condition = "" if date_range: date_range = frappe.parse_json(date_range) from_date, to_date = date_range diff --git a/erpnext/startup/notifications.py b/erpnext/startup/notifications.py index 0965ead57c6..76cb91a4634 100644 --- a/erpnext/startup/notifications.py +++ b/erpnext/startup/notifications.py @@ -6,8 +6,8 @@ import frappe def get_notification_config(): - notifications = { "for_doctype": - { + notifications = { + "for_doctype": { "Issue": {"status": "Open"}, "Warranty Claim": {"status": "Open"}, "Task": {"status": ("in", ("Open", "Overdue"))}, @@ -16,66 +16,46 @@ def get_notification_config(): "Contact": {"status": "Open"}, "Opportunity": {"status": "Open"}, "Quotation": {"docstatus": 0}, - "Sales Order": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, + "Sales Order": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, "Journal Entry": {"docstatus": 0}, - "Sales Invoice": { - "outstanding_amount": (">", 0), - "docstatus": ("<", 2) - }, - "Purchase Invoice": { - "outstanding_amount": (">", 0), - "docstatus": ("<", 2) - }, + "Sales Invoice": {"outstanding_amount": (">", 0), "docstatus": ("<", 2)}, + "Purchase Invoice": {"outstanding_amount": (">", 0), "docstatus": ("<", 2)}, "Payment Entry": {"docstatus": 0}, "Leave Application": {"docstatus": 0}, "Expense Claim": {"docstatus": 0}, "Job Applicant": {"status": "Open"}, - "Delivery Note": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, + "Delivery Note": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, "Stock Entry": {"docstatus": 0}, "Material Request": { "docstatus": ("<", 2), "status": ("not in", ("Stopped",)), - "per_ordered": ("<", 100) + "per_ordered": ("<", 100), }, - "Request for Quotation": { "docstatus": 0 }, + "Request for Quotation": {"docstatus": 0}, "Supplier Quotation": {"docstatus": 0}, - "Purchase Order": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, - "Purchase Receipt": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, - "Work Order": { "status": ("in", ("Draft", "Not Started", "In Process")) }, + "Purchase Order": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, + "Purchase Receipt": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, + "Work Order": {"status": ("in", ("Draft", "Not Started", "In Process"))}, "BOM": {"docstatus": 0}, - "Timesheet": {"status": "Draft"}, - "Lab Test": {"docstatus": 0}, "Sample Collection": {"docstatus": 0}, "Patient Appointment": {"status": "Open"}, - "Patient Encounter": {"docstatus": 0} + "Patient Encounter": {"docstatus": 0}, }, - "targets": { "Company": { - "filters" : { "monthly_sales_target": ( ">", 0 ) }, - "target_field" : "monthly_sales_target", - "value_field" : "total_monthly_sales" + "filters": {"monthly_sales_target": (">", 0)}, + "target_field": "monthly_sales_target", + "value_field": "total_monthly_sales", } - } + }, } - doctype = [d for d in notifications.get('for_doctype')] - for doc in frappe.get_all('DocType', - fields= ["name"], filters = {"name": ("not in", doctype), 'is_submittable': 1}): + doctype = [d for d in notifications.get("for_doctype")] + for doc in frappe.get_all( + "DocType", fields=["name"], filters={"name": ("not in", doctype), "is_submittable": 1} + ): notifications["for_doctype"][doc.name] = {"docstatus": 0} return notifications diff --git a/erpnext/startup/report_data_map.py b/erpnext/startup/report_data_map.py index 65b48bf043b..f8c1b6cca07 100644 --- a/erpnext/startup/report_data_map.py +++ b/erpnext/startup/report_data_map.py @@ -6,90 +6,98 @@ # "remember to add indexes!" data_map = { - "Company": { - "columns": ["name"], - "conditions": ["docstatus < 2"] - }, + "Company": {"columns": ["name"], "conditions": ["docstatus < 2"]}, "Fiscal Year": { "columns": ["name", "year_start_date", "year_end_date"], "conditions": ["docstatus < 2"], }, - # Accounts "Account": { - "columns": ["name", "parent_account", "lft", "rgt", "report_type", - "company", "is_group"], + "columns": ["name", "parent_account", "lft", "rgt", "report_type", "company", "is_group"], "conditions": ["docstatus < 2"], "order_by": "lft", "links": { "company": ["Company", "name"], - } - + }, }, "Cost Center": { "columns": ["name", "lft", "rgt"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "GL Entry": { - "columns": ["name", "account", "posting_date", "cost_center", "debit", "credit", - "is_opening", "company", "voucher_type", "voucher_no", "remarks"], + "columns": [ + "name", + "account", + "posting_date", + "cost_center", + "debit", + "credit", + "is_opening", + "company", + "voucher_type", + "voucher_no", + "remarks", + ], "order_by": "posting_date, account", "links": { "account": ["Account", "name"], "company": ["Company", "name"], - "cost_center": ["Cost Center", "name"] - } + "cost_center": ["Cost Center", "name"], + }, }, - # Stock "Item": { - "columns": ["name", "if(item_name=name, '', item_name) as item_name", "description", - "item_group as parent_item_group", "stock_uom", "brand", "valuation_method"], + "columns": [ + "name", + "if(item_name=name, '', item_name) as item_name", + "description", + "item_group as parent_item_group", + "stock_uom", + "brand", + "valuation_method", + ], # "conditions": ["docstatus < 2"], "order_by": "name", - "links": { - "parent_item_group": ["Item Group", "name"], - "brand": ["Brand", "name"] - } + "links": {"parent_item_group": ["Item Group", "name"], "brand": ["Brand", "name"]}, }, "Item Group": { "columns": ["name", "parent_item_group"], # "conditions": ["docstatus < 2"], - "order_by": "lft" - }, - "Brand": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" - }, - "Project": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" - }, - "Warehouse": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "lft", }, + "Brand": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, + "Project": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, + "Warehouse": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, "Stock Ledger Entry": { - "columns": ["name", "posting_date", "posting_time", "item_code", "warehouse", - "actual_qty as qty", "voucher_type", "voucher_no", "project", - "incoming_rate as incoming_rate", "stock_uom", "serial_no", - "qty_after_transaction", "valuation_rate"], + "columns": [ + "name", + "posting_date", + "posting_time", + "item_code", + "warehouse", + "actual_qty as qty", + "voucher_type", + "voucher_no", + "project", + "incoming_rate as incoming_rate", + "stock_uom", + "serial_no", + "qty_after_transaction", + "valuation_rate", + ], "order_by": "posting_date, posting_time, creation", "links": { "item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"], - "project": ["Project", "name"] + "project": ["Project", "name"], }, - "force_index": "posting_sort_index" + "force_index": "posting_sort_index", }, "Serial No": { "columns": ["name", "purchase_rate as incoming_rate"], "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "name", }, "Stock Entry": { "columns": ["name", "purpose"], @@ -97,227 +105,223 @@ data_map = { "order_by": "posting_date, posting_time, name", }, "Material Request Item": { - "columns": ["item.name as name", "item_code", "warehouse", - "(qty - ordered_qty) as qty"], + "columns": ["item.name as name", "item_code", "warehouse", "(qty - ordered_qty) as qty"], "from": "`tabMaterial Request Item` item, `tabMaterial Request` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > ordered_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > ordered_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, "Purchase Order Item": { - "columns": ["item.name as name", "item_code", "warehouse", - "(qty - received_qty)*conversion_factor as qty"], + "columns": [ + "item.name as name", + "item_code", + "warehouse", + "(qty - received_qty)*conversion_factor as qty", + ], "from": "`tabPurchase Order Item` item, `tabPurchase Order` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > received_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > received_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, - "Sales Order Item": { - "columns": ["item.name as name", "item_code", "(qty - delivered_qty)*conversion_factor as qty", "warehouse"], + "columns": [ + "item.name as name", + "item_code", + "(qty - delivered_qty)*conversion_factor as qty", + "warehouse", + ], "from": "`tabSales Order Item` item, `tabSales Order` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > delivered_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > delivered_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, - # Sales "Customer": { - "columns": ["name", "if(customer_name=name, '', customer_name) as customer_name", - "customer_group as parent_customer_group", "territory as parent_territory"], + "columns": [ + "name", + "if(customer_name=name, '', customer_name) as customer_name", + "customer_group as parent_customer_group", + "territory as parent_territory", + ], "conditions": ["docstatus < 2"], "order_by": "name", "links": { "parent_customer_group": ["Customer Group", "name"], "parent_territory": ["Territory", "name"], - } + }, }, "Customer Group": { "columns": ["name", "parent_customer_group"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "Territory": { "columns": ["name", "parent_territory"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "Sales Invoice": { "columns": ["name", "customer", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Sales Invoice Item": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Sales Invoice", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Sales Invoice", "name"], "item_code": ["Item", "name"]}, }, "Sales Order": { "columns": ["name", "customer", "transaction_date as posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "transaction_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Sales Order Item[Sales Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Sales Order", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Sales Order", "name"], "item_code": ["Item", "name"]}, }, "Delivery Note": { "columns": ["name", "customer", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Delivery Note Item[Sales Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Delivery Note", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Delivery Note", "name"], "item_code": ["Item", "name"]}, }, "Supplier": { - "columns": ["name", "if(supplier_name=name, '', supplier_name) as supplier_name", - "supplier_group as parent_supplier_group"], + "columns": [ + "name", + "if(supplier_name=name, '', supplier_name) as supplier_name", + "supplier_group as parent_supplier_group", + ], "conditions": ["docstatus < 2"], "order_by": "name", "links": { "parent_supplier_group": ["Supplier Group", "name"], - } + }, }, "Supplier Group": { "columns": ["name", "parent_supplier_group"], "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "name", }, "Purchase Invoice": { "columns": ["name", "supplier", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Invoice Item": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Invoice", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Invoice", "name"], "item_code": ["Item", "name"]}, }, "Purchase Order": { "columns": ["name", "supplier", "transaction_date as posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Order Item[Purchase Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Order", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Order", "name"], "item_code": ["Item", "name"]}, }, "Purchase Receipt": { "columns": ["name", "supplier", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Receipt Item[Purchase Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Receipt", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Receipt", "name"], "item_code": ["Item", "name"]}, }, # Support "Issue": { - "columns": ["name","status","creation","resolution_date","first_responded_on"], + "columns": ["name", "status", "creation", "resolution_date", "first_responded_on"], "conditions": ["docstatus < 2"], - "order_by": "creation" + "order_by": "creation", }, - # Manufacturing "Work Order": { - "columns": ["name","status","creation","planned_start_date","planned_end_date","status","actual_start_date","actual_end_date", "modified"], + "columns": [ + "name", + "status", + "creation", + "planned_start_date", + "planned_end_date", + "status", + "actual_start_date", + "actual_end_date", + "modified", + ], "conditions": ["docstatus = 1"], - "order_by": "creation" + "order_by": "creation", }, - - #Medical + # Medical "Patient": { - "columns": ["name", "creation", "owner", "if(patient_name=name, '', patient_name) as patient_name"], + "columns": [ + "name", + "creation", + "owner", + "if(patient_name=name, '', patient_name) as patient_name", + ], "conditions": ["docstatus < 2"], "order_by": "name", - "links": { - "owner" : ["User", "name"] - } + "links": {"owner": ["User", "name"]}, }, "Patient Appointment": { - "columns": ["name", "appointment_type", "patient", "practitioner", "appointment_date", "department", "status", "company"], + "columns": [ + "name", + "appointment_type", + "patient", + "practitioner", + "appointment_date", + "department", + "status", + "company", + ], "order_by": "name", "links": { "practitioner": ["Healthcare Practitioner", "name"], - "appointment_type": ["Appointment Type", "name"] - } + "appointment_type": ["Appointment Type", "name"], + }, }, "Healthcare Practitioner": { "columns": ["name", "department"], "order_by": "name", "links": { "department": ["Department", "name"], - } - + }, }, - "Appointment Type": { - "columns": ["name"], - "order_by": "name" - }, - "Medical Department": { - "columns": ["name"], - "order_by": "name" - } + "Appointment Type": {"columns": ["name"], "order_by": "name"}, + "Medical Department": {"columns": ["name"], "order_by": "name"}, } diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py index 9fd1f0e8ce1..45bf012be85 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -1,19 +1,25 @@ - import frappe from frappe import _ install_docs = [ - {"doctype":"Role", "role_name":"Stock Manager", "name":"Stock Manager"}, - {"doctype":"Role", "role_name":"Item Manager", "name":"Item Manager"}, - {"doctype":"Role", "role_name":"Stock User", "name":"Stock User"}, - {"doctype":"Role", "role_name":"Quality Manager", "name":"Quality Manager"}, - {"doctype":"Item Group", "item_group_name":"All Item Groups", "is_group": 1}, - {"doctype":"Item Group", "item_group_name":"Default", - "parent_item_group":"All Item Groups", "is_group": 0}, + {"doctype": "Role", "role_name": "Stock Manager", "name": "Stock Manager"}, + {"doctype": "Role", "role_name": "Item Manager", "name": "Item Manager"}, + {"doctype": "Role", "role_name": "Stock User", "name": "Stock User"}, + {"doctype": "Role", "role_name": "Quality Manager", "name": "Quality Manager"}, + {"doctype": "Item Group", "item_group_name": "All Item Groups", "is_group": 1}, + { + "doctype": "Item Group", + "item_group_name": "Default", + "parent_item_group": "All Item Groups", + "is_group": 0, + }, ] + def get_warehouse_account_map(company=None): - company_warehouse_account_map = company and frappe.flags.setdefault('warehouse_account_map', {}).get(company) + company_warehouse_account_map = company and frappe.flags.setdefault( + "warehouse_account_map", {} + ).get(company) warehouse_account_map = frappe.flags.warehouse_account_map if not warehouse_account_map or not company_warehouse_account_map or frappe.flags.in_test: @@ -21,18 +27,20 @@ def get_warehouse_account_map(company=None): filters = {} if company: - filters['company'] = company - frappe.flags.setdefault('warehouse_account_map', {}).setdefault(company, {}) + filters["company"] = company + frappe.flags.setdefault("warehouse_account_map", {}).setdefault(company, {}) - for d in frappe.get_all('Warehouse', - fields = ["name", "account", "parent_warehouse", "company", "is_group"], - filters = filters, - order_by="lft, rgt"): + for d in frappe.get_all( + "Warehouse", + fields=["name", "account", "parent_warehouse", "company", "is_group"], + filters=filters, + order_by="lft, rgt", + ): if not d.account: d.account = get_warehouse_account(d, warehouse_account) if d.account: - d.account_currency = frappe.db.get_value('Account', d.account, 'account_currency', cache=True) + d.account_currency = frappe.db.get_value("Account", d.account, "account_currency", cache=True) warehouse_account.setdefault(d.name, d) if company: frappe.flags.warehouse_account_map[company] = warehouse_account @@ -41,6 +49,7 @@ def get_warehouse_account_map(company=None): return frappe.flags.warehouse_account_map.get(company) or frappe.flags.warehouse_account_map + def get_warehouse_account(warehouse, warehouse_account=None): account = warehouse.account if not account and warehouse.parent_warehouse: @@ -49,15 +58,20 @@ def get_warehouse_account(warehouse, warehouse_account=None): account = warehouse_account.get(warehouse.parent_warehouse).account else: from frappe.utils.nestedset import rebuild_tree + rebuild_tree("Warehouse", "parent_warehouse") else: - account = frappe.db.sql(""" + account = frappe.db.sql( + """ select account from `tabWarehouse` where lft <= %s and rgt >= %s and company = %s and account is not null and ifnull(account, '') !='' - order by lft desc limit 1""", (warehouse.lft, warehouse.rgt, warehouse.company), as_list=1) + order by lft desc limit 1""", + (warehouse.lft, warehouse.rgt, warehouse.company), + as_list=1, + ) account = account[0][0] if account else None @@ -65,13 +79,18 @@ def get_warehouse_account(warehouse, warehouse_account=None): account = get_company_default_inventory_account(warehouse.company) if not account and warehouse.company: - account = frappe.db.get_value('Account', - {'account_type': 'Stock', 'is_group': 0, 'company': warehouse.company}, 'name') + account = frappe.db.get_value( + "Account", {"account_type": "Stock", "is_group": 0, "company": warehouse.company}, "name" + ) if not account and warehouse.company and not warehouse.is_group: - frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}") - .format(warehouse.name, warehouse.company)) + frappe.throw( + _("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}").format( + warehouse.name, warehouse.company + ) + ) return account + def get_company_default_inventory_account(company): - return frappe.get_cached_value('Company', company, 'default_inventory_account') + return frappe.get_cached_value("Company", company, "default_inventory_account") diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index 9a83372f3e3..e1c535f8d58 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -1,62 +1,75 @@ - import frappe from frappe.model.db_query import DatabaseQuery from frappe.utils import cint, flt @frappe.whitelist() -def get_data(item_code=None, warehouse=None, item_group=None, - start=0, sort_by='actual_qty', sort_order='desc'): - '''Return data to render the item dashboard''' +def get_data( + item_code=None, warehouse=None, item_group=None, start=0, sort_by="actual_qty", sort_order="desc" +): + """Return data to render the item dashboard""" filters = [] if item_code: - filters.append(['item_code', '=', item_code]) + filters.append(["item_code", "=", item_code]) if warehouse: - filters.append(['warehouse', '=', warehouse]) + filters.append(["warehouse", "=", warehouse]) if item_group: lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) - items = frappe.db.sql_list(""" + items = frappe.db.sql_list( + """ select i.name from `tabItem` i where exists(select name from `tabItem Group` where name=i.item_group and lft >=%s and rgt<=%s) - """, (lft, rgt)) - filters.append(['item_code', 'in', items]) + """, + (lft, rgt), + ) + filters.append(["item_code", "in", items]) try: # check if user has any restrictions based on user permissions on warehouse - if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): - filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + if DatabaseQuery("Warehouse", user=frappe.session.user).build_match_conditions(): + filters.append(["warehouse", "in", [w.name for w in frappe.get_list("Warehouse")]]) except frappe.PermissionError: # user does not have access on warehouse return [] - items = frappe.db.get_all('Bin', fields=['item_code', 'warehouse', 'projected_qty', - 'reserved_qty', 'reserved_qty_for_production', 'reserved_qty_for_sub_contract', 'actual_qty', 'valuation_rate'], + items = frappe.db.get_all( + "Bin", + fields=[ + "item_code", + "warehouse", + "projected_qty", + "reserved_qty", + "reserved_qty_for_production", + "reserved_qty_for_sub_contract", + "actual_qty", + "valuation_rate", + ], or_filters={ - 'projected_qty': ['!=', 0], - 'reserved_qty': ['!=', 0], - 'reserved_qty_for_production': ['!=', 0], - 'reserved_qty_for_sub_contract': ['!=', 0], - 'actual_qty': ['!=', 0], + "projected_qty": ["!=", 0], + "reserved_qty": ["!=", 0], + "reserved_qty_for_production": ["!=", 0], + "reserved_qty_for_sub_contract": ["!=", 0], + "actual_qty": ["!=", 0], }, filters=filters, - order_by=sort_by + ' ' + sort_order, + order_by=sort_by + " " + sort_order, limit_start=start, - limit_page_length='21') + limit_page_length="21", + ) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item in items: - item.update({ - 'item_name': frappe.get_cached_value( - "Item", item.item_code, 'item_name'), - 'disable_quick_entry': frappe.get_cached_value( - "Item", item.item_code, 'has_batch_no') - or frappe.get_cached_value( - "Item", item.item_code, 'has_serial_no'), - 'projected_qty': flt(item.projected_qty, precision), - 'reserved_qty': flt(item.reserved_qty, precision), - 'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision), - 'reserved_qty_for_sub_contract': flt(item.reserved_qty_for_sub_contract, precision), - 'actual_qty': flt(item.actual_qty, precision), - }) + item.update( + { + "item_name": frappe.get_cached_value("Item", item.item_code, "item_name"), + "disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no") + or frappe.get_cached_value("Item", item.item_code, "has_serial_no"), + "projected_qty": flt(item.projected_qty, precision), + "reserved_qty": flt(item.reserved_qty, precision), + "reserved_qty_for_production": flt(item.reserved_qty_for_production, precision), + "reserved_qty_for_sub_contract": flt(item.reserved_qty_for_sub_contract, precision), + "actual_qty": flt(item.actual_qty, precision), + } + ) return items diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py index e8b0bea468e..24e0ef11ffa 100644 --- a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.db_query import DatabaseQuery from frappe.utils import flt, nowdate @@ -7,8 +6,15 @@ from erpnext.stock.utils import get_stock_balance @frappe.whitelist() -def get_data(item_code=None, warehouse=None, parent_warehouse=None, - company=None, start=0, sort_by="stock_capacity", sort_order="desc"): +def get_data( + item_code=None, + warehouse=None, + parent_warehouse=None, + company=None, + start=0, + sort_by="stock_capacity", + sort_order="desc", +): """Return data to render the warehouse capacity dashboard.""" filters = get_filters(item_code, warehouse, parent_warehouse, company) @@ -19,51 +25,59 @@ def get_data(item_code=None, warehouse=None, parent_warehouse=None, capacity_data = get_warehouse_capacity_data(filters, start) asc_desc = -1 if sort_order == "desc" else 1 - capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc)) + capacity_data = sorted(capacity_data, key=lambda i: (i[sort_by] * asc_desc)) return capacity_data -def get_filters(item_code=None, warehouse=None, parent_warehouse=None, - company=None): - filters = [['disable', '=', 0]] + +def get_filters(item_code=None, warehouse=None, parent_warehouse=None, company=None): + filters = [["disable", "=", 0]] if item_code: - filters.append(['item_code', '=', item_code]) + filters.append(["item_code", "=", item_code]) if warehouse: - filters.append(['warehouse', '=', warehouse]) + filters.append(["warehouse", "=", warehouse]) if company: - filters.append(['company', '=', company]) + filters.append(["company", "=", company]) if parent_warehouse: lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"]) - warehouses = frappe.db.sql_list(""" + warehouses = frappe.db.sql_list( + """ select name from `tabWarehouse` where lft >=%s and rgt<=%s - """, (lft, rgt)) - filters.append(['warehouse', 'in', warehouses]) + """, + (lft, rgt), + ) + filters.append(["warehouse", "in", warehouses]) return filters + def get_warehouse_filter_based_on_permissions(filters): try: # check if user has any restrictions based on user permissions on warehouse - if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): - filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + if DatabaseQuery("Warehouse", user=frappe.session.user).build_match_conditions(): + filters.append(["warehouse", "in", [w.name for w in frappe.get_list("Warehouse")]]) return False, filters except frappe.PermissionError: # user does not have access on warehouse return True, [] + def get_warehouse_capacity_data(filters, start): - capacity_data = frappe.db.get_all('Putaway Rule', - fields=['item_code', 'warehouse','stock_capacity', 'company'], + capacity_data = frappe.db.get_all( + "Putaway Rule", + fields=["item_code", "warehouse", "stock_capacity", "company"], filters=filters, limit_start=start, - limit_page_length='11' + limit_page_length="11", ) for entry in capacity_data: balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 - entry.update({ - 'actual_qty': balance_qty, - 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) - }) + entry.update( + { + "actual_qty": balance_qty, + "percent_occupied": flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0), + } + ) return capacity_data diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py index d835420b9e2..dbf6cf05e79 100644 --- a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py @@ -11,27 +11,38 @@ from erpnext.stock.utils import get_stock_value_from_bin @frappe.whitelist() @cache_source -def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None, heatmap_year = None): +def get( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, +): labels, datapoints = [], [] filters = frappe.parse_json(filters) - warehouse_filters = [['is_group', '=', 0]] + warehouse_filters = [["is_group", "=", 0]] if filters and filters.get("company"): - warehouse_filters.append(['company', '=', filters.get("company")]) + warehouse_filters.append(["company", "=", filters.get("company")]) - warehouses = frappe.get_list("Warehouse", fields=['name'], filters=warehouse_filters, order_by='name') + warehouses = frappe.get_list( + "Warehouse", fields=["name"], filters=warehouse_filters, order_by="name" + ) for wh in warehouses: balance = get_stock_value_from_bin(warehouse=wh.name) wh["balance"] = balance[0][0] - warehouses = [x for x in warehouses if not (x.get('balance') == None)] + warehouses = [x for x in warehouses if not (x.get("balance") == None)] if not warehouses: return [] - sorted_warehouse_map = sorted(warehouses, key = lambda i: i['balance'], reverse=True) + sorted_warehouse_map = sorted(warehouses, key=lambda i: i["balance"], reverse=True) if len(sorted_warehouse_map) > 10: sorted_warehouse_map = sorted_warehouse_map[:10] @@ -40,11 +51,8 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d labels.append(_(warehouse.get("name"))) datapoints.append(warehouse.get("balance")) - return{ + return { "labels": labels, - "datasets": [{ - "name": _("Stock Value"), - "values": datapoints - }], - "type": "bar" + "datasets": [{"name": _("Stock Value"), "values": datapoints}], + "type": "bar", } diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index c0cb30b5edb..295a65ef8e6 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -24,7 +24,7 @@ def get_name_from_hash(): temp = None while not temp: temp = frappe.generate_hash()[:7].upper() - if frappe.db.exists('Batch', temp): + if frappe.db.exists("Batch", temp): temp = None return temp @@ -35,7 +35,7 @@ def batch_uses_naming_series(): Verify if the Batch is to be named using a naming series :return: bool """ - use_naming_series = cint(frappe.db.get_single_value('Stock Settings', 'use_naming_series')) + use_naming_series = cint(frappe.db.get_single_value("Stock Settings", "use_naming_series")) return bool(use_naming_series) @@ -47,9 +47,9 @@ def _get_batch_prefix(): is set to use naming series. :return: The naming series. """ - naming_series_prefix = frappe.db.get_single_value('Stock Settings', 'naming_series_prefix') + naming_series_prefix = frappe.db.get_single_value("Stock Settings", "naming_series_prefix") if not naming_series_prefix: - naming_series_prefix = 'BATCH-' + naming_series_prefix = "BATCH-" return naming_series_prefix @@ -63,9 +63,9 @@ def _make_naming_series_key(prefix): :return: The derived key. If no prefix is given, an empty string is returned """ if not text_type(prefix): - return '' + return "" else: - return prefix.upper() + '.#####' + return prefix.upper() + ".#####" def get_batch_naming_series(): @@ -75,7 +75,7 @@ def get_batch_naming_series(): Naming series key is in the format [prefix].[#####] :return: The naming series or empty string if not available """ - series = '' + series = "" if batch_uses_naming_series(): prefix = _get_batch_prefix() key = _make_naming_series_key(prefix) @@ -88,8 +88,9 @@ class Batch(Document): def autoname(self): """Generate random ID for batch if not specified""" if not self.batch_id: - create_new_batch, batch_number_series = frappe.db.get_value('Item', self.item, - ['create_new_batch', 'batch_number_series']) + create_new_batch, batch_number_series = frappe.db.get_value( + "Item", self.item, ["create_new_batch", "batch_number_series"] + ) if create_new_batch: if batch_number_series: @@ -99,12 +100,12 @@ class Batch(Document): else: self.batch_id = get_name_from_hash() else: - frappe.throw(_('Batch ID is mandatory'), frappe.MandatoryError) + frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError) self.name = self.batch_id def onload(self): - self.image = frappe.db.get_value('Item', self.item, 'image') + self.image = frappe.db.get_value("Item", self.item, "image") def after_delete(self): revert_series_if_last(get_batch_naming_series(), self.name) @@ -117,16 +118,21 @@ class Batch(Document): frappe.throw(_("The selected item cannot have Batch")) def before_save(self): - has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days']) + has_expiry_date, shelf_life_in_days = frappe.db.get_value( + "Item", self.item, ["has_expiry_date", "shelf_life_in_days"] + ) if not self.expiry_date and has_expiry_date and shelf_life_in_days: self.expiry_date = add_days(self.manufacturing_date, shelf_life_in_days) if has_expiry_date and not self.expiry_date: - frappe.throw(msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.") \ - .format(frappe.bold("Shelf Life in Days"), + frappe.throw( + msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.").format( + frappe.bold("Shelf Life in Days"), get_link_to_form("Item", self.item), - frappe.bold("Batch Expiry Date")), - title=_("Expiry Date Mandatory")) + frappe.bold("Batch Expiry Date"), + ), + title=_("Expiry Date Mandatory"), + ) def get_name_from_naming_series(self): """ @@ -143,9 +149,11 @@ class Batch(Document): @frappe.whitelist() -def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None): +def get_batch_qty( + batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None +): """Returns batch actual qty if warehouse is passed, - or returns dict of qty by warehouse if warehouse is None + or returns dict of qty by warehouse if warehouse is None The user must pass either batch_no or batch_no + warehouse or item_code + warehouse @@ -157,25 +165,41 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No if batch_no and warehouse: cond = "" if posting_date and posting_time: - cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format(posting_date, - posting_time) + cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( + posting_date, posting_time + ) - out = float(frappe.db.sql("""select sum(actual_qty) + out = float( + frappe.db.sql( + """select sum(actual_qty) from `tabStock Ledger Entry` - where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(cond), - (warehouse, batch_no))[0][0] or 0) + where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format( + cond + ), + (warehouse, batch_no), + )[0][0] + or 0 + ) if batch_no and not warehouse: - out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty + out = frappe.db.sql( + """select warehouse, sum(actual_qty) as qty from `tabStock Ledger Entry` where is_cancelled = 0 and batch_no=%s - group by warehouse''', batch_no, as_dict=1) + group by warehouse""", + batch_no, + as_dict=1, + ) if not batch_no and item_code and warehouse: - out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty + out = frappe.db.sql( + """select batch_no, sum(actual_qty) as qty from `tabStock Ledger Entry` where is_cancelled = 0 and item_code = %s and warehouse=%s - group by batch_no''', (item_code, warehouse), as_dict=1) + group by batch_no""", + (item_code, warehouse), + as_dict=1, + ) return out @@ -184,7 +208,9 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No def get_batches_by_oldest(item_code, warehouse): """Returns the oldest batch and qty for the given item_code and warehouse""" batches = get_batch_qty(item_code=item_code, warehouse=warehouse) - batches_dates = [[batch, frappe.get_value('Batch', batch.batch_no, 'expiry_date')] for batch in batches] + batches_dates = [ + [batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches + ] batches_dates.sort(key=lambda tup: tup[1]) return batches_dates @@ -192,33 +218,25 @@ def get_batches_by_oldest(item_code, warehouse): @frappe.whitelist() def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): """Split the batch into a new batch""" - batch = frappe.get_doc(dict(doctype='Batch', item=item_code, batch_id=new_batch_id)).insert() + batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert() - company = frappe.db.get_value('Stock Ledger Entry', dict( - item_code=item_code, - batch_no=batch_no, - warehouse=warehouse - ), ['company']) + company = frappe.db.get_value( + "Stock Ledger Entry", + dict(item_code=item_code, batch_no=batch_no, warehouse=warehouse), + ["company"], + ) - stock_entry = frappe.get_doc(dict( - doctype='Stock Entry', - purpose='Repack', - company=company, - items=[ - dict( - item_code=item_code, - qty=float(qty or 0), - s_warehouse=warehouse, - batch_no=batch_no - ), - dict( - item_code=item_code, - qty=float(qty or 0), - t_warehouse=warehouse, - batch_no=batch.name - ), - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Repack", + company=company, + items=[ + dict(item_code=item_code, qty=float(qty or 0), s_warehouse=warehouse, batch_no=batch_no), + dict(item_code=item_code, qty=float(qty or 0), t_warehouse=warehouse, batch_no=batch.name), + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() stock_entry.submit() @@ -229,15 +247,20 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): """Automatically select `batch_no` for outgoing items in item table""" for d in doc.get(child_table): - qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 + qty = d.get("stock_qty") or d.get("transfer_qty") or d.get("qty") or 0 warehouse = d.get(warehouse_field, None) - if warehouse and qty > 0 and frappe.db.get_value('Item', d.item_code, 'has_batch_no'): + if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"): if not d.batch_no: d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no) else: batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse) if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")): - frappe.throw(_("Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches").format(d.idx, d.batch_no, batch_qty, qty)) + frappe.throw( + _( + "Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches" + ).format(d.idx, d.batch_no, batch_qty, qty) + ) + @frappe.whitelist() def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): @@ -258,7 +281,11 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): break if not batch_no: - frappe.msgprint(_('Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement').format(frappe.bold(item_code))) + frappe.msgprint( + _( + "Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement" + ).format(frappe.bold(item_code)) + ) if throw: raise UnableToSelectBatchError @@ -267,16 +294,14 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - cond = '' - if serial_no and frappe.get_cached_value('Item', item_code, 'has_batch_no'): + + cond = "" + if serial_no and frappe.get_cached_value("Item", item_code, "has_batch_no"): serial_nos = get_serial_nos(serial_no) - batch = frappe.get_all("Serial No", - fields = ["distinct batch_no"], - filters= { - "item_code": item_code, - "warehouse": warehouse, - "name": ("in", serial_nos) - } + batch = frappe.get_all( + "Serial No", + fields=["distinct batch_no"], + filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)}, ) if not batch: @@ -285,9 +310,10 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): if batch and len(batch) > 1: return [] - cond = " and `tabBatch`.name = %s" %(frappe.db.escape(batch[0].batch_no)) + cond = " and `tabBatch`.name = %s" % (frappe.db.escape(batch[0].batch_no)) - return frappe.db.sql(""" + return frappe.db.sql( + """ select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty from `tabBatch` join `tabStock Ledger Entry` ignore index (item_code, warehouse) @@ -297,24 +323,34 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC - """.format(cond), (item_code, warehouse), as_dict=True) + """.format( + cond + ), + (item_code, warehouse), + as_dict=True, + ) + def validate_serial_no_with_batch(serial_nos, item_code): if frappe.get_cached_value("Serial No", serial_nos[0], "item_code") != item_code: - frappe.throw(_("The serial no {0} does not belong to item {1}") - .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code))) + frappe.throw( + _("The serial no {0} does not belong to item {1}").format( + get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code) + ) + ) - serial_no_link = ','.join(get_link_to_form("Serial No", sn) for sn in serial_nos) + serial_no_link = ",".join(get_link_to_form("Serial No", sn) for sn in serial_nos) message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" - frappe.throw(_("There is no batch found against the {0}: {1}") - .format(message, serial_no_link)) + frappe.throw(_("There is no batch found against the {0}: {1}").format(message, serial_no_link)) + def make_batch(args): if frappe.db.get_value("Item", args.item, "has_batch_no"): args.doctype = "Batch" frappe.get_doc(args).insert().name + @frappe.whitelist() def get_pos_reserved_batch_qty(filters): import json @@ -328,16 +364,22 @@ def get_pos_reserved_batch_qty(filters): item = frappe.qb.DocType("POS Invoice Item").as_("item") sum_qty = Sum(item.qty).as_("qty") - reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where( - (p.name == item.parent) & - (p.consolidated_invoice.isnull()) & - (p.status != "Consolidated") & - (p.docstatus == 1) & - (item.docstatus == 1) & - (item.item_code == filters.get('item_code')) & - (item.warehouse == filters.get('warehouse')) & - (item.batch_no == filters.get('batch_no')) - ).run() + reserved_batch_qty = ( + frappe.qb.from_(p) + .from_(item) + .select(sum_qty) + .where( + (p.name == item.parent) + & (p.consolidated_invoice.isnull()) + & (p.status != "Consolidated") + & (p.docstatus == 1) + & (item.docstatus == 1) + & (item.item_code == filters.get("item_code")) + & (item.warehouse == filters.get("warehouse")) + & (item.batch_no == filters.get("batch_no")) + ) + .run() + ) flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) return flt_reserved_batch_qty diff --git a/erpnext/stock/doctype/batch/batch_dashboard.py b/erpnext/stock/doctype/batch/batch_dashboard.py index afa0fca99a0..84b64f36f40 100644 --- a/erpnext/stock/doctype/batch/batch_dashboard.py +++ b/erpnext/stock/doctype/batch/batch_dashboard.py @@ -1,26 +1,13 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'batch_no', - 'transactions': [ - { - 'label': _('Buy'), - 'items': ['Purchase Invoice', 'Purchase Receipt'] - }, - { - 'label': _('Sell'), - 'items': ['Sales Invoice', 'Delivery Note'] - }, - { - 'label': _('Move'), - 'items': ['Stock Entry'] - }, - { - 'label': _('Quality'), - 'items': ['Quality Inspection'] - } - ] + "fieldname": "batch_no", + "transactions": [ + {"label": _("Buy"), "items": ["Purchase Invoice", "Purchase Receipt"]}, + {"label": _("Sell"), "items": ["Sales Invoice", "Delivery Note"]}, + {"label": _("Move"), "items": ["Stock Entry"]}, + {"label": _("Quality"), "items": ["Quality Inspection"]}, + ], } diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 0a663c2a188..c1190c8fc57 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -3,144 +3,137 @@ import frappe from frappe.exceptions import ValidationError +from frappe.tests.utils import FrappeTestCase from frappe.utils import cint, flt from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty from erpnext.stock.get_item_details import get_item_details -from erpnext.tests.utils import ERPNextTestCase -class TestBatch(ERPNextTestCase): +class TestBatch(FrappeTestCase): def test_item_has_batch_enabled(self): - self.assertRaises(ValidationError, frappe.get_doc({ - "doctype": "Batch", - "name": "_test Batch", - "item": "_Test Item" - }).save) + self.assertRaises( + ValidationError, + frappe.get_doc({"doctype": "Batch", "name": "_test Batch", "item": "_Test Item"}).save, + ) @classmethod def make_batch_item(cls, item_name): from erpnext.stock.doctype.item.test_item import make_item + if not frappe.db.exists(item_name): - return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1)) + return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) - def test_purchase_receipt(self, batch_qty = 100): - '''Test automated batch creation from Purchase Receipt''' - self.make_batch_item('ITEM-BATCH-1') + def test_purchase_receipt(self, batch_qty=100): + """Test automated batch creation from Purchase Receipt""" + self.make_batch_item("ITEM-BATCH-1") - receipt = frappe.get_doc(dict( - doctype='Purchase Receipt', - supplier='_Test Supplier', - company='_Test Company', - items=[ - dict( - item_code='ITEM-BATCH-1', - qty=batch_qty, - rate=10, - warehouse= 'Stores - _TC' - ) - ] - )).insert() + receipt = frappe.get_doc( + dict( + doctype="Purchase Receipt", + supplier="_Test Supplier", + company="_Test Company", + items=[dict(item_code="ITEM-BATCH-1", qty=batch_qty, rate=10, warehouse="Stores - _TC")], + ) + ).insert() receipt.submit() self.assertTrue(receipt.items[0].batch_no) - self.assertEqual(get_batch_qty(receipt.items[0].batch_no, - receipt.items[0].warehouse), batch_qty) + self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), batch_qty) return receipt def test_stock_entry_incoming(self): - '''Test batch creation via Stock Entry (Work Order)''' + """Test batch creation via Stock Entry (Work Order)""" - self.make_batch_item('ITEM-BATCH-1') + self.make_batch_item("ITEM-BATCH-1") - stock_entry = frappe.get_doc(dict( - doctype = 'Stock Entry', - purpose = 'Material Receipt', - company = '_Test Company', - items = [ - dict( - item_code = 'ITEM-BATCH-1', - qty = 90, - t_warehouse = '_Test Warehouse - _TC', - cost_center = 'Main - _TC', - rate = 10 - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Receipt", + company="_Test Company", + items=[ + dict( + item_code="ITEM-BATCH-1", + qty=90, + t_warehouse="_Test Warehouse - _TC", + cost_center="Main - _TC", + rate=10, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() stock_entry.submit() self.assertTrue(stock_entry.items[0].batch_no) - self.assertEqual(get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90) + self.assertEqual( + get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90 + ) def test_delivery_note(self): - '''Test automatic batch selection for outgoing items''' + """Test automatic batch selection for outgoing items""" batch_qty = 15 receipt = self.test_purchase_receipt(batch_qty) - item_code = 'ITEM-BATCH-1' + item_code = "ITEM-BATCH-1" - delivery_note = frappe.get_doc(dict( - doctype='Delivery Note', - customer='_Test Customer', - company=receipt.company, - items=[ - dict( - item_code=item_code, - qty=batch_qty, - rate=10, - warehouse=receipt.items[0].warehouse - ) - ] - )).insert() + delivery_note = frappe.get_doc( + dict( + doctype="Delivery Note", + customer="_Test Customer", + company=receipt.company, + items=[ + dict(item_code=item_code, qty=batch_qty, rate=10, warehouse=receipt.items[0].warehouse) + ], + ) + ).insert() delivery_note.submit() # shipped from FEFO batch self.assertEqual( - delivery_note.items[0].batch_no, - get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) + delivery_note.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) ) def test_delivery_note_fail(self): - '''Test automatic batch selection for outgoing items''' + """Test automatic batch selection for outgoing items""" receipt = self.test_purchase_receipt(100) - delivery_note = frappe.get_doc(dict( - doctype = 'Delivery Note', - customer = '_Test Customer', - company = receipt.company, - items = [ - dict( - item_code = 'ITEM-BATCH-1', - qty = 5000, - rate = 10, - warehouse = receipt.items[0].warehouse - ) - ] - )) + delivery_note = frappe.get_doc( + dict( + doctype="Delivery Note", + customer="_Test Customer", + company=receipt.company, + items=[ + dict(item_code="ITEM-BATCH-1", qty=5000, rate=10, warehouse=receipt.items[0].warehouse) + ], + ) + ) self.assertRaises(UnableToSelectBatchError, delivery_note.insert) def test_stock_entry_outgoing(self): - '''Test automatic batch selection for outgoing stock entry''' + """Test automatic batch selection for outgoing stock entry""" batch_qty = 16 receipt = self.test_purchase_receipt(batch_qty) - item_code = 'ITEM-BATCH-1' + item_code = "ITEM-BATCH-1" - stock_entry = frappe.get_doc(dict( - doctype='Stock Entry', - purpose='Material Issue', - company=receipt.company, - items=[ - dict( - item_code=item_code, - qty=batch_qty, - s_warehouse=receipt.items[0].warehouse, - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Issue", + company=receipt.company, + items=[ + dict( + item_code=item_code, + qty=batch_qty, + s_warehouse=receipt.items[0].warehouse, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() @@ -148,35 +141,38 @@ class TestBatch(ERPNextTestCase): # assert same batch is selected self.assertEqual( - stock_entry.items[0].batch_no, - get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) + stock_entry.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) ) def test_batch_split(self): - '''Test batch splitting''' + """Test batch splitting""" receipt = self.test_purchase_receipt() from erpnext.stock.doctype.batch.batch import split_batch - new_batch = split_batch(receipt.items[0].batch_no, 'ITEM-BATCH-1', receipt.items[0].warehouse, 22) + new_batch = split_batch( + receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22 + ) self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 78) self.assertEqual(get_batch_qty(new_batch, receipt.items[0].warehouse), 22) def test_get_batch_qty(self): - '''Test getting batch quantities by batch_numbers, item_code or warehouse''' - self.make_batch_item('ITEM-BATCH-2') - self.make_new_batch_and_entry('ITEM-BATCH-2', 'batch a', '_Test Warehouse - _TC') - self.make_new_batch_and_entry('ITEM-BATCH-2', 'batch b', '_Test Warehouse - _TC') + """Test getting batch quantities by batch_numbers, item_code or warehouse""" + self.make_batch_item("ITEM-BATCH-2") + self.make_new_batch_and_entry("ITEM-BATCH-2", "batch a", "_Test Warehouse - _TC") + self.make_new_batch_and_entry("ITEM-BATCH-2", "batch b", "_Test Warehouse - _TC") - self.assertEqual(get_batch_qty(item_code = 'ITEM-BATCH-2', warehouse = '_Test Warehouse - _TC'), - [{'batch_no': u'batch a', 'qty': 90.0}, {'batch_no': u'batch b', 'qty': 90.0}]) + self.assertEqual( + get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"), + [{"batch_no": "batch a", "qty": 90.0}, {"batch_no": "batch b", "qty": 90.0}], + ) - self.assertEqual(get_batch_qty('batch a', '_Test Warehouse - _TC'), 90) + self.assertEqual(get_batch_qty("batch a", "_Test Warehouse - _TC"), 90) def test_total_batch_qty(self): - self.make_batch_item('ITEM-BATCH-3') + self.make_batch_item("ITEM-BATCH-3") existing_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) - stock_entry = self.make_new_batch_and_entry('ITEM-BATCH-3', 'B100', '_Test Warehouse - _TC') + stock_entry = self.make_new_batch_and_entry("ITEM-BATCH-3", "B100", "_Test Warehouse - _TC") current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) self.assertEqual(current_batch_qty, existing_batch_qty + 90) @@ -187,32 +183,32 @@ class TestBatch(ERPNextTestCase): @classmethod def make_new_batch_and_entry(cls, item_name, batch_name, warehouse): - '''Make a new stock entry for given target warehouse and batch name of item''' + """Make a new stock entry for given target warehouse and batch name of item""" if not frappe.db.exists("Batch", batch_name): - batch = frappe.get_doc(dict( - doctype = 'Batch', - item = item_name, - batch_id = batch_name - )).insert(ignore_permissions=True) + batch = frappe.get_doc(dict(doctype="Batch", item=item_name, batch_id=batch_name)).insert( + ignore_permissions=True + ) batch.save() - stock_entry = frappe.get_doc(dict( - doctype = 'Stock Entry', - purpose = 'Material Receipt', - company = '_Test Company', - items = [ - dict( - item_code = item_name, - qty = 90, - t_warehouse = warehouse, - cost_center = 'Main - _TC', - rate = 10, - batch_no = batch_name, - allow_zero_valuation_rate = 1 - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Receipt", + company="_Test Company", + items=[ + dict( + item_code=item_name, + qty=90, + t_warehouse=warehouse, + cost_center="Main - _TC", + rate=10, + batch_no=batch_name, + allow_zero_valuation_rate=1, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() @@ -221,28 +217,28 @@ class TestBatch(ERPNextTestCase): return stock_entry def test_batch_name_with_naming_series(self): - stock_settings = frappe.get_single('Stock Settings') + stock_settings = frappe.get_single("Stock Settings") use_naming_series = cint(stock_settings.use_naming_series) if not use_naming_series: - frappe.set_value('Stock Settings', 'Stock Settings', 'use_naming_series', 1) + frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 1) - batch = self.make_new_batch('_Test Stock Item For Batch Test1') + batch = self.make_new_batch("_Test Stock Item For Batch Test1") batch_name = batch.name - self.assertTrue(batch_name.startswith('BATCH-')) + self.assertTrue(batch_name.startswith("BATCH-")) batch.delete() - batch = self.make_new_batch('_Test Stock Item For Batch Test2') + batch = self.make_new_batch("_Test Stock Item For Batch Test2") self.assertEqual(batch_name, batch.name) # reset Stock Settings if not use_naming_series: - frappe.set_value('Stock Settings', 'Stock Settings', 'use_naming_series', 0) + frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0) def make_new_batch(self, item_name, batch_id=None, do_not_insert=0): - batch = frappe.new_doc('Batch') + batch = frappe.new_doc("Batch") item = self.make_batch_item(item_name) batch.item = item.name @@ -255,57 +251,67 @@ class TestBatch(ERPNextTestCase): return batch def test_batch_wise_item_price(self): - if not frappe.db.get_value('Item', '_Test Batch Price Item'): - frappe.get_doc({ - 'doctype': 'Item', - 'is_stock_item': 1, - 'item_code': '_Test Batch Price Item', - 'item_group': 'Products', - 'has_batch_no': 1, - 'create_new_batch': 1 - }).insert(ignore_permissions=True) + if not frappe.db.get_value("Item", "_Test Batch Price Item"): + frappe.get_doc( + { + "doctype": "Item", + "is_stock_item": 1, + "item_code": "_Test Batch Price Item", + "item_group": "Products", + "has_batch_no": 1, + "create_new_batch": 1, + } + ).insert(ignore_permissions=True) - batch1 = create_batch('_Test Batch Price Item', 200, 1) - batch2 = create_batch('_Test Batch Price Item', 300, 1) - batch3 = create_batch('_Test Batch Price Item', 400, 0) + batch1 = create_batch("_Test Batch Price Item", 200, 1) + batch2 = create_batch("_Test Batch Price Item", 300, 1) + batch3 = create_batch("_Test Batch Price Item", 400, 0) company = "_Test Company with perpetual inventory" - currency = frappe.get_cached_value("Company", company, "default_currency") + currency = frappe.get_cached_value("Company", company, "default_currency") - args = frappe._dict({ - "item_code": "_Test Batch Price Item", - "company": company, - "price_list": "_Test Price List", - "currency": currency, - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "customer": "_Test Customer", - "name": None - }) + args = frappe._dict( + { + "item_code": "_Test Batch Price Item", + "company": company, + "price_list": "_Test Price List", + "currency": currency, + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "customer": "_Test Customer", + "name": None, + } + ) - #test price for batch1 - args.update({'batch_no': batch1}) + # test price for batch1 + args.update({"batch_no": batch1}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 200) + self.assertEqual(details.get("price_list_rate"), 200) - #test price for batch2 - args.update({'batch_no': batch2}) + # test price for batch2 + args.update({"batch_no": batch2}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 300) + self.assertEqual(details.get("price_list_rate"), 300) - #test price for batch3 - args.update({'batch_no': batch3}) + # test price for batch3 + args.update({"batch_no": batch3}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 400) + self.assertEqual(details.get("price_list_rate"), 400) + def create_batch(item_code, rate, create_item_price_for_batch): - pi = make_purchase_invoice(company="_Test Company", - warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1, - expense_account ="_Test Account Cost for Goods Sold - _TC", item_code=item_code) + pi = make_purchase_invoice( + company="_Test Company", + warehouse="Stores - _TC", + cost_center="Main - _TC", + update_stock=1, + expense_account="_Test Account Cost for Goods Sold - _TC", + item_code=item_code, + ) - batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) + batch = frappe.db.get_value("Batch", {"item": item_code, "reference_name": pi.name}) if not create_item_price_for_batch: create_price_list_for_batch(item_code, None, rate) @@ -314,24 +320,30 @@ def create_batch(item_code, rate, create_item_price_for_batch): return batch + def create_price_list_for_batch(item_code, batch, rate): - frappe.get_doc({ - 'doctype': 'Item Price', - 'item_code': '_Test Batch Price Item', - 'price_list': '_Test Price List', - 'batch_no': batch, - 'price_list_rate': rate - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": "_Test Batch Price Item", + "price_list": "_Test Price List", + "batch_no": batch, + "price_list_rate": rate, + } + ).insert() + def make_new_batch(**args): args = frappe._dict(args) try: - batch = frappe.get_doc({ - "doctype": "Batch", - "batch_id": args.batch_id, - "item": args.item_code, - }).insert() + batch = frappe.get_doc( + { + "doctype": "Batch", + "batch_id": args.batch_id, + "item": args.item_code, + } + ).insert() except frappe.DuplicateEntryError: batch = frappe.get_doc("Batch", args.batch_id) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index b2ec15690c2..573203a47a8 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -4,44 +4,59 @@ import frappe from frappe.model.document import Document +from frappe.query_builder import Order +from frappe.query_builder.functions import CombineDatetime from frappe.utils import flt class Bin(Document): def before_save(self): if self.get("__islocal") or not self.stock_uom: - self.stock_uom = frappe.get_cached_value('Item', self.item_code, 'stock_uom') + self.stock_uom = frappe.get_cached_value("Item", self.item_code, "stock_uom") self.set_projected_qty() def set_projected_qty(self): - self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty) - + flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty) - - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract)) + self.projected_qty = ( + flt(self.actual_qty) + + flt(self.ordered_qty) + + flt(self.indented_qty) + + flt(self.planned_qty) + - flt(self.reserved_qty) + - flt(self.reserved_qty_for_production) + - flt(self.reserved_qty_for_sub_contract) + ) def get_first_sle(self): - sle = frappe.db.sql(""" + sle = frappe.db.sql( + """ select * from `tabStock Ledger Entry` where item_code = %s and warehouse = %s order by timestamp(posting_date, posting_time) asc, creation asc limit 1 - """, (self.item_code, self.warehouse), as_dict=1) + """, + (self.item_code, self.warehouse), + as_dict=1, + ) return sle and sle[0] or None def update_reserved_qty_for_production(self): - '''Update qty reserved for production from Production Item tables - in open work orders''' + """Update qty reserved for production from Production Item tables + in open work orders""" from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production - self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) + self.reserved_qty_for_production = get_reserved_qty_for_production( + self.item_code, self.warehouse + ) self.set_projected_qty() - self.db_set('reserved_qty_for_production', flt(self.reserved_qty_for_production)) - self.db_set('projected_qty', self.projected_qty) + self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) + self.db_set("projected_qty", self.projected_qty) def update_reserved_qty_for_sub_contracting(self): - #reserved qty - reserved_qty_for_sub_contract = frappe.db.sql(''' + # reserved qty + reserved_qty_for_sub_contract = frappe.db.sql( + """ select ifnull(sum(itemsup.required_qty),0) from `tabPurchase Order` po, `tabPurchase Order Item Supplied` itemsup where @@ -51,10 +66,13 @@ class Bin(Document): and po.is_subcontracted = 'Yes' and po.status != 'Closed' and po.per_received < 100 - and itemsup.reserve_warehouse = %s''', (self.item_code, self.warehouse))[0][0] + and itemsup.reserve_warehouse = %s""", + (self.item_code, self.warehouse), + )[0][0] - #Get Transferred Entries - materials_transferred = frappe.db.sql(""" + # Get Transferred Entries + materials_transferred = frappe.db.sql( + """ select ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0) from @@ -70,16 +88,19 @@ class Bin(Document): and po.is_subcontracted = 'Yes' and po.status != 'Closed' and po.per_received < 100 - """, {'item': self.item_code})[0][0] + """, + {"item": self.item_code}, + )[0][0] if reserved_qty_for_sub_contract > materials_transferred: reserved_qty_for_sub_contract = reserved_qty_for_sub_contract - materials_transferred else: reserved_qty_for_sub_contract = 0 - self.db_set('reserved_qty_for_sub_contract', reserved_qty_for_sub_contract) + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract) self.set_projected_qty() - self.db_set('projected_qty', self.projected_qty) + self.db_set("projected_qty", self.projected_qty) + def on_doctype_update(): frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse") @@ -92,46 +113,71 @@ def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_vou repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) update_qty(bin_name, args) + def get_bin_details(bin_name): - return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', - 'reserved_qty', 'indented_qty', 'planned_qty', 'reserved_qty_for_production', - 'reserved_qty_for_sub_contract'], as_dict=1) + return frappe.db.get_value( + "Bin", + bin_name, + [ + "actual_qty", + "ordered_qty", + "reserved_qty", + "indented_qty", + "planned_qty", + "reserved_qty_for_production", + "reserved_qty_for_sub_contract", + ], + as_dict=1, + ) + def update_qty(bin_name, args): from erpnext.controllers.stock_controller import future_sle_exists bin_details = get_bin_details(bin_name) # actual qty is already updated by processing current voucher - actual_qty = bin_details.actual_qty + actual_qty = bin_details.actual_qty or 0.0 + sle = frappe.qb.DocType("Stock Ledger Entry") # actual qty is not up to date in case of backdated transaction if future_sle_exists(args): - actual_qty = frappe.db.get_value("Stock Ledger Entry", - filters={ - "item_code": args.get("item_code"), - "warehouse": args.get("warehouse"), - "is_cancelled": 0 - }, - fieldname="qty_after_transaction", - order_by="posting_date desc, posting_time desc, creation desc", - ) or 0.0 + last_sle_qty = ( + frappe.qb.from_(sle) + .select(sle.qty_after_transaction) + .where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse"))) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc) + .orderby(sle.creation, order=Order.desc) + .run() + ) + + if last_sle_qty: + actual_qty = last_sle_qty[0][0] ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty")) indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty")) planned_qty = flt(bin_details.planned_qty) + flt(args.get("planned_qty")) - # compute projected qty - projected_qty = (flt(actual_qty) + flt(ordered_qty) - + flt(indented_qty) + flt(planned_qty) - flt(reserved_qty) - - flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract)) + projected_qty = ( + flt(actual_qty) + + flt(ordered_qty) + + flt(indented_qty) + + flt(planned_qty) + - flt(reserved_qty) + - flt(bin_details.reserved_qty_for_production) + - flt(bin_details.reserved_qty_for_sub_contract) + ) - frappe.db.set_value('Bin', bin_name, { - 'actual_qty': actual_qty, - 'ordered_qty': ordered_qty, - 'reserved_qty': reserved_qty, - 'indented_qty': indented_qty, - 'planned_qty': planned_qty, - 'projected_qty': projected_qty - }) + frappe.db.set_value( + "Bin", + bin_name, + { + "actual_qty": actual_qty, + "ordered_qty": ordered_qty, + "reserved_qty": reserved_qty, + "indented_qty": indented_qty, + "planned_qty": planned_qty, + "projected_qty": projected_qty, + }, + ) diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index 250126c6b98..b79dee81e21 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -2,17 +2,15 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.utils import _create_bin -from erpnext.tests.utils import ERPNextTestCase - - -class TestBin(ERPNextTestCase): +class TestBin(FrappeTestCase): def test_concurrent_inserts(self): - """ Ensure no duplicates are possible in case of concurrent inserts""" + """Ensure no duplicates are possible in case of concurrent inserts""" item_code = "_TestConcurrentBin" make_item(item_code) warehouse = "_Test Warehouse - _TC" diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 55a4c956a67..e3222bc8850 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -23,6 +23,10 @@ "is_return", "issue_credit_note", "return_against", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "customer_po_details", "po_no", "column_break_17", @@ -115,7 +119,6 @@ "driver_name", "lr_date", "more_info", - "project", "campaign", "source", "column_break5", @@ -1309,13 +1312,29 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-10-09 14:29:13.428984", + "modified": "2022-04-26 14:48:08.781837", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", @@ -1380,6 +1399,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 00836fc8157..7205758a8e4 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -14,74 +14,77 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no -from erpnext.stock.utils import calculate_mapped_packed_items_return -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class DeliveryNote(SellingController): def __init__(self, *args, **kwargs): super(DeliveryNote, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_field': 'delivered_qty', - 'target_parent_dt': 'Sales Order', - 'target_parent_field': 'per_delivered', - 'target_ref_field': 'qty', - 'source_field': 'qty', - 'percent_join_field': 'against_sales_order', - 'status_field': 'delivery_status', - 'keyword': 'Delivered', - 'second_source_dt': 'Sales Invoice Item', - 'second_source_field': 'qty', - 'second_join_field': 'so_detail', - 'overflow_type': 'delivery', - 'second_source_extra_cond': """ and exists(select name from `tabSales Invoice` - where name=`tabSales Invoice Item`.parent and update_stock = 1)""" - }, - { - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Invoice Item', - 'join_field': 'si_detail', - 'target_field': 'delivered_qty', - 'target_parent_dt': 'Sales Invoice', - 'target_ref_field': 'qty', - 'source_field': 'qty', - 'percent_join_field': 'against_sales_invoice', - 'overflow_type': 'delivery', - 'no_allowance': 1 - }] - if cint(self.is_return): - self.status_updater.extend([{ - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Sales Order', - 'source_field': '-1 * qty', - 'second_source_dt': 'Sales Invoice Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'so_detail', - 'extra_cond': """ and exists (select name from `tabDelivery Note` - where name=`tabDelivery Note Item`.parent and is_return=1)""", - 'second_source_extra_cond': """ and exists (select name from `tabSales Invoice` - where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)""" + self.status_updater = [ + { + "source_dt": "Delivery Note Item", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_field": "delivered_qty", + "target_parent_dt": "Sales Order", + "target_parent_field": "per_delivered", + "target_ref_field": "qty", + "source_field": "qty", + "percent_join_field": "against_sales_order", + "status_field": "delivery_status", + "keyword": "Delivered", + "second_source_dt": "Sales Invoice Item", + "second_source_field": "qty", + "second_join_field": "so_detail", + "overflow_type": "delivery", + "second_source_extra_cond": """ and exists(select name from `tabSales Invoice` + where name=`tabSales Invoice Item`.parent and update_stock = 1)""", }, { - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Delivery Note Item', - 'join_field': 'dn_detail', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Delivery Note', - 'target_parent_field': 'per_returned', - 'target_ref_field': 'stock_qty', - 'source_field': '-1 * stock_qty', - 'percent_join_field_parent': 'return_against' - } - ]) + "source_dt": "Delivery Note Item", + "target_dt": "Sales Invoice Item", + "join_field": "si_detail", + "target_field": "delivered_qty", + "target_parent_dt": "Sales Invoice", + "target_ref_field": "qty", + "source_field": "qty", + "percent_join_field": "against_sales_invoice", + "overflow_type": "delivery", + "no_allowance": 1, + }, + ] + if cint(self.is_return): + self.status_updater.extend( + [ + { + "source_dt": "Delivery Note Item", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_field": "returned_qty", + "target_parent_dt": "Sales Order", + "source_field": "-1 * qty", + "second_source_dt": "Sales Invoice Item", + "second_source_field": "-1 * qty", + "second_join_field": "so_detail", + "extra_cond": """ and exists (select name from `tabDelivery Note` + where name=`tabDelivery Note Item`.parent and is_return=1)""", + "second_source_extra_cond": """ and exists (select name from `tabSales Invoice` + where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)""", + }, + { + "source_dt": "Delivery Note Item", + "target_dt": "Delivery Note Item", + "join_field": "dn_detail", + "target_field": "returned_qty", + "target_parent_dt": "Delivery Note", + "target_parent_field": "per_returned", + "target_ref_field": "stock_qty", + "source_field": "-1 * stock_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) def before_print(self, settings=None): def toggle_print_hide(meta, fieldname): @@ -94,7 +97,7 @@ class DeliveryNote(SellingController): item_meta = frappe.get_meta("Delivery Note Item") print_hide_fields = { "parent": ["grand_total", "rounded_total", "in_words", "currency", "total", "taxes"], - "items": ["rate", "amount", "discount_amount", "price_list_rate", "discount_percentage"] + "items": ["rate", "amount", "discount_amount", "price_list_rate", "discount_percentage"], } for key, fieldname in print_hide_fields.items(): @@ -104,16 +107,19 @@ class DeliveryNote(SellingController): super(DeliveryNote, self).before_print(settings) def set_actual_qty(self): - for d in self.get('items'): + for d in self.get("items"): if d.item_code and d.warehouse: - actual_qty = frappe.db.sql("""select actual_qty from `tabBin` - where item_code = %s and warehouse = %s""", (d.item_code, d.warehouse)) + actual_qty = frappe.db.sql( + """select actual_qty from `tabBin` + where item_code = %s and warehouse = %s""", + (d.item_code, d.warehouse), + ) d.actual_qty = actual_qty and flt(actual_qty[0][0]) or 0 def so_required(self): """check in manage account if sales order required or not""" - if frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes': - for d in self.get('items'): + if frappe.db.get_value("Selling Settings", None, "so_required") == "Yes": + for d in self.get("items"): if not d.against_sales_order: frappe.throw(_("Sales Order required for Item {0}").format(d.item_code)) @@ -129,76 +135,92 @@ class DeliveryNote(SellingController): self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() - # Keeps mapped packed_items in case product bundle is updated. - if self.is_return and self.return_against: - calculate_mapped_packed_items_return(self) - else: - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self) + from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - if self._action != 'submit' and not self.is_return: - set_batch_nos(self, 'warehouse', throw=True) - set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items") + make_packing_list(self) + + if self._action != "submit" and not self.is_return: + set_batch_nos(self, "warehouse", throw=True) + set_batch_nos(self, "warehouse", throw=True, child_table="packed_items") self.update_current_stock() - if not self.installation_status: self.installation_status = 'Not Installed' + if not self.installation_status: + self.installation_status = "Not Installed" self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): - super(DeliveryNote, self).validate_with_previous_doc({ - "Sales Order": { - "ref_dn_field": "against_sales_order", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Sales Order Item": { - "ref_dn_field": "so_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - "Sales Invoice": { - "ref_dn_field": "against_sales_invoice", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Sales Invoice Item": { - "ref_dn_field": "si_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - }) + super(DeliveryNote, self).validate_with_previous_doc( + { + "Sales Order": { + "ref_dn_field": "against_sales_order", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Sales Order Item": { + "ref_dn_field": "so_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + "Sales Invoice": { + "ref_dn_field": "against_sales_invoice", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Sales Invoice Item": { + "ref_dn_field": "si_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } + ) - if cint(frappe.db.get_single_value('Selling Settings', 'maintain_same_sales_rate')) \ - and not self.is_return: - self.validate_rate_with_reference_doc([["Sales Order", "against_sales_order", "so_detail"], - ["Sales Invoice", "against_sales_invoice", "si_detail"]]) + if ( + cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) + and not self.is_return + ): + self.validate_rate_with_reference_doc( + [ + ["Sales Order", "against_sales_order", "so_detail"], + ["Sales Invoice", "against_sales_invoice", "si_detail"], + ] + ) def validate_proj_cust(self): """check for does customer belong to same project as entered..""" if self.project and self.customer: - res = frappe.db.sql("""select name from `tabProject` + res = frappe.db.sql( + """select name from `tabProject` where name = %s and (customer = %s or - ifnull(customer,'')='')""", (self.project, self.customer)) + ifnull(customer,'')='')""", + (self.project, self.customer), + ) if not res: - frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) + frappe.throw( + _("Customer {0} does not belong to project {1}").format(self.customer, self.project) + ) def validate_warehouse(self): super(DeliveryNote, self).validate_warehouse() for d in self.get_item_list(): - if not d['warehouse'] and frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1: + if not d["warehouse"] and frappe.db.get_value("Item", d["item_code"], "is_stock_item") == 1: frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"])) def update_current_stock(self): if self.get("_action") and self._action != "update_after_submit": - for d in self.get('items'): - d.actual_qty = frappe.db.get_value("Bin", {"item_code": d.item_code, - "warehouse": d.warehouse}, "actual_qty") + for d in self.get("items"): + d.actual_qty = frappe.db.get_value( + "Bin", {"item_code": d.item_code, "warehouse": d.warehouse}, "actual_qty" + ) - for d in self.get('packed_items'): - bin_qty = frappe.db.get_value("Bin", {"item_code": d.item_code, - "warehouse": d.warehouse}, ["actual_qty", "projected_qty"], as_dict=True) + for d in self.get("packed_items"): + bin_qty = frappe.db.get_value( + "Bin", + {"item_code": d.item_code, "warehouse": d.warehouse}, + ["actual_qty", "projected_qty"], + as_dict=True, + ) if bin_qty: d.actual_qty = flt(bin_qty.actual_qty) d.projected_qty = flt(bin_qty.projected_qty) @@ -207,7 +229,9 @@ class DeliveryNote(SellingController): self.validate_packed_qty() # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) # update delivered qty in sales order self.update_prevdoc_status() @@ -240,20 +264,27 @@ class DeliveryNote(SellingController): self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") def check_credit_limit(self): from erpnext.selling.doctype.customer.customer import check_credit_limit extra_amount = 0 validate_against_credit_limit = False - bypass_credit_limit_check_at_sales_order = cint(frappe.db.get_value("Customer Credit Limit", - filters={'parent': self.customer, 'parenttype': 'Customer', 'company': self.company}, - fieldname="bypass_credit_limit_check")) + bypass_credit_limit_check_at_sales_order = cint( + frappe.db.get_value( + "Customer Credit Limit", + filters={"parent": self.customer, "parenttype": "Customer", "company": self.company}, + fieldname="bypass_credit_limit_check", + ) + ) if bypass_credit_limit_check_at_sales_order: - validate_against_credit_limit = True - extra_amount = self.base_grand_total + for d in self.get("items"): + if not d.against_sales_invoice: + validate_against_credit_limit = True + extra_amount = self.base_grand_total + break else: for d in self.get("items"): if not (d.against_sales_order or d.against_sales_invoice): @@ -261,48 +292,58 @@ class DeliveryNote(SellingController): break if validate_against_credit_limit: - check_credit_limit(self.customer, self.company, - bypass_credit_limit_check_at_sales_order, extra_amount) + check_credit_limit( + self.customer, self.company, bypass_credit_limit_check_at_sales_order, extra_amount + ) def validate_packed_qty(self): """ - Validate that if packed qty exists, it should be equal to qty + Validate that if packed qty exists, it should be equal to qty """ - if not any(flt(d.get('packed_qty')) for d in self.get("items")): + if not any(flt(d.get("packed_qty")) for d in self.get("items")): return has_error = False for d in self.get("items"): - if flt(d.get('qty')) != flt(d.get('packed_qty')): - frappe.msgprint(_("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx)) + if flt(d.get("qty")) != flt(d.get("packed_qty")): + frappe.msgprint( + _("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx) + ) has_error = True if has_error: raise frappe.ValidationError def check_next_docstatus(self): - submit_rv = frappe.db.sql("""select t1.name + submit_rv = frappe.db.sql( + """select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.delivery_note = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_rv: frappe.throw(_("Sales Invoice {0} has already been submitted").format(submit_rv[0][0])) - submit_in = frappe.db.sql("""select t1.name + submit_in = frappe.db.sql( + """select t1.name from `tabInstallation Note` t1, `tabInstallation Note Item` t2 where t1.name = t2.parent and t2.prevdoc_docname = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_in: frappe.throw(_("Installation Note {0} has already been submitted").format(submit_in[0][0])) def cancel_packing_slips(self): """ - Cancel submitted packing slips related to this delivery note + Cancel submitted packing slips related to this delivery note """ - res = frappe.db.sql("""SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s - AND docstatus = 1""", self.name) + res = frappe.db.sql( + """SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s + AND docstatus = 1""", + self.name, + ) if res: for r in res: - ps = frappe.get_doc('Packing Slip', r[0]) + ps = frappe.get_doc("Packing Slip", r[0]) ps.cancel() frappe.msgprint(_("Packing Slip(s) cancelled")) @@ -315,7 +356,7 @@ class DeliveryNote(SellingController): updated_delivery_notes = [self.name] for d in self.get("items"): if d.si_detail and not d.so_detail: - d.db_set('billed_amt', d.amount, update_modified=update_modified) + d.db_set("billed_amt", d.amount, update_modified=update_modified) elif d.so_detail: updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) @@ -332,11 +373,16 @@ class DeliveryNote(SellingController): return_invoice.save() return_invoice.submit() - credit_note_link = frappe.utils.get_link_to_form('Sales Invoice', return_invoice.name) + credit_note_link = frappe.utils.get_link_to_form("Sales Invoice", return_invoice.name) frappe.msgprint(_("Credit Note {0} has been created automatically").format(credit_note_link)) except Exception: - frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again")) + frappe.throw( + _( + "Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again" + ) + ) + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum @@ -345,25 +391,35 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") sum_amount = Sum(si_item.amount).as_("amount") - billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where( - (si_item.so_detail == so_detail) & - ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & - (si_item.docstatus == 1) - ).run() + billed_against_so = ( + frappe.qb.from_(si_item) + .select(sum_amount) + .where( + (si_item.so_detail == so_detail) + & ((si_item.dn_detail.isnull()) | (si_item.dn_detail == "")) + & (si_item.docstatus == 1) + ) + .run() + ) billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row dn = frappe.qb.DocType("Delivery Note").as_("dn") dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") - dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where( - (dn.name == dn_item.parent) & - (dn_item.so_detail == so_detail) & - (dn.docstatus == 1) & - (dn.is_return == 0) - ).orderby( - dn.posting_date, dn.posting_time, dn.name - ).run(as_dict=True) + dn_details = ( + frappe.qb.from_(dn) + .from_(dn_item) + .select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent) + .where( + (dn.name == dn_item.parent) + & (dn_item.so_detail == so_detail) + & (dn.docstatus == 1) + & (dn.is_return == 0) + ) + .orderby(dn.posting_date, dn.posting_time, dn.name) + .run(as_dict=True) + ) updated_dn = [] for dnd in dn_details: @@ -375,8 +431,11 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): billed_against_so -= billed_amt_agianst_dn else: # Get billed amount directly against Delivery Note - billed_amt_agianst_dn = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` - where dn_detail=%s and docstatus=1""", dnd.name) + billed_amt_agianst_dn = frappe.db.sql( + """select sum(amount) from `tabSales Invoice Item` + where dn_detail=%s and docstatus=1""", + dnd.name, + ) billed_amt_agianst_dn = billed_amt_agianst_dn and billed_amt_agianst_dn[0][0] or 0 # Distribute billed amount directly against SO between DNs based on FIFO @@ -389,57 +448,77 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): billed_amt_agianst_dn += billed_against_so billed_against_so = 0 - frappe.db.set_value("Delivery Note Item", dnd.name, "billed_amt", billed_amt_agianst_dn, update_modified=update_modified) + frappe.db.set_value( + "Delivery Note Item", + dnd.name, + "billed_amt", + billed_amt_agianst_dn, + update_modified=update_modified, + ) updated_dn.append(dnd.parent) return updated_dn + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Shipments'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Shipments"), + } + ) return list_context + def get_invoiced_qty_map(delivery_note): """returns a map: {dn_detail: invoiced_qty}""" invoiced_qty_map = {} - for dn_detail, qty in frappe.db.sql("""select dn_detail, qty from `tabSales Invoice Item` - where delivery_note=%s and docstatus=1""", delivery_note): - if not invoiced_qty_map.get(dn_detail): - invoiced_qty_map[dn_detail] = 0 - invoiced_qty_map[dn_detail] += qty + for dn_detail, qty in frappe.db.sql( + """select dn_detail, qty from `tabSales Invoice Item` + where delivery_note=%s and docstatus=1""", + delivery_note, + ): + if not invoiced_qty_map.get(dn_detail): + invoiced_qty_map[dn_detail] = 0 + invoiced_qty_map[dn_detail] += qty return invoiced_qty_map + def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.dn_detail, abs(dn_item.qty) as qty + returned_qty_map = frappe._dict( + frappe.db.sql( + """select dn_item.dn_detail, abs(dn_item.qty) as qty from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn where dn.name = dn_item.parent and dn.docstatus = 1 and dn.is_return = 1 and dn.return_against = %s - """, delivery_note)) + """, + delivery_note, + ) + ) return returned_qty_map + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): - doc = frappe.get_doc('Delivery Note', source_name) + doc = frappe.get_doc("Delivery Note", source_name) to_make_invoice_qty_map = {} returned_qty_map = get_returned_qty_map(source_name) invoiced_qty_map = get_invoiced_qty_map(source_name) def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("set_po_nos") @@ -450,20 +529,21 @@ def make_sales_invoice(source_name, target_doc=None): # set company address if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) + target.update(get_fetch_values("Sales Invoice", "company_address", target.company_address)) def update_item(source_doc, target_doc, source_parent): target_doc.qty = to_make_invoice_qty_map[source_doc.name] if source_doc.serial_no and source_parent.per_billed > 0 and not source_parent.is_return: - target_doc.serial_no = get_delivery_note_serial_no(source_doc.item_code, - target_doc.qty, source_parent.name) + target_doc.serial_no = get_delivery_note_serial_no( + source_doc.item_code, target_doc.qty, source_parent.name + ) def get_pending_qty(item_row): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) @@ -485,48 +565,52 @@ def make_sales_invoice(source_name, target_doc=None): return pending_qty - doc = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Sales Invoice", - "field_map": { - "is_return": "is_return" + doc = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Sales Invoice", + "field_map": {"is_return": "is_return"}, + "validation": {"docstatus": ["=", 1]}, }, - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Sales Invoice Item", - "field_map": { - "name": "dn_detail", - "parent": "delivery_note", - "so_detail": "so_detail", - "against_sales_order": "sales_order", - "serial_no": "serial_no", - "cost_center": "cost_center" + "Delivery Note Item": { + "doctype": "Sales Invoice Item", + "field_map": { + "name": "dn_detail", + "parent": "delivery_note", + "so_detail": "so_detail", + "against_sales_order": "sales_order", + "serial_no": "serial_no", + "cost_center": "cost_center", + }, + "postprocess": update_item, + "filter": lambda d: get_pending_qty(d) <= 0 + if not doc.get("is_return") + else get_pending_qty(d) > 0, }, - "postprocess": update_item, - "filter": lambda d: get_pending_qty(d) <= 0 if not doc.get("is_return") else get_pending_qty(d) > 0 - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "field_map": { - "incentives": "incentives" + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": { + "doctype": "Sales Team", + "field_map": {"incentives": "incentives"}, + "add_if_empty": True, }, - "add_if_empty": True - } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + automatically_fetch_payment_terms = cint( + frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) if automatically_fetch_payment_terms: doc.set_payment_schedule() + doc.set_onload("ignore_price_list", True) + return doc + @frappe.whitelist() def make_delivery_trip(source_name, target_doc=None): def update_stop_details(source_doc, target_doc, source_parent): @@ -542,96 +626,101 @@ def make_delivery_trip(source_name, target_doc=None): delivery_notes = [] - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Delivery Trip", - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Delivery Stop", - "field_map": { - "parent": "delivery_note" + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": {"doctype": "Delivery Trip", "validation": {"docstatus": ["=", 1]}}, + "Delivery Note Item": { + "doctype": "Delivery Stop", + "field_map": {"parent": "delivery_note"}, + "condition": lambda item: item.parent not in delivery_notes, + "postprocess": update_stop_details, }, - "condition": lambda item: item.parent not in delivery_notes, - "postprocess": update_stop_details - } - }, target_doc) + }, + target_doc, + ) return doclist + @frappe.whitelist() def make_installation_note(source_name, target_doc=None): def update_item(obj, target, source_parent): target.qty = flt(obj.qty) - flt(obj.installed_qty) target.serial_no = obj.serial_no - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Installation Note", - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Installation Note Item", - "field_map": { - "name": "prevdoc_detail_docname", - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": {"doctype": "Installation Note", "validation": {"docstatus": ["=", 1]}}, + "Delivery Note Item": { + "doctype": "Installation Note Item", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + }, + "postprocess": update_item, + "condition": lambda doc: doc.installed_qty < doc.qty, }, - "postprocess": update_item, - "condition": lambda doc: doc.installed_qty < doc.qty - } - }, target_doc) + }, + target_doc, + ) return doclist + @frappe.whitelist() def make_packing_slip(source_name, target_doc=None): - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Packing Slip", - "field_map": { - "name": "delivery_note", - "letter_head": "letter_head" - }, - "validation": { - "docstatus": ["=", 0] + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Packing Slip", + "field_map": {"name": "delivery_note", "letter_head": "letter_head"}, + "validation": {"docstatus": ["=", 0]}, } - } - }, target_doc) + }, + target_doc, + ) return doclist + @frappe.whitelist() def make_shipment(source_name, target_doc=None): def postprocess(source, target): - user = frappe.db.get_value("User", frappe.session.user, ['email', 'full_name', 'phone', 'mobile_no'], as_dict=1) + user = frappe.db.get_value( + "User", frappe.session.user, ["email", "full_name", "phone", "mobile_no"], as_dict=1 + ) target.pickup_contact_email = user.email - pickup_contact_display = '{}'.format(user.full_name) + pickup_contact_display = "{}".format(user.full_name) if user: if user.email: - pickup_contact_display += '
    ' + user.email + pickup_contact_display += "
    " + user.email if user.phone: - pickup_contact_display += '
    ' + user.phone + pickup_contact_display += "
    " + user.phone if user.mobile_no and not user.phone: - pickup_contact_display += '
    ' + user.mobile_no + pickup_contact_display += "
    " + user.mobile_no target.pickup_contact = pickup_contact_display # As we are using session user details in the pickup_contact then pickup_contact_person will be session user target.pickup_contact_person = frappe.session.user - contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) - delivery_contact_display = '{}'.format(source.contact_display) + contact = frappe.db.get_value( + "Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1 + ) + delivery_contact_display = "{}".format(source.contact_display) if contact: if contact.email_id: - delivery_contact_display += '
    ' + contact.email_id + delivery_contact_display += "
    " + contact.email_id if contact.phone: - delivery_contact_display += '
    ' + contact.phone + delivery_contact_display += "
    " + contact.phone if contact.mobile_no and not contact.phone: - delivery_contact_display += '
    ' + contact.mobile_no + delivery_contact_display += "
    " + contact.mobile_no target.delivery_contact = delivery_contact_display if source.shipping_address_name: @@ -641,38 +730,44 @@ def make_shipment(source_name, target_doc=None): target.delivery_address_name = source.customer_address target.delivery_address = source.address_display - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Shipment", - "field_map": { - "grand_total": "value_of_goods", - "company": "pickup_company", - "company_address": "pickup_address_name", - "company_address_display": "pickup_address", - "customer": "delivery_customer", - "contact_person": "delivery_contact_name", - "contact_email": "delivery_contact_email" + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Shipment", + "field_map": { + "grand_total": "value_of_goods", + "company": "pickup_company", + "company_address": "pickup_address_name", + "company_address_display": "pickup_address", + "customer": "delivery_customer", + "contact_person": "delivery_contact_name", + "contact_email": "delivery_contact_email", + }, + "validation": {"docstatus": ["=", 1]}, + }, + "Delivery Note Item": { + "doctype": "Shipment Delivery Note", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + "base_amount": "grand_total", + }, }, - "validation": { - "docstatus": ["=", 1] - } }, - "Delivery Note Item": { - "doctype": "Shipment Delivery Note", - "field_map": { - "name": "prevdoc_detail_docname", - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", - "base_amount": "grand_total" - } - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_sales_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Delivery Note", source_name, target_doc) @@ -681,10 +776,12 @@ def update_delivery_note_status(docname, status): dn = frappe.get_doc("Delivery Note", docname) dn.update_status(status) + @frappe.whitelist() def make_inter_company_purchase_receipt(source_name, target_doc=None): return make_inter_company_transaction("Delivery Note", source_name, target_doc) + def make_inter_company_transaction(doctype, source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( get_inter_company_details, @@ -694,16 +791,16 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): validate_inter_company_transaction, ) - if doctype == 'Delivery Note': + if doctype == "Delivery Note": source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Receipt" - source_document_warehouse_field = 'target_warehouse' - target_document_warehouse_field = 'from_warehouse' + source_document_warehouse_field = "target_warehouse" + target_document_warehouse_field = "from_warehouse" else: source_doc = frappe.get_doc(doctype, source_name) - target_doctype = 'Delivery Note' - source_document_warehouse_field = 'from_warehouse' - target_document_warehouse_field = 'target_warehouse' + target_doctype = "Delivery Note" + source_document_warehouse_field = "from_warehouse" + target_document_warehouse_field = "target_warehouse" validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -712,18 +809,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target.run_method("set_missing_values") set_purchase_references(target) - if target.doctype == 'Purchase Receipt': - master_doctype = 'Purchase Taxes and Charges Template' + if target.doctype == "Purchase Receipt": + master_doctype = "Purchase Taxes and Charges Template" else: - master_doctype = 'Sales Taxes and Charges Template' + master_doctype = "Sales Taxes and Charges Template" - if not target.get('taxes') and target.get('taxes_and_charges'): - for tax in get_taxes_and_charges(master_doctype, target.get('taxes_and_charges')): - target.append('taxes', tax) + if not target.get("taxes") and target.get("taxes_and_charges"): + for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")): + target.append("taxes", tax) def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name - if target_doc.doctype == 'Purchase Receipt': + if target_doc.doctype == "Purchase Receipt": target_doc.company = details.get("company") target_doc.supplier = details.get("party") target_doc.buying_price_list = source_doc.selling_price_list @@ -731,12 +828,20 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc.inter_company_reference = source_doc.name # Invert the address on target doc creation - update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) - update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + ) - update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.supplier_address, - company_address=target_doc.shipping_address) + update_taxes( + target_doc, + party=target_doc.supplier, + party_type="Supplier", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address, + ) else: target_doc.company = details.get("company") target_doc.customer = details.get("party") @@ -746,36 +851,52 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc.inter_company_reference = source_doc.name # Invert the address on target doc creation - update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) - update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) - update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + update_address( + target_doc, "company_address", "company_address_display", source_doc.supplier_address + ) + update_address( + target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address + ) + update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address) - update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.customer_address, - company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + update_taxes( + target_doc, + party=target_doc.customer, + party_type="Customer", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.customer_address, + company_address=target_doc.company_address, + shipping_address_name=target_doc.shipping_address_name, + ) - doclist = get_mapped_doc(doctype, source_name, { - doctype: { - "doctype": target_doctype, - "postprocess": update_details, - "field_no_map": [ - "taxes_and_charges", - "set_warehouse" - ] - }, - doctype +" Item": { - "doctype": target_doctype + " Item", - "field_map": { - source_document_warehouse_field: target_document_warehouse_field, - 'name': 'delivery_note_item', - 'batch_no': 'batch_no', - 'serial_no': 'serial_no' + doclist = get_mapped_doc( + doctype, + source_name, + { + doctype: { + "doctype": target_doctype, + "postprocess": update_details, + "field_no_map": ["taxes_and_charges", "set_warehouse"], }, - "field_no_map": [ - "warehouse" - ] - } - - }, target_doc, set_missing_values) + doctype + + " Item": { + "doctype": target_doctype + " Item", + "field_map": { + source_document_warehouse_field: target_document_warehouse_field, + "name": "delivery_note_item", + "batch_no": "batch_no", + "serial_no": "serial_no", + }, + "field_no_map": ["warehouse"], + }, + }, + target_doc, + set_missing_values, + ) return doclist + + +def on_doctype_update(): + frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"]) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index 61d8de4a06d..fd44e9cee5c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -1,34 +1,21 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'delivery_note', - 'non_standard_fieldnames': { - 'Stock Entry': 'delivery_note_no', - 'Quality Inspection': 'reference_name', - 'Auto Repeat': 'reference_document', + "fieldname": "delivery_note", + "non_standard_fieldnames": { + "Stock Entry": "delivery_note_no", + "Quality Inspection": "reference_name", + "Auto Repeat": "reference_document", }, - 'internal_links': { - 'Sales Order': ['items', 'against_sales_order'], + "internal_links": { + "Sales Order": ["items", "against_sales_order"], }, - 'transactions': [ - { - 'label': _('Related'), - 'items': ['Sales Invoice', 'Packing Slip', 'Delivery Trip'] - }, - { - 'label': _('Reference'), - 'items': ['Sales Order', 'Shipment', 'Quality Inspection'] - }, - { - 'label': _('Returns'), - 'items': ['Stock Entry'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]}, + {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, + {"label": _("Returns"), "items": ["Stock Entry"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index bd18e788ba6..f97e7ca9c68 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2,10 +2,10 @@ # License: GNU General Public License v3. See license.txt - import json import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt, nowdate, nowtime from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -35,10 +35,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ) from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.stock_ledger import get_previous_sle -from erpnext.tests.utils import ERPNextTestCase -class TestDeliveryNote(ERPNextTestCase): +class TestDeliveryNote(FrappeTestCase): def test_over_billing_against_dn(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -54,93 +53,69 @@ class TestDeliveryNote(ERPNextTestCase): self.assertRaises(frappe.ValidationError, frappe.get_doc(si).insert) def test_delivery_note_no_gl_entry(self): - company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') + company = frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company") make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100) - stock_queue = json.loads(get_previous_sle({ - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "posting_date": nowdate(), - "posting_time": nowtime() - }).stock_queue or "[]") + stock_queue = json.loads( + get_previous_sle( + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "posting_date": nowdate(), + "posting_time": nowtime(), + } + ).stock_queue + or "[]" + ) dn = create_delivery_note() - sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name}) + sle = frappe.get_doc( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name} + ) - self.assertEqual(sle.stock_value_difference, flt(-1*stock_queue[0][1], 2)) + self.assertEqual(sle.stock_value_difference, flt(-1 * stock_queue[0][1], 2)) self.assertFalse(get_gl_entries("Delivery Note", dn.name)) - # def test_delivery_note_gl_entry(self): - # company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - - # set_valuation_method("_Test Item", "FIFO") - - # make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - - # stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - # prev_bal = get_balance_on(stock_in_hand_account) - - # dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") - - # gl_entries = get_gl_entries("Delivery Note", dn.name) - # self.assertTrue(gl_entries) - - # stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", - # {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) - - # expected_values = { - # stock_in_hand_account: [0.0, stock_value_difference], - # "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] - # } - # for i, gle in enumerate(gl_entries): - # self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) - - # # check stock in hand balance - # bal = get_balance_on(stock_in_hand_account) - # self.assertEqual(bal, prev_bal - stock_value_difference) - - # # back dated incoming entry - # make_stock_entry(posting_date=add_days(nowdate(), -2), target="Stores - TCP1", - # qty=5, basic_rate=100) - - # gl_entries = get_gl_entries("Delivery Note", dn.name) - # self.assertTrue(gl_entries) - - # stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", - # {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) - - # expected_values = { - # stock_in_hand_account: [0.0, stock_value_difference], - # "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] - # } - # for i, gle in enumerate(gl_entries): - # self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) - - # dn.cancel() - # self.assertTrue(get_gl_entries("Delivery Note", dn.name)) - # set_perpetual_inventory(0, company) - def test_delivery_note_gl_entry_packing_item(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=10, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - TCP1", qty=10, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - TCP1", qty=10, basic_rate=100 + ) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") prev_bal = get_balance_on(stock_in_hand_account) - dn = create_delivery_note(item_code="_Test Product Bundle Item", company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + ) - stock_value_diff_rm1 = abs(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, - "stock_value_difference")) + stock_value_diff_rm1 = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + ) - stock_value_diff_rm2 = abs(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item Home Desktop 100"}, "stock_value_difference")) + stock_value_diff_rm2 = abs( + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item Home Desktop 100", + }, + "stock_value_difference", + ) + ) stock_value_diff = stock_value_diff_rm1 + stock_value_diff_rm2 @@ -149,7 +124,7 @@ class TestDeliveryNote(ERPNextTestCase): expected_values = { stock_in_hand_account: [0.0, stock_value_diff], - "Cost of Goods Sold - TCP1": [stock_value_diff, 0.0] + "Cost of Goods Sold - TCP1": [stock_value_diff, 0.0], } for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) @@ -166,10 +141,7 @@ class TestDeliveryNote(ERPNextTestCase): dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) si = make_sales_invoice(dn.name) si.insert(ignore_permissions=True) @@ -177,17 +149,18 @@ class TestDeliveryNote(ERPNextTestCase): dn.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "" - }) + self.check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""} + ) def test_serialized_partial_sales_invoice(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no) - serial_no = '\n'.join(serial_no) + serial_no = "\n".join(serial_no) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no + ) si = make_sales_invoice(dn.name) si.items[0].qty = 1 @@ -200,15 +173,19 @@ class TestDeliveryNote(ERPNextTestCase): def test_serialize_status(self): from frappe.model.naming import make_autoname - serial_no = frappe.get_doc({ - "doctype": "Serial No", - "item_code": "_Test Serialized Item With Series", - "serial_no": make_autoname("SR", "Serial No") - }) + + serial_no = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": "_Test Serialized Item With Series", + "serial_no": make_autoname("SR", "Serial No"), + } + ) serial_no.save() - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - serial_no=serial_no.name, do_not_submit=True) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", serial_no=serial_no.name, do_not_submit=True + ) self.assertRaises(SerialNoWarehouseError, dn.submit) @@ -218,26 +195,46 @@ class TestDeliveryNote(ERPNextTestCase): self.assertEqual(cstr(serial_no.get(field)), value) def test_sales_return_for_non_bundled_items_partial(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) actual_qty_0 = get_qty_after_transaction(warehouse="Stores - TCP1") - dn = create_delivery_note(qty=5, rate=500, warehouse="Stores - TCP1", company=company, - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + qty=5, + rate=500, + warehouse="Stores - TCP1", + company=company, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) actual_qty_1 = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty_0 - 5, actual_qty_1) # outgoing_rate - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 5 + outgoing_rate = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 5 + ) # return entry - dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, rate=500, - company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", - cost_center="Main - TCP1", do_not_submit=1) + dn1 = create_delivery_note( + is_return=1, + return_against=dn.name, + qty=-2, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_submit=1, + ) dn1.items[0].dn_detail = dn.items[0].name dn1.submit() @@ -245,15 +242,20 @@ class TestDeliveryNote(ERPNextTestCase): self.assertEqual(actual_qty_1 + 2, actual_qty_2) - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) stock_in_hand_account = get_inventory_account(company, dn1.items[0].warehouse) - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, stock_value_difference) @@ -267,6 +269,7 @@ class TestDeliveryNote(ERPNextTestCase): self.assertEqual(dn.per_returned, 40) from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_dn_2 = make_return_doc("Delivery Note", dn.name) # Check if unreturned amount is mapped in 2nd return @@ -281,7 +284,7 @@ class TestDeliveryNote(ERPNextTestCase): # DN should be completed on billing all unreturned amount self.assertEqual(dn.items[0].billed_amt, 1500) self.assertEqual(dn.per_billed, 100) - self.assertEqual(dn.status, 'Completed') + self.assertEqual(dn.status, "Completed") si.load_from_db() si.cancel() @@ -293,19 +296,35 @@ class TestDeliveryNote(ERPNextTestCase): dn.cancel() def test_sales_return_for_non_bundled_items_full(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - make_item("Box", {'is_stock_item': 1}) + make_item("Box", {"is_stock_item": 1}) make_stock_entry(item_code="Box", target="Stores - TCP1", qty=10, basic_rate=100) - dn = create_delivery_note(item_code="Box", qty=5, rate=500, warehouse="Stores - TCP1", company=company, - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="Box", + qty=5, + rate=500, + warehouse="Stores - TCP1", + company=company, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - #return entry - dn1 = create_delivery_note(item_code="Box", is_return=1, return_against=dn.name, qty=-5, rate=500, - company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", - cost_center="Main - TCP1", do_not_submit=1) + # return entry + dn1 = create_delivery_note( + item_code="Box", + is_return=1, + return_against=dn.name, + qty=-5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_submit=1, + ) dn1.items[0].dn_detail = dn.items[0].name dn1.submit() @@ -317,92 +336,157 @@ class TestDeliveryNote(ERPNextTestCase): # Check if Original DN updated self.assertEqual(dn.items[0].returned_qty, 5) self.assertEqual(dn.per_returned, 100) - self.assertEqual(dn.status, 'Return Issued') + self.assertEqual(dn.status, "Return Issued") def test_return_single_item_from_bundled_items(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - create_stock_reconciliation(item_code="_Test Item", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") + create_stock_reconciliation( + item_code="_Test Item", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) - dn = create_delivery_note(item_code="_Test Product Bundle Item", qty=5, rate=500, - company=company, warehouse="Stores - TCP1", - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + qty=5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # Qty after delivery actual_qty_1 = get_qty_after_transaction(warehouse="Stores - TCP1") - self.assertEqual(actual_qty_1, 25) + self.assertEqual(actual_qty_1, 25) # outgoing_rate - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name, "item_code": "_Test Item"}, "stock_value_difference") / 25 + outgoing_rate = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + / 25 + ) # return 'test item' from packed items - dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-10, rate=500, - company=company, warehouse="Stores - TCP1", - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn1 = create_delivery_note( + is_return=1, + return_against=dn.name, + qty=-10, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty_2 = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty_2, 35) # Check incoming rate for return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) stock_in_hand_account = get_inventory_account(company, dn1.items[0].warehouse) # Check gl entry for warehouse - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, stock_value_difference) - def test_return_entire_bundled_items(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - create_stock_reconciliation(item_code="_Test Item", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") + create_stock_reconciliation( + item_code="_Test Item", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 50) - dn = create_delivery_note(item_code="_Test Product Bundle Item", - qty=5, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + qty=5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 25) # return bundled item - dn1 = create_return_delivery_note(source_name=dn.name, rate=500, qty=-2) + dn1 = create_delivery_note( + item_code="_Test Product Bundle Item", + is_return=1, + return_against=dn.name, + qty=-2, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 35) # Check incoming rate for return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 100) - stock_in_hand_account = get_inventory_account('_Test Company', dn1.items[0].warehouse) + stock_in_hand_account = get_inventory_account("_Test Company", dn1.items[0].warehouse) # Check gl entry for warehouse - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, 1400) @@ -410,69 +494,88 @@ class TestDeliveryNote(ERPNextTestCase): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] - dn = create_delivery_note(item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no + ) - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) # return entry - dn1 = create_delivery_note(item_code="_Test Serialized Item With Series", - is_return=1, return_against=dn.name, qty=-1, rate=500, serial_no=serial_no) + dn1 = create_delivery_note( + item_code="_Test Serialized Item With Series", + is_return=1, + return_against=dn.name, + qty=-1, + rate=500, + serial_no=serial_no, + ) - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "" - }) + self.check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""} + ) dn1.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) dn.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "", - "purchase_document_no": se.name - }) + self.check_serial_no_values( + serial_no, + { + "warehouse": "_Test Warehouse - _TC", + "delivery_document_no": "", + "purchase_document_no": se.name, + }, + ) def test_delivery_of_bundled_items_to_target_warehouse(self): from erpnext.selling.doctype.customer.test_customer import create_internal_customer - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") customer_name = create_internal_customer( customer_name="_Test Internal Customer 2", represents_company="_Test Company with perpetual inventory", - allowed_to_interact_with="_Test Company with perpetual inventory" + allowed_to_interact_with="_Test Company with perpetual inventory", ) set_valuation_method("_Test Item", "FIFO") set_valuation_method("_Test Item Home Desktop 100", "FIFO") - target_warehouse = get_warehouse(company=company, abbr="TCP1", - warehouse_name="_Test Customer Warehouse").name + target_warehouse = get_warehouse( + company=company, abbr="TCP1", warehouse_name="_Test Customer Warehouse" + ).name for warehouse in ("Stores - TCP1", target_warehouse): - create_stock_reconciliation(item_code="_Test Item", warehouse=warehouse, company = company, - expense_account = "Stock Adjustment - TCP1", qty=500, rate=100) - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", company = company, - expense_account = "Stock Adjustment - TCP1", warehouse=warehouse, qty=500, rate=100) + create_stock_reconciliation( + item_code="_Test Item", + warehouse=warehouse, + company=company, + expense_account="Stock Adjustment - TCP1", + qty=500, + rate=100, + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + company=company, + expense_account="Stock Adjustment - TCP1", + warehouse=warehouse, + qty=500, + rate=100, + ) dn = create_delivery_note( item_code="_Test Product Bundle Item", company="_Test Company with perpetual inventory", customer=customer_name, - cost_center = 'Main - TCP1', - expense_account = "Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", do_not_submit=True, - qty=5, rate=500, + qty=5, + rate=500, warehouse="Stores - TCP1", - target_warehouse=target_warehouse) + target_warehouse=target_warehouse, + ) dn.submit() @@ -484,16 +587,28 @@ class TestDeliveryNote(ERPNextTestCase): self.assertEqual(actual_qty_at_target, 525) # stock value diff for source warehouse for "_Test Item" - stock_value_difference = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item", "warehouse": "Stores - TCP1"}, - "stock_value_difference") + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + }, + "stock_value_difference", + ) # stock value diff for target warehouse - stock_value_difference1 = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item", "warehouse": target_warehouse}, - "stock_value_difference") + stock_value_difference1 = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item", + "warehouse": target_warehouse, + }, + "stock_value_difference", + ) self.assertEqual(abs(stock_value_difference), stock_value_difference1) @@ -501,13 +616,18 @@ class TestDeliveryNote(ERPNextTestCase): gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) - stock_value_difference = abs(frappe.db.sql("""select sum(stock_value_difference) + stock_value_difference = abs( + frappe.db.sql( + """select sum(stock_value_difference) from `tabStock Ledger Entry` where voucher_type='Delivery Note' and voucher_no=%s - and warehouse='Stores - TCP1'""", dn.name)[0][0]) + and warehouse='Stores - TCP1'""", + dn.name, + )[0][0] + ) expected_values = { "Stock In Hand - TCP1": [0.0, stock_value_difference], - target_warehouse: [stock_value_difference, 0.0] + target_warehouse: [stock_value_difference, 0.0], } for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) @@ -520,8 +640,13 @@ class TestDeliveryNote(ERPNextTestCase): make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', - cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True) + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + do_not_submit=True, + ) dn.submit() @@ -598,6 +723,7 @@ class TestDeliveryNote(ERPNextTestCase): from erpnext.selling.doctype.sales_order.sales_order import ( make_sales_invoice as make_sales_invoice_from_so, ) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) so = make_sales_order() @@ -671,28 +797,31 @@ class TestDeliveryNote(ERPNextTestCase): def test_delivery_note_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - cost_center = "_Test Cost Center for BS Account - TCP1" - create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company with perpetual inventory") - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + cost_center = "_Test Cost Center for BS Account - TCP1" + create_cost_center( + cost_center_name="_Test Cost Center for BS Account", + company="_Test Company with perpetual inventory", + ) set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', expense_account = "Cost of Goods Sold - TCP1", cost_center=cost_center) + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center=cost_center, + ) gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) expected_values = { - "Cost of Goods Sold - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Cost of Goods Sold - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -700,29 +829,28 @@ class TestDeliveryNote(ERPNextTestCase): def test_delivery_note_cost_center_with_balance_sheet_account(self): cost_center = "Main - TCP1" - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", - do_not_submit=1) + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + do_not_submit=1, + ) - dn.get('items')[0].cost_center = None + dn.get("items")[0].cost_center = None dn.submit() gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) expected_values = { - "Cost of Goods Sold - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Cost of Goods Sold - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -750,15 +878,18 @@ class TestDeliveryNote(ERPNextTestCase): from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice dn = create_delivery_note(qty=8, do_not_submit=True) - dn.append("items", { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "rate": 100, - "conversion_factor": 1.0, - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC" - }) + dn.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": 100, + "conversion_factor": 1.0, + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) dn.submit() si1 = make_sales_invoice(dn.name) @@ -775,14 +906,21 @@ class TestDeliveryNote(ERPNextTestCase): self.assertEqual(si2.items[0].qty, 2) self.assertEqual(si2.items[1].qty, 1) - def test_delivery_note_bundle_with_batched_item(self): batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0}) - batched_item = make_item("_Test Batched Item", - {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"} - ) + batched_item = make_item( + "_Test Batched Item", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TESTBATCH.#####", + }, + ) make_product_bundle(parent=batched_bundle.name, items=[batched_item.name]) - make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42) + make_stock_entry( + item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42 + ) try: dn = create_delivery_note(item_code=batched_bundle.name, qty=1) @@ -791,7 +929,9 @@ class TestDeliveryNote(ERPNextTestCase): self.fail("Batch numbers not getting added to bundled items in DN.") raise e - self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + self.assertTrue( + "TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item" + ) def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( @@ -803,13 +943,13 @@ class TestDeliveryNote(ERPNextTestCase): so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() - so.payment_terms_template = 'Test Receivable Template' + so.payment_terms_template = "Test Receivable Template" so.submit() dn = create_dn_against_so(so.name, delivered_qty=10) si = create_sales_invoice(qty=10, do_not_save=1) - si.items[0].delivery_note= dn.name + si.items[0].delivery_note = dn.name si.items[0].dn_detail = dn.items[0].name si.items[0].sales_order = so.name si.items[0].so_detail = so.items[0].name @@ -822,14 +962,6 @@ class TestDeliveryNote(ERPNextTestCase): automatically_fetch_payment_terms(enable=0) -def create_return_delivery_note(**args): - args = frappe._dict(args) - from erpnext.controllers.sales_and_purchase_return import make_return_doc - doc = make_return_doc("Delivery Note", args.source_name, None) - doc.items[0].rate = args.rate - doc.items[0].qty = args.qty - doc.submit() - return doc def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -844,18 +976,21 @@ def create_delivery_note(**args): dn.is_return = args.is_return dn.return_against = args.return_against - dn.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "rate": args.rate if args.get("rate") is not None else 100, - "conversion_factor": 1.0, - "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1, - "expense_account": args.expense_account or "Cost of Goods Sold - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no, - "target_warehouse": args.target_warehouse - }) + dn.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "rate": args.rate if args.get("rate") is not None else 100, + "conversion_factor": 1.0, + "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1, + "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "serial_no": args.serial_no, + "target_warehouse": args.target_warehouse, + }, + ) if not args.do_not_save: dn.insert() @@ -863,4 +998,5 @@ def create_delivery_note(**args): dn.submit() return dn + test_dependencies = ["Product Bundle"] diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index f1f5d96e628..e2eb2a4bbb2 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -74,6 +74,7 @@ "against_sales_invoice", "si_detail", "dn_detail", + "pick_list_item", "section_break_40", "batch_no", "serial_no", @@ -762,13 +763,22 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "fieldname": "pick_list_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Pick List Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-02-24 14:42:20.211085", + "modified": "2022-03-31 18:36:24.671913", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index c749b2e6706..73b250db54b 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -17,9 +17,12 @@ class DeliveryTrip(Document): super(DeliveryTrip, self).__init__(*args, **kwargs) # Google Maps returns distances in meters by default - self.default_distance_uom = frappe.db.get_single_value("Global Defaults", "default_distance_unit") or "Meter" - self.uom_conversion_factor = frappe.db.get_value("UOM Conversion Factor", - {"from_uom": "Meter", "to_uom": self.default_distance_uom}, "value") + self.default_distance_uom = ( + frappe.db.get_single_value("Global Defaults", "default_distance_unit") or "Meter" + ) + self.uom_conversion_factor = frappe.db.get_value( + "UOM Conversion Factor", {"from_uom": "Meter", "to_uom": self.default_distance_uom}, "value" + ) def validate(self): self.validate_stop_addresses() @@ -41,11 +44,7 @@ class DeliveryTrip(Document): stop.customer_address = get_address_display(frappe.get_doc("Address", stop.address).as_dict()) def update_status(self): - status = { - 0: "Draft", - 1: "Scheduled", - 2: "Cancelled" - }[self.docstatus] + status = {0: "Draft", 1: "Scheduled", 2: "Cancelled"}[self.docstatus] if self.docstatus == 1: visited_stops = [stop.visited for stop in self.delivery_stops] @@ -63,17 +62,19 @@ class DeliveryTrip(Document): are removed. Args: - delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. + delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. """ - delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note)) + delivery_notes = list( + set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note) + ) update_fields = { "driver": self.driver, "driver_name": self.driver_name, "vehicle_no": self.vehicle, "lr_no": self.name, - "lr_date": self.departure_time + "lr_date": self.departure_time, } for delivery_note in delivery_notes: @@ -97,7 +98,7 @@ class DeliveryTrip(Document): on the optimized order, before estimating the arrival times. Args: - optimize (bool): True if route needs to be optimized, else False + optimize (bool): True if route needs to be optimized, else False """ departure_datetime = get_datetime(self.departure_time) @@ -134,8 +135,9 @@ class DeliveryTrip(Document): # Include last leg in the final distance calculation self.uom = self.default_distance_uom - total_distance = sum(leg.get("distance", {}).get("value", 0.0) - for leg in directions.get("legs")) # in meters + total_distance = sum( + leg.get("distance", {}).get("value", 0.0) for leg in directions.get("legs") + ) # in meters self.total_distance = total_distance * self.uom_conversion_factor else: idx += len(route) - 1 @@ -149,10 +151,10 @@ class DeliveryTrip(Document): split into sublists at the specified lock position(s). Args: - optimize (bool): `True` if route needs to be optimized, else `False` + optimize (bool): `True` if route needs to be optimized, else `False` Returns: - (list of list of str): List of address routes split at locks, if optimize is `True` + (list of list of str): List of address routes split at locks, if optimize is `True` """ if not self.driver_address: frappe.throw(_("Cannot Calculate Arrival Time as Driver Address is Missing.")) @@ -186,8 +188,8 @@ class DeliveryTrip(Document): for vehicle routing problems. Args: - optimized_order (list of int): The index-based optimized order of the route - start (int): The index at which to start the rearrangement + optimized_order (list of int): The index-based optimized order of the route + start (int): The index at which to start the rearrangement """ stops_order = [] @@ -200,7 +202,7 @@ class DeliveryTrip(Document): self.delivery_stops[old_idx].idx = new_idx stops_order.append(self.delivery_stops[old_idx]) - self.delivery_stops[start:start + len(stops_order)] = stops_order + self.delivery_stops[start : start + len(stops_order)] = stops_order def get_directions(self, route, optimize): """ @@ -212,11 +214,11 @@ class DeliveryTrip(Document): but it only works for routes without any waypoints. Args: - route (list of str): Route addresses (origin -> waypoint(s), if any -> destination) - optimize (bool): `True` if route needs to be optimized, else `False` + route (list of str): Route addresses (origin -> waypoint(s), if any -> destination) + optimize (bool): `True` if route needs to be optimized, else `False` Returns: - (dict): Route legs and, if `optimize` is `True`, optimized waypoint order + (dict): Route legs and, if `optimize` is `True`, optimized waypoint order """ if not frappe.db.get_single_value("Google Settings", "api_key"): frappe.throw(_("Enter API key in Google Settings.")) @@ -231,8 +233,8 @@ class DeliveryTrip(Document): directions_data = { "origin": route[0], "destination": route[-1], - "waypoints": route[1: -1], - "optimize_waypoints": optimize + "waypoints": route[1:-1], + "optimize_waypoints": optimize, } try: @@ -243,7 +245,6 @@ class DeliveryTrip(Document): return directions[0] if directions else False - @frappe.whitelist() def get_contact_and_address(name): out = frappe._dict() @@ -265,7 +266,10 @@ def get_default_contact(out, name): dl.link_doctype="Customer" AND dl.link_name=%s AND dl.parenttype = "Contact" - """, (name), as_dict=1) + """, + (name), + as_dict=1, + ) if contact_persons: for out.contact_person in contact_persons: @@ -288,7 +292,10 @@ def get_default_address(out, name): dl.link_doctype="Customer" AND dl.link_name=%s AND dl.parenttype = "Address" - """, (name), as_dict=1) + """, + (name), + as_dict=1, + ) if shipping_addresses: for out.shipping_address in shipping_addresses: @@ -303,16 +310,18 @@ def get_default_address(out, name): @frappe.whitelist() def get_contact_display(contact): contact_info = frappe.db.get_value( - "Contact", contact, - ["first_name", "last_name", "phone", "mobile_no"], - as_dict=1) + "Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1 + ) - contact_info.html = """ %(first_name)s %(last_name)s
    %(phone)s
    %(mobile_no)s""" % { - "first_name": contact_info.first_name, - "last_name": contact_info.last_name or "", - "phone": contact_info.phone or "", - "mobile_no": contact_info.mobile_no or "" - } + contact_info.html = ( + """ %(first_name)s %(last_name)s
    %(phone)s
    %(mobile_no)s""" + % { + "first_name": contact_info.first_name, + "last_name": contact_info.last_name or "", + "phone": contact_info.phone or "", + "mobile_no": contact_info.mobile_no or "", + } + ) return contact_info.html @@ -322,19 +331,19 @@ def sanitize_address(address): Remove HTML breaks in a given address Args: - address (str): Address to be sanitized + address (str): Address to be sanitized Returns: - (str): Sanitized address + (str): Sanitized address """ if not address: return - address = address.split('
    ') + address = address.split("
    ") # Only get the first 3 blocks of the address - return ', '.join(address[:3]) + return ", ".join(address[:3]) @frappe.whitelist() @@ -349,11 +358,15 @@ def notify_customers(delivery_trip): email_recipients = [] for stop in delivery_trip.delivery_stops: - contact_info = frappe.db.get_value("Contact", stop.contact, ["first_name", "last_name", "email_id"], as_dict=1) + contact_info = frappe.db.get_value( + "Contact", stop.contact, ["first_name", "last_name", "email_id"], as_dict=1 + ) context.update({"items": []}) if stop.delivery_note: - items = frappe.get_all("Delivery Note Item", filters={"parent": stop.delivery_note, "docstatus": 1}, fields=["*"]) + items = frappe.get_all( + "Delivery Note Item", filters={"parent": stop.delivery_note, "docstatus": 1}, fields=["*"] + ) context.update({"items": items}) if contact_info and contact_info.email_id: @@ -363,10 +376,12 @@ def notify_customers(delivery_trip): dispatch_template_name = frappe.db.get_single_value("Delivery Settings", "dispatch_template") dispatch_template = frappe.get_doc("Email Template", dispatch_template_name) - frappe.sendmail(recipients=contact_info.email_id, + frappe.sendmail( + recipients=contact_info.email_id, subject=dispatch_template.subject, message=frappe.render_template(dispatch_template.response, context), - attachments=get_attachments(stop)) + attachments=get_attachments(stop), + ) stop.db_set("email_sent_to", contact_info.email_id) email_recipients.append(contact_info.email_id) @@ -379,29 +394,37 @@ def notify_customers(delivery_trip): def get_attachments(delivery_stop): - if not (frappe.db.get_single_value("Delivery Settings", "send_with_attachment") and delivery_stop.delivery_note): + if not ( + frappe.db.get_single_value("Delivery Settings", "send_with_attachment") + and delivery_stop.delivery_note + ): return [] dispatch_attachment = frappe.db.get_single_value("Delivery Settings", "dispatch_attachment") - attachments = frappe.attach_print("Delivery Note", delivery_stop.delivery_note, - file_name="Delivery Note", print_format=dispatch_attachment) + attachments = frappe.attach_print( + "Delivery Note", + delivery_stop.delivery_note, + file_name="Delivery Note", + print_format=dispatch_attachment, + ) return [attachments] + @frappe.whitelist() def get_driver_email(driver): employee = frappe.db.get_value("Driver", driver, "employee") email = frappe.db.get_value("Employee", employee, "prefered_email") return {"email": email} + @frappe.whitelist() def make_expense_claim(source_name, target_doc=None): - doc = get_mapped_doc("Delivery Trip", source_name, - {"Delivery Trip": { - "doctype": "Expense Claim", - "field_map": { - "name" : "delivery_trip" - } - }}, target_doc) + doc = get_mapped_doc( + "Delivery Trip", + source_name, + {"Delivery Trip": {"doctype": "Expense Claim", "field_map": {"name": "delivery_trip"}}}, + target_doc, + ) return doc diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index 321f48b2c59..555361afbcd 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, now_datetime, nowdate import erpnext @@ -12,10 +13,10 @@ from erpnext.stock.doctype.delivery_trip.delivery_trip import ( make_expense_claim, notify_customers, ) -from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address -class TestDeliveryTrip(ERPNextTestCase): +class TestDeliveryTrip(FrappeTestCase): def setUp(self): super().setUp() driver = create_driver() @@ -105,23 +106,21 @@ class TestDeliveryTrip(ERPNextTestCase): self.delivery_trip.save() self.assertEqual(self.delivery_trip.status, "Completed") + def create_address(driver): if not frappe.db.exists("Address", {"address_title": "_Test Address for Driver"}): - address = frappe.get_doc({ - "doctype": "Address", - "address_title": "_Test Address for Driver", - "address_type": "Office", - "address_line1": "Station Road", - "city": "_Test City", - "state": "Test State", - "country": "India", - "links":[ - { - "link_doctype": "Driver", - "link_name": driver.name - } - ] - }).insert(ignore_permissions=True) + address = frappe.get_doc( + { + "doctype": "Address", + "address_title": "_Test Address for Driver", + "address_type": "Office", + "address_line1": "Station Road", + "city": "_Test City", + "state": "Test State", + "country": "India", + "links": [{"link_doctype": "Driver", "link_name": driver.name}], + } + ).insert(ignore_permissions=True) frappe.db.set_value("Driver", driver.name, "address", address.name) @@ -129,49 +128,57 @@ def create_address(driver): return frappe.get_doc("Address", {"address_title": "_Test Address for Driver"}) + def create_driver(): if not frappe.db.exists("Driver", {"full_name": "Newton Scmander"}): - driver = frappe.get_doc({ - "doctype": "Driver", - "full_name": "Newton Scmander", - "cell_number": "98343424242", - "license_number": "B809", - }).insert(ignore_permissions=True) + driver = frappe.get_doc( + { + "doctype": "Driver", + "full_name": "Newton Scmander", + "cell_number": "98343424242", + "license_number": "B809", + } + ).insert(ignore_permissions=True) return driver return frappe.get_doc("Driver", {"full_name": "Newton Scmander"}) + def create_delivery_notification(): if not frappe.db.exists("Email Template", "Delivery Notification"): - dispatch_template = frappe.get_doc({ - 'doctype': 'Email Template', - 'name': 'Delivery Notification', - 'response': 'Test Delivery Trip', - 'subject': 'Test Subject', - 'owner': frappe.session.user - }) + dispatch_template = frappe.get_doc( + { + "doctype": "Email Template", + "name": "Delivery Notification", + "response": "Test Delivery Trip", + "subject": "Test Subject", + "owner": frappe.session.user, + } + ) dispatch_template.insert() delivery_settings = frappe.get_single("Delivery Settings") - delivery_settings.dispatch_template = 'Delivery Notification' + delivery_settings.dispatch_template = "Delivery Notification" delivery_settings.save() def create_vehicle(): if not frappe.db.exists("Vehicle", "JB 007"): - vehicle = frappe.get_doc({ - "doctype": "Vehicle", - "license_plate": "JB 007", - "make": "Maruti", - "model": "PCM", - "last_odometer": 5000, - "acquisition_date": nowdate(), - "location": "Mumbai", - "chassis_no": "1234ABCD", - "uom": "Litre", - "vehicle_value": flt(500000) - }) + vehicle = frappe.get_doc( + { + "doctype": "Vehicle", + "license_plate": "JB 007", + "make": "Maruti", + "model": "PCM", + "last_odometer": 5000, + "acquisition_date": nowdate(), + "location": "Mumbai", + "chassis_no": "1234ABCD", + "uom": "Litre", + "vehicle_value": flt(500000), + } + ) vehicle.insert() @@ -179,23 +186,27 @@ def create_delivery_trip(driver, address, contact=None): if not contact: contact = get_contact_and_address("_Test Customer") - delivery_trip = frappe.get_doc({ - "doctype": "Delivery Trip", - "company": erpnext.get_default_company(), - "departure_time": add_days(now_datetime(), 5), - "driver": driver.name, - "driver_address": address.name, - "vehicle": "JB 007", - "delivery_stops": [{ - "customer": "_Test Customer", - "address": contact.shipping_address.parent, - "contact": contact.contact_person.parent - }, + delivery_trip = frappe.get_doc( { - "customer": "_Test Customer", - "address": contact.shipping_address.parent, - "contact": contact.contact_person.parent - }] - }).insert(ignore_permissions=True) + "doctype": "Delivery Trip", + "company": erpnext.get_default_company(), + "departure_time": add_days(now_datetime(), 5), + "driver": driver.name, + "driver_address": address.name, + "vehicle": "JB 007", + "delivery_stops": [ + { + "customer": "_Test Customer", + "address": contact.shipping_address.parent, + "contact": contact.contact_person.parent, + }, + { + "customer": "_Test Customer", + "address": contact.shipping_address.parent, + "contact": contact.contact_person.parent, + }, + ], + } + ).insert(ignore_permissions=True) return delivery_trip diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 1ce09f0152c..de6316cc05d 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -55,10 +55,15 @@ frappe.ui.form.on("Item", { if (frm.doc.has_variants) { frm.set_intro(__("This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set"), true); + frm.add_custom_button(__("Show Variants"), function() { frappe.set_route("List", "Item", {"variant_of": frm.doc.name}); }, __("View")); + frm.add_custom_button(__("Item Variant Settings"), function() { + frappe.set_route("Form", "Item Variant Settings"); + }, __("View")); + frm.add_custom_button(__("Variant Details Report"), function() { frappe.set_route("query-report", "Item Variant Details", {"item": frm.doc.name}); }, __("View")); @@ -110,6 +115,13 @@ frappe.ui.form.on("Item", { } }); }, __('Actions')); + } else { + frm.add_custom_button(__("Website Item"), function() { + frappe.db.get_value("Website Item", {item_code: frm.doc.name}, "name", (d) => { + if (!d.name) frappe.throw(__("Website Item not found")); + frappe.set_route("Form", "Website Item", d.name); + }); + }, __("View")); } erpnext.item.edit_prices_button(frm); @@ -131,12 +143,6 @@ frappe.ui.form.on("Item", { frappe.set_route('Form', 'Item', new_item.name); }); - if(frm.doc.has_variants) { - frm.add_custom_button(__("Item Variant Settings"), function() { - frappe.set_route("Form", "Item Variant Settings"); - }, __("View")); - } - const stock_exists = (frm.doc.__onload && frm.doc.__onload.stock_exists) ? 1 : 0; @@ -165,21 +171,21 @@ frappe.ui.form.on("Item", { frm.set_value('has_batch_no', 0); frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); - frm.call({ - method: "set_asset_naming_series", - doc: frm.doc, - callback: function() { + frappe.call({ + method: "erpnext.stock.doctype.item.item.get_asset_naming_series", + callback: function(r) { frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1); - frm.trigger("set_asset_naming_series"); + frm.events.set_asset_naming_series(frm, r.message); } }); frm.trigger('auto_create_assets'); }, - set_asset_naming_series: function(frm) { - if (frm.doc.__onload && frm.doc.__onload.asset_naming_series) { - frm.set_df_property("asset_naming_series", "options", frm.doc.__onload.asset_naming_series); + set_asset_naming_series: function(frm, asset_naming_series) { + if ((frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series) { + let naming_series = (frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series; + frm.set_df_property("asset_naming_series", "options", naming_series); } }, @@ -371,6 +377,17 @@ $.extend(erpnext.item, { } } + frm.set_query('default_provisional_account', 'item_defaults', (doc, cdt, cdn) => { + let row = locals[cdt][cdn]; + return { + filters: { + "company": row.company, + "root_type": ["in", ["Liability", "Asset"]], + "is_group": 0 + } + }; + }); + }, make_dashboard: function(frm) { diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index d364d8a7d95..06baa0fe912 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -659,7 +659,6 @@ }, { "collapsible": 1, - "default": "eval:!doc.is_fixed_asset", "fieldname": "sales_details", "fieldtype": "Section Break", "label": "Sales Details", @@ -919,8 +918,9 @@ }, { "fieldname": "default_item_manufacturer", - "fieldtype": "Data", + "fieldtype": "Link", "label": "Default Item Manufacturer", + "options": "Manufacturer", "read_only": 1 }, { @@ -955,7 +955,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-14 04:13:16.857534", + "modified": "2022-04-28 04:52:10.272256", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1026,4 +1026,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 7bc875ac12f..65930a404b4 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -18,6 +18,7 @@ from frappe.utils import ( now_datetime, nowtime, strip, + strip_html, ) from frappe.utils.html_utils import clean_html @@ -44,21 +45,15 @@ class StockExistsForTemplate(frappe.ValidationError): class InvalidBarcode(frappe.ValidationError): pass + class DataValidationError(frappe.ValidationError): pass + class Item(Document): def onload(self): - self.set_onload('stock_exists', self.stock_ledger_created()) - self.set_asset_naming_series() - - @frappe.whitelist() - def set_asset_naming_series(self): - if not hasattr(self, '_asset_naming_series'): - from erpnext.assets.doctype.asset.asset import get_asset_naming_series - self._asset_naming_series = get_asset_naming_series() - - self.set_onload('asset_naming_series', self._asset_naming_series) + self.set_onload("stock_exists", self.stock_ledger_created()) + self.set_onload("asset_naming_series", get_asset_naming_series()) def autoname(self): if frappe.db.get_default("item_naming_by") == "Naming Series": @@ -68,19 +63,15 @@ class Item(Document): make_variant_item_code(self.variant_of, template_item_name, self) else: from frappe.model.naming import set_name_by_naming_series + set_name_by_naming_series(self) self.item_code = self.name self.item_code = strip(self.item_code) self.name = self.item_code - def before_insert(self): - if not self.description: - self.description = self.item_name - - def after_insert(self): - '''set opening stock and item price''' + """set opening stock and item price""" if self.standard_rate: for default in self.item_defaults or [frappe._dict()]: self.add_price(default.default_price_list) @@ -92,7 +83,7 @@ class Item(Document): if not self.item_name: self.item_name = self.item_code - if not self.description: + if not strip_html(cstr(self.description)).strip(): self.description = self.item_name self.validate_uom() @@ -116,6 +107,7 @@ class Item(Document): self.validate_variant_attributes() self.validate_variant_based_on_change() self.validate_fixed_asset() + self.clear_retain_sample() self.validate_retain_sample() self.validate_uom_conversion_factor() self.validate_customer_provided_part() @@ -136,8 +128,8 @@ class Item(Document): self.update_website_item() def validate_description(self): - '''Clean HTML description if set''' - if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): + """Clean HTML description if set""" + if cint(frappe.db.get_single_value("Stock Settings", "clean_description_html")): self.description = clean_html(self.description) def validate_customer_provided_part(self): @@ -149,24 +141,27 @@ class Item(Document): self.default_material_request_type = "Customer Provided" def add_price(self, price_list=None): - '''Add a new price''' + """Add a new price""" if not price_list: - price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list') - or frappe.db.get_value('Price List', _('Standard Selling'))) + price_list = frappe.db.get_single_value( + "Selling Settings", "selling_price_list" + ) or frappe.db.get_value("Price List", _("Standard Selling")) if price_list: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list, - "item_code": self.name, - "uom": self.stock_uom, - "brand": self.brand, - "currency": erpnext.get_default_currency(), - "price_list_rate": self.standard_rate - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list, + "item_code": self.name, + "uom": self.stock_uom, + "brand": self.brand, + "currency": erpnext.get_default_currency(), + "price_list_rate": self.standard_rate, + } + ) item_price.insert() def set_opening_stock(self): - '''set opening stock''' + """set opening stock""" if not self.is_stock_item or self.has_serial_no or self.has_batch_no: return @@ -179,19 +174,30 @@ class Item(Document): from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry # default warehouse, or Stores - for default in self.item_defaults or [frappe._dict({'company': frappe.defaults.get_defaults().company})]: - default_warehouse = (default.default_warehouse - or frappe.db.get_single_value('Stock Settings', 'default_warehouse')) + for default in self.item_defaults or [ + frappe._dict({"company": frappe.defaults.get_defaults().company}) + ]: + default_warehouse = default.default_warehouse or frappe.db.get_single_value( + "Stock Settings", "default_warehouse" + ) if default_warehouse: warehouse_company = frappe.db.get_value("Warehouse", default_warehouse, "company") if not default_warehouse or warehouse_company != default.company: - default_warehouse = frappe.db.get_value('Warehouse', - {'warehouse_name': _('Stores'), 'company': default.company}) + default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores"), "company": default.company} + ) if default_warehouse: - stock_entry = make_stock_entry(item_code=self.name, target=default_warehouse, qty=self.opening_stock, - rate=self.valuation_rate, company=default.company, posting_date=getdate(), posting_time=nowtime()) + stock_entry = make_stock_entry( + item_code=self.name, + target=default_warehouse, + qty=self.opening_stock, + rate=self.valuation_rate, + company=default.company, + posting_date=getdate(), + posting_time=nowtime(), + ) stock_entry.add_comment("Comment", _("Opening Stock")) @@ -209,14 +215,28 @@ class Item(Document): if not self.is_fixed_asset: asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) if asset: - frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')) + frappe.throw( + _('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item') + ) def validate_retain_sample(self): - if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'): + if self.retain_sample and not frappe.db.get_single_value( + "Stock Settings", "sample_retention_warehouse" + ): frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first")) if self.retain_sample and not self.has_batch_no: - frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( - self.item_code)) + frappe.throw( + _( + "{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item" + ).format(self.item_code) + ) + + def clear_retain_sample(self): + if not self.has_batch_no: + self.retain_sample = None + + if not self.retain_sample: + self.sample_quantity = None def add_default_uom_in_conversion_factor_table(self): if not self.is_new() and self.has_value_changed("stock_uom"): @@ -229,10 +249,7 @@ class Item(Document): uoms_list = [d.uom for d in self.get("uoms")] if self.stock_uom not in uoms_list: - self.append("uoms", { - "uom": self.stock_uom, - "conversion_factor": 1 - }) + self.append("uoms", {"uom": self.stock_uom, "conversion_factor": 1}) def update_website_item(self): """Update Website Item if change in Item impacts it.""" @@ -240,8 +257,7 @@ class Item(Document): if web_item: changed = {} - editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description", - "disabled"] + editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description", "disabled"] doc_before_save = self.get_doc_before_save() for field in editable_fields: @@ -259,7 +275,7 @@ class Item(Document): web_item_doc.save() def validate_item_tax_net_rate_range(self): - for tax in self.get('taxes'): + for tax in self.get("taxes"): if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate): frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate")) @@ -274,23 +290,31 @@ class Item(Document): if not self.get("reorder_levels"): for d in template.get("reorder_levels"): n = {} - for k in ("warehouse", "warehouse_reorder_level", - "warehouse_reorder_qty", "material_request_type"): + for k in ( + "warehouse", + "warehouse_reorder_level", + "warehouse_reorder_qty", + "material_request_type", + ): n[k] = d.get(k) self.append("reorder_levels", n) def validate_conversion_factor(self): check_list = [] - for d in self.get('uoms'): + for d in self.get("uoms"): if cstr(d.uom) in check_list: frappe.throw( - _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(d.uom)) + _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format( + d.uom + ) + ) else: check_list.append(cstr(d.uom)) if d.uom and cstr(d.uom) == cstr(self.stock_uom) and flt(d.conversion_factor) != 1: frappe.throw( - _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx)) + _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx) + ) def validate_item_type(self): if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset: @@ -303,28 +327,32 @@ class Item(Document): for field in ["serial_no_series", "batch_number_series"]: series = self.get(field) if series and "#" in series and "." not in series: - frappe.throw(_("Invalid naming series (. missing) for {0}") - .format(frappe.bold(self.meta.get_field(field).label))) + frappe.throw( + _("Invalid naming series (. missing) for {0}").format( + frappe.bold(self.meta.get_field(field).label) + ) + ) def check_for_active_boms(self): if self.default_bom: bom_item = frappe.db.get_value("BOM", self.default_bom, "item") if bom_item not in (self.name, self.variant_of): frappe.throw( - _("Default BOM ({0}) must be active for this item or its template").format(bom_item)) + _("Default BOM ({0}) must be active for this item or its template").format(bom_item) + ) def fill_customer_code(self): """ - Append all the customer codes and insert into "customer_code" field of item table. - Used to search Item by customer code. + Append all the customer codes and insert into "customer_code" field of item table. + Used to search Item by customer code. """ customer_codes = set(d.ref_code for d in self.get("customer_items", [])) - self.customer_code = ','.join(customer_codes) + self.customer_code = ",".join(customer_codes) def check_item_tax(self): """Check whether Tax Rate is not entered twice for same Tax Type""" check_list = [] - for d in self.get('taxes'): + for d in self.get("taxes"): if d.item_tax_template: if d.item_tax_template in check_list: frappe.throw(_("{0} entered twice in Item Tax").format(d.item_tax_template)) @@ -333,24 +361,39 @@ class Item(Document): def validate_barcode(self): from stdnum import ean + if len(self.barcodes) > 0: for item_barcode in self.barcodes: - options = frappe.get_meta("Item Barcode").get_options("barcode_type").split('\n') + options = frappe.get_meta("Item Barcode").get_options("barcode_type").split("\n") if item_barcode.barcode: duplicate = frappe.db.sql( - """select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) + """select parent from `tabItem Barcode` where barcode = %s and parent != %s""", + (item_barcode.barcode, self.name), + ) if duplicate: - frappe.throw(_("Barcode {0} already used in Item {1}").format( - item_barcode.barcode, duplicate[0][0])) + frappe.throw( + _("Barcode {0} already used in Item {1}").format(item_barcode.barcode, duplicate[0][0]) + ) - item_barcode.barcode_type = "" if item_barcode.barcode_type not in options else item_barcode.barcode_type - if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ('EAN', 'UPC-A', 'EAN-13', 'EAN-8'): + item_barcode.barcode_type = ( + "" if item_barcode.barcode_type not in options else item_barcode.barcode_type + ) + if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ( + "EAN", + "UPC-A", + "EAN-13", + "EAN-8", + ): if not ean.is_valid(item_barcode.barcode): - frappe.throw(_("Barcode {0} is not a valid {1} code").format( - item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode) + frappe.throw( + _("Barcode {0} is not a valid {1} code").format( + item_barcode.barcode, item_barcode.barcode_type + ), + InvalidBarcode, + ) def validate_warehouse_for_reorder(self): - '''Validate Reorder level table for duplicate and conditional mandatory''' + """Validate Reorder level table for duplicate and conditional mandatory""" warehouse = [] for d in self.get("reorder_levels"): if not d.warehouse_group: @@ -358,20 +401,30 @@ class Item(Document): if d.get("warehouse") and d.get("warehouse") not in warehouse: warehouse += [d.get("warehouse")] else: - frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}") - .format(d.idx, d.warehouse), DuplicateReorderRows) + frappe.throw( + _("Row {0}: An Reorder entry already exists for this warehouse {1}").format( + d.idx, d.warehouse + ), + DuplicateReorderRows, + ) if d.warehouse_reorder_level and not d.warehouse_reorder_qty: frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) def stock_ledger_created(self): - if not hasattr(self, '_stock_ledger_created'): - self._stock_ledger_created = len(frappe.db.sql("""select name from `tabStock Ledger Entry` - where item_code = %s and is_cancelled = 0 limit 1""", self.name)) + if not hasattr(self, "_stock_ledger_created"): + self._stock_ledger_created = len( + frappe.db.sql( + """select name from `tabStock Ledger Entry` + where item_code = %s and is_cancelled = 0 limit 1""", + self.name, + ) + ) return self._stock_ledger_created def update_item_price(self): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabItem Price` SET item_name=%(item_name)s, @@ -383,8 +436,8 @@ class Item(Document): item_name=self.item_name, item_description=self.description, brand=self.brand, - item_code=self.name - ) + item_code=self.name, + ), ) def on_trash(self): @@ -401,12 +454,16 @@ class Item(Document): self.validate_properties_before_merge(new_name) self.validate_duplicate_product_bundles_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name) + self.delete_old_bins(old_name) def after_rename(self, old_name, new_name, merge): if merge: self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name) - frappe.msgprint(_("It can take upto few hours for accurate stock values to be visible after merging items."), - indicator="orange", title="Note") + frappe.msgprint( + _("It can take upto few hours for accurate stock values to be visible after merging items."), + indicator="orange", + title="Note", + ) if self.published_in_website: invalidate_cache_for_item(self) @@ -418,36 +475,54 @@ class Item(Document): self.recalculate_bin_qty(new_name) for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"): - for d in frappe.db.sql("""select name, item_wise_tax_detail from `tab{0}` - where ifnull(item_wise_tax_detail, '') != ''""".format(dt), as_dict=1): + for d in frappe.db.sql( + """select name, item_wise_tax_detail from `tab{0}` + where ifnull(item_wise_tax_detail, '') != ''""".format( + dt + ), + as_dict=1, + ): item_wise_tax_detail = json.loads(d.item_wise_tax_detail) if isinstance(item_wise_tax_detail, dict) and old_name in item_wise_tax_detail: item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name] item_wise_tax_detail.pop(old_name) - frappe.db.set_value(dt, d.name, "item_wise_tax_detail", - json.dumps(item_wise_tax_detail), update_modified=False) + frappe.db.set_value( + dt, d.name, "item_wise_tax_detail", json.dumps(item_wise_tax_detail), update_modified=False + ) + + def delete_old_bins(self, old_name): + frappe.db.delete("Bin", {"item_code": old_name}) def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name): - records = frappe.db.sql(""" SELECT parent, COUNT(*) as records + records = frappe.db.sql( + """ SELECT parent, COUNT(*) as records FROM `tabStock Reconciliation Item` WHERE item_code = %s and docstatus = 1 GROUP By item_code, warehouse, parent HAVING records > 1 - """, new_name, as_dict=1) + """, + new_name, + as_dict=1, + ) - if not records: return + if not records: + return document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations") msg = _("The items {0} and {1} are present in the following {2} :").format( - frappe.bold(old_name), frappe.bold(new_name), document) + frappe.bold(old_name), frappe.bold(new_name), document + ) - msg += '
    ' - msg += ', '.join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "

    " + msg += "
    " + msg += ( + ", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "

    " + ) - msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format( - frappe.bold(old_name)) + msg += _( + "Note: To merge the items, create a separate Stock Reconciliation for the old item {0}" + ).format(frappe.bold(old_name)) frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) @@ -466,8 +541,8 @@ class Item(Document): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): "Block merge if both old and new items have product bundles." - old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) - new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name}) + old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name}) + new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name}) if old_bundle and new_bundle: bundle_link = get_link_to_form("Product Bundle", old_bundle) @@ -480,15 +555,14 @@ class Item(Document): def validate_duplicate_website_item_before_merge(self, old_name, new_name): """ - Block merge if both old and new items have website items against them. - This is to avoid duplicate website items after merging. + Block merge if both old and new items have website items against them. + This is to avoid duplicate website items after merging. """ web_items = frappe.get_all( "Website Item", - filters={ - "item_code": ["in", [old_name, new_name]] - }, - fields=["item_code", "name"]) + filters={"item_code": ["in", [old_name, new_name]]}, + fields=["item_code", "name"], + ) if len(web_items) <= 1: return @@ -506,42 +580,61 @@ class Item(Document): def recalculate_bin_qty(self, new_name): from erpnext.stock.stock_balance import repost_stock - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + + existing_allow_negative_stock = frappe.db.get_value( + "Stock Settings", None, "allow_negative_stock" + ) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse - from tabBin where item_code=%s""", new_name) + repost_stock_for_warehouses = frappe.get_all( + "Stock Ledger Entry", + "warehouse", + filters={"item_code": new_name}, + pluck="warehouse", + distinct=True, + ) # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where item_code=%s", new_name) + frappe.db.delete("Bin", {"item_code": new_name}) for warehouse in repost_stock_for_warehouses: repost_stock(new_name, warehouse) - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) + frappe.db.set_value( + "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock + ) def update_bom_item_desc(self): if self.is_new(): return - if self.db_get('description') != self.description: - frappe.db.sql(""" + if self.db_get("description") != self.description: + frappe.db.sql( + """ update `tabBOM` set description = %s where item = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabBOM Item` set description = %s where item_code = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabBOM Explosion Item` set description = %s where item_code = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) def validate_item_defaults(self): companies = {row.company for row in self.item_defaults} @@ -551,41 +644,61 @@ class Item(Document): validate_item_default_company_links(self.item_defaults) - def update_defaults_from_item_group(self): """Get defaults from Item Group""" if self.item_defaults or not self.item_group: return - item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group}, - ['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier', - 'expense_account','selling_cost_center','income_account'], as_dict = 1) + item_defaults = frappe.db.get_values( + "Item Default", + {"parent": self.item_group}, + [ + "company", + "default_warehouse", + "default_price_list", + "buying_cost_center", + "default_supplier", + "expense_account", + "selling_cost_center", + "income_account", + ], + as_dict=1, + ) if item_defaults: for item in item_defaults: - self.append('item_defaults', { - 'company': item.company, - 'default_warehouse': item.default_warehouse, - 'default_price_list': item.default_price_list, - 'buying_cost_center': item.buying_cost_center, - 'default_supplier': item.default_supplier, - 'expense_account': item.expense_account, - 'selling_cost_center': item.selling_cost_center, - 'income_account': item.income_account - }) + self.append( + "item_defaults", + { + "company": item.company, + "default_warehouse": item.default_warehouse, + "default_price_list": item.default_price_list, + "buying_cost_center": item.buying_cost_center, + "default_supplier": item.default_supplier, + "expense_account": item.expense_account, + "selling_cost_center": item.selling_cost_center, + "income_account": item.income_account, + }, + ) else: defaults = frappe.defaults.get_defaults() or {} # To check default warehouse is belong to the default company - if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse", - {'name': defaults.default_warehouse, 'company': defaults.company}): - self.append("item_defaults", { - "company": defaults.get("company"), - "default_warehouse": defaults.default_warehouse - }) + if ( + defaults.get("default_warehouse") + and defaults.company + and frappe.db.exists( + "Warehouse", {"name": defaults.default_warehouse, "company": defaults.company} + ) + ): + self.append( + "item_defaults", + {"company": defaults.get("company"), "default_warehouse": defaults.default_warehouse}, + ) def update_variants(self): - if self.flags.dont_update_variants or \ - frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): + if self.flags.dont_update_variants or frappe.db.get_single_value( + "Item Variant Settings", "do_not_update_variants" + ): return if self.has_variants: variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name}) @@ -594,8 +707,13 @@ class Item(Document): update_variants(variants, self, publish_progress=False) frappe.msgprint(_("Item Variants updated")) else: - frappe.enqueue("erpnext.stock.doctype.item.item.update_variants", - variants=variants, template=self, now=frappe.flags.in_test, timeout=600) + frappe.enqueue( + "erpnext.stock.doctype.item.item.update_variants", + variants=variants, + template=self, + now=frappe.flags.in_test, + timeout=600, + ) def validate_has_variants(self): if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): @@ -627,11 +745,8 @@ class Item(Document): # fetch all attributes of these items item_attributes = frappe.get_all( "Item Variant Attribute", - filters={ - "parent": ["in", items], - "attribute": ["in", deleted_attribute] - }, - fields=["attribute", "parent"] + filters={"parent": ["in", items], "attribute": ["in", deleted_attribute]}, + fields=["attribute", "parent"], ) not_included = defaultdict(list) @@ -650,14 +765,18 @@ class Item(Document): return """ {0} {1} - """.format(title, body) + """.format( + title, body + ) - rows = '' + rows = "" for docname, attr_list in not_included.items(): link = "{0}".format(frappe.bold(_(docname))) rows += table_row(link, body(attr_list)) - error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.') + error_description = _( + "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." + ) message = """
    {0}

    @@ -668,25 +787,37 @@ class Item(Document): {3} - """.format(error_description, _('Variant Items'), _('Attributes'), rows) + """.format( + error_description, _("Variant Items"), _("Attributes"), rows + ) frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True) - def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: - if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) - or self._doc_before_save.variant_of != self.variant_of): - frappe.throw(_("Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.").format(self.name), - StockExistsForTemplate) + if ( + cint(self._doc_before_save.has_variants) != cint(self.has_variants) + or self._doc_before_save.variant_of != self.variant_of + ): + frappe.throw( + _( + "Cannot change Variant properties after stock transaction. You will have to make a new Item to do this." + ).format(self.name), + StockExistsForTemplate, + ) if self.has_variants or self.variant_of: - if not self.is_child_table_same('attributes'): + if not self.is_child_table_same("attributes"): frappe.throw( - _('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item')) + _( + "Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item" + ) + ) def validate_variant_based_on_change(self): - if not self.is_new() and (self.variant_of or (self.has_variants and frappe.get_all("Item", {"variant_of": self.name}))): + if not self.is_new() and ( + self.variant_of or (self.has_variants and frappe.get_all("Item", {"variant_of": self.name})) + ): if self.variant_based_on != frappe.db.get_value("Item", self.name, "variant_based_on"): frappe.throw(_("Variant Based On cannot be changed")) @@ -699,8 +830,11 @@ class Item(Document): if self.variant_of: template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom") if template_uom != self.stock_uom: - frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'") - .format(self.stock_uom, template_uom)) + frappe.throw( + _("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'").format( + self.stock_uom, template_uom + ) + ) def validate_uom_conversion_factor(self): if self.uoms: @@ -714,21 +848,22 @@ class Item(Document): return if not self.variant_based_on: - self.variant_based_on = 'Item Attribute' + self.variant_based_on = "Item Attribute" - if self.variant_based_on == 'Item Attribute': + if self.variant_based_on == "Item Attribute": attributes = [] if not self.attributes: frappe.throw(_("Attribute table is mandatory")) for d in self.attributes: if d.attribute in attributes: frappe.throw( - _("Attribute {0} selected multiple times in Attributes Table").format(d.attribute)) + _("Attribute {0} selected multiple times in Attributes Table").format(d.attribute) + ) else: attributes.append(d.attribute) def validate_variant_attributes(self): - if self.is_new() and self.variant_of and self.variant_based_on == 'Item Attribute': + if self.is_new() and self.variant_of and self.variant_based_on == "Item Attribute": # remove attributes with no attribute_value set self.attributes = [d for d in self.attributes if cstr(d.attribute_value).strip()] @@ -739,8 +874,9 @@ class Item(Document): variant = get_variant(self.variant_of, args, self.name) if variant: - frappe.throw(_("Item variant {0} exists with same attributes") - .format(variant), ItemVariantExistsError) + frappe.throw( + _("Item variant {0} exists with same attributes").format(variant), ItemVariantExistsError + ) validate_item_variant_attributes(self, args) @@ -755,31 +891,52 @@ class Item(Document): fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no") 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 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)))) + 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", - "Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"] + linked_doctypes = [ + "Delivery Note Item", + "Sales Invoice Item", + "POS Invoice Item", + "Purchase Receipt Item", + "Purchase Invoice Item", + "Stock Entry Detail", + "Stock Reconciliation Item", + ] # For "Is Stock Item", following doctypes is important # because reserved_qty, ordered_qty and requested_qty updated from these doctypes if field == "is_stock_item": - linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item", "Product Bundle"] + linked_doctypes += [ + "Sales Order Item", + "Purchase Order Item", + "Material Request Item", + "Product Bundle", + ] for doctype in linked_doctypes: - filters={"item_code": self.name, "docstatus": 1} + filters = {"item_code": self.name, "docstatus": 1} if doctype == "Product Bundle": - filters={"new_item_code": self.name} + filters = {"new_item_code": self.name} - if doctype in ("Purchase Invoice Item", "Sales Invoice Item",): + if doctype in ( + "Purchase Invoice Item", + "Sales Invoice Item", + ): # If Invoice has Stock impact, only then consider it. if self.stock_ledger_created(): return True @@ -789,37 +946,48 @@ class Item(Document): def validate_auto_reorder_enabled_in_stock_settings(self): if self.reorder_levels: - enabled = frappe.db.get_single_value('Stock Settings', 'auto_indent') + enabled = frappe.db.get_single_value("Stock Settings", "auto_indent") if not enabled: - frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange") + frappe.msgprint( + msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), + title=_("Enable Auto Re-Order"), + indicator="orange", + ) def make_item_price(item, price_list_name, item_price): - frappe.get_doc({ - 'doctype': 'Item Price', - 'price_list': price_list_name, - 'item_code': item, - 'price_list_rate': item_price - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list_name, + "item_code": item, + "price_list_rate": item_price, + } + ).insert() + def get_timeline_data(doctype, name): """get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page.""" - items = frappe.db.sql("""select unix_timestamp(posting_date), 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) + group by posting_date""", + name, + ) return dict(items) - 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(): - frappe.throw(_("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life))) + frappe.throw( + _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) + ) if disabled: frappe.throw(_("Item {0} is disabled").format(item_code)) @@ -840,11 +1008,13 @@ def validate_cancelled_item(item_code, docstatus=None): if docstatus == 2: 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""" # get last purchase order item details - last_purchase_order = frappe.db.sql("""\ + last_purchase_order = frappe.db.sql( + """\ select po.name, po.transaction_date, po.conversion_rate, po_item.conversion_factor, po_item.base_price_list_rate, po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate @@ -852,11 +1022,14 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and po.name = po_item.parent order by po.transaction_date desc, po.name desc - limit 1""", (item_code, cstr(doc_name)), as_dict=1) - + limit 1""", + (item_code, cstr(doc_name)), + as_dict=1, + ) # get last purchase receipt item details - last_purchase_receipt = frappe.db.sql("""\ + last_purchase_receipt = frappe.db.sql( + """\ select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate, pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage, pr_item.base_rate, pr_item.base_net_rate @@ -864,20 +1037,29 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and pr.name = pr_item.parent order by pr.posting_date desc, pr.posting_time desc, pr.name desc - limit 1""", (item_code, cstr(doc_name)), as_dict=1) + limit 1""", + (item_code, cstr(doc_name)), + as_dict=1, + ) - purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date - or "1900-01-01") - purchase_receipt_date = getdate(last_purchase_receipt and - last_purchase_receipt[0].posting_date or "1900-01-01") + purchase_order_date = getdate( + last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01" + ) + purchase_receipt_date = getdate( + last_purchase_receipt and last_purchase_receipt[0].posting_date or "1900-01-01" + ) - if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt): + if last_purchase_order and ( + purchase_order_date >= purchase_receipt_date or not last_purchase_receipt + ): # use purchase order last_purchase = last_purchase_order[0] purchase_date = purchase_order_date - elif last_purchase_receipt and (purchase_receipt_date > purchase_order_date or not last_purchase_order): + elif last_purchase_receipt and ( + purchase_receipt_date > purchase_order_date or not last_purchase_order + ): # use purchase receipt last_purchase = last_purchase_receipt[0] purchase_date = purchase_receipt_date @@ -886,22 +1068,25 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): return frappe._dict() conversion_factor = flt(last_purchase.conversion_factor) - out = frappe._dict({ - "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, - "base_rate": flt(last_purchase.base_rate) / conversion_factor, - "base_net_rate": flt(last_purchase.base_net_rate) / conversion_factor, - "discount_percentage": flt(last_purchase.discount_percentage), - "purchase_date": purchase_date - }) - + out = frappe._dict( + { + "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, + "base_rate": flt(last_purchase.base_rate) / conversion_factor, + "base_net_rate": flt(last_purchase.base_net_rate) / conversion_factor, + "discount_percentage": flt(last_purchase.discount_percentage), + "purchase_date": purchase_date, + } + ) conversion_rate = flt(conversion_rate) or 1.0 - out.update({ - "price_list_rate": out.base_price_list_rate / conversion_rate, - "rate": out.base_rate / conversion_rate, - "base_rate": out.base_rate, - "base_net_rate": out.base_net_rate - }) + out.update( + { + "price_list_rate": out.base_price_list_rate / conversion_rate, + "rate": out.base_rate / conversion_rate, + "base_rate": out.base_rate, + "base_net_rate": out.base_net_rate, + } + ) return out @@ -924,39 +1109,51 @@ def invalidate_item_variants_cache_for_website(doc): is_web_item = doc.get("published_in_website") or doc.get("published") if doc.has_variants and is_web_item: item_code = doc.item_code - elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'published_in_website'): + elif doc.variant_of and frappe.db.get_value("Item", doc.variant_of, "published_in_website"): item_code = doc.variant_of if item_code: item_cache = ItemVariantsCacheManager(item_code) item_cache.rebuild_cache() + def check_stock_uom_with_bin(item, stock_uom): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): return - ref_uom = frappe.db.get_value("Stock Ledger Entry", - {"item_code": item}, "stock_uom") + ref_uom = frappe.db.get_value("Stock Ledger Entry", {"item_code": item}, "stock_uom") if ref_uom: if cstr(ref_uom) != cstr(stock_uom): - 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)) + 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) + ) - bin_list = frappe.db.sql(""" + 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) + """, + (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)) + 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)) def get_item_defaults(item_code, company): - item = frappe.get_cached_doc('Item', item_code) + item = frappe.get_cached_doc("Item", item_code) out = item.as_dict() @@ -967,8 +1164,9 @@ def get_item_defaults(item_code, company): out.update(row) return out + def set_item_default(item_code, company, fieldname, value): - item = frappe.get_cached_doc('Item', item_code) + item = frappe.get_cached_doc("Item", item_code) for d in item.item_defaults: if d.company == company: @@ -977,10 +1175,11 @@ def set_item_default(item_code, company, fieldname, value): return # no row found, add a new row for the company - d = item.append('item_defaults', {fieldname: value, "company": company}) + d = item.append("item_defaults", {fieldname: value, "company": company}) d.db_insert() item.clear_cache() + @frappe.whitelist() def get_item_details(item_code, company=None): out = frappe._dict() @@ -992,30 +1191,36 @@ def get_item_details(item_code, company=None): return out + @frappe.whitelist() def get_uom_conv_factor(uom, stock_uom): - """ Get UOM conversion factor from uom to stock_uom - e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0 + """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 - from_uom, to_uom = uom, stock_uom # renaming for readability + from_uom, to_uom = uom, stock_uom # renaming for readability - exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_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 - inverse_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1) + 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(""" + # 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 @@ -1024,7 +1229,10 @@ def get_uom_conv_factor(uom, stock_uom): 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) + """, + {"to_uom": to_uom, "from_uom": from_uom}, + as_dict=1, + ) if intermediate_match: return intermediate_match[0].value @@ -1036,8 +1244,12 @@ def get_item_attribute(parent, attribute_value=""): if not frappe.has_permission("Item"): frappe.throw(_("No Permission")) - return frappe.get_all("Item Attribute Value", fields = ["attribute_value"], - filters = {'parent': parent, 'attribute_value': ("like", f"%{attribute_value}%")}) + return frappe.get_all( + "Item Attribute Value", + fields=["attribute_value"], + filters={"parent": parent, "attribute_value": ("like", f"%{attribute_value}%")}, + ) + def update_variants(variants, template, publish_progress=True): total = len(variants) @@ -1048,6 +1260,7 @@ def update_variants(variants, template, publish_progress=True): if publish_progress: frappe.publish_progress(count / total * 100, title=_("Updating Variants...")) + @erpnext.allow_regional def set_item_tax_from_hsn_code(item): pass @@ -1056,20 +1269,29 @@ def set_item_tax_from_hsn_code(item): def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None: for item_default in item_defaults: for doctype, field in [ - ['Warehouse', 'default_warehouse'], - ['Cost Center', 'buying_cost_center'], - ['Cost Center', 'selling_cost_center'], - ['Account', 'expense_account'], - ['Account', 'income_account'] + ["Warehouse", "default_warehouse"], + ["Cost Center", "buying_cost_center"], + ["Cost Center", "selling_cost_center"], + ["Account", "expense_account"], + ["Account", "income_account"], ]: if item_default.get(field): - company = frappe.db.get_value(doctype, item_default.get(field), 'company', cache=True) + company = frappe.db.get_value(doctype, item_default.get(field), "company", cache=True) if company and company != item_default.company: - frappe.throw(_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.") - .format( + frappe.throw( + _("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.").format( item_default.idx, doctype, frappe.bold(item_default.get(field)), frappe.bold(item_default.company), - frappe.bold(frappe.unscrub(field)) - ), title=_("Invalid Item Defaults")) + frappe.bold(frappe.unscrub(field)), + ), + title=_("Invalid Item Defaults"), + ) + + +@frappe.whitelist() +def get_asset_naming_series(): + from erpnext.assets.doctype.asset.asset import get_asset_naming_series + + return get_asset_naming_series() diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py index b0f1bbc2d52..3caed02d69b 100644 --- a/erpnext/stock/doctype/item/item_dashboard.py +++ b/erpnext/stock/doctype/item/item_dashboard.py @@ -1,48 +1,37 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on stock movement. See {0} for details')\ - .format('' + _('Stock Ledger') + ''), - 'fieldname': 'item_code', - 'non_standard_fieldnames': { - 'Work Order': 'production_item', - 'Product Bundle': 'new_item_code', - 'BOM': 'item', - 'Batch': 'item' + "heatmap": True, + "heatmap_message": _("This is based on stock movement. See {0} for details").format( + '' + _("Stock Ledger") + "" + ), + "fieldname": "item_code", + "non_standard_fieldnames": { + "Work Order": "production_item", + "Product Bundle": "new_item_code", + "BOM": "item", + "Batch": "item", }, - 'transactions': [ + "transactions": [ + {"label": _("Groups"), "items": ["BOM", "Product Bundle", "Item Alternative"]}, + {"label": _("Pricing"), "items": ["Item Price", "Pricing Rule"]}, + {"label": _("Sell"), "items": ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]}, { - 'label': _('Groups'), - 'items': ['BOM', 'Product Bundle', 'Item Alternative'] + "label": _("Buy"), + "items": [ + "Material Request", + "Supplier Quotation", + "Request for Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ], }, - { - 'label': _('Pricing'), - 'items': ['Item Price', 'Pricing Rule'] - }, - { - 'label': _('Sell'), - 'items': ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Buy'), - 'items': ['Material Request', 'Supplier Quotation', 'Request for Quotation', - 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] - }, - { - 'label': _('Manufacture'), - 'items': ['Production Plan', 'Work Order', 'Item Manufacturer'] - }, - { - 'label': _('Traceability'), - 'items': ['Serial No', 'Batch'] - }, - { - 'label': _('Move'), - 'items': ['Stock Entry'] - } - ] + {"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]}, + {"label": _("Traceability"), "items": ["Serial No", "Batch"]}, + {"label": _("Move"), "items": ["Stock Entry"]}, + {"label": _("E-commerce"), "items": ["Website Item"]}, + ], } diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index c912101a4ac..29d6bf2df8b 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -6,6 +6,7 @@ import json import frappe from frappe.test_runner import make_test_objects +from frappe.tests.utils import FrappeTestCase, change_settings from erpnext.controllers.item_variant import ( InvalidItemAttributeValueError, @@ -24,22 +25,27 @@ from erpnext.stock.doctype.item.item import ( ) 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 ERPNextTestCase, change_settings test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] -def make_item(item_code, properties=None): + +def make_item(item_code=None, properties=None): + if not item_code: + item_code = frappe.generate_hash(length=16) + if frappe.db.exists("Item", item_code): return frappe.get_doc("Item", item_code) - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) @@ -52,7 +58,8 @@ def make_item(item_code, properties=None): return item -class TestItem(ERPNextTestCase): + +class TestItem(FrappeTestCase): def setUp(self): super().setUp() frappe.flags.attribute_values = None @@ -94,56 +101,91 @@ class TestItem(ERPNextTestCase): make_test_objects("Item Price") company = "_Test Company" - currency = frappe.get_cached_value("Company", company, "default_currency") + currency = frappe.get_cached_value("Company", company, "default_currency") - details = get_item_details({ - "item_code": "_Test Item", - "company": company, - "price_list": "_Test Price List", - "currency": currency, - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": currency, - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "conversion_factor": 1, - "price_list_uom_dependant": 1, - "ignore_pricing_rule": 1 - }) + details = get_item_details( + { + "item_code": "_Test Item", + "company": company, + "price_list": "_Test Price List", + "currency": currency, + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": currency, + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "conversion_factor": 1, + "price_list_uom_dependant": 1, + "ignore_pricing_rule": 1, + } + ) for key, value in to_check.items(): self.assertEqual(value, details.get(key)) def test_item_tax_template(self): expected_item_tax_template = [ - {"item_code": "_Test Item With Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, - {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 15 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"}, - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Item Tax Template 1 - _TC"}, - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + }, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 15 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 20 - _TC", + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Item Tax Template 1 - _TC", + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, ] expected_item_tax_map = { @@ -152,43 +194,55 @@ class TestItem(ERPNextTestCase): "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12}, "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15}, "_Test Account Excise Duty @ 20 - _TC": {"_Test Account Excise Duty - _TC": 20}, - "_Test Item Tax Template 1 - _TC": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10, - "_Test Account S&H Education Cess - _TC": 15} + "_Test Item Tax Template 1 - _TC": { + "_Test Account Excise Duty - _TC": 5, + "_Test Account Education Cess - _TC": 10, + "_Test Account S&H Education Cess - _TC": 15, + }, } for data in expected_item_tax_template: - details = get_item_details({ - "item_code": data['item_code'], - "tax_category": data['tax_category'], - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "conversion_factor": 1, - "price_list_uom_dependant": 1, - "ignore_pricing_rule": 1 - }) + details = get_item_details( + { + "item_code": data["item_code"], + "tax_category": data["tax_category"], + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "conversion_factor": 1, + "price_list_uom_dependant": 1, + "ignore_pricing_rule": 1, + } + ) - self.assertEqual(details.item_tax_template, data['item_tax_template']) - self.assertEqual(json.loads(details.item_tax_rate), expected_item_tax_map[details.item_tax_template]) + self.assertEqual(details.item_tax_template, data["item_tax_template"]) + self.assertEqual( + json.loads(details.item_tax_rate), expected_item_tax_map[details.item_tax_template] + ) def test_item_defaults(self): frappe.delete_doc_if_exists("Item", "Test Item With Defaults", force=1) - make_item("Test Item With Defaults", { - "item_group": "_Test Item Group", - "brand": "_Test Brand With Item Defaults", - "item_defaults": [{ - "company": "_Test Company", - "default_warehouse": "_Test Warehouse 2 - _TC", # no override - "expense_account": "_Test Account Stock Expenses - _TC", # override brand default - "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default - }] - }) + make_item( + "Test Item With Defaults", + { + "item_group": "_Test Item Group", + "brand": "_Test Brand With Item Defaults", + "item_defaults": [ + { + "company": "_Test Company", + "default_warehouse": "_Test Warehouse 2 - _TC", # no override + "expense_account": "_Test Account Stock Expenses - _TC", # override brand default + "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default + } + ], + }, + ) sales_item_check = { "item_code": "Test Item With Defaults", @@ -197,17 +251,19 @@ class TestItem(ERPNextTestCase): "expense_account": "_Test Account Stock Expenses - _TC", # from item "cost_center": "_Test Cost Center 2 - _TC", # from item group } - sales_item_details = get_item_details({ - "item_code": "Test Item With Defaults", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "customer": "_Test Customer", - }) + sales_item_details = get_item_details( + { + "item_code": "Test Item With Defaults", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "customer": "_Test Customer", + } + ) for key, value in sales_item_check.items(): self.assertEqual(value, sales_item_details.get(key)) @@ -216,38 +272,47 @@ class TestItem(ERPNextTestCase): "warehouse": "_Test Warehouse 2 - _TC", # from item "expense_account": "_Test Account Stock Expenses - _TC", # from item "income_account": "_Test Account Sales - _TC", # from brand - "cost_center": "_Test Write Off Cost Center - _TC" # from item + "cost_center": "_Test Write Off Cost Center - _TC", # from item } - purchase_item_details = get_item_details({ - "item_code": "Test Item With Defaults", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Purchase Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "supplier": "_Test Supplier", - }) + purchase_item_details = get_item_details( + { + "item_code": "Test Item With Defaults", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Purchase Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "supplier": "_Test Supplier", + } + ) for key, value in purchase_item_check.items(): self.assertEqual(value, purchase_item_details.get(key)) def test_item_default_validations(self): with self.assertRaises(frappe.ValidationError) as ve: - make_item("Bad Item defaults", { - "item_group": "_Test Item Group", - "item_defaults": [{ - "company": "_Test Company 1", - "default_warehouse": "_Test Warehouse - _TC", - "expense_account": "Stock In Hand - _TC", - "buying_cost_center": "_Test Cost Center - _TC", - "selling_cost_center": "_Test Cost Center - _TC", - }] - }) + make_item( + "Bad Item defaults", + { + "item_group": "_Test Item Group", + "item_defaults": [ + { + "company": "_Test Company 1", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "Stock In Hand - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + } + ], + }, + ) - self.assertTrue("belong to company" in str(ve.exception).lower(), - msg="Mismatching company entities in item defaults should not be allowed.") + self.assertTrue( + "belong to company" in str(ve.exception).lower(), + msg="Mismatching company entities in item defaults should not be allowed.", + ) def test_item_attribute_change_after_variant(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) @@ -255,7 +320,7 @@ class TestItem(ERPNextTestCase): variant = create_variant("_Test Variant Item", {"Test Size": "Large"}) variant.save() - attribute = frappe.get_doc('Item Attribute', 'Test Size') + attribute = frappe.get_doc("Item Attribute", "Test Size") attribute.item_attribute_values = [] # reset flags @@ -278,20 +343,18 @@ class TestItem(ERPNextTestCase): def test_copy_fields_from_template_to_variants(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-XL", force=1) - fields = [{'field_name': 'item_group'}, {'field_name': 'is_stock_item'}] - allow_fields = [d.get('field_name') for d in fields] + fields = [{"field_name": "item_group"}, {"field_name": "is_stock_item"}] + allow_fields = [d.get("field_name") for d in fields] set_item_variant_settings(fields) - if not frappe.db.get_value('Item Attribute Value', - {'parent': 'Test Size', 'attribute_value': 'Extra Large'}, 'name'): - item_attribute = frappe.get_doc('Item Attribute', 'Test Size') - item_attribute.append('item_attribute_values', { - 'attribute_value' : 'Extra Large', - 'abbr': 'XL' - }) + if not frappe.db.get_value( + "Item Attribute Value", {"parent": "Test Size", "attribute_value": "Extra Large"}, "name" + ): + item_attribute = frappe.get_doc("Item Attribute", "Test Size") + item_attribute.append("item_attribute_values", {"attribute_value": "Extra Large", "abbr": "XL"}) item_attribute.save() - template = frappe.get_doc('Item', '_Test Variant Item') + template = frappe.get_doc("Item", "_Test Variant Item") template.item_group = "_Test Item Group D" template.save() @@ -300,93 +363,94 @@ class TestItem(ERPNextTestCase): variant.item_name = "_Test Variant Item-XL" variant.save() - variant = frappe.get_doc('Item', '_Test Variant Item-XL') + variant = frappe.get_doc("Item", "_Test Variant Item-XL") for fieldname in allow_fields: self.assertEqual(template.get(fieldname), variant.get(fieldname)) - template = frappe.get_doc('Item', '_Test Variant Item') + template = frappe.get_doc("Item", "_Test Variant Item") template.item_group = "_Test Item Group Desktops" template.save() def test_make_item_variant_with_numeric_values(self): # cleanup - for d in frappe.db.get_all('Item', filters={'variant_of': - '_Test Numeric Template Item'}): + for d in frappe.db.get_all("Item", filters={"variant_of": "_Test Numeric Template Item"}): frappe.delete_doc_if_exists("Item", d.name) frappe.delete_doc_if_exists("Item", "_Test Numeric Template Item") frappe.delete_doc_if_exists("Item Attribute", "Test Item Length") - frappe.db.sql('''delete from `tabItem Variant Attribute` - where attribute="Test Item Length"''') + frappe.db.sql( + '''delete from `tabItem Variant Attribute` + where attribute="Test Item Length"''' + ) frappe.flags.attribute_values = None # make item attribute - frappe.get_doc({ - "doctype": "Item Attribute", - "attribute_name": "Test Item Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0.5 - }).insert() + frappe.get_doc( + { + "doctype": "Item Attribute", + "attribute_name": "Test Item Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0.5, + } + ).insert() # make template item - make_item("_Test Numeric Template Item", { - "attributes": [ - { - "attribute": "Test Size" - }, - { - "attribute": "Test Item Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0.5 - } - ], - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - } - ], - "has_variants": 1 - }) + make_item( + "_Test Numeric Template Item", + { + "attributes": [ + {"attribute": "Test Size"}, + { + "attribute": "Test Item Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0.5, + }, + ], + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + "has_variants": 1, + }, + ) - variant = create_variant("_Test Numeric Template Item", - {"Test Size": "Large", "Test Item Length": 1.1}) + variant = create_variant( + "_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.1} + ) self.assertEqual(variant.item_code, "_Test Numeric Template Item-L-1.1") variant.item_code = "_Test Numeric Variant-L-1.1" variant.item_name = "_Test Numeric Variant Large 1.1m" self.assertRaises(InvalidItemAttributeValueError, variant.save) - variant = create_variant("_Test Numeric Template Item", - {"Test Size": "Large", "Test Item Length": 1.5}) + variant = create_variant( + "_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.5} + ) self.assertEqual(variant.item_code, "_Test Numeric Template Item-L-1.5") variant.item_code = "_Test Numeric Variant-L-1.5" variant.item_name = "_Test Numeric Variant Large 1.5m" variant.save() def test_item_merging(self): - create_item("Test Item for Merging 1") - create_item("Test Item for Merging 2") + old = create_item(frappe.generate_hash(length=20)).name + new = create_item(frappe.generate_hash(length=20)).name - make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC", - qty=1, rate=100) - make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC", - qty=1, rate=100) + make_stock_entry(item_code=old, target="_Test Warehouse - _TC", qty=1, rate=100) + make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC", qty=1, rate=100) + make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC", qty=1, rate=100) - frappe.rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True) + frappe.rename_doc("Item", old, new, merge=True) - self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1")) + self.assertFalse(frappe.db.exists("Item", old)) - self.assertTrue(frappe.db.get_value("Bin", - {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse - _TC"})) - - self.assertTrue(frappe.db.get_value("Bin", - {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + self.assertTrue( + frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"}) + ) + self.assertTrue( + frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"}) + ) def test_item_merging_with_product_bundle(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle @@ -409,13 +473,12 @@ class TestItem(ERPNextTestCase): self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1")) def test_uom_conversion_factor(self): - if frappe.db.exists('Item', 'Test Item UOM'): - frappe.delete_doc('Item', 'Test Item UOM') + if frappe.db.exists("Item", "Test Item UOM"): + frappe.delete_doc("Item", "Test Item UOM") - item_doc = make_item("Test Item UOM", { - "stock_uom": "Gram", - "uoms": [dict(uom='Carat'), dict(uom='Kg')] - }) + item_doc = make_item( + "Test Item UOM", {"stock_uom": "Gram", "uoms": [dict(uom="Carat"), dict(uom="Kg")]} + ) for d in item_doc.uoms: value = get_uom_conv_factor(d.uom, item_doc.stock_uom) @@ -435,48 +498,46 @@ class TestItem(ERPNextTestCase): self.assertEqual(factor, 1.0) def test_item_variant_by_manufacturer(self): - fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}] + fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}] set_item_variant_settings(fields) - if frappe.db.exists('Item', '_Test Variant Mfg'): - frappe.delete_doc('Item', '_Test Variant Mfg') - if frappe.db.exists('Item', '_Test Variant Mfg-1'): - frappe.delete_doc('Item', '_Test Variant Mfg-1') - if frappe.db.exists('Manufacturer', 'MSG1'): - frappe.delete_doc('Manufacturer', 'MSG1') + if frappe.db.exists("Item", "_Test Variant Mfg"): + frappe.delete_doc("Item", "_Test Variant Mfg") + if frappe.db.exists("Item", "_Test Variant Mfg-1"): + frappe.delete_doc("Item", "_Test Variant Mfg-1") + if frappe.db.exists("Manufacturer", "MSG1"): + frappe.delete_doc("Manufacturer", "MSG1") - template = frappe.get_doc(dict( - doctype='Item', - item_code='_Test Variant Mfg', - has_variant=1, - item_group='Products', - variant_based_on='Manufacturer' - )).insert() + template = frappe.get_doc( + dict( + doctype="Item", + item_code="_Test Variant Mfg", + has_variant=1, + item_group="Products", + variant_based_on="Manufacturer", + ) + ).insert() - manufacturer = frappe.get_doc(dict( - doctype='Manufacturer', - short_name='MSG1' - )).insert() + manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert() variant = get_variant(template.name, manufacturer=manufacturer.name) - self.assertEqual(variant.item_code, '_Test Variant Mfg-1') - self.assertEqual(variant.description, '_Test Variant Mfg') - self.assertEqual(variant.manufacturer, 'MSG1') + self.assertEqual(variant.item_code, "_Test Variant Mfg-1") + self.assertEqual(variant.description, "_Test Variant Mfg") + self.assertEqual(variant.manufacturer, "MSG1") variant.insert() - variant = get_variant(template.name, manufacturer=manufacturer.name, - manufacturer_part_no='007') - self.assertEqual(variant.item_code, '_Test Variant Mfg-2') - self.assertEqual(variant.description, '_Test Variant Mfg') - self.assertEqual(variant.manufacturer, 'MSG1') - self.assertEqual(variant.manufacturer_part_no, '007') + variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007") + self.assertEqual(variant.item_code, "_Test Variant Mfg-2") + self.assertEqual(variant.description, "_Test Variant Mfg") + self.assertEqual(variant.manufacturer, "MSG1") + self.assertEqual(variant.manufacturer_part_no, "007") def test_stock_exists_against_template_item(self): - stock_item = frappe.get_all('Stock Ledger Entry', fields = ["item_code"], limit=1) + stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1) if stock_item: item_code = stock_item[0].item_code - item_doc = frappe.get_doc('Item', item_code) + item_doc = frappe.get_doc("Item", item_code) item_doc.has_variants = 1 self.assertRaises(StockExistsForTemplate, item_doc.save) @@ -489,37 +550,27 @@ class TestItem(ERPNextTestCase): # Create new item and add barcodes barcode_properties_list = [ - { - "barcode": "0012345678905", - "barcode_type": "EAN" - }, - { - "barcode": "012345678905", - "barcode_type": "UAN" - }, + {"barcode": "0012345678905", "barcode_type": "EAN"}, + {"barcode": "012345678905", "barcode_type": "UAN"}, { "barcode": "ARBITRARY_TEXT", - } + }, ] create_item(item_code) for barcode_properties in barcode_properties_list: - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") new_barcode.update(barcode_properties) item_doc.save() # Check values saved correctly barcodes = frappe.get_all( - 'Item Barcode', - fields=['barcode', 'barcode_type'], - filters={'parent': item_code}) + "Item Barcode", fields=["barcode", "barcode_type"], filters={"parent": item_code} + ) for barcode_properties in barcode_properties_list: - barcode_to_find = barcode_properties['barcode'] - matching_barcodes = [ - x for x in barcodes - if x['barcode'] == barcode_to_find - ] + barcode_to_find = barcode_properties["barcode"] + matching_barcodes = [x for x in barcodes if x["barcode"] == barcode_to_find] self.assertEqual(len(matching_barcodes), 1) details = matching_barcodes[0] @@ -527,20 +578,21 @@ class TestItem(ERPNextTestCase): self.assertEqual(value, details.get(key)) # Add barcode again - should cause DuplicateEntryError - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") new_barcode.update(barcode_properties_list[0]) self.assertRaises(frappe.UniqueValidationError, item_doc.save) # Add invalid barcode - should cause InvalidBarcode - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') - new_barcode.barcode = '9999999999999' - new_barcode.barcode_type = 'EAN' + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") + new_barcode.barcode = "9999999999999" + 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)) @@ -572,20 +624,17 @@ class TestItem(ERPNextTestCase): 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 - }) + 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 - }) + update_bin_qty(item.item_code, "_Test Warehouse - _TC", {"reserved_qty": 0}) item.load_from_db() item.stock_uom = "Kilometer" @@ -618,12 +667,36 @@ class TestItem(ERPNextTestCase): item.item_group = "All Item Groups" item.save() # if item code saved without item_code then series worked + @change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"}) + def test_retain_sample(self): + item = make_item( + "_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1} + ) + + self.assertEqual(item.has_batch_no, 1) + self.assertEqual(item.retain_sample, 1) + self.assertEqual(item.sample_quantity, 1) + + item.has_batch_no = None + item.save() + self.assertEqual(item.retain_sample, None) + self.assertEqual(item.sample_quantity, None) + item.delete() + + def test_empty_description(self): + item = make_item(properties={"description": "

    "}) + self.assertEqual(item.description, item.item_name) + item.description = "" + item.save() + self.assertEqual(item.description, item.item_name) + def set_item_variant_settings(fields): - doc = frappe.get_doc('Item Variant Settings') - doc.set('fields', fields) + doc = frappe.get_doc("Item Variant Settings") + doc.set("fields", fields) doc.save() + def make_item_variant(): if not frappe.db.exists("Item", "_Test Variant Item-S"): variant = create_variant("_Test Variant Item", """{"Test Size": "Small"}""") @@ -631,11 +704,23 @@ def make_item_variant(): variant.item_name = "_Test Variant Item-S" variant.save() -test_records = frappe.get_test_records('Item') -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, is_fixed_asset=0, - asset_category=None, company="_Test Company"): +test_records = frappe.get_test_records("Item") + + +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, + is_fixed_asset=0, + asset_category=None, + company="_Test Company", +): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") item.item_code = item_code @@ -649,11 +734,8 @@ def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test W 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, - "company": company - }) + item.customer = customer or "" + item.append("item_defaults", {"default_warehouse": warehouse, "company": company}) item.save() else: item = frappe.get_doc("Item", item_code) diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index 766647b32e5..0f93bb9e95b 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -14,8 +14,7 @@ class ItemAlternative(Document): self.validate_duplicate() def has_alternative_item(self): - if (self.item_code and - not frappe.db.get_value('Item', self.item_code, 'allow_alternative_item')): + if self.item_code and not frappe.db.get_value("Item", self.item_code, "allow_alternative_item"): frappe.throw(_("Not allow to set alternative item for the item {0}").format(self.item_code)) def validate_alternative_item(self): @@ -23,19 +22,32 @@ class ItemAlternative(Document): frappe.throw(_("Alternative item must not be same as item code")) item_meta = frappe.get_meta("Item") - fields = ["is_stock_item", "include_item_in_manufacturing","has_serial_no", "has_batch_no", "allow_alternative_item"] + fields = [ + "is_stock_item", + "include_item_in_manufacturing", + "has_serial_no", + "has_batch_no", + "allow_alternative_item", + ] item_data = frappe.db.get_value("Item", self.item_code, fields, as_dict=1) - alternative_item_data = frappe.db.get_value("Item", self.alternative_item_code, fields, as_dict=1) + alternative_item_data = frappe.db.get_value( + "Item", self.alternative_item_code, fields, as_dict=1 + ) for field in fields: - if item_data.get(field) != alternative_item_data.get(field): + if item_data.get(field) != alternative_item_data.get(field): raise_exception, alert = [1, False] if field == "is_stock_item" else [0, True] - frappe.msgprint(_("The value of {0} differs between Items {1} and {2}") \ - .format(frappe.bold(item_meta.get_label(field)), - frappe.bold(self.alternative_item_code), - frappe.bold(self.item_code)), - alert=alert, raise_exception=raise_exception, indicator="Orange") + frappe.msgprint( + _("The value of {0} differs between Items {1} and {2}").format( + frappe.bold(item_meta.get_label(field)), + frappe.bold(self.alternative_item_code), + frappe.bold(self.item_code), + ), + alert=alert, + raise_exception=raise_exception, + indicator="Orange", + ) alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {}") @@ -44,24 +56,30 @@ class ItemAlternative(Document): if self.two_way and not alternative_item_data.allow_alternative_item: frappe.throw(alternate_item_check_msg.format(self.item_code)) - - - def validate_duplicate(self): - if frappe.db.get_value("Item Alternative", {'item_code': self.item_code, - 'alternative_item_code': self.alternative_item_code, 'name': ('!=', self.name)}): + if frappe.db.get_value( + "Item Alternative", + { + "item_code": self.item_code, + "alternative_item_code": self.alternative_item_code, + "name": ("!=", self.name), + }, + ): frappe.throw(_("Already record exists for the item {0}").format(self.item_code)) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql(""" (select alternative_item_code from `tabItem Alternative` + return frappe.db.sql( + """ (select alternative_item_code from `tabItem Alternative` where item_code = %(item_code)s and alternative_item_code like %(txt)s) union (select item_code from `tabItem Alternative` where alternative_item_code = %(item_code)s and item_code like %(txt)s and two_way = 1) limit {0}, {1} - """.format(start, page_len), { - "item_code": filters.get('item_code'), - "txt": '%' + txt + '%' - }) + """.format( + start, page_len + ), + {"item_code": filters.get("item_code"), "txt": "%" + txt + "%"}, + ) diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 3976af4e88c..d829b2cbf39 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -4,6 +4,7 @@ import json import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import ( @@ -18,130 +19,189 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.tests.utils import ERPNextTestCase -class TestItemAlternative(ERPNextTestCase): +class TestItemAlternative(FrappeTestCase): def setUp(self): super().setUp() make_items() def test_alternative_item_for_subcontract_rm(self): - frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', 'BOM') + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) - create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) - create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) + create_stock_reconciliation( + item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + create_stock_reconciliation( + item_code="Test FG A RW 2", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) supplier_warehouse = "Test Supplier Warehouse - _TC" - po = create_purchase_order(item = "Test Finished Goods - A", - is_subcontracted='Yes', qty=5, rate=3000, supplier_warehouse=supplier_warehouse) + po = create_purchase_order( + item="Test Finished Goods - A", + is_subcontracted="Yes", + qty=5, + rate=3000, + supplier_warehouse=supplier_warehouse, + ) - rm_item = [{"item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 1", "item_name":"Test FG A RW 1", - "qty":5, "warehouse":"_Test Warehouse - _TC", "rate":2000, "amount":10000, "stock_uom":"Nos"}, - {"item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 2", "item_name":"Test FG A RW 2", - "qty":5, "warehouse":"_Test Warehouse - _TC", "rate":2000, "amount":10000, "stock_uom":"Nos"}] + rm_item = [ + { + "item_code": "Test Finished Goods - A", + "rm_item_code": "Test FG A RW 1", + "item_name": "Test FG A RW 1", + "qty": 5, + "warehouse": "_Test Warehouse - _TC", + "rate": 2000, + "amount": 10000, + "stock_uom": "Nos", + }, + { + "item_code": "Test Finished Goods - A", + "rm_item_code": "Test FG A RW 2", + "item_name": "Test FG A RW 2", + "qty": 5, + "warehouse": "_Test Warehouse - _TC", + "rate": 2000, + "amount": 10000, + "stock_uom": "Nos", + }, + ] rm_item_string = json.dumps(rm_item) - reserved_qty_for_sub_contract = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_sub_contract') + reserved_qty_for_sub_contract = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_sub_contract", + ) se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) se.to_warehouse = supplier_warehouse se.insert() - doc = frappe.get_doc('Stock Entry', se.name) + doc = frappe.get_doc("Stock Entry", se.name) for item in doc.items: - if item.item_code == 'Test FG A RW 1': - item.item_code = 'Alternate Item For A RW 1' - item.item_name = 'Alternate Item For A RW 1' - item.description = 'Alternate Item For A RW 1' - item.original_item = 'Test FG A RW 1' + if item.item_code == "Test FG A RW 1": + item.item_code = "Alternate Item For A RW 1" + item.item_name = "Alternate Item For A RW 1" + item.description = "Alternate Item For A RW 1" + item.original_item = "Test FG A RW 1" doc.save() doc.submit() - after_transfer_reserved_qty_for_sub_contract = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_sub_contract') + after_transfer_reserved_qty_for_sub_contract = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_sub_contract", + ) - self.assertEqual(after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)) + self.assertEqual( + after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5) + ) pr = make_purchase_receipt(po.name) pr.save() - pr = frappe.get_doc('Purchase Receipt', pr.name) + pr = frappe.get_doc("Purchase Receipt", pr.name) status = False for d in pr.supplied_items: - if d.rm_item_code == 'Alternate Item For A RW 1': + if d.rm_item_code == "Alternate Item For A RW 1": status = True self.assertEqual(status, True) - frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract') + frappe.db.set_value( + "Buying Settings", + None, + "backflush_raw_materials_of_subcontract_based_on", + "Material Transferred for Subcontract", + ) def test_alternative_item_for_production_rm(self): - create_stock_reconciliation(item_code='Alternate Item For A RW 1', - warehouse='_Test Warehouse - _TC',qty=5, rate=2000) - create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) - pro_order = make_wo_order_test_record(production_item='Test Finished Goods - A', - qty=5, source_warehouse='_Test Warehouse - _TC', wip_warehouse='Test Supplier Warehouse - _TC') + create_stock_reconciliation( + item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + create_stock_reconciliation( + item_code="Test FG A RW 2", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + pro_order = make_wo_order_test_record( + production_item="Test Finished Goods - A", + qty=5, + source_warehouse="_Test Warehouse - _TC", + wip_warehouse="Test Supplier Warehouse - _TC", + ) - reserved_qty_for_production = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_production') + reserved_qty_for_production = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_production", + ) ste = frappe.get_doc(make_stock_entry(pro_order.name, "Material Transfer for Manufacture", 5)) ste.insert() for item in ste.items: - if item.item_code == 'Test FG A RW 1': - item.item_code = 'Alternate Item For A RW 1' - item.item_name = 'Alternate Item For A RW 1' - item.description = 'Alternate Item For A RW 1' - item.original_item = 'Test FG A RW 1' + if item.item_code == "Test FG A RW 1": + item.item_code = "Alternate Item For A RW 1" + item.item_name = "Alternate Item For A RW 1" + item.description = "Alternate Item For A RW 1" + item.original_item = "Test FG A RW 1" ste.submit() - reserved_qty_for_production_after_transfer = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_production') + reserved_qty_for_production_after_transfer = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_production", + ) - self.assertEqual(reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5)) + self.assertEqual( + reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5) + ) ste1 = frappe.get_doc(make_stock_entry(pro_order.name, "Manufacture", 5)) status = False for d in ste1.items: - if d.item_code == 'Alternate Item For A RW 1': + if d.item_code == "Alternate Item For A RW 1": status = True self.assertEqual(status, True) ste1.submit() + def make_items(): - items = ['Test Finished Goods - A', 'Test FG A RW 1', 'Test FG A RW 2', 'Alternate Item For A RW 1'] + items = [ + "Test Finished Goods - A", + "Test FG A RW 1", + "Test FG A RW 2", + "Alternate Item For A RW 1", + ] for item_code in items: - if not frappe.db.exists('Item', item_code): + if not frappe.db.exists("Item", item_code): create_item(item_code) - create_stock_reconciliation(item_code="Test FG A RW 1", - warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + create_stock_reconciliation( + item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000 + ) - if frappe.db.exists('Item', 'Test FG A RW 1'): - doc = frappe.get_doc('Item', 'Test FG A RW 1') + if frappe.db.exists("Item", "Test FG A RW 1"): + doc = frappe.get_doc("Item", "Test FG A RW 1") doc.allow_alternative_item = 1 doc.save() - if frappe.db.exists('Item', 'Test Finished Goods - A'): - doc = frappe.get_doc('Item', 'Test Finished Goods - A') + if frappe.db.exists("Item", "Test Finished Goods - A"): + doc = frappe.get_doc("Item", "Test Finished Goods - A") doc.is_sub_contracted_item = 1 doc.save() - if not frappe.db.get_value('BOM', - {'item': 'Test Finished Goods - A', 'docstatus': 1}): - make_bom(item = 'Test Finished Goods - A', raw_materials = ['Test FG A RW 1', 'Test FG A RW 2']) + if not frappe.db.get_value("BOM", {"item": "Test Finished Goods - A", "docstatus": 1}): + make_bom(item="Test Finished Goods - A", raw_materials=["Test FG A RW 1", "Test FG A RW 2"]) - if not frappe.db.get_value('Warehouse', {'warehouse_name': 'Test Supplier Warehouse'}): - frappe.get_doc({ - 'doctype': 'Warehouse', - 'warehouse_name': 'Test Supplier Warehouse', - 'company': '_Test Company' - }).insert(ignore_permissions=True) + if not frappe.db.get_value("Warehouse", {"warehouse_name": "Test Supplier Warehouse"}): + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Supplier Warehouse", + "company": "_Test Company", + } + ).insert(ignore_permissions=True) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 5a28a9e231c..391ff06918a 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -14,7 +14,9 @@ from erpnext.controllers.item_variant import ( ) -class ItemAttributeIncrementError(frappe.ValidationError): pass +class ItemAttributeIncrementError(frappe.ValidationError): + pass + class ItemAttribute(Document): def __setup__(self): @@ -29,11 +31,12 @@ class ItemAttribute(Document): self.validate_exising_items() def validate_exising_items(self): - '''Validate that if there are existing items with attributes, they are valid''' + """Validate that if there are existing items with attributes, they are valid""" attributes_list = [d.attribute_value for d in self.item_attribute_values] # Get Item Variant Attribute details of variant items - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select i.name, iva.attribute_value as value from @@ -41,13 +44,18 @@ class ItemAttribute(Document): where iva.attribute = %(attribute)s and iva.parent = i.name and - i.variant_of is not null and i.variant_of != ''""", {"attribute" : self.name}, as_dict=1) + i.variant_of is not null and i.variant_of != ''""", + {"attribute": self.name}, + as_dict=1, + ) for item in items: if self.numeric_values: validate_is_incremental(self, self.name, item.value, item.name) else: - validate_item_attribute_value(attributes_list, self.name, item.value, item.name, from_variant=False) + validate_item_attribute_value( + attributes_list, self.name, item.value, item.name, from_variant=False + ) def validate_numeric(self): if self.numeric_values: diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py index 0b7ca257151..a30f0e999f7 100644 --- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py @@ -4,27 +4,30 @@ import frappe -test_records = frappe.get_test_records('Item Attribute') +test_records = frappe.get_test_records("Item Attribute") + +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError -from erpnext.tests.utils import ERPNextTestCase -class TestItemAttribute(ERPNextTestCase): +class TestItemAttribute(FrappeTestCase): def setUp(self): super().setUp() if frappe.db.exists("Item Attribute", "_Test_Length"): frappe.delete_doc("Item Attribute", "_Test_Length") def test_numeric_item_attribute(self): - item_attribute = frappe.get_doc({ - "doctype": "Item Attribute", - "attribute_name": "_Test_Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0 - }) + item_attribute = frappe.get_doc( + { + "doctype": "Item Attribute", + "attribute_name": "_Test_Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0, + } + ) self.assertRaises(ItemAttributeIncrementError, item_attribute.save) diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json index d89ca55a4f3..eef70c95d05 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.json +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -1,109 +1,42 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:barcode", - "beta": 0, - "creation": "2017-12-09 18:54:50.562438", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "hash", + "creation": "2022-02-11 11:26:22.155183", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "barcode", + "barcode_type" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "barcode", - "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": "Barcode", - "length": 0, - "no_copy": 1, - "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, + "fieldname": "barcode", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Barcode", + "no_copy": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "barcode_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": 0, - "label": "Barcode Type", - "length": 0, - "no_copy": 0, - "options": "\nEAN\nUPC-A", - "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": "barcode_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Barcode Type", + "options": "\nEAN\nUPC-A" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-13 06:03:09.814357", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Barcode", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2022-04-01 05:54:27.314030", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Barcode", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.py b/erpnext/stock/doctype/item_barcode/item_barcode.py index 64c39dabde1..c2c042143ea 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.py +++ b/erpnext/stock/doctype/item_barcode/item_barcode.py @@ -6,4 +6,4 @@ from frappe.model.document import Document class ItemBarcode(Document): - pass + pass diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index bc171604f43..042d398256a 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -15,6 +15,7 @@ "default_supplier", "column_break_8", "expense_account", + "default_provisional_account", "selling_defaults", "selling_cost_center", "column_break_12", @@ -101,11 +102,17 @@ "fieldtype": "Link", "label": "Default Discount Account", "options": "Account" + }, + { + "fieldname": "default_provisional_account", + "fieldtype": "Link", + "label": "Default Provisional Account", + "options": "Account" } ], "istable": 1, "links": [], - "modified": "2021-07-13 01:26:03.860065", + "modified": "2022-04-10 20:18:54.148195", "modified_by": "Administrator", "module": "Stock", "name": "Item Default", @@ -114,5 +121,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py index 469ccd8f2df..b65ba98a8bf 100644 --- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py +++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py @@ -18,14 +18,17 @@ class ItemManufacturer(Document): def validate_duplicate_entry(self): if self.is_new(): filters = { - 'item_code': self.item_code, - 'manufacturer': self.manufacturer, - 'manufacturer_part_no': self.manufacturer_part_no + "item_code": self.item_code, + "manufacturer": self.manufacturer, + "manufacturer_part_no": self.manufacturer_part_no, } if frappe.db.exists("Item Manufacturer", filters): - frappe.throw(_("Duplicate entry against the item code {0} and manufacturer {1}") - .format(self.item_code, self.manufacturer)) + frappe.throw( + _("Duplicate entry against the item code {0} and manufacturer {1}").format( + self.item_code, self.manufacturer + ) + ) def manage_default_item_manufacturer(self, delete=False): from frappe.model.utils import set_default @@ -37,11 +40,9 @@ class ItemManufacturer(Document): if not self.is_default: # if unchecked and default in Item master, clear it. if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no: - frappe.db.set_value("Item", item.name, - { - "default_item_manufacturer": None, - "default_manufacturer_part_no": None - }) + frappe.db.set_value( + "Item", item.name, {"default_item_manufacturer": None, "default_manufacturer_part_no": None} + ) elif self.is_default: set_default(self, "item_code") @@ -50,18 +51,26 @@ class ItemManufacturer(Document): if delete: manufacturer, manufacturer_part_no = None, None - elif (default_manufacturer != self.manufacturer) or \ - (default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no): + elif (default_manufacturer != self.manufacturer) or ( + default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no + ): manufacturer = self.manufacturer manufacturer_part_no = self.manufacturer_part_no - frappe.db.set_value("Item", item.name, - { - "default_item_manufacturer": manufacturer, - "default_manufacturer_part_no": manufacturer_part_no - }) + frappe.db.set_value( + "Item", + item.name, + { + "default_item_manufacturer": manufacturer, + "default_manufacturer_part_no": manufacturer_part_no, + }, + ) + @frappe.whitelist() def get_item_manufacturer_part_no(item_code, manufacturer): - return frappe.db.get_value("Item Manufacturer", - {'item_code': item_code, 'manufacturer': manufacturer}, 'manufacturer_part_no') + return frappe.db.get_value( + "Item Manufacturer", + {"item_code": item_code, "manufacturer": manufacturer}, + "manufacturer_part_no", + ) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 010e01a78ba..562f7b9e12f 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -13,7 +13,6 @@ class ItemPriceDuplicateItem(frappe.ValidationError): class ItemPrice(Document): - def validate(self): self.validate_item() self.validate_dates() @@ -32,22 +31,26 @@ class ItemPrice(Document): def update_price_list_details(self): if self.price_list: - price_list_details = frappe.db.get_value("Price List", - {"name": self.price_list, "enabled": 1}, - ["buying", "selling", "currency"]) + price_list_details = frappe.db.get_value( + "Price List", {"name": self.price_list, "enabled": 1}, ["buying", "selling", "currency"] + ) if not price_list_details: - link = frappe.utils.get_link_to_form('Price List', self.price_list) + link = frappe.utils.get_link_to_form("Price List", self.price_list) frappe.throw("The price list {0} does not exist or is disabled".format(link)) self.buying, self.selling, self.currency = price_list_details def update_item_details(self): if self.item_code: - self.item_name, self.item_description = frappe.db.get_value("Item", self.item_code,["item_name", "description"]) + self.item_name, self.item_description = frappe.db.get_value( + "Item", self.item_code, ["item_name", "description"] + ) def check_duplicates(self): - conditions = """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" + conditions = ( + """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" + ) for field in [ "uom", @@ -56,21 +59,31 @@ class ItemPrice(Document): "packing_unit", "customer", "supplier", - "batch_no"]: + "batch_no", + ]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: conditions += "and (isnull({0}) or {0} = '')".format(field) - price_list_rate = frappe.db.sql(""" + price_list_rate = frappe.db.sql( + """ select price_list_rate from `tabItem Price` {conditions} - """.format(conditions=conditions), - self.as_dict(),) + """.format( + conditions=conditions + ), + self.as_dict(), + ) if price_list_rate: - frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) + frappe.throw( + _( + "Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates." + ), + ItemPriceDuplicateItem, + ) def before_save(self): if self.selling: diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index f81770e487d..30d933e247d 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -4,13 +4,13 @@ import frappe from frappe.test_runner import make_test_records_for_doctype +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem from erpnext.stock.get_item_details import get_price_list_rate_for, process_args -from erpnext.tests.utils import ERPNextTestCase -class TestItemPrice(ERPNextTestCase): +class TestItemPrice(FrappeTestCase): def setUp(self): super().setUp() frappe.db.sql("delete from `tabItem Price`") @@ -23,8 +23,14 @@ class TestItemPrice(ERPNextTestCase): def test_addition_of_new_fields(self): # Based on https://github.com/frappe/erpnext/issues/8456 test_fields_existance = [ - 'supplier', 'customer', 'uom', 'lead_time_days', - 'packing_unit', 'valid_from', 'valid_upto', 'note' + "supplier", + "customer", + "uom", + "lead_time_days", + "packing_unit", + "valid_from", + "valid_upto", + "note", ] doc_fields = frappe.copy_doc(test_records[1]).__dict__.keys() @@ -45,10 +51,10 @@ class TestItemPrice(ERPNextTestCase): args = { "price_list": doc.price_list, - "customer": doc.customer, - "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 10 + "customer": doc.customer, + "uom": "_Test UOM", + "transaction_date": "2017-04-18", + "qty": 10, } price = get_price_list_rate_for(process_args(args), doc.item_code) @@ -61,13 +67,12 @@ class TestItemPrice(ERPNextTestCase): "price_list": doc.price_list, "customer": doc.customer, "uom": "_Test UOM", - "transaction_date": '2017-04-18', + "transaction_date": "2017-04-18", } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, None) - def test_prices_at_date(self): # Check correct price at first date doc = frappe.copy_doc(test_records[2]) @@ -76,35 +81,35 @@ class TestItemPrice(ERPNextTestCase): "price_list": doc.price_list, "customer": "_Test Customer", "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 7 + "transaction_date": "2017-04-18", + "qty": 7, } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, 20) def test_prices_at_invalid_date(self): - #Check correct price at invalid date + # Check correct price at invalid date doc = frappe.copy_doc(test_records[3]) args = { "price_list": doc.price_list, "qty": 7, "uom": "_Test UOM", - "transaction_date": "01-15-2019" + "transaction_date": "01-15-2019", } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, None) def test_prices_outside_of_date(self): - #Check correct price when outside of the date + # Check correct price when outside of the date doc = frappe.copy_doc(test_records[4]) args = { "price_list": doc.price_list, - "customer": "_Test Customer", - "uom": "_Test UOM", + "customer": "_Test Customer", + "uom": "_Test UOM", "transaction_date": "2017-04-25", "qty": 7, } @@ -113,7 +118,7 @@ class TestItemPrice(ERPNextTestCase): self.assertEqual(price, None) def test_lowest_price_when_no_date_provided(self): - #Check lowest price when no date provided + # Check lowest price when no date provided doc = frappe.copy_doc(test_records[1]) args = { @@ -125,7 +130,6 @@ class TestItemPrice(ERPNextTestCase): price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, 10) - def test_invalid_item(self): doc = frappe.copy_doc(test_records[1]) # Enter invalid item code @@ -150,8 +154,8 @@ class TestItemPrice(ERPNextTestCase): args = { "price_list": doc.price_list, "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 7 + "transaction_date": "2017-04-18", + "qty": 7, } price = get_price_list_rate_for(args, doc.item_code) @@ -159,4 +163,5 @@ class TestItemPrice(ERPNextTestCase): self.assertEqual(price, 21) -test_records = frappe.get_test_records('Item Price') + +test_records = frappe.get_test_records("Item Price") diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py index 62bf842be83..49f311684c2 100644 --- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py +++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py @@ -8,29 +8,46 @@ from frappe.model.document import Document class ItemVariantSettings(Document): - invalid_fields_for_copy_fields_in_variants = ['barcodes'] + invalid_fields_for_copy_fields_in_variants = ["barcodes"] def set_default_fields(self): self.fields = [] - fields = frappe.get_meta('Item').fields - exclude_fields = {"naming_series", "item_code", "item_name", "published_in_website", - "standard_rate", "opening_stock", "image", "description", - "variant_of", "valuation_rate", "description", "barcodes", - "has_variants", "attributes"} + fields = frappe.get_meta("Item").fields + exclude_fields = { + "naming_series", + "item_code", + "item_name", + "published_in_website", + "standard_rate", + "opening_stock", + "image", + "description", + "variant_of", + "valuation_rate", + "description", + "barcodes", + "has_variants", + "attributes", + } for d in fields: - if not d.no_copy and d.fieldname not in exclude_fields and \ - d.fieldtype not in ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only']: - self.append('fields', { - 'field_name': d.fieldname - }) + if ( + not d.no_copy + and d.fieldname not in exclude_fields + and d.fieldtype not in ["HTML", "Section Break", "Column Break", "Button", "Read Only"] + ): + self.append("fields", {"field_name": d.fieldname}) def remove_invalid_fields_for_copy_fields_in_variants(self): - fields = [row for row in self.fields if row.field_name not in self.invalid_fields_for_copy_fields_in_variants] + fields = [ + row + for row in self.fields + if row.field_name not in self.invalid_fields_for_copy_fields_in_variants + ] self.fields = fields self.save() def validate(self): for d in self.fields: if d.field_name in self.invalid_fields_for_copy_fields_in_variants: - frappe.throw(_('Cannot set the field {0} for copying in variants').format(d.field_name)) + frappe.throw(_("Cannot set the field {0} for copying in variants").format(d.field_name)) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 7aff95d1e81..b3af309359a 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -19,13 +19,19 @@ class LandedCostVoucher(Document): self.set("items", []) for pr in self.get("purchase_receipts"): if pr.receipt_document_type and pr.receipt_document: - pr_items = frappe.db.sql("""select pr_item.item_code, pr_item.description, + pr_items = frappe.db.sql( + """select pr_item.item_code, pr_item.description, pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name, pr_item.cost_center, pr_item.is_fixed_asset from `tab{doctype} Item` pr_item where parent = %s and exists(select name from tabItem where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1)) - """.format(doctype=pr.receipt_document_type), pr.receipt_document, as_dict=True) + """.format( + doctype=pr.receipt_document_type + ), + pr.receipt_document, + as_dict=True, + ) for d in pr_items: item = self.append("items") @@ -33,8 +39,7 @@ class LandedCostVoucher(Document): item.description = d.description item.qty = d.qty item.rate = d.base_rate - item.cost_center = d.cost_center or \ - erpnext.get_default_cost_center(self.company) + item.cost_center = d.cost_center or erpnext.get_default_cost_center(self.company) item.amount = d.base_amount item.receipt_document_type = pr.receipt_document_type item.receipt_document = pr.receipt_document @@ -52,26 +57,30 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() self.validate_applicable_charges_for_item() - def check_mandatory(self): if not self.get("purchase_receipts"): frappe.throw(_("Please enter Receipt Document")) - def validate_receipt_documents(self): receipt_documents = [] for d in self.get("purchase_receipts"): docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") if docstatus != 1: - msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" + msg = ( + f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" + ) frappe.throw(_(msg), title=_("Invalid Document")) if d.receipt_document_type == "Purchase Invoice": update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock") if not update_stock: - msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document)) - msg += "
    " + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.") + msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format( + d.idx, frappe.bold(d.receipt_document) + ) + msg += "
    " + _( + "Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled." + ) frappe.throw(msg, title=_("Incorrect Invoice")) receipt_documents.append(d.receipt_document) @@ -81,52 +90,64 @@ class LandedCostVoucher(Document): frappe.throw(_("Item must be added using 'Get Items from Purchase Receipts' button")) elif item.receipt_document not in receipt_documents: - frappe.throw(_("Item Row {0}: {1} {2} does not exist in above '{1}' table") - .format(item.idx, item.receipt_document_type, item.receipt_document)) + frappe.throw( + _("Item Row {0}: {1} {2} does not exist in above '{1}' table").format( + item.idx, item.receipt_document_type, item.receipt_document + ) + ) if not item.cost_center: - frappe.throw(_("Row {0}: Cost center is required for an item {1}") - .format(item.idx, item.item_code)) + frappe.throw( + _("Row {0}: Cost center is required for an item {1}").format(item.idx, item.item_code) + ) def set_total_taxes_and_charges(self): self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes")) def set_applicable_charges_on_item(self): - if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually': + if self.get("taxes") and self.distribute_charges_based_on != "Distribute Manually": total_item_cost = 0.0 total_charges = 0.0 item_count = 0 based_on_field = frappe.scrub(self.distribute_charges_based_on) - for item in self.get('items'): + for item in self.get("items"): total_item_cost += item.get(based_on_field) - for item in self.get('items'): - item.applicable_charges = flt(flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), - item.precision('applicable_charges')) + for item in self.get("items"): + item.applicable_charges = flt( + flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), + item.precision("applicable_charges"), + ) total_charges += item.applicable_charges item_count += 1 if total_charges != self.total_taxes_and_charges: diff = self.total_taxes_and_charges - total_charges - self.get('items')[item_count - 1].applicable_charges += diff + self.get("items")[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): based_on = self.distribute_charges_based_on.lower() - if based_on != 'distribute manually': + if based_on != "distribute manually": total = sum(flt(d.get(based_on)) for d in self.get("items")) else: # consider for proportion while distributing manually - total = sum(flt(d.get('applicable_charges')) for d in self.get("items")) + total = sum(flt(d.get("applicable_charges")) for d in self.get("items")) if not total: - frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) + frappe.throw( + _( + "Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'" + ).format(based_on) + ) total_applicable_charges = sum(flt(d.applicable_charges) for d in self.get("items")) - precision = get_field_precision(frappe.get_meta("Landed Cost Item").get_field("applicable_charges"), - currency=frappe.get_cached_value('Company', self.company, "default_currency")) + precision = get_field_precision( + frappe.get_meta("Landed Cost Item").get_field("applicable_charges"), + currency=frappe.get_cached_value("Company", self.company, "default_currency"), + ) diff = flt(self.total_taxes_and_charges) - flt(total_applicable_charges) diff = flt(diff, precision) @@ -134,7 +155,11 @@ class LandedCostVoucher(Document): if abs(diff) < (2.0 / (10**precision)): self.items[-1].applicable_charges += diff else: - frappe.throw(_("Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges")) + frappe.throw( + _( + "Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges" + ) + ) def on_submit(self): self.update_landed_cost() @@ -177,25 +202,41 @@ class LandedCostVoucher(Document): doc.repost_future_sle_and_gle() def validate_asset_qty_and_status(self, receipt_document_type, receipt_document): - for item in self.get('items'): + for item in self.get("items"): if item.is_fixed_asset: - receipt_document_type = 'purchase_invoice' if item.receipt_document_type == 'Purchase Invoice' \ - else 'purchase_receipt' - docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, - 'item_code': item.item_code }, fields=['name', 'docstatus']) + receipt_document_type = ( + "purchase_invoice" if item.receipt_document_type == "Purchase Invoice" else "purchase_receipt" + ) + docs = frappe.db.get_all( + "Asset", + filters={receipt_document_type: item.receipt_document, "item_code": item.item_code}, + fields=["name", "docstatus"], + ) if not docs or len(docs) != item.qty: - frappe.throw(_('There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document.').format( - item.receipt_document, item.qty)) + frappe.throw( + _( + "There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document." + ).format(item.receipt_document, item.qty) + ) if docs: for d in docs: if d.docstatus == 1: - frappe.throw(_('{2} {0} has submitted Assets. Remove Item {1} from table to continue.').format( - item.receipt_document, item.item_code, item.receipt_document_type)) + frappe.throw( + _( + "{2} {0} has submitted Assets. Remove Item {1} from table to continue." + ).format( + item.receipt_document, item.item_code, item.receipt_document_type + ) + ) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): for item in receipt_document.get("items"): if not item.is_fixed_asset and item.serial_no: serial_nos = get_serial_nos(item.serial_no) if serial_nos: - frappe.db.sql("update `tabSerial No` set purchase_rate=%s where name in ({0})" - .format(", ".join(["%s"]*len(serial_nos))), tuple([item.valuation_rate] + serial_nos)) + frappe.db.sql( + "update `tabSerial No` set purchase_rate=%s where name in ({0})".format( + ", ".join(["%s"] * len(serial_nos)) + ), + tuple([item.valuation_rate] + serial_nos), + ) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 1ea0596d333..1af99534516 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -2,8 +2,8 @@ # License: GNU General Public License v3. See license.txt - import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, flt, now from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -15,41 +15,56 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_gl_entries, make_purchase_receipt, ) -from erpnext.tests.utils import ERPNextTestCase -class TestLandedCostVoucher(ERPNextTestCase): +class TestLandedCostVoucher(FrappeTestCase): def test_landed_cost_voucher(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", - get_multiple_items = True, get_taxes_and_charges = True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + ) - last_sle = frappe.db.get_value("Stock Ledger Entry", { + last_sle = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", "warehouse": "Stores - TCP1", "is_cancelled": 0, }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount") + pr_lc_value = frappe.db.get_value( + "Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount" + ) self.assertEqual(pr_lc_value, 25.0) - last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", { + last_sle_after_landed_cost = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", "warehouse": "Stores - TCP1", "is_cancelled": 0, }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) - self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) + self.assertEqual( + last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction + ) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0) # assert after submit @@ -57,24 +72,20 @@ class TestLandedCostVoucher(ERPNextTestCase): # Mess up cancelled SLE modified timestamp to check # if they aren't effective in any business logic. - frappe.db.set_value("Stock Ledger Entry", - { - "is_cancelled": 1, - "voucher_type": pr.doctype, - "voucher_no": pr.name - }, - "is_cancelled", 1, - modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True) + frappe.db.set_value( + "Stock Ledger Entry", + {"is_cancelled": 1, "voucher_type": pr.doctype, "voucher_no": pr.name}, + "is_cancelled", + 1, + modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True), ) items, warehouses = pr.get_items_and_warehouses() - update_gl_entries_after(pr.posting_date, pr.posting_time, - warehouses, items, company=pr.company) + update_gl_entries_after(pr.posting_date, pr.posting_time, warehouses, items, company=pr.company) # reassert after reposting self.assertPurchaseReceiptLCVGLEntries(pr) - def assertPurchaseReceiptLCVGLEntries(self, pr): gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -90,54 +101,74 @@ class TestLandedCostVoucher(ERPNextTestCase): "Stock Received But Not Billed - TCP1": [0.0, 500.0], "Expenses Included In Valuation - TCP1": [0.0, 50.0], "_Test Account Customs Duty - TCP1": [0.0, 150], - "_Test Account Shipping Charges - TCP1": [0.0, 100.00] + "_Test Account Shipping Charges - TCP1": [0.0, 100.00], } else: expected_values = { stock_in_hand_account: [400.0, 0.0], fixed_asset_account: [400.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "Expenses Included In Valuation - TCP1": [0.0, 300.0] + "Expenses Included In Valuation - TCP1": [0.0, 300.0], } for gle in gl_entries: - if not gle.get('is_cancelled'): - self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}") - self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}") - + if not gle.get("is_cancelled"): + self.assertEqual( + expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}" + ) + self.assertEqual( + expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}" + ) def test_landed_cost_voucher_against_purchase_invoice(self): - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", - company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", - warehouse= "Stores - TCP1", cost_center = "Main - TCP1", - expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + cash_bank_account="Cash - TCP1", + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) - last_sle = frappe.db.get_value("Stock Ledger Entry", { + last_sle = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pi.doctype, "voucher_no": pi.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company) - pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name}, - "landed_cost_voucher_amount") + pi_lc_value = frappe.db.get_value( + "Purchase Invoice Item", {"parent": pi.name}, "landed_cost_voucher_amount" + ) self.assertEqual(pi_lc_value, 50.0) - last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", { + last_sle_after_landed_cost = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pi.doctype, "voucher_no": pi.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) - self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) + self.assertEqual( + last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction + ) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) @@ -149,20 +180,26 @@ class TestLandedCostVoucher(ERPNextTestCase): expected_values = { stock_in_hand_account: [300.0, 0.0], "Creditors - TCP1": [0.0, 250.0], - "Expenses Included In Valuation - TCP1": [0.0, 50.0] + "Expenses Included In Valuation - TCP1": [0.0, 50.0], } for gle in gl_entries: - if not gle.get('is_cancelled'): + if not gle.get("is_cancelled"): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) - def test_landed_cost_voucher_for_serialized_item(self): - frappe.db.sql("delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')") - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True, - get_taxes_and_charges = True, do_not_submit = True) + frappe.db.sql( + "delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')" + ) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + do_not_submit=True, + ) pr.items[0].item_code = "_Test Serialized Item" pr.items[0].serial_no = "SN001\nSN002\nSN003\nSN004\nSN005" @@ -172,8 +209,7 @@ class TestLandedCostVoucher(ERPNextTestCase): create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - serial_no = frappe.db.get_value("Serial No", "SN001", - ["warehouse", "purchase_rate"], as_dict=1) + serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0) self.assertEqual(serial_no.warehouse, "Stores - TCP1") @@ -183,60 +219,82 @@ class TestLandedCostVoucher(ERPNextTestCase): landed costs, this should be allowed for serial nos too. Case: - - receipt a serial no @ X rate - - delivery the serial no @ X rate - - add LCV to receipt X + Y - - LCV should be successful - - delivery should reflect X+Y valuation. + - receipt a serial no @ X rate + - delivery the serial no @ X rate + - add LCV to receipt X + Y + - LCV should be successful + - delivery should reflect X+Y valuation. """ serial_no = "LCV_TEST_SR_NO" item_code = "_Test Serialized Item" warehouse = "Stores - TCP1" - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse=warehouse, qty=1, rate=200, - item_code=item_code, serial_no=serial_no) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse=warehouse, + qty=1, + rate=200, + item_code=item_code, + serial_no=serial_no, + ) serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") # deliver it before creating LCV - dn = create_delivery_note(item_code=item_code, - company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', - serial_no=serial_no, qty=1, rate=500, - cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + dn = create_delivery_note( + item_code=item_code, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + serial_no=serial_no, + qty=1, + rate=500, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + ) charges = 10 create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) new_purchase_rate = serial_no_rate + charges - serial_no = frappe.db.get_value("Serial No", serial_no, - ["warehouse", "purchase_rate"], as_dict=1) + serial_no = frappe.db.get_value( + "Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1 + ) self.assertEqual(serial_no.purchase_rate, new_purchase_rate) - stock_value_difference = frappe.db.get_value("Stock Ledger Entry", - filters={ - "voucher_no": dn.name, - "voucher_type": dn.doctype, - "is_cancelled": 0 # LCV cancels with same name. - }, - fieldname="stock_value_difference") + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + filters={ + "voucher_no": dn.name, + "voucher_type": dn.doctype, + "is_cancelled": 0, # LCV cancels with same name. + }, + fieldname="stock_value_difference", + ) # reposting should update the purchase rate in future delivery self.assertEqual(stock_value_difference, -new_purchase_rate) - def test_landed_cost_voucher_for_odd_numbers (self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) + def test_landed_cost_voucher_for_odd_numbers(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + do_not_save=True, + ) pr.items[0].cost_center = "Main - TCP1" for x in range(2): - pr.append("items", { - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "cost_center": "Main - TCP1", - "qty": 5, - "rate": 50 - }) + pr.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + "cost_center": "Main - TCP1", + "qty": 5, + "rate": 50, + }, + ) pr.submit() lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) @@ -245,37 +303,50 @@ class TestLandedCostVoucher(ERPNextTestCase): self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08) def test_multiple_landed_cost_voucher_against_pr(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Stores - TCP1", do_not_save=True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Stores - TCP1", + do_not_save=True, + ) - pr.append("items", { - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "cost_center": "Main - TCP1", - "qty": 5, - "rate": 100 - }) + pr.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + "cost_center": "Main - TCP1", + "qty": 5, + "rate": 100, + }, + ) pr.submit() - lcv1 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt', - receipt_document=pr.name, charges=100, do_not_save=True) + lcv1 = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) lcv1.insert() - lcv1.set('items', [ - lcv1.get('items')[0] - ]) + lcv1.set("items", [lcv1.get("items")[0]]) distribute_landed_cost_on_items(lcv1) lcv1.submit() - lcv2 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt', - receipt_document=pr.name, charges=100, do_not_save=True) + lcv2 = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) lcv2.insert() - lcv2.set('items', [ - lcv2.get('items')[1] - ]) + lcv2.set("items", [lcv2.get("items")[1]]) distribute_landed_cost_on_items(lcv2) lcv2.submit() @@ -294,22 +365,31 @@ class TestLandedCostVoucher(ERPNextTestCase): save_new_records(test_records) ## Create USD Shipping charges_account - usd_shipping = create_account(account_name="Shipping Charges USD", - parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", - account_currency="USD") + usd_shipping = create_account( + account_name="Shipping Charges USD", + parent_account="Duties and Taxes - TCP1", + company="_Test Company with perpetual inventory", + account_currency="USD", + ) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Stores - TCP1") + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Stores - TCP1", + ) pr.submit() - lcv = make_landed_cost_voucher(company = pr.company, receipt_document_type = "Purchase Receipt", - receipt_document=pr.name, charges=100, do_not_save=True) + lcv = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) - lcv.append("taxes", { - "description": "Shipping Charges", - "expense_account": usd_shipping, - "amount": 10 - }) + lcv.append( + "taxes", {"description": "Shipping Charges", "expense_account": usd_shipping, "amount": 10} + ) lcv.save() lcv.submit() @@ -319,12 +399,18 @@ class TestLandedCostVoucher(ERPNextTestCase): self.assertEqual(lcv.total_taxes_and_charges, 729) self.assertEqual(pr.items[0].landed_cost_voucher_amount, 729) - gl_entries = frappe.get_all("GL Entry", fields=["account", "credit", "credit_in_account_currency"], - filters={"voucher_no": pr.name, "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"])}) + gl_entries = frappe.get_all( + "GL Entry", + fields=["account", "credit", "credit_in_account_currency"], + filters={ + "voucher_no": pr.name, + "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"]), + }, + ) expected_gl_entries = { "Shipping Charges USD - TCP1": [629, 10], - "Expenses Included In Valuation - TCP1": [100, 100] + "Expenses Included In Valuation - TCP1": [100, 100], } for entry in gl_entries: @@ -334,7 +420,9 @@ class TestLandedCostVoucher(ERPNextTestCase): def test_asset_lcv(self): "Check if LCV for an Asset updates the Assets Gross Purchase Amount correctly." - frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC") + frappe.db.set_value( + "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" + ) if not frappe.db.exists("Asset Category", "Computers"): create_asset_category() @@ -345,15 +433,16 @@ class TestLandedCostVoucher(ERPNextTestCase): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=50000) # check if draft asset was created - assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) + assets = frappe.db.get_all("Asset", filters={"purchase_receipt": pr.name}) self.assertEqual(len(assets), 1) lcv = make_landed_cost_voucher( - company = pr.company, - receipt_document_type = "Purchase Receipt", + company=pr.company, + receipt_document_type="Purchase Receipt", receipt_document=pr.name, charges=80, - expense_account="Expenses Included In Valuation - _TC") + expense_account="Expenses Included In Valuation - _TC", + ) lcv.save() lcv.submit() @@ -365,27 +454,38 @@ class TestLandedCostVoucher(ERPNextTestCase): lcv.cancel() pr.cancel() -def make_landed_cost_voucher(** args): + +def make_landed_cost_voucher(**args): args = frappe._dict(args) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) - lcv = frappe.new_doc('Landed Cost Voucher') - lcv.company = args.company or '_Test Company' - lcv.distribute_charges_based_on = 'Amount' + lcv = frappe.new_doc("Landed Cost Voucher") + lcv.company = args.company or "_Test Company" + lcv.distribute_charges_based_on = "Amount" - lcv.set('purchase_receipts', [{ - "receipt_document_type": args.receipt_document_type, - "receipt_document": args.receipt_document, - "supplier": ref_doc.supplier, - "posting_date": ref_doc.posting_date, - "grand_total": ref_doc.grand_total - }]) + lcv.set( + "purchase_receipts", + [ + { + "receipt_document_type": args.receipt_document_type, + "receipt_document": args.receipt_document, + "supplier": ref_doc.supplier, + "posting_date": ref_doc.posting_date, + "grand_total": ref_doc.grand_total, + } + ], + ) - lcv.set("taxes", [{ - "description": "Shipping Charges", - "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1", - "amount": args.charges - }]) + lcv.set( + "taxes", + [ + { + "description": "Shipping Charges", + "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1", + "amount": args.charges, + } + ], + ) if not args.do_not_save: lcv.insert() @@ -400,21 +500,31 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company, lcv = frappe.new_doc("Landed Cost Voucher") lcv.company = company - lcv.distribute_charges_based_on = 'Amount' + lcv.distribute_charges_based_on = "Amount" - lcv.set("purchase_receipts", [{ - "receipt_document_type": receipt_document_type, - "receipt_document": receipt_document, - "supplier": ref_doc.supplier, - "posting_date": ref_doc.posting_date, - "grand_total": ref_doc.base_grand_total - }]) + lcv.set( + "purchase_receipts", + [ + { + "receipt_document_type": receipt_document_type, + "receipt_document": receipt_document, + "supplier": ref_doc.supplier, + "posting_date": ref_doc.posting_date, + "grand_total": ref_doc.base_grand_total, + } + ], + ) - lcv.set("taxes", [{ - "description": "Insurance Charges", - "expense_account": "Expenses Included In Valuation - TCP1", - "amount": charges - }]) + lcv.set( + "taxes", + [ + { + "description": "Insurance Charges", + "expense_account": "Expenses Included In Valuation - TCP1", + "amount": charges, + } + ], + ) lcv.insert() @@ -424,6 +534,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company, return lcv + def distribute_landed_cost_on_items(lcv): based_on = lcv.distribute_charges_based_on.lower() total = sum(flt(d.get(based_on)) for d in lcv.get("items")) @@ -432,4 +543,5 @@ def distribute_landed_cost_on_items(lcv): item.applicable_charges = flt(item.get(based_on)) * flt(lcv.total_taxes_and_charges) / flt(total) item.applicable_charges = flt(item.applicable_charges, lcv.precision("applicable_charges", item)) -test_records = frappe.get_test_records('Landed Cost Voucher') + +test_records = frappe.get_test_records("Landed Cost Voucher") diff --git a/erpnext/stock/doctype/manufacturer/test_manufacturer.py b/erpnext/stock/doctype/manufacturer/test_manufacturer.py index 66323478c83..e176b28b85a 100644 --- a/erpnext/stock/doctype/manufacturer/test_manufacturer.py +++ b/erpnext/stock/doctype/manufacturer/test_manufacturer.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Manufacturer') + class TestManufacturer(unittest.TestCase): pass diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 087a7883e09..cc64b5caa52 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -214,6 +214,7 @@ frappe.ui.form.on('Material Request', { material_request_type: frm.doc.material_request_type, plc_conversion_rate: 1, rate: item.rate, + uom: item.uom, conversion_factor: item.conversion_factor }, overwrite_warehouse: overwrite_warehouse @@ -392,6 +393,7 @@ frappe.ui.form.on("Material Request Item", { item_code: function(frm, doctype, name) { const item = locals[doctype][name]; item.rate = 0; + item.uom = ''; set_schedule_date(frm); frm.events.get_item_data(frm, item, true); }, diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 50d43171f80..415c45cf1fe 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -19,9 +19,8 @@ from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty -form_grid_templates = { - "items": "templates/form_grid/material_request_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"} + class MaterialRequest(BuyingController): def get_feed(self): @@ -31,8 +30,8 @@ class MaterialRequest(BuyingController): pass def validate_qty_against_so(self): - so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}} - for d in self.get('items'): + so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}} + for d in self.get("items"): if d.sales_order: if not d.sales_order in so_items: so_items[d.sales_order] = {d.item_code: flt(d.qty)} @@ -44,24 +43,34 @@ class MaterialRequest(BuyingController): for so_no in so_items.keys(): for item in so_items[so_no].keys(): - already_indented = frappe.db.sql("""select sum(qty) + already_indented = frappe.db.sql( + """select sum(qty) from `tabMaterial Request Item` where item_code = %s and sales_order = %s and - docstatus = 1 and parent != %s""", (item, so_no, self.name)) + docstatus = 1 and parent != %s""", + (item, so_no, self.name), + ) already_indented = already_indented and flt(already_indented[0][0]) or 0 - actual_so_qty = frappe.db.sql("""select sum(stock_qty) from `tabSales Order Item` - where parent = %s and item_code = %s and docstatus = 1""", (so_no, item)) + actual_so_qty = frappe.db.sql( + """select sum(stock_qty) from `tabSales Order Item` + where parent = %s and item_code = %s and docstatus = 1""", + (so_no, item), + ) actual_so_qty = actual_so_qty and flt(actual_so_qty[0][0]) or 0 if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty): - frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no)) + frappe.throw( + _("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format( + actual_so_qty - already_indented, item, so_no + ) + ) def validate(self): super(MaterialRequest, self).validate() self.validate_schedule_date() - self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order') + self.check_for_on_hold_or_closed_status("Sales Order", "sales_order") self.validate_uom_is_integer("uom", "qty") self.validate_material_request_type() @@ -69,9 +78,22 @@ class MaterialRequest(BuyingController): self.status = "Draft" from erpnext.controllers.status_updater import validate_status - validate_status(self.status, - ["Draft", "Submitted", "Stopped", "Cancelled", "Pending", - "Partially Ordered", "Ordered", "Issued", "Transferred", "Received"]) + + validate_status( + self.status, + [ + "Draft", + "Submitted", + "Stopped", + "Cancelled", + "Pending", + "Partially Ordered", + "Ordered", + "Issued", + "Transferred", + "Received", + ], + ) validate_for_items(self) @@ -83,23 +105,26 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def before_update_after_submit(self): + self.validate_schedule_date() + def validate_material_request_type(self): - """ Validate fields in accordance with selected type """ + """Validate fields in accordance with selected type""" if self.material_request_type != "Customer Provided": self.customer = None def set_title(self): - '''Set title as comma separated list of items''' + """Set title as comma separated list of items""" if not self.title: - items = ', '.join([d.item_name for d in self.items][:3]) - self.title = _('{0} Request for {1}').format(self.material_request_type, items)[:100] + items = ", ".join([d.item_name for d in self.items][:3]) + self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100] def on_submit(self): # frappe.db.set(self, 'status', 'Submitted') self.update_requested_qty() self.update_requested_qty_in_production_plan() - if self.material_request_type == 'Purchase': + if self.material_request_type == "Purchase": self.validate_budget() def before_save(self): @@ -112,13 +137,15 @@ class MaterialRequest(BuyingController): # if MRQ is already closed, no point saving the document check_on_hold_or_closed_status(self.doctype, self.name) - self.set_status(update=True, status='Cancelled') + self.set_status(update=True, status="Cancelled") def check_modified_date(self): - mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", - self.name) - date_diff = frappe.db.sql("""select TIMEDIFF('%s', '%s')""" - % (mod_db[0][0], cstr(self.modified))) + mod_db = frappe.db.sql( + """select modified from `tabMaterial Request` where name = %s""", self.name + ) + date_diff = frappe.db.sql( + """select TIMEDIFF('%s', '%s')""" % (mod_db[0][0], cstr(self.modified)) + ) if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name)) @@ -134,22 +161,24 @@ class MaterialRequest(BuyingController): validates that `status` is acceptable for the present controller status and throws an Exception if otherwise. """ - if self.status and self.status == 'Cancelled': + if self.status and self.status == "Cancelled": # cancelled documents cannot change if status != self.status: frappe.throw( - _("{0} {1} is cancelled so the action cannot be completed"). - format(_(self.doctype), self.name), - frappe.InvalidStatusError + _("{0} {1} is cancelled so the action cannot be completed").format( + _(self.doctype), self.name + ), + frappe.InvalidStatusError, ) - elif self.status and self.status == 'Draft': + elif self.status and self.status == "Draft": # draft document to pending only - if status != 'Pending': + if status != "Pending": frappe.throw( - _("{0} {1} has not been submitted so the action cannot be completed"). - format(_(self.doctype), self.name), - frappe.InvalidStatusError + _("{0} {1} has not been submitted so the action cannot be completed").format( + _(self.doctype), self.name + ), + frappe.InvalidStatusError, ) def on_cancel(self): @@ -166,67 +195,90 @@ class MaterialRequest(BuyingController): for d in self.get("items"): if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): - d.ordered_qty = flt(frappe.db.sql("""select sum(transfer_qty) + d.ordered_qty = flt( + frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail` where material_request = %s and material_request_item = %s and docstatus = 1""", - (self.name, d.name))[0][0]) - mr_qty_allowance = frappe.db.get_single_value('Stock Settings', 'mr_qty_allowance') + (self.name, d.name), + )[0][0] + ) + mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") if mr_qty_allowance: - allowed_qty = d.qty + (d.qty * (mr_qty_allowance/100)) + allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100)) if d.ordered_qty and d.ordered_qty > allowed_qty: - frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ - cannot be greater than allowed requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, allowed_qty, d.item_code)) + frappe.throw( + _( + "The total Issue / Transfer quantity {0} in Material Request {1} \ + cannot be greater than allowed requested quantity {2} for Item {3}" + ).format(d.ordered_qty, d.parent, allowed_qty, d.item_code) + ) elif d.ordered_qty and d.ordered_qty > d.stock_qty: - frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ - cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code)) + frappe.throw( + _( + "The total Issue / Transfer quantity {0} in Material Request {1} \ + cannot be greater than requested quantity {2} for Item {3}" + ).format(d.ordered_qty, d.parent, d.qty, d.item_code) + ) elif self.material_request_type == "Manufacture": - d.ordered_qty = flt(frappe.db.sql("""select sum(qty) + d.ordered_qty = flt( + frappe.db.sql( + """select sum(qty) from `tabWork Order` where material_request = %s and material_request_item = %s and docstatus = 1""", - (self.name, d.name))[0][0]) + (self.name, d.name), + )[0][0] + ) frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty) - self._update_percent_field({ - "target_dt": "Material Request Item", - "target_parent_dt": self.doctype, - "target_parent_field": "per_ordered", - "target_ref_field": "stock_qty", - "target_field": "ordered_qty", - "name": self.name, - }, update_modified) + self._update_percent_field( + { + "target_dt": "Material Request Item", + "target_parent_dt": self.doctype, + "target_parent_field": "per_ordered", + "target_ref_field": "stock_qty", + "target_field": "ordered_qty", + "name": self.name, + }, + update_modified, + ) def update_requested_qty(self, mr_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] for d in self.get("items"): - if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \ - and d.warehouse and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 : + if ( + (not mr_item_rows or d.name in mr_item_rows) + and [d.item_code, d.warehouse] not in item_wh_list + and d.warehouse + and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 + ): item_wh_list.append([d.item_code, d.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, { - "indented_qty": get_indented_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"indented_qty": get_indented_qty(item_code, warehouse)}) def update_requested_qty_in_production_plan(self): production_plans = [] - for d in self.get('items'): + for d in self.get("items"): if d.production_plan and d.material_request_plan_item: qty = d.qty if self.docstatus == 1 else 0 - frappe.db.set_value('Material Request Plan Item', - d.material_request_plan_item, 'requested_qty', qty) + frappe.db.set_value( + "Material Request Plan Item", d.material_request_plan_item, "requested_qty", qty + ) if d.production_plan not in production_plans: production_plans.append(d.production_plan) for production_plan in production_plans: - doc = frappe.get_doc('Production Plan', production_plan) + doc = frappe.get_doc("Production Plan", production_plan) doc.set_status() - doc.db_set('status', doc.status) + doc.db_set("status", doc.status) + def update_completed_and_requested_qty(stock_entry, method): if stock_entry.doctype == "Stock Entry": @@ -241,43 +293,55 @@ def update_completed_and_requested_qty(stock_entry, method): mr_obj = frappe.get_doc("Material Request", mr) if mr_obj.status in ["Stopped", "Cancelled"]: - frappe.throw(_("{0} {1} is cancelled or stopped").format(_("Material Request"), mr), - frappe.InvalidStatusError) + frappe.throw( + _("{0} {1} is cancelled or stopped").format(_("Material Request"), mr), + frappe.InvalidStatusError, + ) mr_obj.update_completed_qty(mr_item_rows) mr_obj.update_requested_qty(mr_item_rows) + def set_missing_values(source, target_doc): - if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()): + if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate( + nowdate() + ): target_doc.schedule_date = None target_doc.run_method("set_missing_values") target_doc.run_method("calculate_taxes_and_totals") + def update_item(obj, target, source_parent): target.conversion_factor = obj.conversion_factor - target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor - target.stock_qty = (target.qty * target.conversion_factor) + target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + target.stock_qty = target.qty * target.conversion_factor if getdate(target.schedule_date) < getdate(nowdate()): target.schedule_date = None + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Material Request'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Material Request"), + } + ) return list_context + @frappe.whitelist() def update_status(name, status): - material_request = frappe.get_doc('Material Request', name) - material_request.check_permission('write') + material_request = frappe.get_doc("Material Request", name) + material_request.check_permission("write") material_request.update_status(status) + @frappe.whitelist() def make_purchase_order(source_name, target_doc=None, args=None): if args is None: @@ -290,7 +354,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): # items only for given default supplier supplier_items = [] for d in target_doc.items: - default_supplier = get_item_defaults(d.item_code, target_doc.company).get('default_supplier') + default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier") if frappe.flags.args.default_supplier == default_supplier: supplier_items.append(d) target_doc.items = supplier_items @@ -298,58 +362,65 @@ def make_purchase_order(source_name, target_doc=None, args=None): set_missing_values(source, target_doc) def select_item(d): - filtered_items = args.get('filtered_children', []) + filtered_items = args.get("filtered_children", []) child_filter = d.name in filtered_items if filtered_items else True return d.ordered_qty < d.stock_qty and child_filter - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Purchase Order", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Purchase Order", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "stock_uom"], + ["uom", "uom"], + ["sales_order", "sales_order"], + ["sales_order_item", "sales_order_item"], + ], + "postprocess": update_item, + "condition": select_item, + }, }, - "Material Request Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "stock_uom"], - ["uom", "uom"], - ["sales_order", "sales_order"], - ["sales_order_item", "sales_order_item"] - ], - "postprocess": update_item, - "condition": select_item - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Request for Quotation", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Request for Quotation", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Request for Quotation Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "uom"], + ], + }, }, - "Material Request Item": { - "doctype": "Request for Quotation Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "uom"] - ] - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=None): mr = source_name @@ -360,43 +431,59 @@ def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=Non target_doc.supplier = args.get("supplier") if getdate(target_doc.schedule_date) < getdate(nowdate()): target_doc.schedule_date = None - target_doc.set("items", [d for d in target_doc.get("items") - if d.get("item_code") in supplier_items and d.get("qty") > 0]) + target_doc.set( + "items", + [ + d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0 + ], + ) set_missing_values(source, target_doc) - target_doc = get_mapped_doc("Material Request", mr, { - "Material Request": { - "doctype": "Purchase Order", + target_doc = get_mapped_doc( + "Material Request", + mr, + { + "Material Request": { + "doctype": "Purchase Order", + }, + "Material Request Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "stock_uom"], + ["uom", "uom"], + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.qty, + }, }, - "Material Request Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "stock_uom"], - ["uom", "uom"] - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.qty - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return target_doc + @frappe.whitelist() def get_items_based_on_default_supplier(supplier): - supplier_items = [d.parent for d in frappe.db.get_all("Item Default", - {"default_supplier": supplier, "parenttype": "Item"}, 'parent')] + supplier_items = [ + d.parent + for d in frappe.db.get_all( + "Item Default", {"default_supplier": supplier, "parenttype": "Item"}, "parent" + ) + ] return supplier_items + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters): conditions = "" if txt: - conditions += "and mr.name like '%%"+txt+"%%' " + conditions += "and mr.name like '%%" + txt + "%%' " if filters.get("transaction_date"): date = filters.get("transaction_date")[1] @@ -408,7 +495,8 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa if not supplier_items: frappe.throw(_("{0} is not the default supplier for any items.").format(supplier)) - material_requests = frappe.db.sql("""select distinct mr.name, transaction_date,company + material_requests = frappe.db.sql( + """select distinct mr.name, transaction_date,company from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr.name = mr_item.parent and mr_item.item_code in ({0}) @@ -419,12 +507,16 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa and mr.company = '{1}' {2} order by mr_item.item_code ASC - limit {3} offset {4} """ \ - .format(', '.join(['%s']*len(supplier_items)), filters.get("company"), conditions, page_len, start), - tuple(supplier_items), as_dict=1) + limit {3} offset {4} """.format( + ", ".join(["%s"] * len(supplier_items)), filters.get("company"), conditions, page_len, start + ), + tuple(supplier_items), + as_dict=1, + ) return material_requests + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters): @@ -433,47 +525,63 @@ def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filte for d in doc.items: item_list.append(d.item_code) - return frappe.db.sql("""select default_supplier + return frappe.db.sql( + """select default_supplier from `tabItem Default` where parent in ({0}) and default_supplier IS NOT NULL - """.format(', '.join(['%s']*len(item_list))),tuple(item_list)) + """.format( + ", ".join(["%s"] * len(item_list)) + ), + tuple(item_list), + ) + @frappe.whitelist() def make_supplier_quotation(source_name, target_doc=None): def postprocess(source, target_doc): set_missing_values(source, target_doc) - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Supplier Quotation", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Supplier Quotation", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Supplier Quotation Item", + "field_map": { + "name": "material_request_item", + "parent": "material_request", + "sales_order": "sales_order", + }, + }, }, - "Material Request Item": { - "doctype": "Supplier Quotation Item", - "field_map": { - "name": "material_request_item", - "parent": "material_request", - "sales_order": "sales_order" - } - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_stock_entry(source_name, target_doc=None): def update_item(obj, target, source_parent): - qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor \ - if flt(obj.stock_qty) > flt(obj.ordered_qty) else 0 + qty = ( + flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + if flt(obj.stock_qty) > flt(obj.ordered_qty) + else 0 + ) target.qty = qty target.transfer_qty = qty * obj.conversion_factor target.conversion_factor = obj.conversion_factor - if source_parent.material_request_type == "Material Transfer" or source_parent.material_request_type == "Customer Provided": + if ( + source_parent.material_request_type == "Material Transfer" + or source_parent.material_request_type == "Customer Provided" + ): target.t_warehouse = obj.warehouse else: target.s_warehouse = obj.warehouse @@ -487,7 +595,7 @@ def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.purpose = source.material_request_type if source.job_card: - target.purpose = 'Material Transfer for Manufacture' + target.purpose = "Material Transfer for Manufacture" if source.material_request_type == "Customer Provided": target.purpose = "Material Receipt" @@ -496,101 +604,119 @@ def make_stock_entry(source_name, target_doc=None): target.set_stock_entry_type() target.set_job_card_data() - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]] - } - }, - "Material Request Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "name": "material_request_item", - "parent": "material_request", - "uom": "stock_uom", - "job_card_item": "job_card_item" + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Stock Entry", + "validation": { + "docstatus": ["=", 1], + "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]], + }, }, - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty - } - }, target_doc, set_missing_values) + "Material Request Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "name": "material_request_item", + "parent": "material_request", + "uom": "stock_uom", + "job_card_item": "job_card_item", + }, + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty, + }, + }, + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def raise_work_orders(material_request): - mr= frappe.get_doc("Material Request", material_request) - errors =[] + mr = frappe.get_doc("Material Request", material_request) + errors = [] work_orders = [] - default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") + default_wip_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_wip_warehouse" + ) for d in mr.items: if (d.stock_qty - d.ordered_qty) > 0: if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}): wo_order = frappe.new_doc("Work Order") - wo_order.update({ - "production_item": d.item_code, - "qty": d.stock_qty - d.ordered_qty, - "fg_warehouse": d.warehouse, - "wip_warehouse": default_wip_warehouse, - "description": d.description, - "stock_uom": d.stock_uom, - "expected_delivery_date": d.schedule_date, - "sales_order": d.sales_order, - "sales_order_item": d.get("sales_order_item"), - "bom_no": get_item_details(d.item_code).bom_no, - "material_request": mr.name, - "material_request_item": d.name, - "planned_start_date": mr.transaction_date, - "company": mr.company - }) + wo_order.update( + { + "production_item": d.item_code, + "qty": d.stock_qty - d.ordered_qty, + "fg_warehouse": d.warehouse, + "wip_warehouse": default_wip_warehouse, + "description": d.description, + "stock_uom": d.stock_uom, + "expected_delivery_date": d.schedule_date, + "sales_order": d.sales_order, + "sales_order_item": d.get("sales_order_item"), + "bom_no": get_item_details(d.item_code).bom_no, + "material_request": mr.name, + "material_request_item": d.name, + "planned_start_date": mr.transaction_date, + "company": mr.company, + } + ) wo_order.set_work_order_operations() wo_order.save() work_orders.append(wo_order.name) else: - errors.append(_("Row {0}: Bill of Materials not found for the Item {1}") - .format(d.idx, get_link_to_form("Item", d.item_code))) + errors.append( + _("Row {0}: Bill of Materials not found for the Item {1}").format( + d.idx, get_link_to_form("Item", d.item_code) + ) + ) if work_orders: work_orders_list = [get_link_to_form("Work Order", d) for d in work_orders] if len(work_orders) > 1: - msgprint(_("The following {0} were created: {1}") - .format(frappe.bold(_("Work Orders")), '
    ' + ', '.join(work_orders_list))) + msgprint( + _("The following {0} were created: {1}").format( + frappe.bold(_("Work Orders")), "
    " + ", ".join(work_orders_list) + ) + ) else: - msgprint(_("The {0} {1} created sucessfully") - .format(frappe.bold(_("Work Order")), work_orders_list[0])) + msgprint( + _("The {0} {1} created sucessfully").format(frappe.bold(_("Work Order")), work_orders_list[0]) + ) if errors: - frappe.throw(_("Work Order cannot be created for following reason:
    {0}") - .format(new_line_sep(errors))) + frappe.throw( + _("Work Order cannot be created for following reason:
    {0}").format(new_line_sep(errors)) + ) return work_orders + @frappe.whitelist() def create_pick_list(source_name, target_doc=None): - doc = get_mapped_doc('Material Request', source_name, { - 'Material Request': { - 'doctype': 'Pick List', - 'field_map': { - 'material_request_type': 'purpose' + doc = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Pick List", + "field_map": {"material_request_type": "purpose"}, + "validation": {"docstatus": ["=", 1]}, }, - 'validation': { - 'docstatus': ['=', 1] - } - }, - 'Material Request Item': { - 'doctype': 'Pick List Item', - 'field_map': { - 'name': 'material_request_item', - 'qty': 'stock_qty' + "Material Request Item": { + "doctype": "Pick List Item", + "field_map": {"name": "material_request_item", "qty": "stock_qty"}, }, }, - }, target_doc) + target_doc, + ) doc.set_item_locations() diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index 9133859b24f..b073e6a22ee 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -1,23 +1,15 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'material_request', - 'transactions': [ + "fieldname": "material_request", + "transactions": [ { - 'label': _('Reference'), - 'items': ['Request for Quotation', 'Supplier Quotation', 'Purchase Order'] + "label": _("Reference"), + "items": ["Request for Quotation", "Supplier Quotation", "Purchase Order"], }, - { - 'label': _('Stock'), - 'items': ['Stock Entry', 'Purchase Receipt', 'Pick List'] - - }, - { - 'label': _('Manufacturing'), - 'items': ['Work Order'] - } - ] + {"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]}, + {"label": _("Manufacturing"), "items": ["Work Order"]}, + ], } diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 383b0ae806e..78af1532ea8 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -6,6 +6,7 @@ import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt, today from erpnext.stock.doctype.item.test_item import create_item @@ -15,15 +16,13 @@ from erpnext.stock.doctype.material_request.material_request import ( make_supplier_quotation, raise_work_orders, ) -from erpnext.tests.utils import ERPNextTestCase -class TestMaterialRequest(ERPNextTestCase): +class TestMaterialRequest(FrappeTestCase): def test_make_purchase_order(self): mr = frappe.copy_doc(test_records[0]).insert() - self.assertRaises(frappe.ValidationError, make_purchase_order, - mr.name) + self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.submit() @@ -44,7 +43,6 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(sq.doctype, "Supplier Quotation") self.assertEqual(len(sq.get("items")), len(mr.get("items"))) - def test_make_stock_entry(self): mr = frappe.copy_doc(test_records[0]).insert() @@ -58,42 +56,44 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(len(se.get("items")), len(mr.get("items"))) - def _insert_stock_entry(self, qty1, qty2, warehouse = None ): - se = frappe.get_doc({ - "company": "_Test Company", - "doctype": "Stock Entry", - "posting_date": "2013-03-01", - "posting_time": "00:00:00", - "purpose": "Material Receipt", - "items": [ - { - "conversion_factor": 1.0, - "doctype": "Stock Entry Detail", - "item_code": "_Test Item Home Desktop 100", - "parentfield": "items", - "basic_rate": 100, - "qty": qty1, - "stock_uom": "_Test UOM 1", - "transfer_qty": qty1, - "uom": "_Test UOM 1", - "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", - "cost_center": "_Test Cost Center - _TC" - }, - { - "conversion_factor": 1.0, - "doctype": "Stock Entry Detail", - "item_code": "_Test Item Home Desktop 200", - "parentfield": "items", - "basic_rate": 100, - "qty": qty2, - "stock_uom": "_Test UOM 1", - "transfer_qty": qty2, - "uom": "_Test UOM 1", - "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", - "cost_center": "_Test Cost Center - _TC" - } - ] - }) + def _insert_stock_entry(self, qty1, qty2, warehouse=None): + se = frappe.get_doc( + { + "company": "_Test Company", + "doctype": "Stock Entry", + "posting_date": "2013-03-01", + "posting_time": "00:00:00", + "purpose": "Material Receipt", + "items": [ + { + "conversion_factor": 1.0, + "doctype": "Stock Entry Detail", + "item_code": "_Test Item Home Desktop 100", + "parentfield": "items", + "basic_rate": 100, + "qty": qty1, + "stock_uom": "_Test UOM 1", + "transfer_qty": qty1, + "uom": "_Test UOM 1", + "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "conversion_factor": 1.0, + "doctype": "Stock Entry Detail", + "item_code": "_Test Item Home Desktop 200", + "parentfield": "items", + "basic_rate": 100, + "qty": qty2, + "stock_uom": "_Test UOM 1", + "transfer_qty": qty2, + "uom": "_Test UOM 1", + "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ], + } + ) se.set_stock_entry_type() se.insert() @@ -106,19 +106,19 @@ class TestMaterialRequest(ERPNextTestCase): mr.load_from_db() mr.cancel() - self.assertRaises(frappe.ValidationError, mr.update_status, 'Stopped') + self.assertRaises(frappe.ValidationError, mr.update_status, "Stopped") def test_mr_changes_from_stopped_to_pending_after_reopen(self): mr = frappe.copy_doc(test_records[0]) mr.insert() mr.submit() - self.assertEqual('Pending', mr.status) + self.assertEqual("Pending", mr.status) - mr.update_status('Stopped') - self.assertEqual('Stopped', mr.status) + mr.update_status("Stopped") + self.assertEqual("Stopped", mr.status) - mr.update_status('Submitted') - self.assertEqual('Pending', mr.status) + mr.update_status("Submitted") + self.assertEqual("Pending", mr.status) def test_cannot_submit_cancelled_mr(self): mr = frappe.copy_doc(test_records[0]) @@ -133,7 +133,7 @@ class TestMaterialRequest(ERPNextTestCase): mr.insert() mr.submit() mr.cancel() - self.assertEqual('Cancelled', mr.status) + self.assertEqual("Cancelled", mr.status) def test_cannot_change_cancelled_mr(self): mr = frappe.copy_doc(test_records[0]) @@ -142,12 +142,12 @@ class TestMaterialRequest(ERPNextTestCase): mr.load_from_db() mr.cancel() - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Draft') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Stopped') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Ordered') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Issued') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Transferred') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Pending') + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Draft") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Ordered") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Issued") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Transferred") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Pending") def test_cannot_submit_deleted_material_request(self): mr = frappe.copy_doc(test_records[0]) @@ -169,9 +169,9 @@ class TestMaterialRequest(ERPNextTestCase): mr.submit() mr.load_from_db() - mr.update_status('Stopped') - mr.update_status('Submitted') - self.assertEqual(mr.status, 'Pending') + mr.update_status("Stopped") + mr.update_status("Submitted") + self.assertEqual(mr.status, "Pending") def test_pending_mr_changes_to_stopped_after_stop(self): mr = frappe.copy_doc(test_records[0]) @@ -179,17 +179,21 @@ class TestMaterialRequest(ERPNextTestCase): mr.submit() mr.load_from_db() - mr.update_status('Stopped') - self.assertEqual(mr.status, 'Stopped') + mr.update_status("Stopped") + self.assertEqual(mr.status, "Stopped") def test_cannot_stop_unsubmitted_mr(self): mr = frappe.copy_doc(test_records[0]) mr.insert() - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Stopped') + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped") def test_completed_qty_for_purchase(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -206,19 +210,18 @@ class TestMaterialRequest(ERPNextTestCase): po_doc.get("items")[0].schedule_date = "2013-07-09" po_doc.get("items")[1].schedule_date = "2013-07-09" - # check for stopped status of Material Request po = frappe.copy_doc(po_doc) po.insert() po.load_from_db() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, po.submit) frappe.db.set(po, "docstatus", 1) self.assertRaises(frappe.InvalidStatusError, po.cancel) # resubmit and check for per complete mr.load_from_db() - mr.update_status('Submitted') + mr.update_status("Submitted") po = frappe.copy_doc(po_doc) po.insert() po.submit() @@ -229,8 +232,12 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 27.0) self.assertEqual(mr.get("items")[1].ordered_qty, 1.5) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5) @@ -242,15 +249,23 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) def test_completed_qty_for_transfer(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -264,31 +279,31 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "01:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 27.0, - "transfer_qty": 27.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) - se_doc.get("items")[1].update({ - "qty": 1.5, - "transfer_qty": 1.5, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "01:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + {"qty": 27.0, "transfer_qty": 27.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) + se_doc.get("items")[1].update( + {"qty": 1.5, "transfer_qty": 1.5, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) # make available the qty in _Test Warehouse 1 before transfer self._insert_stock_entry(27.0, 1.5) @@ -296,17 +311,17 @@ class TestMaterialRequest(ERPNextTestCase): # check for stopped status of Material Request se = frappe.copy_doc(se_doc) se.insert() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.submit) - mr.update_status('Submitted') + mr.update_status("Submitted") se.flags.ignore_validate_update_after_submit = True se.submit() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.cancel) - mr.update_status('Submitted') + mr.update_status("Submitted") se = frappe.copy_doc(se_doc) se.insert() se.submit() @@ -317,8 +332,12 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 27.0) self.assertEqual(mr.get("items")[1].ordered_qty, 1.5) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5) @@ -330,56 +349,70 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) def test_over_transfer_qty_allowance(self): - mr = frappe.new_doc('Material Request') + mr = frappe.new_doc("Material Request") mr.company = "_Test Company" mr.scheduled_date = today() - mr.append('items',{ - "item_code": "_Test FG Item", - "item_name": "_Test FG Item", - "qty": 10, - "schedule_date": today(), - "uom": "_Test UOM 1", - "warehouse": "_Test Warehouse - _TC" - }) + mr.append( + "items", + { + "item_code": "_Test FG Item", + "item_name": "_Test FG Item", + "qty": 10, + "schedule_date": today(), + "uom": "_Test UOM 1", + "warehouse": "_Test Warehouse - _TC", + }, + ) mr.material_request_type = "Material Transfer" mr.insert() mr.submit() - frappe.db.set_value('Stock Settings', None, 'mr_qty_allowance', 20) + frappe.db.set_value("Stock Settings", None, "mr_qty_allowance", 20) # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": today(), - "posting_time": "00:00", - }) - se_doc.get("items")[0].update({ - "qty": 13, - "transfer_qty": 12.0, - "s_warehouse": "_Test Warehouse - _TC", - "t_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": today(), + "posting_time": "00:00", + } + ) + se_doc.get("items")[0].update( + { + "qty": 13, + "transfer_qty": 12.0, + "s_warehouse": "_Test Warehouse - _TC", + "t_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0, + } + ) # make available the qty in _Test Warehouse 1 before transfer sr = frappe.new_doc("Stock Reconciliation") sr.company = "_Test Company" sr.purpose = "Opening Stock" - sr.append('items', { - "item_code": "_Test FG Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 20, - "valuation_rate": 0.01 - }) + sr.append( + "items", + { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 20, + "valuation_rate": 0.01, + }, + ) sr.insert() sr.submit() se = frappe.copy_doc(se_doc) @@ -389,8 +422,12 @@ class TestMaterialRequest(ERPNextTestCase): se.submit() def test_completed_qty_for_over_transfer(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -401,23 +438,19 @@ class TestMaterialRequest(ERPNextTestCase): # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "00:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 54.0, - "transfer_qty": 54.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) - se_doc.get("items")[1].update({ - "qty": 3.0, - "transfer_qty": 3.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "00:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + {"qty": 54.0, "transfer_qty": 54.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) + se_doc.get("items")[1].update( + {"qty": 3.0, "transfer_qty": 3.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) # make available the qty in _Test Warehouse 1 before transfer self._insert_stock_entry(60.0, 3.0) @@ -426,11 +459,11 @@ class TestMaterialRequest(ERPNextTestCase): se = frappe.copy_doc(se_doc) se.set_stock_entry_type() se.insert() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.submit) self.assertRaises(frappe.InvalidStatusError, se.cancel) - mr.update_status('Submitted') + mr.update_status("Submitted") se = frappe.copy_doc(se_doc) se.set_stock_entry_type() se.insert() @@ -443,8 +476,12 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 54.0) self.assertEqual(mr.get("items")[1].ordered_qty, 3.0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2) @@ -456,8 +493,12 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) @@ -470,25 +511,31 @@ class TestMaterialRequest(ERPNextTestCase): mr.submit() se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "00:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 60.0, - "transfer_qty": 60.0, - "s_warehouse": "_Test Warehouse - _TC", - "t_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) - se_doc.get("items")[1].update({ - "item_code": "_Test Item Home Desktop 100", - "qty": 3.0, - "transfer_qty": 3.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "00:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + { + "qty": 60.0, + "transfer_qty": 60.0, + "s_warehouse": "_Test Warehouse - _TC", + "t_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0, + } + ) + se_doc.get("items")[1].update( + { + "item_code": "_Test Item Home Desktop 100", + "qty": 3.0, + "transfer_qty": 3.0, + "s_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0, + } + ) # check for stopped status of Material Request se = frappe.copy_doc(se_doc) @@ -505,18 +552,20 @@ class TestMaterialRequest(ERPNextTestCase): def test_warehouse_company_validation(self): from erpnext.stock.utils import InvalidWarehouseCompany + mr = frappe.copy_doc(test_records[0]) mr.company = "_Test Company 1" self.assertRaises(InvalidWarehouseCompany, mr.insert) def _get_requested_qty(self, item_code, warehouse): - return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty")) + return flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty") + ) def test_make_stock_entry_for_material_issue(self): mr = frappe.copy_doc(test_records[0]).insert() - self.assertRaises(frappe.ValidationError, make_stock_entry, - mr.name) + self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.material_request_type = "Material Issue" @@ -528,8 +577,13 @@ class TestMaterialRequest(ERPNextTestCase): def test_completed_qty_for_issue(self): def _get_requested_qty(): - return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100", - "warehouse": "_Test Warehouse - _TC"}, "indented_qty")) + return flt( + frappe.db.get_value( + "Bin", + {"item_code": "_Test Item Home Desktop 100", "warehouse": "_Test Warehouse - _TC"}, + "indented_qty", + ) + ) existing_requested_qty = _get_requested_qty() @@ -537,7 +591,7 @@ class TestMaterialRequest(ERPNextTestCase): mr.material_request_type = "Material Issue" mr.submit() - #testing bin value after material request is submitted + # testing bin value after material request is submitted self.assertEqual(_get_requested_qty(), existing_requested_qty - 54.0) # receive items to allow issue @@ -556,7 +610,7 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 54.0) self.assertEqual(mr.get("items")[1].ordered_qty, 3.0) - #testing bin requested qty after issuing stock against material request + # testing bin requested qty after issuing stock against material request self.assertEqual(_get_requested_qty(), existing_requested_qty) def test_material_request_type_manufacture(self): @@ -564,8 +618,11 @@ class TestMaterialRequest(ERPNextTestCase): mr = frappe.get_doc("Material Request", mr.name) mr.submit() completed_qty = mr.items[0].ordered_qty - requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] prod_order = raise_work_orders(mr.name) po = frappe.get_doc("Work Order", prod_order[0]) @@ -575,8 +632,11 @@ class TestMaterialRequest(ERPNextTestCase): mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(completed_qty + po.qty, mr.items[0].ordered_qty) - new_requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + new_requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] self.assertEqual(requested_qty - po.qty, new_requested_qty) @@ -585,17 +645,24 @@ class TestMaterialRequest(ERPNextTestCase): mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(completed_qty, mr.items[0].ordered_qty) - new_requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + new_requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] self.assertEqual(requested_qty, new_requested_qty) def test_requested_qty_multi_uom(self): - existing_requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + existing_requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") - mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture', - uom="_Test UOM 1", conversion_factor=12) + mr = make_material_request( + item_code="_Test FG Item", + material_request_type="Manufacture", + uom="_Test UOM 1", + conversion_factor=12, + ) - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 120) @@ -605,42 +672,36 @@ class TestMaterialRequest(ERPNextTestCase): wo.wip_warehouse = "_Test Warehouse 1 - _TC" wo.submit() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 70) wo.cancel() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 120) mr.reload() mr.cancel() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty) - def test_multi_uom_for_purchase(self): mr = frappe.copy_doc(test_records[0]) - mr.material_request_type = 'Purchase' + mr.material_request_type = "Purchase" item = mr.items[0] mr.schedule_date = today() - if not frappe.db.get_value('UOM Conversion Detail', - {'parent': item.item_code, 'uom': 'Kg'}): - item_doc = frappe.get_doc('Item', item.item_code) - item_doc.append('uoms', { - 'uom': 'Kg', - 'conversion_factor': 5 - }) - item_doc.save(ignore_permissions=True) + if not frappe.db.get_value("UOM Conversion Detail", {"parent": item.item_code, "uom": "Kg"}): + item_doc = frappe.get_doc("Item", item.item_code) + item_doc.append("uoms", {"uom": "Kg", "conversion_factor": 5}) + item_doc.save(ignore_permissions=True) - item.uom = 'Kg' + item.uom = "Kg" for item in mr.items: item.schedule_date = mr.schedule_date mr.insert() - self.assertRaises(frappe.ValidationError, make_purchase_order, - mr.name) + self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.submit() @@ -654,17 +715,19 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(po.doctype, "Purchase Order") self.assertEqual(len(po.get("items")), len(mr.get("items"))) - po.supplier = '_Test Supplier' + po.supplier = "_Test Supplier" po.insert() po.submit() mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(mr.per_ordered, 100) def test_customer_provided_parts_mr(self): - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) + create_item( + "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC") - mr = make_material_request(item_code='CUST-0987', material_request_type='Customer Provided') + mr = make_material_request(item_code="CUST-0987", material_request_type="Customer Provided") se = make_stock_entry(mr.name) se.insert() se.submit() @@ -677,25 +740,30 @@ class TestMaterialRequest(ERPNextTestCase): self.assertEqual(mr.per_ordered, 100) self.assertEqual(existing_requested_qty, current_requested_qty) + def make_material_request(**args): args = frappe._dict(args) mr = frappe.new_doc("Material Request") mr.material_request_type = args.material_request_type or "Purchase" mr.company = args.company or "_Test Company" - mr.customer = args.customer or '_Test Customer' - mr.append("items", { - "item_code": args.item_code or "_Test Item", - "qty": args.qty or 10, - "uom": args.uom or "_Test UOM", - "conversion_factor": args.conversion_factor or 1, - "schedule_date": args.schedule_date or today(), - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC" - }) + mr.customer = args.customer or "_Test Customer" + mr.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "qty": args.qty or 10, + "uom": args.uom or "_Test UOM", + "conversion_factor": args.conversion_factor or 1, + "schedule_date": args.schedule_date or today(), + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + }, + ) mr.insert() if not args.do_not_submit: mr.submit() return mr + test_dependencies = ["Currency Exchange", "BOM"] -test_records = frappe.get_test_records('Material Request') +test_records = frappe.get_test_records("Material Request") diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 2bad42a0ebb..dd66cfff8be 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -177,6 +177,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "bold": 1, "columns": 2, "fieldname": "schedule_date", @@ -459,7 +460,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-11-03 14:40:24.409826", + "modified": "2022-03-10 18:42:42.705190", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py index 32407d0fb09..08c9ed27427 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.py +++ b/erpnext/stock/doctype/material_request_item/material_request_item.py @@ -11,5 +11,6 @@ from frappe.model.document import Document class MaterialRequestItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Material Request Item", ["item_code", "warehouse"]) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index d2d47897658..cb8eb30cb30 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -26,8 +26,10 @@ "section_break_13", "actual_qty", "projected_qty", + "ordered_qty", "column_break_16", "incoming_rate", + "picked_qty", "page_break", "prevdoc_doctype", "parent_detail_docname" @@ -222,15 +224,31 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Rate", + "options": "currency", "print_hide": 1, "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "picked_qty", + "fieldtype": "Float", + "label": "Picked Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-28 16:03:30.780111", + "modified": "2022-04-27 05:23:08.683245", "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 07c2f1f0dd3..4d05d7a345c 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -23,19 +23,23 @@ def make_packing_list(doc): return parent_items_price, reset = {}, False - set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates") + set_price_from_children = frappe.db.get_single_value( + "Selling Settings", "editable_bundle_item_rates" + ) stale_packed_items_table = get_indexed_packed_items_table(doc) reset = reset_packing_list(doc) for item_row in doc.get("items"): - if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): + if is_product_bundle(item_row.item_code): for bundle_item in get_product_bundle_items(item_row.item_code): pi_row = add_packed_item_row( - doc=doc, packing_item=bundle_item, - main_item_row=item_row, packed_items_table=stale_packed_items_table, - reset=reset + doc=doc, + packing_item=bundle_item, + main_item_row=item_row, + packed_items_table=stale_packed_items_table, + reset=reset, ) item_data = get_packed_item_details(bundle_item.item_code, doc.company) update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data) @@ -43,18 +47,23 @@ def make_packing_list(doc): update_packed_item_price_data(pi_row, item_data, doc) update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc) - if set_price_from_children: # create/update bundle item wise price dict + if set_price_from_children: # create/update bundle item wise price dict update_product_bundle_rate(parent_items_price, pi_row) if parent_items_price: - set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item + set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item + + +def is_product_bundle(item_code: str) -> bool: + return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code})) + def get_indexed_packed_items_table(doc): """ - Create dict from stale packed items table like: - {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}} + Create dict from stale packed items table like: + {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}} - Use: to quickly retrieve/check if row existed in table instead of looping n times + Use: to quickly retrieve/check if row existed in table instead of looping n times """ indexed_table = {} for packed_item in doc.get("packed_items"): @@ -63,6 +72,7 @@ def get_indexed_packed_items_table(doc): return indexed_table + def reset_packing_list(doc): "Conditionally reset the table and return if it was reset or not." reset_table = False @@ -86,33 +96,34 @@ def reset_packing_list(doc): doc.set("packed_items", []) return reset_table + def get_product_bundle_items(item_code): product_bundle = frappe.qb.DocType("Product Bundle") product_bundle_item = frappe.qb.DocType("Product Bundle Item") query = ( frappe.qb.from_(product_bundle_item) - .join(product_bundle).on(product_bundle_item.parent == product_bundle.name) + .join(product_bundle) + .on(product_bundle_item.parent == product_bundle.name) .select( product_bundle_item.item_code, product_bundle_item.qty, product_bundle_item.uom, - product_bundle_item.description - ).where( - product_bundle.new_item_code == item_code - ).orderby( - product_bundle_item.idx + product_bundle_item.description, ) + .where(product_bundle.new_item_code == item_code) + .orderby(product_bundle_item.idx) ) return query.run(as_dict=True) + def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, reset): """Add and return packed item row. - doc: Transaction document - packing_item (dict): Packed Item details - main_item_row (dict): Items table row corresponding to packed item - packed_items_table (dict): Packed Items table before save (indexed) - reset (bool): State if table is reset or preserved as is + doc: Transaction document + packing_item (dict): Packed Item details + main_item_row (dict): Items table row corresponding to packed item + packed_items_table (dict): Packed Items table before save (indexed) + reset (bool): State if table is reset or preserved as is """ exists, pi_row = False, {} @@ -122,33 +133,34 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re pi_row, exists = packed_items_table.get(key), True if not exists: - pi_row = doc.append('packed_items', {}) - elif reset: # add row if row exists but table is reset + pi_row = doc.append("packed_items", {}) + elif reset: # add row if row exists but table is reset pi_row.idx, pi_row.name = None, None - pi_row = doc.append('packed_items', pi_row) + pi_row = doc.append("packed_items", pi_row) return pi_row + def get_packed_item_details(item_code, company): item = frappe.qb.DocType("Item") item_default = frappe.qb.DocType("Item Default") query = ( frappe.qb.from_(item) .left_join(item_default) - .on( - (item_default.parent == item.name) - & (item_default.company == company) - ).select( - item.item_name, item.is_stock_item, - item.description, item.stock_uom, + .on((item_default.parent == item.name) & (item_default.company == company)) + .select( + item.item_name, + item.is_stock_item, + item.description, + item.stock_uom, item.valuation_rate, - item_default.default_warehouse - ).where( - item.name == item_code + item_default.default_warehouse, ) + .where(item.name == item_code) ) return query.run(as_dict=True)[0] + def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data): pi_row.parent_item = main_item_row.item_code pi_row.parent_detail_docname = main_item_row.name @@ -161,12 +173,16 @@ def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data if not pi_row.description: pi_row.description = packing_item.get("description") + def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data, doc): # TODO batch_no, actual_batch_qty, incoming_rate if not pi_row.warehouse and not doc.amended_from: - fetch_warehouse = (doc.get('is_pos') or item_data.is_stock_item or not item_data.default_warehouse) - pi_row.warehouse = (main_item_row.warehouse if (fetch_warehouse and main_item_row.warehouse) - else item_data.default_warehouse) + fetch_warehouse = doc.get("is_pos") or item_data.is_stock_item or not item_data.default_warehouse + pi_row.warehouse = ( + main_item_row.warehouse + if (fetch_warehouse and main_item_row.warehouse) + else item_data.default_warehouse + ) if not pi_row.target_warehouse: pi_row.target_warehouse = main_item_row.get("target_warehouse") @@ -175,6 +191,7 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data pi_row.actual_qty = flt(bin.get("actual_qty")) pi_row.projected_qty = flt(bin.get("projected_qty")) + def update_packed_item_price_data(pi_row, item_data, doc): "Set price as per price list or from the Item master." if pi_row.rate: @@ -182,49 +199,60 @@ def update_packed_item_price_data(pi_row, item_data, doc): item_doc = frappe.get_cached_doc("Item", pi_row.item_code) row_data = pi_row.as_dict().copy() - row_data.update({ - "company": doc.get("company"), - "price_list": doc.get("selling_price_list"), - "currency": doc.get("currency") - }) + row_data.update( + { + "company": doc.get("company"), + "price_list": doc.get("selling_price_list"), + "currency": doc.get("currency"), + "conversion_rate": doc.get("conversion_rate"), + } + ) rate = get_price_list_rate(row_data, item_doc).get("price_list_rate") pi_row.rate = rate or item_data.get("valuation_rate") or 0.0 + def update_packed_item_from_cancelled_doc(main_item_row, packing_item, pi_row, doc): "Update packed item row details from cancelled doc into amended doc." prev_doc_packed_items_map = None if doc.amended_from: prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items) - if prev_doc_packed_items_map and prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)): + if prev_doc_packed_items_map and prev_doc_packed_items_map.get( + (packing_item.item_code, main_item_row.item_code) + ): prev_doc_row = prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)) pi_row.batch_no = prev_doc_row[0].batch_no pi_row.serial_no = prev_doc_row[0].serial_no pi_row.warehouse = prev_doc_row[0].warehouse + def get_packed_item_bin_qty(item, warehouse): bin_data = frappe.db.get_values( "Bin", fieldname=["actual_qty", "projected_qty"], filters={"item_code": item, "warehouse": warehouse}, - as_dict=True + as_dict=True, ) return bin_data[0] if bin_data else {} + def get_cancelled_doc_packed_item_details(old_packed_items): prev_doc_packed_items_map = {} for items in old_packed_items: - prev_doc_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict()) + prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append( + items.as_dict() + ) return prev_doc_packed_items_map + def update_product_bundle_rate(parent_items_price, pi_row): """ - Update the price dict of Product Bundles based on the rates of the Items in the bundle. + Update the price dict of Product Bundles based on the rates of the Items in the bundle. - Stucture: - {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0} + Stucture: + {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0} """ key = (pi_row.parent_item, pi_row.parent_detail_docname) rate = parent_items_price.get(key) @@ -233,6 +261,7 @@ def update_product_bundle_rate(parent_items_price, pi_row): parent_items_price[key] += flt(pi_row.rate) + def set_product_bundle_rate_amount(doc, parent_items_price): "Set cumulative rate and amount in bundle item." for item in doc.get("items"): @@ -241,6 +270,7 @@ def set_product_bundle_rate_amount(doc, parent_items_price): item.rate = bundle_rate item.amount = flt(bundle_rate * item.qty) + def on_doctype_update(): frappe.db.add_index("Packed Item", ["item_code", "warehouse"]) @@ -251,10 +281,7 @@ def get_items_from_product_bundle(row): bundled_items = get_product_bundle_items(row["item_code"]) for item in bundled_items: - row.update({ - "item_code": item.item_code, - "qty": flt(row["quantity"]) * flt(item.qty) - }) + row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)}) items.append(get_item_details(row)) return items diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 2521ac9fe72..ad7fd9a6976 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,36 +1,62 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from typing import List, Optional, Tuple + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_to_date, nowdate -from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestPackedItem(ERPNextTestCase): +def create_product_bundle( + quantities: Optional[List[int]] = None, warehouse: Optional[str] = None +) -> Tuple[str, List[str]]: + """Get a new product_bundle for use in tests. + + Create 10x required stock if warehouse is specified. + """ + if not quantities: + quantities = [2, 2] + + bundle = make_item(properties={"is_stock_item": 0}).name + + bundle_doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": bundle}) + + components = [] + for qty in quantities: + compoenent = make_item().name + components.append(compoenent) + bundle_doc.append("items", {"item_code": compoenent, "qty": qty}) + if warehouse: + make_stock_entry(item=compoenent, to_warehouse=warehouse, qty=10 * qty, rate=100) + + bundle_doc.insert() + + return bundle, components + + +class TestPackedItem(FrappeTestCase): "Test impact on Packed Items table in various scenarios." + @classmethod def setUpClass(cls) -> None: super().setUpClass() - cls.bundle = "_Test Product Bundle X" - cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] - make_item(cls.bundle, {"is_stock_item": 0}) - for item in cls.bundle_items: - make_item(item, {"is_stock_item": 1}) + cls.warehouse = "_Test Warehouse - _TC" - make_item("_Test Normal Stock Item", {"is_stock_item": 1}) + cls.bundle, cls.bundle_items = create_product_bundle(warehouse=cls.warehouse) + cls.bundle2, cls.bundle2_items = create_product_bundle(warehouse=cls.warehouse) - make_product_bundle(cls.bundle, cls.bundle_items, qty=2) + cls.normal_item = make_item().name def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." - so = make_sales_order(item_code = self.bundle, qty=1, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) self.assertEqual(so.items[0].qty, 1) self.assertEqual(len(so.packed_items), 2) @@ -41,14 +67,14 @@ class TestPackedItem(ERPNextTestCase): "Test impact on packed items if bundle item row is updated." so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) - so.items[0].qty = 2 # change qty + so.items[0].qty = 2 # change qty so.save() self.assertEqual(so.packed_items[0].qty, 4) self.assertEqual(so.packed_items[1].qty, 4) # change item code to non bundle item - so.items[0].item_code = "_Test Normal Stock Item" + so.items[0].item_code = self.normal_item so.save() self.assertEqual(len(so.packed_items), 0) @@ -57,12 +83,9 @@ class TestPackedItem(ERPNextTestCase): "Test impact on packed items if same bundle item is added and removed." so_items = [] for qty in [2, 4, 6, 8]: - so_items.append({ - "item_code": self.bundle, - "qty": qty, - "rate": 400, - "warehouse": "_Test Warehouse - _TC" - }) + so_items.append( + {"item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC"} + ) # create SO with recurring bundle item so = make_sales_order(item_list=so_items, do_not_submit=True) @@ -110,18 +133,15 @@ class TestPackedItem(ERPNextTestCase): "Test impact on packed items in newly mapped DN from SO." so_items = [] for qty in [2, 4]: - so_items.append({ - "item_code": self.bundle, - "qty": qty, - "rate": 400, - "warehouse": "_Test Warehouse - _TC" - }) + so_items.append( + {"item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC"} + ) # create SO with recurring bundle item so = make_sales_order(item_list=so_items) dn = make_delivery_note(so.name) - dn.items[1].qty = 3 # change second row qty for inserting doc + dn.items[1].qty = 3 # change second row qty for inserting doc dn.save() self.assertEqual(len(dn.packed_items), 4) @@ -138,7 +158,7 @@ class TestPackedItem(ERPNextTestCase): for item in self.bundle_items: make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today) - so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse) + so = make_sales_order(item_code=self.bundle, qty=1, company=company, warehouse=warehouse) dn = make_delivery_note(so.name) dn.save() @@ -149,10 +169,111 @@ class TestPackedItem(ERPNextTestCase): # backdated stock entry for item in self.bundle_items: - make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday) + make_stock_entry( + item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday + ) # assert correct reposting gles = get_gl_entries(dn.doctype, dn.name) credit_after_reposting = sum(gle.credit for gle in gles) self.assertNotEqual(credit_before_repost, credit_after_reposting) self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost) + + def assertReturns(self, original, returned): + self.assertEqual(len(original), len(returned)) + + sort_function = lambda p: (p.parent_item, p.item_code, p.qty) + + for sent, returned in zip( + sorted(original, key=sort_function), sorted(returned, key=sort_function) + ): + self.assertEqual(sent.item_code, returned.item_code) + self.assertEqual(sent.parent_item, returned.parent_item) + self.assertEqual(sent.qty, -1 * returned.qty) + + def test_returning_full_bundles(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_list = [ + { + "item_code": self.bundle, + "warehouse": self.warehouse, + "qty": 1, + "rate": 100, + }, + { + "item_code": self.bundle2, + "warehouse": self.warehouse, + "qty": 1, + "rate": 100, + }, + ] + so = make_sales_order(item_list=item_list, warehouse=self.warehouse) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + # create return + dn_ret = make_sales_return(dn.name) + dn_ret.save() + dn_ret.submit() + self.assertReturns(dn.packed_items, dn_ret.packed_items) + + def test_returning_partial_bundles(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_list = [ + { + "item_code": self.bundle, + "warehouse": self.warehouse, + "qty": 1, + "rate": 100, + }, + { + "item_code": self.bundle2, + "warehouse": self.warehouse, + "qty": 1, + "rate": 100, + }, + ] + so = make_sales_order(item_list=item_list, warehouse=self.warehouse) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + # create return + dn_ret = make_sales_return(dn.name) + # remove bundle 2 + dn_ret.items.pop() + + dn_ret.save() + dn_ret.submit() + dn_ret.reload() + + self.assertTrue(all(d.parent_item == self.bundle for d in dn_ret.packed_items)) + + expected_returns = [d for d in dn.packed_items if d.parent_item == self.bundle] + self.assertReturns(expected_returns, dn_ret.packed_items) + + def test_returning_partial_bundle_qty(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty=2) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + # create return + dn_ret = make_sales_return(dn.name) + # halve the qty + dn_ret.items[0].qty = -1 + dn_ret.save() + dn_ret.submit() + + expected_returns = dn.packed_items + for d in expected_returns: + d.qty /= 2 + self.assertReturns(expected_returns, dn_ret.packed_items) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index b092862415a..e9ccf5fc779 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -10,14 +10,13 @@ from frappe.utils import cint, flt class PackingSlip(Document): - def validate(self): """ - * Validate existence of submitted Delivery Note - * Case nos do not overlap - * Check if packed qty doesn't exceed actual qty of delivery note + * Validate existence of submitted Delivery Note + * Case nos do not overlap + * Check if packed qty doesn't exceed actual qty of delivery note - It is necessary to validate case nos before checking quantity + It is necessary to validate case nos before checking quantity """ self.validate_delivery_note() self.validate_items_mandatory() @@ -25,12 +24,13 @@ class PackingSlip(Document): self.validate_qty() from erpnext.utilities.transaction_base import validate_uom_is_integer + validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") def validate_delivery_note(self): """ - Validates if delivery note has status as draft + Validates if delivery note has status as draft """ if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0: frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note)) @@ -42,27 +42,33 @@ class PackingSlip(Document): def validate_case_nos(self): """ - Validate if case nos overlap. If they do, recommend next case no. + Validate if case nos overlap. If they do, recommend next case no. """ if not cint(self.from_case_no): frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1) elif not self.to_case_no: self.to_case_no = self.from_case_no elif cint(self.from_case_no) > cint(self.to_case_no): - frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), - raise_exception=1) + frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1) - res = frappe.db.sql("""SELECT name FROM `tabPacking Slip` + res = frappe.db.sql( + """SELECT name FROM `tabPacking Slip` WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND ((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no)) - """, {"delivery_note":self.delivery_note, - "from_case_no":self.from_case_no, - "to_case_no":self.to_case_no}) + """, + { + "delivery_note": self.delivery_note, + "from_case_no": self.from_case_no, + "to_case_no": self.to_case_no, + }, + ) if res: - frappe.throw(_("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no())) + frappe.throw( + _("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no()) + ) def validate_qty(self): """Check packed qty across packing slips and delivery note""" @@ -70,36 +76,37 @@ class PackingSlip(Document): dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing() for item in dn_details: - new_packed_qty = (flt(ps_item_qty[item['item_code']]) * no_of_cases) + \ - flt(item['packed_qty']) - if new_packed_qty > flt(item['qty']) and no_of_cases: + new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"]) + if new_packed_qty > flt(item["qty"]) and no_of_cases: self.recommend_new_qty(item, ps_item_qty, no_of_cases) - def get_details_for_packing(self): """ - Returns - * 'Delivery Note Items' query result as a list of dict - * Item Quantity dict of current packing slip doc - * No. of Cases of this packing slip + Returns + * 'Delivery Note Items' query result as a list of dict + * Item Quantity dict of current packing slip doc + * No. of Cases of this packing slip """ rows = [d.item_code for d in self.get("items")] # also pick custom fields from delivery note - custom_fields = ', '.join('dni.`{0}`'.format(d.fieldname) + custom_fields = ", ".join( + "dni.`{0}`".format(d.fieldname) for d in frappe.get_meta("Delivery Note Item").get_custom_fields() - if d.fieldtype not in no_value_fields) + if d.fieldtype not in no_value_fields + ) if custom_fields: - custom_fields = ', ' + custom_fields + custom_fields = ", " + custom_fields condition = "" if rows: - condition = " and item_code in (%s)" % (", ".join(["%s"]*len(rows))) + condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows))) # gets item code, qty per item code, latest packed qty per item code and stock uom - res = frappe.db.sql("""select item_code, sum(qty) as qty, + res = frappe.db.sql( + """select item_code, sum(qty) as qty, (select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1)) from `tabPacking Slip` ps, `tabPacking Slip Item` psi where ps.name = psi.parent and ps.docstatus = 1 @@ -107,47 +114,57 @@ class PackingSlip(Document): stock_uom, item_name, description, dni.batch_no {custom_fields} from `tabDelivery Note Item` dni where parent=%s {condition} - group by item_code""".format(condition=condition, custom_fields=custom_fields), - tuple([self.delivery_note] + rows), as_dict=1) + group by item_code""".format( + condition=condition, custom_fields=custom_fields + ), + tuple([self.delivery_note] + rows), + as_dict=1, + ) ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")]) no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1 return res, ps_item_qty, no_of_cases - def recommend_new_qty(self, item, ps_item_qty, no_of_cases): """ - Recommend a new quantity and raise a validation exception + Recommend a new quantity and raise a validation exception """ - item['recommended_qty'] = (flt(item['qty']) - flt(item['packed_qty'])) / no_of_cases - item['specified_qty'] = flt(ps_item_qty[item['item_code']]) - if not item['packed_qty']: item['packed_qty'] = 0 + item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases + item["specified_qty"] = flt(ps_item_qty[item["item_code"]]) + if not item["packed_qty"]: + item["packed_qty"] = 0 - frappe.throw(_("Quantity for Item {0} must be less than {1}").format(item.get("item_code"), item.get("recommended_qty"))) + frappe.throw( + _("Quantity for Item {0} must be less than {1}").format( + item.get("item_code"), item.get("recommended_qty") + ) + ) def update_item_details(self): """ - Fill empty columns in Packing Slip Item + Fill empty columns in Packing Slip Item """ if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() for d in self.get("items"): - res = frappe.db.get_value("Item", d.item_code, - ["weight_per_unit", "weight_uom"], as_dict=True) + res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True) - if res and len(res)>0: + if res and len(res) > 0: d.net_weight = res["weight_per_unit"] d.weight_uom = res["weight_uom"] def get_recommended_case_no(self): """ - Returns the next case no. for a new packing slip for a delivery - note + Returns the next case no. for a new packing slip for a delivery + note """ - recommended_case_no = frappe.db.sql("""SELECT MAX(to_case_no) FROM `tabPacking Slip` - WHERE delivery_note = %s AND docstatus=1""", self.delivery_note) + recommended_case_no = frappe.db.sql( + """SELECT MAX(to_case_no) FROM `tabPacking Slip` + WHERE delivery_note = %s AND docstatus=1""", + self.delivery_note, + ) return cint(recommended_case_no[0][0]) + 1 @@ -160,7 +177,7 @@ class PackingSlip(Document): dn_details = self.get_details_for_packing()[0] for item in dn_details: if flt(item.qty) > flt(item.packed_qty): - ch = self.append('items', {}) + ch = self.append("items", {}) ch.item_code = item.item_code ch.item_name = item.item_name ch.stock_uom = item.stock_uom @@ -175,14 +192,18 @@ class PackingSlip(Document): self.update_item_details() + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_details(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond - return frappe.db.sql("""select name, item_name, description from `tabItem` + + return frappe.db.sql( + """select name, item_name, description from `tabItem` where name in ( select item_code FROM `tabDelivery Note Item` where parent= %s) and %s like "%s" %s - limit %s, %s """ % ("%s", searchfield, "%s", - get_match_cond(doctype), "%s", "%s"), - ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len)) + limit %s, %s """ + % ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"), + ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len), + ) diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index 5eb6b7399ae..bc405b20995 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -4,7 +4,7 @@ import unittest # test_records = frappe.get_test_records('Packing Slip') -from erpnext.tests.utils import ERPNextTestCase +from frappe.tests.utils import FrappeTestCase class TestPackingSlip(unittest.TestCase): diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 730fd7a829c..13b74b5eb16 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -146,10 +146,6 @@ frappe.ui.form.on('Pick List', { customer: frm.doc.customer }; frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => { - if (!frm.doc.customer) { - frappe.msgprint(__('Please select Customer first')); - return; - } erpnext.utils.map_current_doc({ method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list', source_doctype: 'Sales Order', diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index c604c711ef5..e984c082d48 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -114,6 +114,7 @@ "set_only_once": 1 }, { + "collapsible": 1, "fieldname": "print_settings_section", "fieldtype": "Section Break", "label": "Print Settings" @@ -129,7 +130,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-10-05 15:08:40.369957", + "modified": "2022-04-21 07:56:40.646473", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", @@ -199,5 +200,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index b7987543f2b..858481aa7b9 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -3,13 +3,15 @@ import json from collections import OrderedDict, defaultdict +from itertools import groupby +from typing import Dict, List, Set import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.utils import cint, floor, flt, today -from six import iteritems +from frappe.utils.nestedset import get_descendants_of from erpnext.selling.doctype.sales_order.sales_order import ( make_delivery_note as create_delivery_note_from_sales_order, @@ -18,6 +20,7 @@ from erpnext.stock.get_item_details import get_conversion_factor # TODO: Prioritize SO or WO group warehouse + class PickList(Document): def validate(self): self.validate_for_qty() @@ -25,18 +28,86 @@ class PickList(Document): def before_save(self): self.set_item_locations() + # set percentage picked in SO + for location in self.get("locations"): + if ( + location.sales_order + and frappe.db.get_value("Sales Order", location.sales_order, "per_picked") == 100 + ): + frappe.throw("Row " + str(location.idx) + " has been picked already!") + def before_submit(self): + update_sales_orders = set() for item in self.locations: - if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'): + # if the user has not entered any picked qty, set it to stock_qty, before submit + if item.picked_qty == 0: + item.picked_qty = item.stock_qty + + if item.sales_order_item: + # update the picked_qty in SO Item + self.update_sales_order_item(item, item.picked_qty, item.item_code) + update_sales_orders.add(item.sales_order) + + if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): continue if not item.serial_no: - frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}").format( - frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)), - title=_("Serial Nos Required")) - if len(item.serial_no.split('\n')) == item.picked_qty: + frappe.throw( + _("Row #{0}: {1} does not have any available serial numbers in {2}").format( + frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse) + ), + title=_("Serial Nos Required"), + ) + if len(item.serial_no.split("\n")) == item.picked_qty: continue - frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity') - .format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch")) + frappe.throw( + _( + "For item {0} at row {1}, count of serial numbers does not match with the picked quantity" + ).format(frappe.bold(item.item_code), frappe.bold(item.idx)), + title=_("Quantity Mismatch"), + ) + + self.update_bundle_picked_qty() + self.update_sales_order_picking_status(update_sales_orders) + + def before_cancel(self): + """Deduct picked qty on cancelling pick list""" + updated_sales_orders = set() + + for item in self.get("locations"): + if item.sales_order_item: + self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code) + updated_sales_orders.add(item.sales_order) + + self.update_bundle_picked_qty() + self.update_sales_order_picking_status(updated_sales_orders) + + def update_sales_order_item(self, item, picked_qty, item_code): + item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item" + stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty" + + already_picked, actual_qty = frappe.db.get_value( + item_table, + item.sales_order_item, + ["picked_qty", stock_qty_field], + ) + + if self.docstatus == 1: + if (((already_picked + picked_qty) / actual_qty) * 100) > ( + 100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")) + ): + frappe.throw( + _( + "You are picking more than required quantity for {}. Check if there is any other pick list created for {}" + ).format(item_code, item.sales_order) + ) + + frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty) + + @staticmethod + def update_sales_order_picking_status(sales_orders: Set[str]) -> None: + for sales_order in sales_orders: + if sales_order: + frappe.get_doc("Sales Order", sales_order).update_picking_status() @frappe.whitelist() def set_item_locations(self, save=False): @@ -46,48 +117,55 @@ class PickList(Document): from_warehouses = None if self.parent_warehouse: - from_warehouses = frappe.db.get_descendants('Warehouse', self.parent_warehouse) + from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) # Create replica before resetting, to handle empty table on update after submit. - locations_replica = self.get('locations') + locations_replica = self.get("locations") # reset - self.delete_key('locations') + self.delete_key("locations") for item_doc in items: item_code = item_doc.item_code - self.item_location_map.setdefault(item_code, - get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code), self.company)) + self.item_location_map.setdefault( + item_code, + get_available_item_locations( + item_code, from_warehouses, self.item_count_map.get(item_code), self.company + ), + ) - locations = get_items_with_location_and_quantity(item_doc, self.item_location_map, self.docstatus) + locations = get_items_with_location_and_quantity( + item_doc, self.item_location_map, self.docstatus + ) item_doc.idx = None item_doc.name = None for row in locations: - row.update({ - 'picked_qty': row.stock_qty - }) - location = item_doc.as_dict() location.update(row) - self.append('locations', location) + self.append("locations", location) # If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red # and give feedback to the user. This is to avoid empty Pick Lists. - if not self.get('locations') and self.docstatus == 1: + if not self.get("locations") and self.docstatus == 1: for location in locations_replica: location.stock_qty = 0 location.picked_qty = 0 - self.append('locations', location) - frappe.msgprint(_("Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List."), - title=_("Out of Stock"), indicator="red") + self.append("locations", location) + frappe.msgprint( + _( + "Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List." + ), + title=_("Out of Stock"), + indicator="red", + ) if save: self.save() def aggregate_item_qty(self): - locations = self.get('locations') + locations = self.get("locations") self.item_count_map = {} # aggregate qty for same item item_map = OrderedDict() @@ -114,20 +192,20 @@ class PickList(Document): return item_map.values() def validate_for_qty(self): - if self.purpose == "Material Transfer for Manufacture" \ - and (self.for_qty is None or self.for_qty == 0): + if self.purpose == "Material Transfer for Manufacture" and ( + self.for_qty is None or self.for_qty == 0 + ): frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) def before_print(self, settings=None): - if self.get("group_same_items"): - self.group_similar_items() + self.group_similar_items() def group_similar_items(self): group_item_qty = defaultdict(float) group_picked_qty = defaultdict(float) for item in self.locations: - group_item_qty[(item.item_code, item.warehouse)] += item.qty + group_item_qty[(item.item_code, item.warehouse)] += item.qty group_picked_qty[(item.item_code, item.warehouse)] += item.picked_qty duplicate_list = [] @@ -146,42 +224,104 @@ class PickList(Document): for idx, item in enumerate(self.locations, start=1): item.idx = idx + def update_bundle_picked_qty(self): + product_bundles = self._get_product_bundles() + product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) + + for so_row, item_code in product_bundles.items(): + picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) + item_table = "Sales Order Item" + already_picked = frappe.db.get_value(item_table, so_row, "picked_qty") + frappe.db.set_value( + item_table, + so_row, + "picked_qty", + already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)), + ) + + def _get_product_bundles(self) -> Dict[str, str]: + # Dict[so_item_row: item_code] + product_bundles = {} + for item in self.locations: + if not item.product_bundle_item: + continue + product_bundles[item.product_bundle_item] = frappe.db.get_value( + "Sales Order Item", + item.product_bundle_item, + "item_code", + ) + return product_bundles + + def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]: + # bundle_item_code: Dict[component, qty] + product_bundle_qty_map = {} + for bundle_item_code in bundles: + bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code}) + product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} + return product_bundle_qty_map + + def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int: + """Compute how many full bundles can be created from picked items.""" + precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction") + + possible_bundles = [] + for item in self.locations: + if item.product_bundle_item != bundle_row: + continue + + qty_in_bundle = bundle_items.get(item.item_code) + if qty_in_bundle: + possible_bundles.append(item.picked_qty / qty_in_bundle) + else: + possible_bundles.append(0) + return int(flt(min(possible_bundles), precision or 6)) + def validate_item_locations(pick_list): if not pick_list.locations: frappe.throw(_("Add items in the Item Locations table")) + def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus): available_locations = item_location_map.get(item_doc.item_code) locations = [] # if stock qty is zero on submitted entry, show positive remaining qty to recalculate in case of restock. - remaining_stock_qty = item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty + remaining_stock_qty = ( + item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty + ) while remaining_stock_qty > 0 and available_locations: item_location = available_locations.pop(0) item_location = frappe._dict(item_location) - stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty + stock_qty = ( + remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty + ) qty = stock_qty / (item_doc.conversion_factor or 1) - uom_must_be_whole_number = frappe.db.get_value('UOM', item_doc.uom, 'must_be_whole_number') + uom_must_be_whole_number = frappe.db.get_value("UOM", item_doc.uom, "must_be_whole_number") if uom_must_be_whole_number: qty = floor(qty) stock_qty = qty * item_doc.conversion_factor - if not stock_qty: break + if not stock_qty: + break serial_nos = None if item_location.serial_no: - serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)]) + serial_nos = "\n".join(item_location.serial_no[0 : cint(stock_qty)]) - locations.append(frappe._dict({ - 'qty': qty, - 'stock_qty': stock_qty, - 'warehouse': item_location.warehouse, - 'serial_no': serial_nos, - 'batch_no': item_location.batch_no - })) + locations.append( + frappe._dict( + { + "qty": qty, + "stock_qty": stock_qty, + "warehouse": item_location.warehouse, + "serial_no": serial_nos, + "batch_no": item_location.batch_no, + } + ) + ) remaining_stock_qty -= stock_qty @@ -191,73 +331,87 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) item_location.qty = qty_diff if item_location.serial_no: # set remaining serial numbers - item_location.serial_no = item_location.serial_no[-int(qty_diff):] + item_location.serial_no = item_location.serial_no[-int(qty_diff) :] available_locations = [item_location] + available_locations # update available locations for the item item_location_map[item_doc.item_code] = available_locations return locations -def get_available_item_locations(item_code, from_warehouses, required_qty, company, ignore_validation=False): + +def get_available_item_locations( + item_code, from_warehouses, required_qty, company, ignore_validation=False +): locations = [] - has_serial_no = frappe.get_cached_value('Item', item_code, 'has_serial_no') - has_batch_no = frappe.get_cached_value('Item', item_code, 'has_batch_no') + has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") + has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") if has_batch_no and has_serial_no: - locations = get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_serial_and_batched_item( + item_code, from_warehouses, required_qty, company + ) elif has_serial_no: - locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_serialized_item( + item_code, from_warehouses, required_qty, company + ) elif has_batch_no: - locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company + ) else: - locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_other_item( + item_code, from_warehouses, required_qty, company + ) - total_qty_available = sum(location.get('qty') for location in locations) + total_qty_available = sum(location.get("qty") for location in locations) remaining_qty = required_qty - total_qty_available if remaining_qty > 0 and not ignore_validation: - frappe.msgprint(_('{0} units of Item {1} is not available.') - .format(remaining_qty, frappe.get_desk_link('Item', item_code)), - title=_("Insufficient Stock")) + frappe.msgprint( + _("{0} units of Item {1} is not available.").format( + remaining_qty, frappe.get_desk_link("Item", item_code) + ), + title=_("Insufficient Stock"), + ) return locations -def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company): - filters = frappe._dict({ - 'item_code': item_code, - 'company': company, - 'warehouse': ['!=', ''] - }) +def get_available_item_locations_for_serialized_item( + item_code, from_warehouses, required_qty, company +): + filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]}) if from_warehouses: - filters.warehouse = ['in', from_warehouses] + filters.warehouse = ["in", from_warehouses] - serial_nos = frappe.get_all('Serial No', - fields=['name', 'warehouse'], + serial_nos = frappe.get_all( + "Serial No", + fields=["name", "warehouse"], filters=filters, limit=required_qty, - order_by='purchase_date', - as_list=1) + order_by="purchase_date", + as_list=1, + ) warehouse_serial_nos_map = frappe._dict() for serial_no, warehouse in serial_nos: warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no) locations = [] - for warehouse, serial_nos in iteritems(warehouse_serial_nos_map): - locations.append({ - 'qty': len(serial_nos), - 'warehouse': warehouse, - 'serial_no': serial_nos - }) + for warehouse, serial_nos in warehouse_serial_nos_map.items(): + locations.append({"qty": len(serial_nos), "warehouse": warehouse, "serial_no": serial_nos}) return locations -def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company): - warehouse_condition = 'and warehouse in %(warehouses)s' if from_warehouses else '' - batch_locations = frappe.db.sql(""" + +def get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company +): + warehouse_condition = "and warehouse in %(warehouses)s" if from_warehouses else "" + batch_locations = frappe.db.sql( + """ SELECT sle.`warehouse`, sle.`batch_no`, @@ -273,118 +427,171 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s {warehouse_condition} GROUP BY - `warehouse`, - `batch_no`, - `item_code` + sle.`warehouse`, + sle.`batch_no`, + sle.`item_code` HAVING `qty` > 0 ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation` - """.format(warehouse_condition=warehouse_condition), { #nosec - 'item_code': item_code, - 'company': company, - 'today': today(), - 'warehouses': from_warehouses - }, as_dict=1) + """.format( + warehouse_condition=warehouse_condition + ), + { # nosec + "item_code": item_code, + "company": company, + "today": today(), + "warehouses": from_warehouses, + }, + as_dict=1, + ) return batch_locations -def get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company): - # Get batch nos by FIFO - locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company) - filters = frappe._dict({ - 'item_code': item_code, - 'company': company, - 'warehouse': ['!=', ''], - 'batch_no': '' - }) +def get_available_item_locations_for_serial_and_batched_item( + item_code, from_warehouses, required_qty, company +): + # Get batch nos by FIFO + locations = get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company + ) + + filters = frappe._dict( + {"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""} + ) # Get Serial Nos by FIFO for Batch No for location in locations: filters.batch_no = location.batch_no filters.warehouse = location.warehouse - location.qty = required_qty if location.qty > required_qty else location.qty # if extra qty in batch + location.qty = ( + required_qty if location.qty > required_qty else location.qty + ) # if extra qty in batch - serial_nos = frappe.get_list('Serial No', - fields=['name'], - filters=filters, - limit=location.qty, - order_by='purchase_date') + serial_nos = frappe.get_list( + "Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date" + ) serial_nos = [sn.name for sn in serial_nos] location.serial_no = serial_nos return locations + def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): # gets all items available in different warehouses - warehouses = [x.get('name') for x in frappe.get_list("Warehouse", {'company': company}, "name")] + warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")] - filters = frappe._dict({ - 'item_code': item_code, - 'warehouse': ['in', warehouses], - 'actual_qty': ['>', 0] - }) + filters = frappe._dict( + {"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]} + ) if from_warehouses: - filters.warehouse = ['in', from_warehouses] + filters.warehouse = ["in", from_warehouses] - item_locations = frappe.get_all('Bin', - fields=['warehouse', 'actual_qty as qty'], + item_locations = frappe.get_all( + "Bin", + fields=["warehouse", "actual_qty as qty"], filters=filters, limit=required_qty, - order_by='creation') + order_by="creation", + ) return item_locations @frappe.whitelist() def create_delivery_note(source_name, target_doc=None): - pick_list = frappe.get_doc('Pick List', source_name) + pick_list = frappe.get_doc("Pick List", source_name) validate_item_locations(pick_list) - - sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order] - sales_orders = set(sales_orders) - + sales_dict = dict() + sales_orders = [] delivery_note = None - for sales_order in sales_orders: - delivery_note = create_delivery_note_from_sales_order(sales_order, - delivery_note, skip_item_mapping=True) + for location in pick_list.locations: + if location.sales_order: + sales_orders.append( + frappe.db.get_value( + "Sales Order", location.sales_order, ["customer", "name as sales_order"], as_dict=True + ) + ) - # map rows without sales orders as well - if not delivery_note: - delivery_note = frappe.new_doc("Delivery Note") + for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]): + sales_dict[customer] = {row.sales_order for row in rows} - item_table_mapper = { - 'doctype': 'Delivery Note Item', - 'field_map': { - 'rate': 'rate', - 'name': 'so_detail', - 'parent': 'against_sales_order', - }, - 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 - } + if sales_dict: + delivery_note = create_dn_with_so(sales_dict, pick_list) + + if not all(item.sales_order for item in pick_list.locations): + delivery_note = create_dn_wo_so(pick_list) + + frappe.msgprint(_("Delivery Note(s) created for the Pick List")) + return delivery_note + + +def create_dn_wo_so(pick_list): + delivery_note = frappe.new_doc("Delivery Note") item_table_mapper_without_so = { - 'doctype': 'Delivery Note Item', - 'field_map': { - 'rate': 'rate', - 'name': 'name', - 'parent': '', - } + "doctype": "Delivery Note Item", + "field_map": { + "rate": "rate", + "name": "name", + "parent": "", + }, + } + map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note) + delivery_note.insert(ignore_mandatory=True) + + return delivery_note + + +def create_dn_with_so(sales_dict, pick_list): + delivery_note = None + + item_table_mapper = { + "doctype": "Delivery Note Item", + "field_map": { + "rate": "rate", + "name": "so_detail", + "parent": "against_sales_order", + }, + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1, } + for customer in sales_dict: + for so in sales_dict[customer]: + delivery_note = None + delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True) + break + if delivery_note: + # map all items of all sales orders of that customer + for so in sales_dict[customer]: + map_pl_locations(pick_list, item_table_mapper, delivery_note, so) + delivery_note.flags.ignore_mandatory = True + delivery_note.insert() + update_packed_item_details(pick_list, delivery_note) + delivery_note.save() + + return delivery_note + + +def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): + for location in pick_list.locations: + if location.sales_order != sales_order or location.product_bundle_item: + continue + if location.sales_order_item: - sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item}) + sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item) else: sales_order_item = None - source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \ - else [location, item_table_mapper_without_so] + source_doc = sales_order_item or location - dn_item = map_child_doc(source_doc, delivery_note, table_mapper) + dn_item = map_child_doc(source_doc, delivery_note, item_mapper) if dn_item: + dn_item.pick_list_item = location.name dn_item.warehouse = location.warehouse dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1) dn_item.batch_no = location.batch_no @@ -392,29 +599,74 @@ def create_delivery_note(source_name, target_doc=None): update_delivery_note_item(source_doc, dn_item, delivery_note) + add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper) set_delivery_note_missing_values(delivery_note) delivery_note.pick_list = pick_list.name - delivery_note.customer = pick_list.customer if pick_list.customer else None + delivery_note.company = pick_list.company + delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer") + + +def add_product_bundles_to_delivery_note( + pick_list: "PickList", delivery_note, item_mapper +) -> None: + """Add product bundles found in pick list to delivery note. + + When mapping pick list items, the bundle item itself isn't part of the + locations. Dynamically fetch and add parent bundle item into DN.""" + product_bundles = pick_list._get_product_bundles() + product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) + + for so_row, item_code in product_bundles.items(): + sales_order_item = frappe.get_doc("Sales Order Item", so_row) + dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) + dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( + so_row, product_bundle_qty_map[item_code] + ) + update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) + + +def update_packed_item_details(pick_list: "PickList", delivery_note) -> None: + """Update stock details on packed items table of delivery note.""" + + def _find_so_row(packed_item): + for item in delivery_note.items: + if packed_item.parent_detail_docname == item.name: + return item.so_detail + + def _find_pick_list_location(bundle_row, packed_item): + if not bundle_row: + return + for loc in pick_list.locations: + if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code: + return loc + + for packed_item in delivery_note.packed_items: + so_row = _find_so_row(packed_item) + location = _find_pick_list_location(so_row, packed_item) + if not location: + continue + packed_item.warehouse = location.warehouse + packed_item.batch_no = location.batch_no + packed_item.serial_no = location.serial_no - return delivery_note @frappe.whitelist() def create_stock_entry(pick_list): pick_list = frappe.get_doc(json.loads(pick_list)) validate_item_locations(pick_list) - if stock_entry_exists(pick_list.get('name')): - return frappe.msgprint(_('Stock Entry has been already created against this Pick List')) + if stock_entry_exists(pick_list.get("name")): + return frappe.msgprint(_("Stock Entry has been already created against this Pick List")) - stock_entry = frappe.new_doc('Stock Entry') - stock_entry.pick_list = pick_list.get('name') - stock_entry.purpose = pick_list.get('purpose') + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.pick_list = pick_list.get("name") + stock_entry.purpose = pick_list.get("purpose") stock_entry.set_stock_entry_type() - if pick_list.get('work_order'): + if pick_list.get("work_order"): stock_entry = update_stock_entry_based_on_work_order(pick_list, stock_entry) - elif pick_list.get('material_request'): + elif pick_list.get("material_request"): stock_entry = update_stock_entry_based_on_material_request(pick_list, stock_entry) else: stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry) @@ -424,9 +676,11 @@ def create_stock_entry(pick_list): return stock_entry.as_dict() + @frappe.whitelist() def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filters, as_dict): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `name`, `company`, `planned_start_date` FROM @@ -442,25 +696,27 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte LIMIT %(start)s, %(page_length)s""", { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace('%', ''), - 'start': start, - 'page_length': frappe.utils.cint(page_length), - 'company': filters.get('company') - }, as_dict=as_dict) + "txt": "%%%s%%" % txt, + "_txt": txt.replace("%", ""), + "start": start, + "page_length": frappe.utils.cint(page_length), + "company": filters.get("company"), + }, + as_dict=as_dict, + ) + @frappe.whitelist() def target_document_exists(pick_list_name, purpose): - if purpose == 'Delivery': - return frappe.db.exists('Delivery Note', { - 'pick_list': pick_list_name - }) + if purpose == "Delivery": + return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name}) return stock_entry_exists(pick_list_name) + @frappe.whitelist() def get_item_details(item_code, uom=None): - details = frappe.db.get_value('Item', item_code, ['stock_uom', 'name'], as_dict=1) + details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1) details.uom = uom or details.stock_uom if uom: details.update(get_conversion_factor(item_code, uom)) @@ -469,37 +725,37 @@ def get_item_details(item_code, uom=None): def update_delivery_note_item(source, target, delivery_note): - cost_center = frappe.db.get_value('Project', delivery_note.project, 'cost_center') + cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center") if not cost_center: - cost_center = get_cost_center(source.item_code, 'Item', delivery_note.company) + cost_center = get_cost_center(source.item_code, "Item", delivery_note.company) if not cost_center: - cost_center = get_cost_center(source.item_group, 'Item Group', delivery_note.company) + cost_center = get_cost_center(source.item_group, "Item Group", delivery_note.company) target.cost_center = cost_center + def get_cost_center(for_item, from_doctype, company): - '''Returns Cost Center for Item or Item Group''' - return frappe.db.get_value('Item Default', - fieldname=['buying_cost_center'], - filters={ - 'parent': for_item, - 'parenttype': from_doctype, - 'company': company - }) + """Returns Cost Center for Item or Item Group""" + return frappe.db.get_value( + "Item Default", + fieldname=["buying_cost_center"], + filters={"parent": for_item, "parenttype": from_doctype, "company": company}, + ) + def set_delivery_note_missing_values(target): - target.run_method('set_missing_values') - target.run_method('set_po_nos') - target.run_method('calculate_taxes_and_totals') + target.run_method("set_missing_values") + target.run_method("set_po_nos") + target.run_method("calculate_taxes_and_totals") + def stock_entry_exists(pick_list_name): - return frappe.db.exists('Stock Entry', { - 'pick_list': pick_list_name - }) + return frappe.db.exists("Stock Entry", {"pick_list": pick_list_name}) + def update_stock_entry_based_on_work_order(pick_list, stock_entry): - work_order = frappe.get_doc("Work Order", pick_list.get('work_order')) + work_order = frappe.get_doc("Work Order", pick_list.get("work_order")) stock_entry.work_order = work_order.name stock_entry.company = work_order.company @@ -508,10 +764,11 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): stock_entry.use_multi_level_bom = work_order.use_multi_level_bom stock_entry.fg_completed_qty = pick_list.for_qty if work_order.bom_no: - stock_entry.inspection_required = frappe.db.get_value('BOM', - work_order.bom_no, 'inspection_required') + stock_entry.inspection_required = frappe.db.get_value( + "BOM", work_order.bom_no, "inspection_required" + ) - is_wip_warehouse_group = frappe.db.get_value('Warehouse', work_order.wip_warehouse, 'is_group') + is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group") if not (is_wip_warehouse_group and work_order.skip_transfer): wip_warehouse = work_order.wip_warehouse else: @@ -525,32 +782,36 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): update_common_item_properties(item, location) item.t_warehouse = wip_warehouse - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_stock_entry_based_on_material_request(pick_list, stock_entry): for location in pick_list.locations: target_warehouse = None if location.material_request_item: - target_warehouse = frappe.get_value('Material Request Item', - location.material_request_item, 'warehouse') + target_warehouse = frappe.get_value( + "Material Request Item", location.material_request_item, "warehouse" + ) item = frappe._dict() update_common_item_properties(item, location) item.t_warehouse = target_warehouse - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_stock_entry_items_with_no_reference(pick_list, stock_entry): for location in pick_list.locations: item = frappe._dict() update_common_item_properties(item, location) - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_common_item_properties(item, location): item.item_code = location.item_code item.s_warehouse = location.warehouse diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index d19bedeeafe..92e57bed220 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): return { - 'fieldname': 'pick_list', - 'transactions': [ - { - 'items': ['Stock Entry', 'Delivery Note'] - }, - ] + "fieldname": "pick_list", + "transactions": [ + {"items": ["Stock Entry", "Delivery Note"]}, + ], } diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 41e3150f0d7..e8cebc8e622 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -3,162 +3,193 @@ import frappe from frappe import _dict +from frappe.tests.utils import FrappeTestCase -test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] - -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.selling.doctype.sales_order.sales_order import create_pick_list +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.item.test_item import create_item, make_item +from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( EmptyStockReconciliationItemsError, ) -from erpnext.tests.utils import ERPNextTestCase + +test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"] -class TestPickList(ERPNextTestCase): - +class TestPickList(FrappeTestCase): def test_pick_list_picks_warehouse_for_each_item(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 5 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 5, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) def test_pick_list_splits_row_according_to_warehouse_availability(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'warehouse': '_Test Warehouse Group-C1 - _TC', - 'valuation_rate': 100, - 'qty': 5 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "warehouse": "_Test Warehouse Group-C1 - _TC", + "valuation_rate": 100, + "qty": 5, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'warehouse': '_Test Warehouse 2 - _TC', - 'valuation_rate': 400, - 'qty': 10 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "warehouse": "_Test Warehouse 2 - _TC", + "valuation_rate": 400, + "qty": 10, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'qty': 1000, - 'stock_qty': 1000, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "qty": 1000, + "stock_qty": 1000, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item Warehouse Group Wise Reorder') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse Group-C1 - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item Warehouse Group Wise Reorder") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse Group-C1 - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[1].item_code, '_Test Item Warehouse Group Wise Reorder') - self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse 2 - _TC') + self.assertEqual(pick_list.locations[1].item_code, "_Test Item Warehouse Group Wise Reorder") + self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse 2 - _TC") self.assertEqual(pick_list.locations[1].qty, 10) def test_pick_list_shows_serial_no_for_serialized_item(self): - stock_reconciliation = frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'purpose': 'Stock Reconciliation', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Serialized Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 5, - 'serial_no': '123450\n123451\n123452\n123453\n123454' - }] - }) + stock_reconciliation = frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "purpose": "Stock Reconciliation", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Serialized Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 5, + "serial_no": "123450\n123451\n123452\n123453\n123454", + } + ], + } + ) try: stock_reconciliation.submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Serialized Item', - 'qty': 1000, - 'stock_qty': 1000, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Serialized Item", + "qty": 1000, + "stock_qty": 1000, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Serialized Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[0].serial_no, '123450\n123451\n123452\n123453\n123454') + self.assertEqual(pick_list.locations[0].serial_no, "123450\n123451\n123452\n123453\n123454") def test_pick_list_shows_batch_no_for_batched_item(self): # check if oldest batch no is picked - item = frappe.db.exists("Item", {'item_name': 'Batched Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched Item"}) if not item: item = create_item("Batched Item") item.has_batch_no = 1 @@ -166,7 +197,7 @@ class TestPickList(ERPNextTestCase): item.batch_number_series = "B-BATCH-.##" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched Item"}) pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0) @@ -175,28 +206,30 @@ class TestPickList(ERPNextTestCase): pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0) - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'purpose': 'Material Transfer', - 'locations': [{ - 'item_code': 'Batched Item', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Material Transfer", + "locations": [ + { + "item_code": "Batched Item", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) pr1.cancel() pr2.cancel() - def test_pick_list_for_batched_and_serialised_item(self): # check if oldest batch no and serial nos are picked - item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) if not item: item = create_item("Batched and Serialised Item") item.has_batch_no = 1 @@ -206,7 +239,7 @@ class TestPickList(ERPNextTestCase): item.serial_no_series = "S-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) pr1 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0) @@ -216,17 +249,21 @@ class TestPickList(ERPNextTestCase): pr2 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0) - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'purpose': 'Material Transfer', - 'locations': [{ - 'item_code': 'Batched and Serialised Item', - 'qty': 2, - 'stock_qty': 2, - 'conversion_factor': 1, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Material Transfer", + "locations": [ + { + "item_code": "Batched and Serialised Item", + "qty": 2, + "stock_qty": 2, + "conversion_factor": 1, + } + ], + } + ) pick_list.set_item_locations() self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) @@ -237,64 +274,71 @@ class TestPickList(ERPNextTestCase): def test_pick_list_for_items_from_multiple_sales_orders(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 10 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 10, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - sales_order = frappe.get_doc({ - 'doctype': "Sales Order", - 'customer': '_Test Customer', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item', - 'qty': 10, - 'delivery_date': frappe.utils.today() - }], - }) + sales_order = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [{"item_code": "_Test Item", "qty": 10, "delivery_date": frappe.utils.today()}], + } + ) sales_order.submit() - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }, { - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[0].name, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + }, + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[0].name, + }, + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[0].sales_order_item, '_T-Sales Order-1_item') + self.assertEqual(pick_list.locations[0].sales_order_item, "_T-Sales Order-1_item") - self.assertEqual(pick_list.locations[1].item_code, '_Test Item') - self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[1].item_code, "_Test Item") + self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[1].qty, 5) self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name) @@ -302,46 +346,57 @@ class TestPickList(ERPNextTestCase): purchase_receipt = make_purchase_receipt(item_code="_Test Item", qty=10) purchase_receipt.submit() - sales_order = frappe.get_doc({ - 'doctype': 'Sales Order', - 'customer': '_Test Customer', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item', - 'qty': 1, - 'conversion_factor': 5, - 'delivery_date': frappe.utils.today() - }, { - 'item_code': '_Test Item', - 'qty': 1, - 'conversion_factor': 1, - 'delivery_date': frappe.utils.today() - }], - }).insert() + sales_order = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 5, + "stock_qty": 5, + "delivery_date": frappe.utils.today(), + }, + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + }, + ], + } + ).insert() sales_order.submit() - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 1, - 'stock_qty': 5, - 'conversion_factor': 5, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[0].name , - }, { - 'item_code': '_Test Item', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[1].name , - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 2, + "stock_qty": 1, + "conversion_factor": 0.5, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[0].name, + }, + { + "item_code": "_Test Item", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[1].name, + }, + ], + } + ) pick_list.set_item_locations() pick_list.submit() @@ -349,7 +404,9 @@ class TestPickList(ERPNextTestCase): self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) - self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor) + self.assertEqual( + sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor + ) pick_list.cancel() sales_order.cancel() @@ -362,22 +419,30 @@ class TestPickList(ERPNextTestCase): self.assertEqual(b.get(key), value, msg=f"{key} doesn't match") # nothing should be grouped - pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[ - _dict(item_code="A", warehouse="X", qty=1, picked_qty=2), - _dict(item_code="B", warehouse="X", qty=1, picked_qty=2), - _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2), - _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2), - ]) + pl = frappe.get_doc( + doctype="Pick List", + group_same_items=True, + locations=[ + _dict(item_code="A", warehouse="X", qty=1, picked_qty=2), + _dict(item_code="B", warehouse="X", qty=1, picked_qty=2), + _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2), + _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2), + ], + ) pl.before_print() self.assertEqual(len(pl.locations), 4) # grouping should halve the number of items - pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[ - _dict(item_code="A", warehouse="X", qty=5, picked_qty=1), - _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2), - _dict(item_code="A", warehouse="X", qty=3, picked_qty=2), - _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2), - ]) + pl = frappe.get_doc( + doctype="Pick List", + group_same_items=True, + locations=[ + _dict(item_code="A", warehouse="X", qty=5, picked_qty=1), + _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2), + _dict(item_code="A", warehouse="X", qty=3, picked_qty=2), + _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2), + ], + ) pl.before_print() self.assertEqual(len(pl.locations), 2) @@ -388,14 +453,195 @@ class TestPickList(ERPNextTestCase): for expected_item, created_item in zip(expected_items, pl.locations): _compare_dicts(expected_item, created_item) - # def test_pick_list_skips_items_in_expired_batch(self): - # pass + def test_multiple_dn_creation(self): + sales_order_1 = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + } + ], + } + ).insert() + sales_order_1.submit() + sales_order_2 = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer 1", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item 2", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + }, + ], + } + ).insert() + sales_order_2.submit() + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "picker": "P001", + "locations": [ + { + "item_code": "_Test Item ", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order_1.name, + "sales_order_item": sales_order_1.items[0].name, + }, + { + "item_code": "_Test Item 2", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order_2.name, + "sales_order_item": sales_order_2.items[0].name, + }, + ], + } + ) + pick_list.set_item_locations() + pick_list.submit() + create_delivery_note(pick_list.name) + for dn in frappe.get_all( + "Delivery Note", + filters={"pick_list": pick_list.name, "customer": "_Test Customer"}, + fields={"name"}, + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + self.assertEqual(dn_item.item_code, "_Test Item") + self.assertEqual(dn_item.against_sales_order, sales_order_1.name) + self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name) - # def test_pick_list_from_sales_order(self): - # pass + for dn in frappe.get_all( + "Delivery Note", + filters={"pick_list": pick_list.name, "customer": "_Test Customer 1"}, + fields={"name"}, + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + self.assertEqual(dn_item.item_code, "_Test Item 2") + self.assertEqual(dn_item.against_sales_order, sales_order_2.name) + # test DN creation without so + pick_list_1 = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Delivery", + "picker": "P001", + "locations": [ + { + "item_code": "_Test Item ", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + }, + { + "item_code": "_Test Item 2", + "qty": 2, + "stock_qty": 2, + "conversion_factor": 1, + }, + ], + } + ) + pick_list_1.set_item_locations() + pick_list_1.submit() + create_delivery_note(pick_list_1.name) + for dn in frappe.get_all( + "Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"} + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + if dn_item.item_code == "_Test Item": + self.assertEqual(dn_item.qty, 1) + if dn_item.item_code == "_Test Item 2": + self.assertEqual(dn_item.qty, 2) - # def test_pick_list_from_work_order(self): - # pass + def test_picklist_with_multi_uom(self): + warehouse = "_Test Warehouse - _TC" + item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name + make_stock_entry(item=item, to_warehouse=warehouse, qty=1000) - # def test_pick_list_from_material_request(self): - # pass + so = make_sales_order(item_code=item, qty=10, rate=42, uom="Box") + pl = create_pick_list(so.name) + # pick half the qty + for loc in pl.locations: + loc.picked_qty = loc.stock_qty / 2 + pl.save() + pl.submit() + + so.reload() + self.assertEqual(so.per_picked, 50) + + def test_picklist_with_bundles(self): + warehouse = "_Test Warehouse - _TC" + + quantities = [5, 2] + bundle, components = create_product_bundle(quantities, warehouse=warehouse) + bundle_items = dict(zip(components, quantities)) + + so = make_sales_order(item_code=bundle, qty=3, rate=42) + + pl = create_pick_list(so.name) + pl.save() + self.assertEqual(len(pl.locations), 2) + for item in pl.locations: + self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3) + + # check picking status on sales order + pl.submit() + so.reload() + self.assertEqual(so.per_picked, 100) + + # deliver + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + self.assertEqual(dn.packed_items[0].warehouse, warehouse) + so.reload() + self.assertEqual(so.per_delivered, 100) + + def test_picklist_with_partial_bundles(self): + # from test_records.json + warehouse = "_Test Warehouse - _TC" + + quantities = [5, 2] + bundle, components = create_product_bundle(quantities, warehouse=warehouse) + + so = make_sales_order(item_code=bundle, qty=4, rate=42) + + pl = create_pick_list(so.name) + for loc in pl.locations: + loc.picked_qty = loc.qty / 2 + + pl.save().submit() + so.reload() + self.assertEqual(so.per_picked, 50) + + # deliver half qty + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + so.reload() + self.assertEqual(so.per_delivered, 50) + + pl = create_pick_list(so.name) + pl.save().submit() + so.reload() + self.assertEqual(so.per_picked, 100) + + # deliver remaining + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + so.reload() + self.assertEqual(so.per_delivered, 100) diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index 805286ddcc0..a96ebfcdee6 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -27,6 +27,7 @@ "column_break_15", "sales_order", "sales_order_item", + "product_bundle_item", "material_request", "material_request_item" ], @@ -146,6 +147,7 @@ { "fieldname": "sales_order_item", "fieldtype": "Data", + "hidden": 1, "label": "Sales Order Item", "read_only": 1 }, @@ -177,11 +179,19 @@ "fieldtype": "Data", "label": "Item Group", "read_only": 1 + }, + { + "description": "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle", + "fieldname": "product_bundle_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Product Bundle Item", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-09-28 12:02:16.923056", + "modified": "2022-04-22 05:27:38.497997", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", @@ -190,5 +200,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 8a3172e9e22..554055fd839 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -31,9 +31,11 @@ class PriceList(Document): frappe.set_value("Buying Settings", "Buying Settings", "buying_price_list", self.name) def update_item_price(self): - frappe.db.sql("""update `tabItem Price` set currency=%s, + frappe.db.sql( + """update `tabItem Price` set currency=%s, buying=%s, selling=%s, modified=NOW() where price_list=%s""", - (self.currency, cint(self.buying), cint(self.selling), self.name)) + (self.currency, cint(self.buying), cint(self.selling), self.name), + ) def check_impact_on_shopping_cart(self): "Check if Price List currency change impacts E Commerce Cart." @@ -66,12 +68,14 @@ class PriceList(Document): def delete_price_list_details_key(self): frappe.cache().hdel("price_list_details", self.name) + def get_price_list_details(price_list): price_list_details = frappe.cache().hget("price_list_details", price_list) if not price_list_details: - price_list_details = frappe.get_cached_value("Price List", price_list, - ["currency", "price_not_uom_dependent", "enabled"], as_dict=1) + price_list_details = frappe.get_cached_value( + "Price List", price_list, ["currency", "price_not_uom_dependent", "enabled"], as_dict=1 + ) if not price_list_details or not price_list_details.get("enabled"): throw(_("Price List {0} is disabled or does not exist").format(price_list)) diff --git a/erpnext/stock/doctype/price_list/test_price_list.py b/erpnext/stock/doctype/price_list/test_price_list.py index b8218b942e7..93660930c79 100644 --- a/erpnext/stock/doctype/price_list/test_price_list.py +++ b/erpnext/stock/doctype/price_list/test_price_list.py @@ -6,4 +6,4 @@ import frappe # test_ignore = ["Item"] -test_records = frappe.get_test_records('Price List') +test_records = frappe.get_test_records("Price List") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index b54a90eed35..355f0e593ef 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -24,6 +24,10 @@ "apply_putaway_rule", "is_return", "return_against", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "section_addresses", "supplier_address", "contact_person", @@ -106,10 +110,7 @@ "terms", "bill_no", "bill_date", - "accounting_details_section", - "provisional_expense_account", "more_info", - "project", "status", "amended_from", "range", @@ -1149,23 +1150,26 @@ }, { "collapsible": 1, - "fieldname": "accounting_details_section", + "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Details" + "label": "Accounting Dimensions" }, { - "fieldname": "provisional_expense_account", + "fieldname": "cost_center", "fieldtype": "Link", - "hidden": 1, - "label": "Provisional Expense Account", - "options": "Account" + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-02-01 11:40:52.690984", + "modified": "2022-04-26 13:41:32.625197", "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 afaa8b02a89..db94beccbcf 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -17,82 +17,85 @@ from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class PurchaseReceipt(BuyingController): def __init__(self, *args, **kwargs): super(PurchaseReceipt, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'target_dt': 'Purchase Order Item', - 'join_field': 'purchase_order_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Purchase Order', - 'target_parent_field': 'per_received', - 'target_ref_field': 'qty', - 'source_dt': 'Purchase Receipt Item', - 'source_field': 'received_qty', - 'second_source_dt': 'Purchase Invoice Item', - 'second_source_field': 'received_qty', - 'second_join_field': 'po_detail', - 'percent_join_field': 'purchase_order', - 'overflow_type': 'receipt', - 'second_source_extra_cond': """ and exists(select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""" - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Material Request Item', - 'join_field': 'material_request_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Material Request', - 'target_parent_field': 'per_received', - 'target_ref_field': 'stock_qty', - 'source_field': 'stock_qty', - 'percent_join_field': 'material_request' - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Invoice Item', - 'join_field': 'purchase_invoice_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Purchase Invoice', - 'target_parent_field': 'per_received', - 'target_ref_field': 'qty', - 'source_field': 'received_qty', - 'percent_join_field': 'purchase_invoice', - 'overflow_type': 'receipt' - }] + self.status_updater = [ + { + "target_dt": "Purchase Order Item", + "join_field": "purchase_order_item", + "target_field": "received_qty", + "target_parent_dt": "Purchase Order", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_dt": "Purchase Receipt Item", + "source_field": "received_qty", + "second_source_dt": "Purchase Invoice Item", + "second_source_field": "received_qty", + "second_join_field": "po_detail", + "percent_join_field": "purchase_order", + "overflow_type": "receipt", + "second_source_extra_cond": """ and exists(select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "received_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_received", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + "percent_join_field": "material_request", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Invoice Item", + "join_field": "purchase_invoice_item", + "target_field": "received_qty", + "target_parent_dt": "Purchase Invoice", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_field": "received_qty", + "percent_join_field": "purchase_invoice", + "overflow_type": "receipt", + }, + ] if cint(self.is_return): - self.status_updater.extend([ - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'purchase_order_item', - 'target_field': 'returned_qty', - 'source_field': '-1 * qty', - 'second_source_dt': 'Purchase Invoice Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'po_detail', - 'extra_cond': """ and exists (select name from `tabPurchase Receipt` + self.status_updater.extend( + [ + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Order Item", + "join_field": "purchase_order_item", + "target_field": "returned_qty", + "source_field": "-1 * qty", + "second_source_dt": "Purchase Invoice Item", + "second_source_field": "-1 * qty", + "second_join_field": "po_detail", + "extra_cond": """ and exists (select name from `tabPurchase Receipt` where name=`tabPurchase Receipt Item`.parent and is_return=1)""", - 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Receipt Item', - 'join_field': 'purchase_receipt_item', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Purchase Receipt', - 'target_parent_field': 'per_returned', - 'target_ref_field': 'received_stock_qty', - 'source_field': '-1 * received_stock_qty', - 'percent_join_field_parent': 'return_against' - } - ]) + "second_source_extra_cond": """ and exists (select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Receipt Item", + "join_field": "purchase_receipt_item", + "target_field": "returned_qty", + "target_parent_dt": "Purchase Receipt", + "target_parent_field": "per_returned", + "target_ref_field": "received_stock_qty", + "source_field": "-1 * received_stock_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule @@ -104,8 +107,8 @@ class PurchaseReceipt(BuyingController): self.validate_posting_time() super(PurchaseReceipt, self).validate() - if self._action=="submit": - self.make_batches('warehouse') + if self._action == "submit": + self.make_batches("warehouse") else: self.set_status() @@ -125,77 +128,95 @@ class PurchaseReceipt(BuyingController): self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") - def validate_cwip_accounts(self): - for item in self.get('items'): + for item in self.get("items"): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): # check cwip accounts before making auto assets # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account arbnb_account = self.get_company_default("asset_received_but_not_billed") - cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ - company = self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + ) break def validate_provisional_expense_account(self): - provisional_accounting_for_non_stock_items = \ - cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) - if provisional_accounting_for_non_stock_items: - default_provisional_account = self.get_company_default("default_provisional_account") - if not self.provisional_expense_account: - self.provisional_expense_account = default_provisional_account + if not provisional_accounting_for_non_stock_items: + return + + default_provisional_account = self.get_company_default("default_provisional_account") + for item in self.get("items"): + if not item.get("provisional_expense_account"): + item.provisional_expense_account = default_provisional_account def validate_with_previous_doc(self): - super(PurchaseReceipt, self).validate_with_previous_doc({ - "Purchase Order": { - "ref_dn_field": "purchase_order", - "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], - }, - "Purchase Order Item": { - "ref_dn_field": "purchase_order_item", - "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True + super(PurchaseReceipt, self).validate_with_previous_doc( + { + "Purchase Order": { + "ref_dn_field": "purchase_order", + "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], + }, + "Purchase Order Item": { + "ref_dn_field": "purchase_order_item", + "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, } - }) + ) - if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')) and not self.is_return: - self.validate_rate_with_reference_doc([["Purchase Order", "purchase_order", "purchase_order_item"]]) + if ( + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return + ): + self.validate_rate_with_reference_doc( + [["Purchase Order", "purchase_order", "purchase_order_item"]] + ) def po_required(self): - if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes': - for d in self.get('items'): + if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": + for d in self.get("items"): if not d.purchase_order: frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) def get_already_received_qty(self, po, po_detail): - qty = frappe.db.sql("""select sum(qty) from `tabPurchase Receipt Item` + qty = frappe.db.sql( + """select sum(qty) from `tabPurchase Receipt Item` where purchase_order_item = %s and docstatus = 1 and purchase_order=%s - and parent != %s""", (po_detail, po, self.name)) + and parent != %s""", + (po_detail, po, self.name), + ) return qty and flt(qty[0][0]) or 0.0 def get_po_qty_and_warehouse(self, po_detail): - po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, - ["qty", "warehouse"]) + po_qty, po_warehouse = frappe.db.get_value( + "Purchase Order Item", po_detail, ["qty", "warehouse"] + ) return po_qty, po_warehouse # Check for Closed status def check_on_hold_or_closed_status(self): - check_list =[] - for d in self.get('items'): - if (d.meta.get_field('purchase_order') and d.purchase_order - and d.purchase_order not in check_list): + check_list = [] + for d in self.get("items"): + if ( + d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list + ): check_list.append(d.purchase_order) - check_on_hold_or_closed_status('Purchase Order', d.purchase_order) + check_on_hold_or_closed_status("Purchase Order", d.purchase_order) # on submit def on_submit(self): super(PurchaseReceipt, self).on_submit() # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total + ) self.update_prevdoc_status() if flt(self.per_billed) < 100: @@ -203,13 +224,13 @@ class PurchaseReceipt(BuyingController): else: self.db_set("status", "Completed") - # Updating stock ledger should always be called after updating prevdoc status, # because updating ordered qty, reserved_qty_for_subcontract in bin # depends upon updated ordered qty in PO self.update_stock_ledger() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") self.make_gl_entries() @@ -217,10 +238,12 @@ class PurchaseReceipt(BuyingController): self.set_consumed_qty_in_po() def check_next_docstatus(self): - submit_rv = frappe.db.sql("""select t1.name + submit_rv = frappe.db.sql( + """select t1.name from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2 where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_rv: frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0])) @@ -229,10 +252,12 @@ class PurchaseReceipt(BuyingController): self.check_on_hold_or_closed_status() # Check if Purchase Invoice has been submitted against current Purchase Order - submitted = frappe.db.sql("""select t1.name + submitted = frappe.db.sql( + """select t1.name from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2 where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""", - self.name) + self.name, + ) if submitted: frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0])) @@ -244,19 +269,24 @@ class PurchaseReceipt(BuyingController): self.update_stock_ledger() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.delete_auto_created_batches() self.set_consumed_qty_in_po() @frappe.whitelist() def get_current_stock(self): - for d in self.get('supplied_items'): + for d in self.get("supplied_items"): if self.supplier_warehouse: - bin = frappe.db.sql("select actual_qty from `tabBin` where item_code = %s and warehouse = %s", (d.rm_item_code, self.supplier_warehouse), as_dict = 1) - d.current_stock = bin and flt(bin[0]['actual_qty']) or 0 + bin = frappe.db.sql( + "select actual_qty from `tabBin` where item_code = %s and warehouse = %s", + (d.rm_item_code, self.supplier_warehouse), + as_dict=1, + ) + d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0 def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import process_gl_map + gl_entries = [] self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) @@ -273,29 +303,44 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() - provisional_accounting_for_non_stock_items = \ - cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): if warehouse_account.get(d.warehouse): - stock_value_diff = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": self.name, - "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference") + stock_value_diff = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": self.name, + "voucher_detail_no": d.name, + "warehouse": d.warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) warehouse_account_name = warehouse_account[d.warehouse]["account"] warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") - supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get("account_currency") + supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( + "account_currency" + ) remarks = self.get("remarks") or _("Accounting Entry for Stock") # If PR is sub-contracted and fg item rate is zero # in that case if account for source and target warehouse are same, # then GL entries should not be posted - if flt(stock_value_diff) == flt(d.rm_supp_cost) \ - and warehouse_account.get(self.supplier_warehouse) \ - and warehouse_account_name == supplier_warehouse_account: - continue + if ( + flt(stock_value_diff) == flt(d.rm_supp_cost) + and warehouse_account.get(self.supplier_warehouse) + and warehouse_account_name == supplier_warehouse_account + ): + continue self.add_gl_entry( gl_entries=gl_entries, @@ -306,18 +351,24 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=stock_rbnb, account_currency=warehouse_account_currency, - item=d) + item=d, + ) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation - credit_currency = get_account_currency(warehouse_account[d.from_warehouse]['account']) \ - if d.from_warehouse else get_account_currency(stock_rbnb) + credit_currency = ( + get_account_currency(warehouse_account[d.from_warehouse]["account"]) + if d.from_warehouse + else get_account_currency(stock_rbnb) + ) - credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ - if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) + credit_amount = ( + flt(d.base_net_amount, d.precision("base_net_amount")) + if credit_currency == self.company_currency + else flt(d.net_amount, d.precision("net_amount")) + ) if credit_amount: - account = warehouse_account[d.from_warehouse]['account'] \ - if d.from_warehouse else stock_rbnb + account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb self.add_gl_entry( gl_entries=gl_entries, @@ -329,14 +380,18 @@ class PurchaseReceipt(BuyingController): against_account=warehouse_account_name, debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, - item=d) + item=d, + ) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): account_currency = get_account_currency(account) - credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or - account_currency!=self.company_currency) else flt(amount["amount"])) + credit_amount = ( + flt(amount["base_amount"]) + if (amount["base_amount"] or account_currency != self.company_currency) + else flt(amount["amount"]) + ) self.add_gl_entry( gl_entries=gl_entries, @@ -349,7 +404,8 @@ class PurchaseReceipt(BuyingController): credit_in_account_currency=flt(amount["amount"]), account_currency=account_currency, project=d.project, - item=d) + item=d, + ) # sub-contracting warehouse if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): @@ -362,22 +418,32 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=warehouse_account_name, account_currency=supplier_warehouse_account_currency, - item=d) + item=d, + ) # divisional loss adjustment - valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \ - flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) + valuation_amount_as_per_doc = ( + flt(d.base_net_amount, d.precision("base_net_amount")) + + flt(d.landed_cost_voucher_amount) + + flt(d.rm_supp_cost) + + flt(d.item_tax_amount) + ) - divisional_loss = flt(valuation_amount_as_per_doc - stock_value_diff, - d.precision("base_net_amount")) + divisional_loss = flt( + valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount") + ) if divisional_loss: if self.is_return or flt(d.item_tax_amount): loss_account = expenses_included_in_valuation else: - loss_account = self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb + loss_account = ( + self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb + ) - cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") + cost_center = d.cost_center or frappe.get_cached_value( + "Company", self.company, "cost_center" + ) self.add_gl_entry( gl_entries=gl_entries, @@ -389,21 +455,35 @@ class PurchaseReceipt(BuyingController): against_account=warehouse_account_name, account_currency=credit_currency, project=d.project, - item=d) + item=d, + ) - elif d.warehouse not in warehouse_with_no_account or \ - d.rejected_warehouse not in warehouse_with_no_account: - warehouse_with_no_account.append(d.warehouse) - elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and provisional_accounting_for_non_stock_items: - self.add_provisional_gl_entry(d, gl_entries, self.posting_date) + elif ( + d.warehouse not in warehouse_with_no_account + or d.rejected_warehouse not in warehouse_with_no_account + ): + warehouse_with_no_account.append(d.warehouse) + elif ( + d.item_code not in stock_items + and not d.is_fixed_asset + and flt(d.qty) + and provisional_accounting_for_non_stock_items + ): + self.add_provisional_gl_entry( + d, gl_entries, self.posting_date, d.get("provisional_expense_account") + ) if warehouse_with_no_account: - frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + - "\n".join(warehouse_with_no_account)) + frappe.msgprint( + _("No accounting entries for the following warehouses") + + ": \n" + + "\n".join(warehouse_with_no_account) + ) - def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0): - provisional_expense_account = self.get('provisional_expense_account') - credit_currency = get_account_currency(provisional_expense_account) + def add_provisional_gl_entry( + self, item, gl_entries, posting_date, provisional_account, reverse=0 + ): + credit_currency = get_account_currency(provisional_account) debit_currency = get_account_currency(item.expense_account) expense_account = item.expense_account remarks = self.get("remarks") or _("Accounting Entry for Service") @@ -411,11 +491,13 @@ class PurchaseReceipt(BuyingController): if reverse: multiplication_factor = -1 - expense_account = frappe.db.get_value('Purchase Receipt Item', {'name': item.get('pr_detail')}, ['expense_account']) + expense_account = frappe.db.get_value( + "Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"] + ) self.add_gl_entry( gl_entries=gl_entries, - account=provisional_expense_account, + account=provisional_account, cost_center=item.cost_center, debit=0.0, credit=multiplication_factor * item.amount, @@ -425,7 +507,8 @@ class PurchaseReceipt(BuyingController): project=item.project, voucher_detail_no=item.name, item=item, - posting_date=posting_date) + posting_date=posting_date, + ) self.add_gl_entry( gl_entries=gl_entries, @@ -434,28 +517,36 @@ class PurchaseReceipt(BuyingController): debit=multiplication_factor * item.amount, credit=0.0, remarks=remarks, - against_account=provisional_expense_account, - account_currency = debit_currency, + against_account=provisional_account, + account_currency=debit_currency, project=item.project, voucher_detail_no=item.name, item=item, - posting_date=posting_date) + posting_date=posting_date, + ) def make_tax_gl_entries(self, gl_entries): if erpnext.is_perpetual_inventory_enabled(self.company): expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')]) + negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} for tax in self.get("taxes"): - if tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): + if tax.category in ("Valuation", "Valuation and Total") and flt( + tax.base_tax_amount_after_discount_amount + ): if not tax.cost_center: - frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) + frappe.throw( + _("Cost Center is required in row {0} in Taxes table for type {1}").format( + tax.idx, _(tax.category) + ) + ) valuation_tax.setdefault(tax.name, 0) - valuation_tax[tax.name] += \ - (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount) + valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt( + tax.base_tax_amount_after_discount_amount + ) if negative_expense_to_be_booked and valuation_tax: # Backward compatibility: @@ -464,10 +555,13 @@ class PurchaseReceipt(BuyingController): # post valuation related charges on "Stock Received But Not Billed" # introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account - negative_expense_booked_in_pi = frappe.db.sql("""select name from `tabPurchase Invoice Item` pi + negative_expense_booked_in_pi = frappe.db.sql( + """select name from `tabPurchase Invoice Item` pi where docstatus = 1 and purchase_receipt=%s and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' - and voucher_no=pi.parent and account=%s)""", (self.name, expenses_included_in_valuation)) + and voucher_no=pi.parent and account=%s)""", + (self.name, expenses_included_in_valuation), + ) against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) @@ -485,7 +579,9 @@ class PurchaseReceipt(BuyingController): if i == len(valuation_tax): applicable_amount = amount_including_divisional_loss else: - applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) + applicable_amount = negative_expense_to_be_booked * ( + valuation_tax[tax.name] / total_valuation_amount + ) amount_including_divisional_loss -= applicable_amount self.add_gl_entry( @@ -496,13 +592,28 @@ class PurchaseReceipt(BuyingController): credit=applicable_amount, remarks=self.remarks or _("Accounting Entry for Stock"), against_account=against_account, - item=tax) + item=tax, + ) i += 1 - def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account, - debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None, - project=None, voucher_detail_no=None, item=None, posting_date=None): + def add_gl_entry( + self, + gl_entries, + account, + cost_center, + debit, + credit, + remarks, + against_account, + debit_in_account_currency=None, + credit_in_account_currency=None, + account_currency=None, + project=None, + voucher_detail_no=None, + item=None, + posting_date=None, + ): gl_entry = { "account": account, @@ -542,17 +653,19 @@ class PurchaseReceipt(BuyingController): def add_asset_gl_entries(self, item, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") # This returns category's cwip account if not then fallback to company's default cwip account - cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ - company = self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + ) - asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) + asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) remarks = self.get("remarks") or _("Accounting Entry for Asset") cwip_account_currency = get_account_currency(cwip_account) # debit cwip account - debit_in_account_currency = (base_asset_amount - if cwip_account_currency == self.company_currency else asset_amount) + debit_in_account_currency = ( + base_asset_amount if cwip_account_currency == self.company_currency else asset_amount + ) self.add_gl_entry( gl_entries=gl_entries, @@ -563,12 +676,14 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=arbnb_account, debit_in_account_currency=debit_in_account_currency, - item=item) + item=item, + ) asset_rbnb_currency = get_account_currency(arbnb_account) # credit arbnb account - credit_in_account_currency = (base_asset_amount - if asset_rbnb_currency == self.company_currency else asset_amount) + credit_in_account_currency = ( + base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount + ) self.add_gl_entry( gl_entries=gl_entries, @@ -579,13 +694,17 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=cwip_account, credit_in_account_currency=credit_in_account_currency, - item=item) + item=item, + ) def add_lcv_gl_entries(self, item, gl_entries): - expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") + expenses_included_in_asset_valuation = self.get_company_default( + "expenses_included_in_asset_valuation" + ) if not is_cwip_accounting_enabled(item.asset_category): - asset_account = get_asset_category_account(asset_category=item.asset_category, \ - fieldname='fixed_asset_account', company=self.company) + asset_account = get_asset_category_account( + asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company + ) else: # This returns company's default cwip account asset_account = get_asset_account("capital_work_in_progress_account", company=self.company) @@ -601,7 +720,8 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=asset_account, project=item.project, - item=item) + item=item, + ) self.add_gl_entry( gl_entries=gl_entries, @@ -612,11 +732,12 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=expenses_included_in_asset_valuation, project=item.project, - item=item) + item=item, + ) def update_assets(self, item, valuation_rate): - assets = frappe.db.get_all('Asset', - filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } + assets = frappe.db.get_all( + "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} ) for asset in assets: @@ -632,7 +753,7 @@ class PurchaseReceipt(BuyingController): updated_pr = [self.name] for d in self.get("items"): if d.get("purchase_invoice") and d.get("purchase_invoice_item"): - d.db_set('billed_amt', d.amount, update_modified=update_modified) + 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) @@ -642,24 +763,35 @@ class PurchaseReceipt(BuyingController): self.load_from_db() + def update_billed_amount_based_on_po(po_detail, update_modified=True): # Billed against Sales Order directly - billed_against_po = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` - where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", po_detail) + billed_against_po = frappe.db.sql( + """select sum(amount) from `tabPurchase Invoice Item` + where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", + po_detail, + ) billed_against_po = billed_against_po and billed_against_po[0][0] or 0 # Get all Purchase Receipt Item rows against the Purchase Order Item row - pr_details = frappe.db.sql("""select pr_item.name, pr_item.amount, pr_item.parent + pr_details = frappe.db.sql( + """select pr_item.name, pr_item.amount, pr_item.parent from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name=pr_item.parent and pr_item.purchase_order_item=%s and pr.docstatus=1 and pr.is_return = 0 - order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", po_detail, as_dict=1) + order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", + po_detail, + as_dict=1, + ) updated_pr = [] for pr_item in pr_details: # Get billed amount directly against Purchase Receipt - billed_amt_agianst_pr = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` - where pr_detail=%s and docstatus=1""", pr_item.name) + billed_amt_agianst_pr = frappe.db.sql( + """select sum(amount) from `tabPurchase Invoice Item` + where pr_detail=%s and docstatus=1""", + pr_item.name, + ) billed_amt_agianst_pr = billed_amt_agianst_pr and billed_amt_agianst_pr[0][0] or 0 # Distribute billed amount directly against PO between PRs based on FIFO @@ -672,12 +804,19 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): billed_amt_agianst_pr += billed_against_po billed_against_po = 0 - frappe.db.set_value("Purchase Receipt Item", pr_item.name, "billed_amt", billed_amt_agianst_pr, update_modified=update_modified) + frappe.db.set_value( + "Purchase Receipt Item", + pr_item.name, + "billed_amt", + billed_amt_agianst_pr, + update_modified=update_modified, + ) updated_pr.append(pr_item.parent) return updated_pr + def update_billing_percentage(pr_doc, update_modified=True): # Reload as billed amount was set in db directly pr_doc.load_from_db() @@ -685,15 +824,15 @@ def update_billing_percentage(pr_doc, update_modified=True): # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 for item in pr_doc.items: - return_data = frappe.db.get_list("Purchase Receipt", - fields = [ - "sum(abs(`tabPurchase Receipt Item`.qty)) as qty" - ], - filters = [ + return_data = frappe.db.get_list( + "Purchase Receipt", + fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], + filters=[ ["Purchase Receipt", "docstatus", "=", 1], ["Purchase Receipt", "is_return", "=", 1], - ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name] - ]) + ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name], + ], + ) returned_qty = return_data[0].qty if return_data else 0 returned_amount = flt(returned_qty) * flt(item.rate) @@ -711,11 +850,12 @@ def update_billing_percentage(pr_doc, update_modified=True): pr_doc.set_status(update=True) pr_doc.notify_update() + @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from erpnext.accounts.party import get_payment_terms_template - doc = frappe.get_doc('Purchase Receipt', source_name) + doc = frappe.get_doc("Purchase Receipt", source_name) returned_qty_map = get_returned_qty_map(source_name) invoiced_qty_map = get_invoiced_qty_map(source_name) @@ -724,8 +864,9 @@ def make_purchase_invoice(source_name, target_doc=None): frappe.throw(_("All items have already been Invoiced/Returned")) doc = frappe.get_doc(target) - doc.ignore_pricing_rule = 1 - doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) + doc.payment_terms_template = get_payment_terms_template( + source.supplier, "Supplier", source.company + ) doc.run_method("onload") doc.run_method("set_missing_values") doc.run_method("calculate_taxes_and_totals") @@ -733,14 +874,20 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + if frappe.db.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ): target_doc.rejected_qty = 0 - target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) + target_doc.stock_qty = flt(target_doc.qty) * flt( + target_doc.conversion_factor, target_doc.precision("conversion_factor") + ) returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): qty = item_row.qty - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + if frappe.db.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ): qty = item_row.received_qty pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = flt(returned_qty_map.get(item_row.name, 0)) @@ -753,68 +900,85 @@ def make_purchase_invoice(source_name, target_doc=None): returned_qty = 0 return pending_qty, returned_qty - - doclist = get_mapped_doc("Purchase Receipt", source_name, { - "Purchase Receipt": { - "doctype": "Purchase Invoice", - "field_map": { - "supplier_warehouse":"supplier_warehouse", - "is_return": "is_return", - "bill_date": "bill_date" + doclist = get_mapped_doc( + "Purchase Receipt", + source_name, + { + "Purchase Receipt": { + "doctype": "Purchase Invoice", + "field_map": { + "supplier_warehouse": "supplier_warehouse", + "is_return": "is_return", + "bill_date": "bill_date", + }, + "validation": { + "docstatus": ["=", 1], + }, }, - "validation": { - "docstatus": ["=", 1], + "Purchase Receipt Item": { + "doctype": "Purchase Invoice Item", + "field_map": { + "name": "pr_detail", + "parent": "purchase_receipt", + "purchase_order_item": "po_detail", + "purchase_order": "purchase_order", + "is_fixed_asset": "is_fixed_asset", + "asset_location": "asset_location", + "asset_category": "asset_category", + }, + "postprocess": update_item, + "filter": lambda d: get_pending_qty(d)[0] <= 0 + if not doc.get("is_return") + else get_pending_qty(d)[0] > 0, }, + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, }, - "Purchase Receipt Item": { - "doctype": "Purchase Invoice Item", - "field_map": { - "name": "pr_detail", - "parent": "purchase_receipt", - "purchase_order_item": "po_detail", - "purchase_order": "purchase_order", - "is_fixed_asset": "is_fixed_asset", - "asset_location": "asset_location", - "asset_category": 'asset_category' - }, - "postprocess": update_item, - "filter": lambda d: get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0 - }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges", - "add_if_empty": True - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) + doclist.set_onload("ignore_price_list", True) return doclist + def get_invoiced_qty_map(purchase_receipt): """returns a map: {pr_detail: invoiced_qty}""" invoiced_qty_map = {} - for pr_detail, qty in frappe.db.sql("""select pr_detail, qty from `tabPurchase Invoice Item` - where purchase_receipt=%s and docstatus=1""", purchase_receipt): - if not invoiced_qty_map.get(pr_detail): - invoiced_qty_map[pr_detail] = 0 - invoiced_qty_map[pr_detail] += qty + for pr_detail, qty in frappe.db.sql( + """select pr_detail, qty from `tabPurchase Invoice Item` + where purchase_receipt=%s and docstatus=1""", + purchase_receipt, + ): + if not invoiced_qty_map.get(pr_detail): + invoiced_qty_map[pr_detail] = 0 + invoiced_qty_map[pr_detail] += qty return invoiced_qty_map + def get_returned_qty_map(purchase_receipt): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty + returned_qty_map = frappe._dict( + frappe.db.sql( + """select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name = pr_item.parent and pr.docstatus = 1 and pr.is_return = 1 and pr.return_against = %s - """, purchase_receipt)) + """, + purchase_receipt, + ) + ) return returned_qty_map + @frappe.whitelist() def make_purchase_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Purchase Receipt", source_name, target_doc) @@ -823,35 +987,47 @@ def update_purchase_receipt_status(docname, status): pr = frappe.get_doc("Purchase Receipt", docname) pr.update_status(status) + @frappe.whitelist() -def make_stock_entry(source_name,target_doc=None): +def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.stock_entry_type = "Material Transfer" - target.purpose = "Material Transfer" + target.purpose = "Material Transfer" - doclist = get_mapped_doc("Purchase Receipt", source_name,{ - "Purchase Receipt": { - "doctype": "Stock Entry", - }, - "Purchase Receipt Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "warehouse": "s_warehouse", - "parent": "reference_purchase_receipt", - "batch_no": "batch_no" + doclist = get_mapped_doc( + "Purchase Receipt", + source_name, + { + "Purchase Receipt": { + "doctype": "Stock Entry", + }, + "Purchase Receipt Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "warehouse": "s_warehouse", + "parent": "reference_purchase_receipt", + "batch_no": "batch_no", + }, }, }, - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def make_inter_company_delivery_note(source_name, target_doc=None): return make_inter_company_transaction("Purchase Receipt", source_name, target_doc) + def get_item_account_wise_additional_cost(purchase_document): - landed_cost_vouchers = frappe.get_all("Landed Cost Purchase Receipt", fields=["parent"], - filters = {"receipt_document": purchase_document, "docstatus": 1}) + landed_cost_vouchers = frappe.get_all( + "Landed Cost Purchase Receipt", + fields=["parent"], + filters={"receipt_document": purchase_document, "docstatus": 1}, + ) if not landed_cost_vouchers: return @@ -861,9 +1037,9 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) - #Use amount field for total item cost for manually cost distributed LCVs - if landed_cost_voucher_doc.distribute_charges_based_on == 'Distribute Manually': - based_on_field = 'amount' + # Use amount field for total item cost for manually cost distributed LCVs + if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually": + based_on_field = "amount" else: based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) @@ -876,15 +1052,20 @@ def get_item_account_wise_additional_cost(purchase_document): if item.receipt_document == purchase_document: for account in landed_cost_voucher_doc.taxes: item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, { - "amount": 0.0, - "base_amount": 0.0 - }) + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault( + account.expense_account, {"amount": 0.0, "base_amount": 0.0} + ) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["amount"] += \ - account.amount * item.get(based_on_field) / total_item_cost + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ + "amount" + ] += (account.amount * item.get(based_on_field) / total_item_cost) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["base_amount"] += \ - account.base_amount * item.get(based_on_field) / total_item_cost + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ + "base_amount" + ] += (account.base_amount * item.get(based_on_field) / total_item_cost) return item_account_wise_cost + + +def on_doctype_update(): + frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index 18da88c3753..06ba9365561 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -1,38 +1,25 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'purchase_receipt_no', - 'non_standard_fieldnames': { - 'Purchase Invoice': 'purchase_receipt', - 'Asset': 'purchase_receipt', - 'Landed Cost Voucher': 'receipt_document', - 'Auto Repeat': 'reference_document', - 'Purchase Receipt': 'return_against' + "fieldname": "purchase_receipt_no", + "non_standard_fieldnames": { + "Purchase Invoice": "purchase_receipt", + "Asset": "purchase_receipt", + "Landed Cost Voucher": "receipt_document", + "Auto Repeat": "reference_document", + "Purchase Receipt": "return_against", }, - 'internal_links': { - 'Purchase Order': ['items', 'purchase_order'], - 'Project': ['items', 'project'], - 'Quality Inspection': ['items', 'quality_inspection'], + "internal_links": { + "Purchase Order": ["items", "purchase_order"], + "Project": ["items", "project"], + "Quality Inspection": ["items", "quality_inspection"], }, - 'transactions': [ - { - 'label': _('Related'), - 'items': ['Purchase Invoice', 'Landed Cost Voucher', 'Asset'] - }, - { - 'label': _('Reference'), - 'items': ['Purchase Order', 'Quality Inspection', 'Project'] - }, - { - 'label': _('Returns'), - 'items': ['Purchase Receipt'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]}, + {"label": _("Reference"), "items": ["Purchase Order", "Quality Inspection", "Project"]}, + {"label": _("Returns"), "items": ["Purchase Receipt"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b13d6d3d05a..65c30de0978 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -7,6 +7,7 @@ import unittest from collections import defaultdict import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, cstr, flt, today from six import iteritems @@ -18,24 +19,19 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestPurchaseReceipt(ERPNextTestCase): +class TestPurchaseReceipt(FrappeTestCase): def setUp(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) def test_purchase_receipt_received_qty(self): """ - 1. Test if received qty is validated against accepted + rejected - 2. Test if received qty is auto set on save + 1. Test if received qty is validated against accepted + rejected + 2. Test if received qty is auto set on save """ pr = make_purchase_receipt( - qty=1, - rejected_qty=1, - received_qty=3, - item_code="_Test Item Home Desktop 200", - do_not_save=True + qty=1, rejected_qty=1, received_qty=3, item_code="_Test Item Home Desktop 200", do_not_save=True ) self.assertRaises(QtyMismatchError, pr.save) @@ -52,11 +48,8 @@ class TestPurchaseReceipt(ERPNextTestCase): sl_entry = frappe.db.get_all( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name - }, - ['actual_qty'] + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + ["actual_qty"], ) self.assertEqual(len(sl_entry), 1) @@ -66,47 +59,44 @@ class TestPurchaseReceipt(ERPNextTestCase): sl_entry_cancelled = frappe.db.get_all( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name - }, - ['actual_qty'], - order_by='creation' + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + ["actual_qty"], + order_by="creation", ) self.assertEqual(len(sl_entry_cancelled), 2) self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) def test_make_purchase_invoice(self): - if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'): - frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Purchase Invoice', - 'allocate_payment_based_on_payment_terms': 1, - 'terms': [ - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 00 - }, - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - }] - }).insert() + if not frappe.db.exists( + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" + ): + frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Purchase Invoice", + "allocate_payment_based_on_payment_terms": 1, + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 00, + }, + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + }, + ], + } + ).insert() template = frappe.db.get_value( - "Payment Terms Template", - "_Test Payment Terms Template For Purchase Invoice" - ) - old_template_in_supplier = frappe.db.get_value( - "Supplier", - "_Test Supplier", - "payment_terms" + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ) + old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms") frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template) pr = make_purchase_receipt(do_not_save=True) @@ -124,23 +114,17 @@ class TestPurchaseReceipt(ERPNextTestCase): # test if payment terms are fetched and set in PI self.assertEqual(pi.payment_terms_template, template) - self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total) / 2) self.assertEqual(pi.payment_schedule[0].invoice_portion, 50) - self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total) / 2) self.assertEqual(pi.payment_schedule[1].invoice_portion, 50) # teardown - pi.delete() # draft PI + pi.delete() # draft PI pr.cancel() - frappe.db.set_value( - "Supplier", - "_Test Supplier", - "payment_terms", - old_template_in_supplier - ) + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier) frappe.get_doc( - "Payment Terms Template", - "_Test Payment Terms Template For Purchase Invoice" + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ).delete() def test_purchase_receipt_no_gl_entry(self): @@ -148,27 +132,19 @@ class TestPurchaseReceipt(ERPNextTestCase): existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - ["actual_qty", "stock_value"] + {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "stock_value"], ) if existing_bin_qty < 0: make_stock_entry( - item_code="_Test Item", - target="_Test Warehouse - _TC", - qty=abs(existing_bin_qty) + item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty) ) existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - ["actual_qty", "stock_value"] + {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "stock_value"], ) pr = make_purchase_receipt() @@ -179,20 +155,15 @@ class TestPurchaseReceipt(ERPNextTestCase): "voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" + "warehouse": "_Test Warehouse - _TC", }, - "stock_value_difference" + "stock_value_difference", ) self.assertEqual(stock_value_difference, 250) current_bin_stock_value = frappe.db.get_value( - "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - "stock_value" + "Bin", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, "stock_value" ) self.assertEqual(current_bin_stock_value, existing_bin_stock_value + 250) @@ -201,7 +172,7 @@ class TestPurchaseReceipt(ERPNextTestCase): pr.cancel() def test_batched_serial_no_purchase(self): - item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched Serialized Item"}) if not item: item = create_item("Batched Serialized Item") item.has_batch_no = 1 @@ -211,34 +182,30 @@ class TestPurchaseReceipt(ERPNextTestCase): item.serial_no_series = "BS-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched Serialized Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched Serialized Item"}) pr = make_purchase_receipt(item_code=item.name, qty=5, rate=500) - self.assertTrue( - frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}) - ) + self.assertTrue(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) pr.load_from_db() batch_no = pr.items[0].batch_no pr.cancel() - self.assertFalse( - frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}) - ) - self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no})) + self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) + self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no})) def test_duplicate_serial_nos(self): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - item = frappe.db.exists("Item", {'item_name': 'Test Serialized Item 123'}) + item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"}) if not item: item = create_item("Test Serialized Item 123") item.has_serial_no = 1 item.serial_no_series = "TSI123-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Test Serialized Item 123'}) + item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"}) # First make purchase receipt pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500) @@ -246,12 +213,8 @@ class TestPurchaseReceipt(ERPNextTestCase): serial_nos = frappe.db.get_value( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name, - "item_code": item.name - }, - "serial_no" + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name}, + "serial_no", ) serial_nos = get_serial_nos(serial_nos) @@ -263,21 +226,16 @@ class TestPurchaseReceipt(ERPNextTestCase): item_code=item.name, qty=2, rate=500, - serial_no='\n'.join(serial_nos), - company='_Test Company 1', + serial_no="\n".join(serial_nos), + company="_Test Company 1", do_not_submit=True, - warehouse = 'Stores - _TC1' + warehouse="Stores - _TC1", ) self.assertRaises(SerialNoDuplicateError, pr_different_company.submit) # Then made delivery note to remove the serial nos from stock - dn = create_delivery_note( - item_code=item.name, - qty=2, - rate=1500, - serial_no='\n'.join(serial_nos) - ) + dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos)) dn.load_from_db() self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos) @@ -289,8 +247,8 @@ class TestPurchaseReceipt(ERPNextTestCase): qty=2, rate=500, posting_date=posting_date, - serial_no='\n'.join(serial_nos), - do_not_submit=True + serial_no="\n".join(serial_nos), + do_not_submit=True, ) self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit) @@ -301,29 +259,28 @@ class TestPurchaseReceipt(ERPNextTestCase): qty=2, rate=500, posting_date=posting_date, - serial_no='\n'.join(serial_nos), + serial_no="\n".join(serial_nos), company="_Test Company 1", do_not_submit=True, - warehouse="Stores - _TC1" + warehouse="Stores - _TC1", ) self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit) # Receive the same serial nos after the delivery note posting date and time - make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - serial_no='\n'.join(serial_nos) - ) + make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos)) # Raise the error for backdated deliver note entry cancel self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel) def test_purchase_receipt_gl_entry(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", - get_multiple_items = True, get_taxes_and_charges = True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + ) self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1) @@ -339,14 +296,14 @@ class TestPurchaseReceipt(ERPNextTestCase): stock_in_hand_account: [750.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], "_Test Account Shipping Charges - TCP1": [0.0, 100.0], - "_Test Account Customs Duty - TCP1": [0.0, 150.0] + "_Test Account Customs Duty - TCP1": [0.0, 150.0], } else: expected_values = { stock_in_hand_account: [375.0, 0.0], fixed_asset_account: [375.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "_Test Account Shipping Charges - TCP1": [0.0, 250.0] + "_Test Account Shipping Charges - TCP1": [0.0, 250.0], } for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.debit) @@ -359,22 +316,19 @@ class TestPurchaseReceipt(ERPNextTestCase): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry frappe.db.set_value( - "Buying Settings", None, - "backflush_raw_materials_of_subcontract_based_on", "BOM" + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" ) make_stock_entry( - item_code="_Test Item", qty=100, - target="_Test Warehouse 1 - _TC", basic_rate=100 + item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 ) make_stock_entry( - item_code="_Test Item Home Desktop 100", qty=100, - target="_Test Warehouse 1 - _TC", basic_rate=100 - ) - pr = make_purchase_receipt( - item_code="_Test FG Item", qty=10, - rate=500, is_subcontracted="Yes" + item_code="_Test Item Home Desktop 100", + qty=100, + target="_Test Warehouse 1 - _TC", + basic_rate=100, ) + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes") self.assertEqual(len(pr.get("supplied_items")), 2) rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) @@ -384,32 +338,35 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + frappe.db.set_value( - "Buying Settings", None, - "backflush_raw_materials_of_subcontract_based_on", "BOM" + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" ) se1 = make_stock_entry( item_code="_Test Item", target="Work In Progress - TCP1", - qty=100, basic_rate=100, - company="_Test Company with perpetual inventory" + qty=100, + basic_rate=100, + company="_Test Company with perpetual inventory", ) se2 = make_stock_entry( item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", - qty=100, basic_rate=100, - company="_Test Company with perpetual inventory" + qty=100, + basic_rate=100, + company="_Test Company with perpetual inventory", ) pr = make_purchase_receipt( item_code="_Test FG Item", - qty=10, rate=0, + qty=10, + rate=0, is_subcontracted="Yes", company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work In Progress - TCP1" + supplier_warehouse="Work In Progress - TCP1", ) gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -422,9 +379,9 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_subcontracting_over_receipt(self): """ - Behaviour: Raise multiple PRs against one PO that in total - receive more than the required qty in the PO. - Expected Result: Error Raised for Over Receipt against PO. + Behaviour: Raise multiple PRs against one PO that in total + receive more than the required qty in the PO. + Expected Result: Error Raised for Over Receipt against PO. """ from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.doctype.purchase_order.purchase_order import ( @@ -443,26 +400,21 @@ class TestPurchaseReceipt(ERPNextTestCase): po = create_purchase_order( item_code=item_code, - qty=1, include_exploded_items=0, + qty=1, + include_exploded_items=0, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC" + supplier_warehouse="_Test Warehouse 1 - _TC", ) # stock raw materials in a warehouse before transfer se1 = make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Test Extra Item 1", - qty=10, basic_rate=100 + target="_Test Warehouse - _TC", 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 + 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 + target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100 ) rm_items = [ @@ -472,7 +424,7 @@ class TestPurchaseReceipt(ERPNextTestCase): "item_name": "_Test FG Item", "qty": po.supplied_items[0].required_qty, "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos" + "stock_uom": "Nos", }, { "item_code": item_code, @@ -480,8 +432,8 @@ class TestPurchaseReceipt(ERPNextTestCase): "item_name": "Test Extra Item 1", "qty": po.supplied_items[1].required_qty, "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos" - } + "stock_uom": "Nos", + }, ] rm_item_string = json.dumps(rm_items) se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) @@ -503,8 +455,9 @@ class TestPurchaseReceipt(ERPNextTestCase): po.reload() pr2.load_from_db() - if pr2.docstatus == 1 and frappe.db.get_value('Stock Ledger Entry', - {'voucher_no': pr2.name, 'is_cancelled': 0}, 'name'): + if pr2.docstatus == 1 and frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "name" + ): pr2.cancel() po.load_from_db() @@ -514,15 +467,10 @@ class TestPurchaseReceipt(ERPNextTestCase): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr_row_1_serial_no = pr.get("items")[0].serial_no - self.assertEqual( - frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), - pr.supplier - ) + self.assertEqual(frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), pr.supplier) pr.cancel() - self.assertFalse( - frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse") - ) + self.assertFalse(frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse")) def test_rejected_serial_no(self): pr = frappe.copy_doc(test_records[0]) @@ -537,32 +485,34 @@ class TestPurchaseReceipt(ERPNextTestCase): accepted_serial_nos = pr.get("items")[0].serial_no.split("\n") self.assertEqual(len(accepted_serial_nos), 3) for serial_no in accepted_serial_nos: - self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), - pr.get("items")[0].warehouse) + self.assertEqual( + frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].warehouse + ) rejected_serial_nos = pr.get("items")[0].rejected_serial_no.split("\n") self.assertEqual(len(rejected_serial_nos), 2) for serial_no in rejected_serial_nos: - self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), - pr.get("items")[0].rejected_warehouse) + self.assertEqual( + frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse + ) pr.cancel() def test_purchase_return_partial(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, - do_not_submit=1 + do_not_submit=1, ) return_pr.items[0].purchase_receipt_item = pr.items[0].name return_pr.submit() @@ -570,16 +520,12 @@ class TestPurchaseReceipt(ERPNextTestCase): # check sle outgoing_rate = frappe.db.get_value( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name - }, - "outgoing_rate" + {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, + "outgoing_rate", ) self.assertEqual(outgoing_rate, 50) - # check gl entries for return gl_entries = get_gl_entries("Purchase Receipt", return_pr.name) @@ -605,6 +551,7 @@ class TestPurchaseReceipt(ERPNextTestCase): self.assertEqual(pr.per_returned, 40) from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_pr_2 = make_return_doc("Purchase Receipt", pr.name) # Check if unreturned amount is mapped in 2nd return @@ -627,7 +574,7 @@ class TestPurchaseReceipt(ERPNextTestCase): # PR should be completed on billing all unreturned amount self.assertEqual(pr.items[0].billed_amt, 150) self.assertEqual(pr.per_billed, 100) - self.assertEqual(pr.status, 'Completed') + self.assertEqual(pr.status, "Completed") pi.load_from_db() pi.cancel() @@ -641,18 +588,18 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_purchase_return_full(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, - do_not_submit=1 + do_not_submit=1, ) return_pr.items[0].purchase_receipt_item = pr.items[0].name return_pr.submit() @@ -665,7 +612,7 @@ class TestPurchaseReceipt(ERPNextTestCase): # Check if Original PR updated self.assertEqual(pr.items[0].returned_qty, 5) self.assertEqual(pr.per_returned, 100) - self.assertEqual(pr.status, 'Return Issued') + self.assertEqual(pr.status, "Return Issued") return_pr.cancel() pr.cancel() @@ -673,32 +620,32 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_purchase_return_for_rejected_qty(self): from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse - rejected_warehouse="_Test Rejected Warehouse - TCP1" + rejected_warehouse = "_Test Rejected Warehouse - TCP1" if not frappe.db.exists("Warehouse", rejected_warehouse): get_warehouse( - company = "_Test Company with perpetual inventory", - abbr = " - TCP1", - warehouse_name = "_Test Rejected Warehouse" + company="_Test Company with perpetual inventory", + abbr=" - TCP1", + warehouse_name="_Test Rejected Warehouse", ).name pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", qty=2, rejected_qty=2, - rejected_warehouse=rejected_warehouse + rejected_warehouse=rejected_warehouse, ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, - rejected_qty = -2, - rejected_warehouse=rejected_warehouse + rejected_qty=-2, + rejected_warehouse=rejected_warehouse, ) actual_qty = frappe.db.get_value( @@ -706,9 +653,9 @@ class TestPurchaseReceipt(ERPNextTestCase): { "voucher_type": "Purchase Receipt", "voucher_no": return_pr.name, - "warehouse": return_pr.items[0].rejected_warehouse + "warehouse": return_pr.items[0].rejected_warehouse, }, - "actual_qty" + "actual_qty", ) self.assertEqual(actual_qty, -2) @@ -716,6 +663,44 @@ class TestPurchaseReceipt(ERPNextTestCase): return_pr.cancel() pr.cancel() + def test_purchase_receipt_for_rejected_gle_without_accepted_warehouse(self): + from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse + + rejected_warehouse = "_Test Rejected Warehouse - TCP1" + if not frappe.db.exists("Warehouse", rejected_warehouse): + get_warehouse( + company="_Test Company with perpetual inventory", + abbr=" - TCP1", + warehouse_name="_Test Rejected Warehouse", + ).name + + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + received_qty=2, + rejected_qty=2, + rejected_warehouse=rejected_warehouse, + do_not_save=True, + ) + + pr.items[0].qty = 0.0 + pr.items[0].warehouse = "" + pr.submit() + + actual_qty = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "warehouse": pr.items[0].rejected_warehouse, + "is_cancelled": 0, + }, + "actual_qty", + ) + + self.assertEqual(actual_qty, 2) + self.assertFalse(pr.items[0].warehouse) + pr.cancel() def test_purchase_return_for_serialized_items(self): def _check_serial_no_values(serial_no, field_values): @@ -729,24 +714,22 @@ class TestPurchaseReceipt(ERPNextTestCase): serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0] - _check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "purchase_document_no": pr.name - }) + _check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "purchase_document_no": pr.name} + ) return_pr = make_purchase_receipt( item_code="_Test Serialized Item With Series", qty=-1, is_return=1, return_against=pr.name, - serial_no=serial_no + serial_no=serial_no, ) - _check_serial_no_values(serial_no, { - "warehouse": "", - "purchase_document_no": pr.name, - "delivery_document_no": return_pr.name - }) + _check_serial_no_values( + serial_no, + {"warehouse": "", "purchase_document_no": pr.name, "delivery_document_no": return_pr.name}, + ) return_pr.cancel() pr.reload() @@ -754,20 +737,12 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_purchase_return_for_multi_uom(self): item_code = "_Test Purchase Return For Multi-UOM" - if not frappe.db.exists('Item', item_code): - item = make_item(item_code, {'stock_uom': 'Box'}) - row = item.append('uoms', { - 'uom': 'Unit', - 'conversion_factor': 0.1 - }) + if not frappe.db.exists("Item", item_code): + item = make_item(item_code, {"stock_uom": "Box"}) + row = item.append("uoms", {"uom": "Unit", "conversion_factor": 0.1}) row.db_update() - pr = make_purchase_receipt( - item_code=item_code, - qty=1, - uom="Box", - conversion_factor=1.0 - ) + pr = make_purchase_receipt(item_code=item_code, qty=1, uom="Box", conversion_factor=1.0) return_pr = make_purchase_receipt( item_code=item_code, qty=-10, @@ -775,7 +750,7 @@ class TestPurchaseReceipt(ERPNextTestCase): stock_uom="Box", conversion_factor=0.1, is_return=1, - return_against=pr.name + return_against=pr.name, ) self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0) @@ -788,22 +763,19 @@ class TestPurchaseReceipt(ERPNextTestCase): update_purchase_receipt_status, ) - pr = make_purchase_receipt(do_not_submit=True) - pr.submit() + pr = make_purchase_receipt() update_purchase_receipt_status(pr.name, "Closed") - self.assertEqual( - frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed" - ) + self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed") pr.reload() pr.cancel() def test_pr_billing_status(self): """Flow: - 1. PO -> PR1 -> PI - 2. PO -> PI - 3. PO -> PR2. + 1. PO -> PR1 -> PI + 2. PO -> PI + 3. PO -> PR2. """ from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_purchase_invoice_from_po, @@ -865,19 +837,15 @@ class TestPurchaseReceipt(ERPNextTestCase): item = make_item(item_code, dict(has_serial_no=1)) serial_no = "12903812901" - pr_doc = make_purchase_receipt(item_code=item_code, - qty=1, serial_no = serial_no) + pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no) self.assertEqual( serial_no, frappe.db.get_value( "Serial No", - { - "purchase_document_type": "Purchase Receipt", - "purchase_document_no": pr_doc.name - }, - "name" - ) + {"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name}, + "name", + ), ) pr_doc.cancel() @@ -894,12 +862,9 @@ class TestPurchaseReceipt(ERPNextTestCase): serial_no, frappe.db.get_value( "Serial No", - { - "purchase_document_type": "Purchase Receipt", - "purchase_document_no": new_pr_doc.name - }, - "name" - ) + {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, + "name", + ), ) new_pr_doc.cancel() @@ -907,40 +872,52 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_auto_asset_creation(self): asset_item = "Test Asset Item" - if not frappe.db.exists('Item', asset_item): - asset_category = frappe.get_all('Asset Category') + if not frappe.db.exists("Item", asset_item): + asset_category = frappe.get_all("Asset Category") if asset_category: asset_category = asset_category[0].name if not asset_category: - doc = frappe.get_doc({ - 'doctype': 'Asset Category', - 'asset_category_name': 'Test Asset Category', - 'depreciation_method': 'Straight Line', - 'total_number_of_depreciations': 12, - 'frequency_of_depreciation': 1, - 'accounts': [{ - 'company_name': '_Test Company', - 'fixed_asset_account': '_Test Fixed Asset - _TC', - 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC', - 'depreciation_expense_account': '_Test Depreciations - _TC' - }] - }).insert() + doc = frappe.get_doc( + { + "doctype": "Asset Category", + "asset_category_name": "Test Asset Category", + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "accounts": [ + { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC", + } + ], + } + ).insert() asset_category = doc.name - item_data = make_item(asset_item, {'is_stock_item':0, - 'stock_uom': 'Box', 'is_fixed_asset': 1, 'auto_create_assets': 1, - 'asset_category': asset_category, 'asset_naming_series': 'ABC.###'}) + item_data = make_item( + asset_item, + { + "is_stock_item": 0, + "stock_uom": "Box", + "is_fixed_asset": 1, + "auto_create_assets": 1, + "asset_category": asset_category, + "asset_naming_series": "ABC.###", + }, + ) asset_item = item_data.item_code pr = make_purchase_receipt(item_code=asset_item, qty=3) - assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) + assets = frappe.db.get_all("Asset", filters={"purchase_receipt": pr.name}) self.assertEqual(len(assets), 3) - location = frappe.db.get_value('Asset', assets[0].name, 'location') + location = frappe.db.get_value("Asset", assets[0].name, "location") self.assertEqual(location, "Test Location") pr.cancel() @@ -950,17 +927,18 @@ class TestPurchaseReceipt(ERPNextTestCase): pr = make_purchase_receipt(item_code="Test Asset Item", qty=1) - asset = frappe.get_doc("Asset", { - 'purchase_receipt': pr.name - }) + asset = frappe.get_doc("Asset", {"purchase_receipt": pr.name}) asset.available_for_use_date = frappe.utils.nowdate() asset.gross_purchase_amount = 50.0 - asset.append("finance_books", { - "expected_value_after_useful_life": 10, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 1 - }) + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 1, + }, + ) asset.submit() pr_return = make_purchase_return(pr.name) @@ -980,36 +958,27 @@ class TestPurchaseReceipt(ERPNextTestCase): cost_center = "_Test Cost Center for BS Account - TCP1" create_cost_center( cost_center_name="_Test Cost Center for BS Account", - company="_Test Company with perpetual inventory" + company="_Test Company with perpetual inventory", ) - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() pr = make_purchase_receipt( cost_center=cost_center, company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) - stock_in_hand_account = get_inventory_account( - pr.company, pr.get("items")[0].warehouse - ) + stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) expected_values = { - "Stock Received But Not Billed - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Stock Received But Not Billed - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -1017,33 +986,24 @@ class TestPurchaseReceipt(ERPNextTestCase): pr.cancel() def test_purchase_receipt_cost_center_with_balance_sheet_account(self): - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) - stock_in_hand_account = get_inventory_account( - pr.company, pr.get("items")[0].warehouse - ) + stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) - cost_center = pr.get('items')[0].cost_center + cost_center = pr.get("items")[0].cost_center expected_values = { - "Stock Received But Not Billed - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Stock Received But Not Billed - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -1059,11 +1019,7 @@ class TestPurchaseReceipt(ERPNextTestCase): po = create_purchase_order() pr = create_pr_against_po(po.name) - pr1 = make_purchase_receipt( - qty=-1, - is_return=1, return_against=pr.name, - do_not_submit=True - ) + pr1 = make_purchase_receipt(qty=-1, is_return=1, return_against=pr.name, do_not_submit=True) pr1.items[0].purchase_order = po.name pr1.items[0].purchase_order_item = po.items[0].name pr1.items[0].purchase_receipt_item = pr.items[0].name @@ -1080,14 +1036,17 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self): pr1 = make_purchase_receipt(qty=8, do_not_submit=True) - pr1.append("items", { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "received_qty": 1, - "rate": 100, - "conversion_factor": 1.0, - }) + pr1.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "received_qty": 1, + "rate": 100, + "conversion_factor": 1.0, + }, + ) pr1.submit() pi1 = make_purchase_invoice(pr1.name) @@ -1096,11 +1055,7 @@ class TestPurchaseReceipt(ERPNextTestCase): pi1.save() pi1.submit() - pr2 = make_purchase_receipt( - qty=-2, - is_return=1, return_against=pr1.name, - do_not_submit=True - ) + pr2 = make_purchase_receipt(qty=-2, is_return=1, return_against=pr1.name, do_not_submit=True) pr2.items[0].purchase_receipt_item = pr1.items[0].name pr2.submit() @@ -1114,26 +1069,25 @@ class TestPurchaseReceipt(ERPNextTestCase): pr1.cancel() def test_stock_transfer_from_purchase_receipt(self): - pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', - company="_Test Company with perpetual inventory") + pr1 = make_purchase_receipt( + warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory" + ) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", do_not_save=1) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 + ) - pr.supplier_warehouse = '' - pr.items[0].from_warehouse = 'Work In Progress - TCP1' + pr.supplier_warehouse = "" + pr.items[0].from_warehouse = "Work In Progress - TCP1" pr.submit() - gl_entries = get_gl_entries('Purchase Receipt', pr.name) - sl_entries = get_sl_entries('Purchase Receipt', pr.name) + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + sl_entries = get_sl_entries("Purchase Receipt", pr.name) self.assertFalse(gl_entries) - expected_sle = { - 'Work In Progress - TCP1': -5, - 'Stores - TCP1': 5 - } + expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5} for sle in sl_entries: self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) @@ -1145,48 +1099,45 @@ class TestPurchaseReceipt(ERPNextTestCase): create_warehouse( "_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", - properties={"account": '_Test Account Stock In Hand - TCP1'} + properties={"account": "_Test Account Stock In Hand - TCP1"}, ) pr1 = make_purchase_receipt( - warehouse = '_Test Warehouse for Valuation - TCP1', - company="_Test Company with perpetual inventory" + warehouse="_Test Warehouse for Valuation - TCP1", + company="_Test Company with perpetual inventory", ) pr = make_purchase_receipt( - company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - do_not_save=1 + company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 ) - pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1' - pr.supplier_warehouse = '' + pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" + pr.supplier_warehouse = "" - - pr.append('taxes', { - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Shipping Charges - TCP1', - 'category': 'Valuation and Total', - 'cost_center': 'Main - TCP1', - 'description': 'Test', - 'rate': 9 - }) + pr.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Test", + "rate": 9, + }, + ) pr.submit() - gl_entries = get_gl_entries('Purchase Receipt', pr.name) - sl_entries = get_sl_entries('Purchase Receipt', pr.name) + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + sl_entries = get_sl_entries("Purchase Receipt", pr.name) expected_gle = [ - ['Stock In Hand - TCP1', 272.5, 0.0], - ['_Test Account Stock In Hand - TCP1', 0.0, 250.0], - ['_Test Account Shipping Charges - TCP1', 0.0, 22.5] + ["Stock In Hand - TCP1", 272.5, 0.0], + ["_Test Account Stock In Hand - TCP1", 0.0, 250.0], + ["_Test Account Shipping Charges - TCP1", 0.0, 22.5], ] - expected_sle = { - '_Test Warehouse for Valuation - TCP1': -5, - 'Stores - TCP1': 5 - } + expected_sle = {"_Test Warehouse for Valuation - TCP1": -5, "Stores - TCP1": 5} for sle in sl_entries: self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) @@ -1199,7 +1150,6 @@ class TestPurchaseReceipt(ERPNextTestCase): pr.cancel() pr1.cancel() - def test_subcontracted_pr_for_multi_transfer_batches(self): from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_receipt, @@ -1214,49 +1164,57 @@ class TestPurchaseReceipt(ERPNextTestCase): update_backflush_based_on("Material Transferred for Subcontract") item_code = "_Test Subcontracted FG Item 3" - make_item('Sub Contracted Raw Material 3', { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'has_batch_no': 1, - 'create_new_batch': 1 - }) + make_item( + "Sub Contracted Raw Material 3", + {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1}, + ) - create_subcontracted_item(item_code=item_code, has_batch_no=1, - raw_materials=["Sub Contracted Raw Material 3"]) + create_subcontracted_item( + item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"] + ) order_qty = 500 - po = create_purchase_order(item_code=item_code, qty=order_qty, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + item_code=item_code, + qty=order_qty, + is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC", + ) - ste1=make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100) - ste2=make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100) + ste1 = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 3", + qty=300, + basic_rate=100, + ) + ste2 = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 3", + qty=200, + basic_rate=100, + ) - transferred_batch = { - ste1.items[0].batch_no : 300, - ste2.items[0].batch_no : 200 - } + transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200} rm_items = [ { - "item_code":item_code, - "rm_item_code":"Sub Contracted Raw Material 3", - "item_name":"_Test Item", - "qty":300, - "warehouse":"_Test Warehouse - _TC", - "stock_uom":"Nos", - "name": po.supplied_items[0].name + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 3", + "item_name": "_Test Item", + "qty": 300, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": po.supplied_items[0].name, }, { - "item_code":item_code, - "rm_item_code":"Sub Contracted Raw Material 3", - "item_name":"_Test Item", - "qty":200, - "warehouse":"_Test Warehouse - _TC", - "stock_uom":"Nos", - "name": po.supplied_items[0].name - } + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 3", + "item_name": "_Test Item", + "qty": 200, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": po.supplied_items[0].name, + }, ] rm_item_string = json.dumps(rm_items) @@ -1268,11 +1226,8 @@ class TestPurchaseReceipt(ERPNextTestCase): supplied_qty = frappe.db.get_value( "Purchase Order Item Supplied", - { - "parent": po.name, - "rm_item_code": "Sub Contracted Raw Material 3" - }, - "supplied_qty" + {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, + "supplied_qty", ) self.assertEqual(supplied_qty, 500.00) @@ -1292,12 +1247,11 @@ class TestPurchaseReceipt(ERPNextTestCase): ste1.cancel() po.cancel() - def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - - Create PO - - Create PI from PO and submit - - Create PR from PO and submit + - Create PO + - Create PI from PO and submit + - Create PR from PO and submit """ from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order @@ -1316,16 +1270,16 @@ class TestPurchaseReceipt(ERPNextTestCase): def test_po_to_pi_and_po_to_pr_worflow_partial(self): """Test following behaviour: - - Create PO - - Create partial PI from PO and submit - - Create PR from PO and submit + - Create PO + - Create partial PI from PO and submit + - Create PR from PO and submit """ from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order po = test_purchase_order.create_purchase_order() pi = purchase_order.make_purchase_invoice(po.name) - pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item. + pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item. pi.submit() pr = purchase_order.make_purchase_receipt(po.name) @@ -1357,10 +1311,9 @@ class TestPurchaseReceipt(ERPNextTestCase): automatically_fetch_payment_terms() - po = create_purchase_order(qty=10, rate=100, do_not_save=1) create_payment_terms_template() - po.payment_terms_template = 'Test Receivable Template' + po.payment_terms_template = "Test Receivable Template" po.submit() pr = make_pr_against_po(po.name, received_qty=10) @@ -1387,7 +1340,9 @@ class TestPurchaseReceipt(ERPNextTestCase): account = "Stock Received But Not Billed - TCP1" make_item(item_code) - se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0) + se = make_stock_entry( + item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0 + ) se.items[0].allow_zero_valuation_rate = 1 se.save() se.submit() @@ -1408,93 +1363,112 @@ class TestPurchaseReceipt(ERPNextTestCase): def get_sl_entries(voucher_type, voucher_no): - return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference + return frappe.db.sql( + """ select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s - order by posting_time desc""", (voucher_type, voucher_no), as_dict=1) + order by posting_time desc""", + (voucher_type, voucher_no), + as_dict=1, + ) + def get_gl_entries(voucher_type, voucher_no): - return frappe.db.sql("""select account, debit, credit, cost_center, is_cancelled + return frappe.db.sql( + """select account, debit, credit, cost_center, is_cancelled from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account desc""", (voucher_type, voucher_no), as_dict=1) + order by account desc""", + (voucher_type, voucher_no), + as_dict=1, + ) + def get_taxes(**args): args = frappe._dict(args) - return [{'account_head': '_Test Account Shipping Charges - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Valuation and Total', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'Shipping Charges', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 100.0, - 'tax_amount': 100.0}, - {'account_head': '_Test Account VAT - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Total', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'VAT', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 120.0, - 'tax_amount': 120.0}, - {'account_head': '_Test Account Customs Duty - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Valuation', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'Customs Duty', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 150.0, - 'tax_amount': 150.0}] + return [ + { + "account_head": "_Test Account Shipping Charges - TCP1", + "add_deduct_tax": "Add", + "category": "Valuation and Total", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "Shipping Charges", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 100.0, + "tax_amount": 100.0, + }, + { + "account_head": "_Test Account VAT - TCP1", + "add_deduct_tax": "Add", + "category": "Total", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "VAT", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 120.0, + "tax_amount": 120.0, + }, + { + "account_head": "_Test Account Customs Duty - TCP1", + "add_deduct_tax": "Add", + "category": "Valuation", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "Customs Duty", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 150.0, + "tax_amount": 150.0, + }, + ] + def get_items(**args): args = frappe._dict(args) - return [{ - "base_amount": 250.0, - "conversion_factor": 1.0, - "description": "_Test Item", - "doctype": "Purchase Receipt Item", - "item_code": "_Test Item", - "item_name": "_Test Item", - "parentfield": "items", - "qty": 5.0, - "rate": 50.0, - "received_qty": 5.0, - "rejected_qty": 0.0, - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "cost_center": args.cost_center or "Main - _TC" - }, - { - "base_amount": 250.0, - "conversion_factor": 1.0, - "description": "_Test Item Home Desktop 100", - "doctype": "Purchase Receipt Item", - "item_code": "_Test Item Home Desktop 100", - "item_name": "_Test Item Home Desktop 100", - "parentfield": "items", - "qty": 5.0, - "rate": 50.0, - "received_qty": 5.0, - "rejected_qty": 0.0, - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": args.warehouse or "_Test Warehouse 1 - _TC", - "cost_center": args.cost_center or "Main - _TC" - }] + return [ + { + "base_amount": 250.0, + "conversion_factor": 1.0, + "description": "_Test Item", + "doctype": "Purchase Receipt Item", + "item_code": "_Test Item", + "item_name": "_Test Item", + "parentfield": "items", + "qty": 5.0, + "rate": 50.0, + "received_qty": 5.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "uom": "_Test UOM", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "cost_center": args.cost_center or "Main - _TC", + }, + { + "base_amount": 250.0, + "conversion_factor": 1.0, + "description": "_Test Item Home Desktop 100", + "doctype": "Purchase Receipt Item", + "item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "parentfield": "items", + "qty": 5.0, + "rate": 50.0, + "received_qty": 5.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "uom": "_Test UOM", + "warehouse": args.warehouse or "_Test Warehouse 1 - _TC", + "cost_center": args.cost_center or "Main - _TC", + }, + ] + def make_purchase_receipt(**args): - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) pr = frappe.new_doc("Purchase Receipt") @@ -1518,27 +1492,33 @@ def make_purchase_receipt(**args): item_code = args.item or args.item_code or "_Test Item" uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM" - pr.append("items", { - "item_code": item_code, - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": qty, - "received_qty": received_qty, - "rejected_qty": rejected_qty, - "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", - "rate": args.rate if args.rate != None else 50, - "conversion_factor": args.conversion_factor or 1.0, - "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), - "serial_no": args.serial_no, - "stock_uom": args.stock_uom or "_Test UOM", - "uom": uom, - "cost_center": args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center'), - "asset_location": args.location or "Test Location" - }) + pr.append( + "items", + { + "item_code": item_code, + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": qty, + "received_qty": received_qty, + "rejected_qty": rejected_qty, + "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" + if rejected_qty != 0 + else "", + "rate": args.rate if args.rate != None else 50, + "conversion_factor": args.conversion_factor or 1.0, + "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), + "serial_no": args.serial_no, + "stock_uom": args.stock_uom or "_Test UOM", + "uom": uom, + "cost_center": args.cost_center + or frappe.get_cached_value("Company", pr.company, "cost_center"), + "asset_location": args.location or "Test Location", + }, + ) if args.get_multiple_items: pr.items = [] - company_cost_center = frappe.get_cached_value('Company', pr.company, 'cost_center') + company_cost_center = frappe.get_cached_value("Company", pr.company, "cost_center") cost_center = args.cost_center or company_cost_center for item in get_items(warehouse=args.warehouse, cost_center=cost_center): @@ -1554,33 +1534,44 @@ def make_purchase_receipt(**args): pr.submit() return pr + def create_subcontracted_item(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom args = frappe._dict(args) - if not frappe.db.exists('Item', args.item_code): - make_item(args.item_code, { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'has_batch_no': args.get("has_batch_no") or 0 - }) + if not frappe.db.exists("Item", args.item_code): + make_item( + args.item_code, + { + "is_stock_item": 1, + "is_sub_contracted_item": 1, + "has_batch_no": args.get("has_batch_no") or 0, + }, + ) if not args.raw_materials: - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item 1"): + make_item( + "Test Extra Item 1", + { + "is_stock_item": 1, + }, + ) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item 2"): + make_item( + "Test Extra Item 2", + { + "is_stock_item": 1, + }, + ) - args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] + + if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): + make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) - if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): - make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) test_dependencies = ["BOM", "Item Price", "Location"] -test_records = frappe.get_test_records('Purchase Receipt') +test_records = frappe.get_test_records("Purchase Receipt") diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index e5994b2dd48..aa217441c0b 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -96,7 +96,6 @@ "include_exploded_items", "batch_no", "rejected_serial_no", - "expense_account", "item_tax_rate", "item_weight_details", "weight_per_unit", @@ -107,6 +106,10 @@ "manufacturer", "column_break_16", "manufacturer_part_no", + "accounting_details_section", + "expense_account", + "column_break_102", + "provisional_expense_account", "accounting_dimensions_section", "project", "dimension_col_break", @@ -971,12 +974,27 @@ "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "fieldname": "provisional_expense_account", + "fieldtype": "Link", + "label": "Provisional Expense Account", + "options": "Account" + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_102", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-02-01 11:32:27.980524", + "modified": "2022-04-11 13:07:32.061402", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 4e472a92dc1..623fbde2b0b 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -24,11 +24,16 @@ class PutawayRule(Document): self.set_stock_capacity() def validate_duplicate_rule(self): - existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) + existing_rule = frappe.db.exists( + "Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse} + ) if existing_rule and existing_rule != self.name: - frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.") - .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)), - title=_("Duplicate")) + frappe.throw( + _("Putaway Rule already exists for Item {0} in Warehouse {1}.").format( + frappe.bold(self.item_code), frappe.bold(self.warehouse) + ), + title=_("Duplicate"), + ) def validate_priority(self): if self.priority < 1: @@ -37,18 +42,24 @@ class PutawayRule(Document): def validate_warehouse_and_company(self): company = frappe.db.get_value("Warehouse", self.warehouse, "company") if company != self.company: - frappe.throw(_("Warehouse {0} does not belong to Company {1}.") - .format(frappe.bold(self.warehouse), frappe.bold(self.company)), - title=_("Invalid Warehouse")) + frappe.throw( + _("Warehouse {0} does not belong to Company {1}.").format( + frappe.bold(self.warehouse), frappe.bold(self.company) + ), + title=_("Invalid Warehouse"), + ) def validate_capacity(self): stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) if flt(self.stock_capacity) < flt(balance_qty): - frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") - .format(self.item_code, frappe.bold(balance_qty), stock_uom), - title=_("Insufficient Capacity")) + frappe.throw( + _( + "Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}." + ).format(self.item_code, frappe.bold(balance_qty), stock_uom), + title=_("Insufficient Capacity"), + ) if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) @@ -56,23 +67,26 @@ class PutawayRule(Document): def set_stock_capacity(self): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) + @frappe.whitelist() def get_available_putaway_capacity(rule): - stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, - ["stock_capacity", "item_code", "warehouse"]) + stock_capacity, item_code, warehouse = frappe.db.get_value( + "Putaway Rule", rule, ["stock_capacity", "item_code", "warehouse"] + ) balance_qty = get_stock_balance(item_code, warehouse, nowdate()) free_space = flt(stock_capacity) - flt(balance_qty) return free_space if free_space > 0 else 0 + @frappe.whitelist() def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): - """ Applies Putaway Rule on line items. + """Applies Putaway Rule on line items. - items: List of Purchase Receipt/Stock Entry Items - company: Company in the Purchase Receipt/Stock Entry - doctype: Doctype to apply rule on - purpose: Purpose of Stock Entry - sync (optional): Sync with client side only for client side calls + items: List of Purchase Receipt/Stock Entry Items + company: Company in the Purchase Receipt/Stock Entry + doctype: Doctype to apply rule on + purpose: Purpose of Stock Entry + sync (optional): Sync with client side only for client side calls """ if isinstance(items, str): items = json.loads(items) @@ -89,16 +103,18 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): item.conversion_factor = flt(item.conversion_factor) or 1.0 pending_qty, item_code = flt(item.qty), item.item_code pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) - uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + uom_must_be_whole_number = frappe.db.get_value("UOM", item.uom, "must_be_whole_number") if not pending_qty or not item_code: updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) continue - at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) + at_capacity, rules = get_ordered_putaway_rules( + item_code, company, source_warehouse=source_warehouse + ) if not rules: - warehouse = source_warehouse or item.get('warehouse') + warehouse = source_warehouse or item.get("warehouse") if at_capacity: # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) @@ -117,23 +133,28 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): for rule in item_wise_rules[key]: if pending_stock_qty > 0 and rule.free_space: - stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + stock_qty_to_allocate = ( + flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + ) qty_to_allocate = stock_qty_to_allocate / item.conversion_factor if uom_must_be_whole_number: qty_to_allocate = floor(qty_to_allocate) stock_qty_to_allocate = qty_to_allocate * item.conversion_factor - if not qty_to_allocate: break + if not qty_to_allocate: + break - updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, - rule.name, serial_nos=serial_nos) + updated_table = add_row( + item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos + ) pending_stock_qty -= stock_qty_to_allocate pending_qty -= qty_to_allocate rule["free_space"] -= stock_qty_to_allocate - if not pending_stock_qty > 0: break + if not pending_stock_qty > 0: + break # if pending qty after applying all rules, add row without warehouse if pending_stock_qty > 0: @@ -146,13 +167,14 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): items[:] = updated_table frappe.msgprint(_("Applied putaway rules."), alert=True) - if sync and json.loads(sync): # sync with client side + if sync and json.loads(sync): # sync with client side return items -def _items_changed(old, new, doctype: str) -> bool: - """ Check if any items changed by application of putaway rules. - If not, changing item table can have side effects since `name` items also changes. +def _items_changed(old, new, doctype: str) -> bool: + """Check if any items changed by application of putaway rules. + + If not, changing item table can have side effects since `name` items also changes. """ if len(old) != len(new): return True @@ -161,13 +183,22 @@ def _items_changed(old, new, doctype: str) -> bool: if doctype == "Stock Entry": compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no") - sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa - flt(item.transfer_qty), cstr(item.serial_no)) + sort_key = lambda item: ( # noqa + item.item_code, + cstr(item.t_warehouse), + flt(item.transfer_qty), + cstr(item.serial_no), + ) else: # purchase receipt / invoice compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no") - sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa - flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no)) + sort_key = lambda item: ( # noqa + item.item_code, + cstr(item.warehouse), + flt(item.stock_qty), + flt(item.received_qty), + cstr(item.serial_no), + ) old_sorted = sorted(old, key=sort_key) new_sorted = sorted(new, key=sort_key) @@ -182,18 +213,16 @@ def _items_changed(old, new, doctype: str) -> bool: def get_ordered_putaway_rules(item_code, company, source_warehouse=None): """Returns an ordered list of putaway rules to apply on an item.""" - filters = { - "item_code": item_code, - "company": company, - "disable": 0 - } + filters = {"item_code": item_code, "company": company, "disable": 0} if source_warehouse: filters.update({"warehouse": ["!=", source_warehouse]}) - rules = frappe.get_all("Putaway Rule", + rules = frappe.get_all( + "Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters=filters, - order_by="priority asc, capacity desc") + order_by="priority asc, capacity desc", + ) if not rules: return False, None @@ -211,10 +240,11 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None): # then there is not enough space left in any rule return True, None - vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space'])) + vacant_rules = sorted(vacant_rules, key=lambda i: (i["priority"], -i["free_space"])) return False, vacant_rules + def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): new_updated_table_row = copy.deepcopy(item) new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 @@ -223,7 +253,9 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N if item.doctype == "Stock Entry Detail": new_updated_table_row.t_warehouse = warehouse - new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + new_updated_table_row.transfer_qty = flt(to_allocate) * flt( + new_updated_table_row.conversion_factor + ) else: new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse @@ -238,6 +270,7 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N updated_table.append(new_updated_table_row) return updated_table + def show_unassigned_items_message(items_not_accomodated): msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " formatted_item_rows = "" @@ -247,7 +280,9 @@ def show_unassigned_items_message(items_not_accomodated): formatted_item_rows += """ {0} {1} - """.format(item_link, frappe.bold(entry[1])) + """.format( + item_link, frappe.bold(entry[1]) + ) msg += """ @@ -257,13 +292,17 @@ def show_unassigned_items_message(items_not_accomodated): {2}
    - """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + """.format( + _("Item"), _("Unassigned Qty"), formatted_item_rows + ) frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + def get_serial_nos_to_allocate(serial_nos, to_allocate): if serial_nos: - allocated_serial_nos = serial_nos[0: cint(to_allocate)] - serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list + allocated_serial_nos = serial_nos[0 : cint(to_allocate)] + serial_nos[:] = serial_nos[cint(to_allocate) :] # pop out allocated serial nos and modify list return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" - else: return "" + else: + return "" diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index ff1c19a8275..82c32c378ce 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.item.test_item import make_item @@ -9,18 +10,14 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.get_item_details import get_conversion_factor -from erpnext.tests.utils import ERPNextTestCase -class TestPutawayRule(ERPNextTestCase): +class TestPutawayRule(FrappeTestCase): def setUp(self): if not frappe.db.exists("Item", "_Rice"): - make_item("_Rice", { - 'is_stock_item': 1, - 'has_batch_no' : 1, - 'create_new_batch': 1, - 'stock_uom': 'Kg' - }) + make_item( + "_Rice", {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "stock_uom": "Kg"} + ) if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}): create_warehouse("Rack 1") @@ -36,10 +33,10 @@ class TestPutawayRule(ERPNextTestCase): new_uom.save() def assertUnchangedItemsOnResave(self, doc): - """ Check if same items remain even after reapplication of rules. + """Check if same items remain even after reapplication of rules. - This is required since some business logic like subcontracting - depends on `name` of items to be same if item isn't changed. + This is required since some business logic like subcontracting + depends on `name` of items to be same if item isn't changed. """ doc.reload() old_items = {d.name for d in doc.items} @@ -49,13 +46,14 @@ class TestPutawayRule(ERPNextTestCase): def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=300, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=300, uom="Kg", priority=2 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, self.warehouse_1) @@ -71,16 +69,19 @@ class TestPutawayRule(ERPNextTestCase): def test_putaway_rules_with_same_priority(self): """Test if rule with more free space is applied, among two rules with same priority and capacity.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg" + ) # out of 500 kg capacity, occupy 100 kg in warehouse_1 - stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50) + stock_receipt = make_stock_entry( + item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 500) # warehouse_2 has 500 kg free space, it is given priority @@ -96,13 +97,14 @@ class TestPutawayRule(ERPNextTestCase): def test_putaway_rules_with_insufficient_capacity(self): """Test if qty exceeding capacity, is handled.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg" + ) - pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, self.warehouse_2) @@ -118,24 +120,32 @@ class TestPutawayRule(ERPNextTestCase): """Test rules applied on uom other than stock uom.""" item = frappe.get_doc("Item", "_Rice") if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): - item.append("uoms", { - "uom": "Bag", - "conversion_factor": 1000 - }) + item.append("uoms", {"uom": "Bag", "conversion_factor": 1000}) item.save() - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, - uom="Bag") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag" + ) self.assertEqual(rule_1.stock_capacity, 3000) - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, - uom="Bag") + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag" + ) self.assertEqual(rule_2.stock_capacity, 4000) # populate 'Rack 1' with 1 Bag, making the free space 2 Bags - stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50) + stock_receipt = make_stock_entry( + item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", - conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + pr = make_purchase_receipt( + item_code="_Rice", + qty=6, + uom="Bag", + stock_uom="Kg", + conversion_factor=1000, + apply_putaway_rule=1, + do_not_submit=1, + ) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 4) self.assertEqual(pr.items[0].warehouse, self.warehouse_2) @@ -151,25 +161,30 @@ class TestPutawayRule(ERPNextTestCase): """Test if whole UOMs are handled.""" item = frappe.get_doc("Item", "_Rice") if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): - item.append("uoms", { - "uom": "Bag", - "conversion_factor": 1000 - }) + item.append("uoms", {"uom": "Bag", "conversion_factor": 1000}) item.save() frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) # Putaway Rule in different UOM - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, - uom="Bag") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag" + ) self.assertEqual(rule_1.stock_capacity, 1000) # Putaway Rule in Stock UOM rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500) self.assertEqual(rule_2.stock_capacity, 500) # total capacity is 1500 Kg - pr = make_purchase_receipt(item_code="_Rice", qty=2, uom="Bag", stock_uom="Kg", - conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + pr = make_purchase_receipt( + item_code="_Rice", + qty=2, + uom="Bag", + stock_uom="Kg", + conversion_factor=1000, + apply_putaway_rule=1, + do_not_submit=1, + ) self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 1) self.assertEqual(pr.items[0].warehouse, self.warehouse_1) @@ -184,23 +199,26 @@ class TestPutawayRule(ERPNextTestCase): def test_putaway_rules_with_reoccurring_item(self): """Test rules on same item entered multiple times with different rate.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # total capacity is 200 Kg - pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, - do_not_submit=1) - pr.append("items", { - "item_code": "_Rice", - "warehouse": "_Test Warehouse - _TC", - "qty": 200, - "uom": "Kg", - "stock_uom": "Kg", - "stock_qty": 200, - "received_qty": 200, - "rate": 100, - "conversion_factor": 1.0, - }) # same item entered again in PR but with different rate + pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, do_not_submit=1) + pr.append( + "items", + { + "item_code": "_Rice", + "warehouse": "_Test Warehouse - _TC", + "qty": 200, + "uom": "Kg", + "stock_uom": "Kg", + "stock_qty": 200, + "received_qty": 200, + "rate": 100, + "conversion_factor": 1.0, + }, + ) # same item entered again in PR but with different rate pr.save() self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 100) @@ -208,7 +226,7 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(pr.items[0].putaway_rule, rule_1.name) # same rule applied to second item row # with previous assignment considered - self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 + self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 self.assertEqual(pr.items[1].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].putaway_rule, rule_1.name) @@ -219,13 +237,13 @@ class TestPutawayRule(ERPNextTestCase): def test_validate_over_receipt_in_warehouse(self): """Test if overreceipt is blocked in the presence of putaway rules.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) - pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 1) - self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg + self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[0].putaway_rule, rule_1.name) @@ -240,21 +258,29 @@ class TestPutawayRule(ERPNextTestCase): def test_putaway_rule_on_stock_entry_material_transfer(self): """Test if source warehouse is considered while applying rules.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") # higher priority - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # higher priority + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg", priority=2 + ) - stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_1, qty=200, - target="_Test Warehouse - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + source=self.warehouse_1, + qty=200, + target="_Test Warehouse - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_submit=1, + ) stock_entry_item = stock_entry.get("items")[0] # since source warehouse is Rack 1, rule 1 (for Rack 1) will be avoided # even though it has more free space and higher priority self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_2) - self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg + self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) self.assertUnchangedItemsOnResave(stock_entry) @@ -265,37 +291,48 @@ class TestPutawayRule(ERPNextTestCase): def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self): """Test if reoccuring item is correctly considered.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=600, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=300, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=600, uom="Kg", priority=2 + ) # create SE with first row having source warehouse as Rack 2 - stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_2, qty=200, - target="_Test Warehouse - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + source=self.warehouse_2, + qty=200, + target="_Test Warehouse - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_submit=1, + ) # Add rows with source warehouse as Rack 1 - stock_entry.extend("items", [ - { - "item_code": "_Rice", - "s_warehouse": self.warehouse_1, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 100, - "basic_rate": 50, - "conversion_factor": 1.0, - "transfer_qty": 100 - }, - { - "item_code": "_Rice", - "s_warehouse": self.warehouse_1, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 200, - "basic_rate": 60, - "conversion_factor": 1.0, - "transfer_qty": 200 - } - ]) + stock_entry.extend( + "items", + [ + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 100, + "basic_rate": 50, + "conversion_factor": 1.0, + "transfer_qty": 100, + }, + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 200, + "basic_rate": 60, + "conversion_factor": 1.0, + "transfer_qty": 200, + }, + ], + ) stock_entry.save() @@ -323,19 +360,24 @@ class TestPutawayRule(ERPNextTestCase): def test_putaway_rule_on_stock_entry_material_transfer_batch_serial_item(self): """Test if batch and serial items are split correctly.""" if not frappe.db.exists("Item", "Water Bottle"): - make_item("Water Bottle", { - "is_stock_item": 1, - "has_batch_no" : 1, - "create_new_batch": 1, - "has_serial_no": 1, - "serial_no_series": "BOTTL-.####", - "stock_uom": "Nos" - }) + make_item( + "Water Bottle", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "BOTTL-.####", + "stock_uom": "Nos", + }, + ) - rule_1 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, - uom="Nos") - rule_2 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, - uom="Nos") + rule_1 = create_putaway_rule( + item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, uom="Nos" + ) + rule_2 = create_putaway_rule( + item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, uom="Nos" + ) make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle") @@ -344,12 +386,20 @@ class TestPutawayRule(ERPNextTestCase): pr.save() pr.submit() - serial_nos = frappe.get_list("Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}) + serial_nos = frappe.get_list( + "Serial No", filters={"purchase_document_no": pr.name, "status": "Active"} + ) serial_nos = [d.name for d in serial_nos] - stock_entry = make_stock_entry(item_code="Water Bottle", source="_Test Warehouse - _TC", qty=5, - target="Finished Goods - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_save=1) + stock_entry = make_stock_entry( + item_code="Water Bottle", + source="_Test Warehouse - _TC", + qty=5, + target="Finished Goods - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_save=1, + ) stock_entry.items[0].batch_no = "BOTTL-BATCH-1" stock_entry.items[0].serial_no = "\n".join(serial_nos) stock_entry.save() @@ -375,14 +425,21 @@ class TestPutawayRule(ERPNextTestCase): def test_putaway_rule_on_stock_entry_material_receipt(self): """Test if rules are applied in Stock Entry of type Receipt.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") # more capacity - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # more capacity + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg" + ) - stock_entry = make_stock_entry(item_code="_Rice", qty=100, - target="_Test Warehouse - _TC", purpose="Material Receipt", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + qty=100, + target="_Test Warehouse - _TC", + purpose="Material Receipt", + apply_putaway_rule=1, + do_not_submit=1, + ) stock_entry_item = stock_entry.get("items")[0] @@ -396,6 +453,7 @@ class TestPutawayRule(ERPNextTestCase): rule_1.delete() rule_2.delete() + def create_putaway_rule(**args): args = frappe._dict(args) putaway = frappe.new_doc("Putaway Rule") @@ -408,7 +466,9 @@ def create_putaway_rule(**args): putaway.capacity = args.capacity or 1 putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") putaway.uom = args.uom or putaway.stock_uom - putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor'] + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)[ + "conversion_factor" + ] if not args.do_not_save: putaway.save() diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 4e3b80aa761..331d3e812b2 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -18,8 +18,8 @@ class QualityInspection(Document): if not self.readings and self.item_code: self.get_item_specification_details() - if self.inspection_type=="In Process" and self.reference_type=="Job Card": - item_qi_template = frappe.db.get_value("Item", self.item_code, 'quality_inspection_template') + if self.inspection_type == "In Process" and self.reference_type == "Job Card": + item_qi_template = frappe.db.get_value("Item", self.item_code, "quality_inspection_template") parameters = get_template_details(item_qi_template) for reading in self.readings: for d in parameters: @@ -33,26 +33,28 @@ class QualityInspection(Document): @frappe.whitelist() def get_item_specification_details(self): if not self.quality_inspection_template: - self.quality_inspection_template = frappe.db.get_value('Item', - self.item_code, 'quality_inspection_template') + self.quality_inspection_template = frappe.db.get_value( + "Item", self.item_code, "quality_inspection_template" + ) - if not self.quality_inspection_template: return + if not self.quality_inspection_template: + return - self.set('readings', []) + self.set("readings", []) parameters = get_template_details(self.quality_inspection_template) for d in parameters: - child = self.append('readings', {}) + child = self.append("readings", {}) child.update(d) child.status = "Accepted" @frappe.whitelist() def get_quality_inspection_template(self): - template = '' + template = "" if self.bom_no: - template = frappe.db.get_value('BOM', self.bom_no, 'quality_inspection_template') + template = frappe.db.get_value("BOM", self.bom_no, "quality_inspection_template") if not template: - template = frappe.db.get_value('BOM', self.item_code, 'quality_inspection_template') + template = frappe.db.get_value("BOM", self.item_code, "quality_inspection_template") self.quality_inspection_template = template self.get_item_specification_details() @@ -66,21 +68,25 @@ class QualityInspection(Document): def update_qc_reference(self): quality_inspection = self.name if self.docstatus == 1 else "" - if self.reference_type == 'Job Card': + if self.reference_type == "Job Card": if self.reference_name: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{doctype}` SET quality_inspection = %s, modified = %s WHERE name = %s and production_item = %s - """.format(doctype=self.reference_type), - (quality_inspection, self.modified, self.reference_name, self.item_code)) + """.format( + doctype=self.reference_type + ), + (quality_inspection, self.modified, self.reference_name, self.item_code), + ) else: args = [quality_inspection, self.modified, self.reference_name, self.item_code] - doctype = self.reference_type + ' Item' + doctype = self.reference_type + " Item" - if self.reference_type == 'Stock Entry': - doctype = 'Stock Entry Detail' + if self.reference_type == "Stock Entry": + doctype = "Stock Entry Detail" if self.reference_type and self.reference_name: conditions = "" @@ -88,11 +94,12 @@ class QualityInspection(Document): conditions += " and t1.batch_no = %s" args.append(self.batch_no) - if self.docstatus == 2: # if cancel, then remove qi link wherever same name + if self.docstatus == 2: # if cancel, then remove qi link wherever same name conditions += " and t1.quality_inspection = %s" args.append(self.name) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 SET @@ -102,12 +109,15 @@ class QualityInspection(Document): and t1.item_code = %s and t1.parent = t2.name {conditions} - """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions), - args) + """.format( + parent_doc=self.reference_type, child_doc=doctype, conditions=conditions + ), + args, + ) def inspect_and_set_status(self): for reading in self.readings: - if not reading.manual_inspection: # dont auto set status if manual + if not reading.manual_inspection: # dont auto set status if manual if reading.formula_based_criteria: self.set_status_based_on_acceptance_formula(reading) else: @@ -129,13 +139,16 @@ class QualityInspection(Document): reading_value = reading.get("reading_" + str(i)) if reading_value is not None and reading_value.strip(): result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value")) - if not result: return False + if not result: + return False return True def set_status_based_on_acceptance_formula(self, reading): if not reading.acceptance_formula: - frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx), - title=_("Missing Formula")) + frappe.throw( + _("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx), + title=_("Missing Formula"), + ) condition = reading.acceptance_formula data = self.get_formula_evaluation_data(reading) @@ -145,12 +158,17 @@ class QualityInspection(Document): reading.status = "Accepted" if result else "Rejected" except NameError as e: field = frappe.bold(e.args[0].split()[1]) - frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.") - .format(reading.idx, field), - title=_("Invalid Formula")) + frappe.throw( + _("Row #{0}: {1} is not a valid reading field. Please refer to the field description.").format( + reading.idx, field + ), + title=_("Invalid Formula"), + ) except Exception: - frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), - title=_("Invalid Formula")) + frappe.throw( + _("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), + title=_("Invalid Formula"), + ) def get_formula_evaluation_data(self, reading): data = {} @@ -168,6 +186,7 @@ class QualityInspection(Document): def calculate_mean(self, reading): """Calculate mean of all non-empty readings.""" from statistics import mean + readings_list = [] for i in range(1, 11): @@ -178,65 +197,90 @@ class QualityInspection(Document): actual_mean = mean(readings_list) if readings_list else 0 return actual_mean + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): if filters.get("from"): from frappe.desk.reportview import get_match_cond + mcond = get_match_cond(filters["from"]) cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')" if filters.get("parent"): - if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']\ - and filters.get("inspection_type") != "In Process": + if ( + filters.get("from") in ["Purchase Invoice Item", "Purchase Receipt Item"] + and filters.get("inspection_type") != "In Process" + ): cond = """and item_code in (select name from `tabItem` where inspection_required_before_purchase = 1)""" - elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']\ - and filters.get("inspection_type") != "In Process": + elif ( + filters.get("from") in ["Sales Invoice Item", "Delivery Note Item"] + and filters.get("inspection_type") != "In Process" + ): cond = """and item_code in (select name from `tabItem` where inspection_required_before_delivery = 1)""" - elif filters.get('from') == 'Stock Entry Detail': + elif filters.get("from") == "Stock Entry Detail": cond = """and s_warehouse is null""" - if filters.get('from') in ['Supplier Quotation Item']: + if filters.get("from") in ["Supplier Quotation Item"]: qi_condition = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT item_code FROM `tab{doc}` WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s {qi_condition} {cond} {mcond} ORDER BY item_code limit {start}, {page_len} - """.format(doc=filters.get('from'), - cond = cond, mcond = mcond, start = start, - page_len = page_len, qi_condition = qi_condition), - {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) + """.format( + doc=filters.get("from"), + cond=cond, + mcond=mcond, + start=start, + page_len=page_len, + qi_condition=qi_condition, + ), + {"parent": filters.get("parent"), "txt": "%%%s%%" % txt}, + ) elif filters.get("reference_name"): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT production_item FROM `tab{doc}` WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s {qi_condition} {cond} {mcond} ORDER BY production_item LIMIT {start}, {page_len} - """.format(doc=filters.get("from"), - cond = cond, mcond = mcond, start = start, - page_len = page_len, qi_condition = qi_condition), - {'reference_name': filters.get('reference_name'), 'txt': "%%%s%%" % txt}) + """.format( + doc=filters.get("from"), + cond=cond, + mcond=mcond, + start=start, + page_len=page_len, + qi_condition=qi_condition, + ), + {"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt}, + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.get_all('Quality Inspection', + return frappe.get_all( + "Quality Inspection", limit_start=start, limit_page_length=page_len, - filters = { - 'docstatus': 1, - 'name': ('like', '%%%s%%' % txt), - 'item_code': filters.get("item_code"), - 'reference_name': ('in', [filters.get("reference_name", ''), '']) - }, as_list=1) + filters={ + "docstatus": 1, + "name": ("like", "%%%s%%" % txt), + "item_code": filters.get("item_code"), + "reference_name": ("in", [filters.get("reference_name", ""), ""]), + }, + as_list=1, + ) + @frappe.whitelist() def make_quality_inspection(source_name, target_doc=None): @@ -244,19 +288,18 @@ def make_quality_inspection(source_name, target_doc=None): doc.inspected_by = frappe.session.user doc.get_quality_inspection_template() - doc = get_mapped_doc("BOM", source_name, { - 'BOM': { - "doctype": "Quality Inspection", - "validation": { - "docstatus": ["=", 1] - }, - "field_map": { - "name": "bom_no", - "item": "item_code", - "stock_uom": "uom", - "stock_qty": "qty" - }, - } - }, target_doc, postprocess) + doc = get_mapped_doc( + "BOM", + source_name, + { + "BOM": { + "doctype": "Quality Inspection", + "validation": {"docstatus": ["=", 1]}, + "field_map": {"name": "bom_no", "item": "item_code", "stock_uom": "uom", "stock_qty": "qty"}, + } + }, + target_doc, + postprocess, + ) return doc diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 308c62875d5..144f13880b1 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from erpnext.controllers.stock_controller import ( @@ -13,25 +14,19 @@ from erpnext.controllers.stock_controller import ( 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.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase # test_records = frappe.get_test_records('Quality Inspection') -class TestQualityInspection(ERPNextTestCase): +class TestQualityInspection(FrappeTestCase): def setUp(self): super().setUp() 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 + 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) @@ -71,21 +66,18 @@ class TestQualityInspection(ERPNextTestCase): "specification": "Iron Content", # numeric reading "min_value": 0.1, "max_value": 0.9, - "reading_1": "0.4" + "reading_1": "0.4", }, { "specification": "Particle Inspection Needed", # non-numeric reading "numeric": 0, "value": "Yes", - "reading_value": "Yes" - } + "reading_value": "Yes", + }, ] qa = create_quality_inspection( - reference_type="Delivery Note", - reference_name=dn.name, - readings=readings, - do_not_save=True + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True ) qa.save() @@ -104,13 +96,13 @@ class TestQualityInspection(ERPNextTestCase): "specification": "Iron Content", # numeric reading "formula_based_criteria": 1, "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", - "reading_1": "0.4" + "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" + "reading_1": "0.7", }, { "specification": "Mg Content", # numeric reading @@ -118,22 +110,19 @@ class TestQualityInspection(ERPNextTestCase): "acceptance_formula": "mean < 0.9", "reading_1": "0.5", "reading_2": "0.7", - "reading_3": "random text" # check if random string input causes issues + "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" - } + "reading_value": "Grade B", + }, ] qa = create_quality_inspection( - reference_type="Delivery Note", - reference_name=dn.name, - readings=readings, - do_not_save=True + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True ) qa.save() @@ -167,32 +156,26 @@ class TestQualityInspection(ERPNextTestCase): qty=1, basic_rate=100, inspection_required=True, - do_not_submit=True + do_not_submit=True, ) readings = [ - { - "specification": "Iron Content", - "min_value": 0.1, - "max_value": 0.9, - "reading_1": "0.4" - } + {"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "0.4"} ] qa = create_quality_inspection( - reference_type="Stock Entry", - reference_name=se.name, - readings=readings, - status="Rejected" + reference_type="Stock Entry", reference_name=se.name, readings=readings, status="Rejected" ) frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") se.reload() - self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI + self.assertRaises( + QualityInspectionRejectedError, se.submit + ) # when blocked in Stock settings, block rejected QI frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn") se.reload() - se.submit() # when allowed in Stock settings, allow rejected QI + se.submit() # when allowed in Stock settings, allow rejected QI # teardown qa.reload() @@ -201,6 +184,7 @@ class TestQualityInspection(ERPNextTestCase): se.cancel() frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + def create_quality_inspection(**args): args = frappe._dict(args) qa = frappe.new_doc("Quality Inspection") @@ -238,8 +222,6 @@ def create_quality_inspection(**args): 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() + frappe.get_doc( + {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} + ).insert() diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py index 7f8c871a93d..9b8f5d6378c 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -9,11 +9,22 @@ from frappe.model.document import Document class QualityInspectionTemplate(Document): pass -def get_template_details(template): - if not template: return [] - return frappe.get_all('Item Quality Inspection Parameter', - fields=["specification", "value", "acceptance_formula", - "numeric", "formula_based_criteria", "min_value", "max_value"], - filters={'parenttype': 'Quality Inspection Template', 'parent': template}, - order_by="idx") +def get_template_details(template): + if not template: + return [] + + return frappe.get_all( + "Item Quality Inspection Parameter", + fields=[ + "specification", + "value", + "acceptance_formula", + "numeric", + "formula_based_criteria", + "min_value", + "max_value", + ], + filters={"parenttype": "Quality Inspection Template", "parent": template}, + order_by="idx", + ) diff --git a/erpnext/stock/doctype/quality_inspection_template/test_records.json b/erpnext/stock/doctype/quality_inspection_template/test_records.json new file mode 100644 index 00000000000..980f49a80aa --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_template/test_records.json @@ -0,0 +1,13 @@ +[ + { + "quality_inspection_template_name" : "_Test Quality Inspection Template", + "doctype": "Quality Inspection Template", + "item_quality_inspection_parameter" : [ + { + "specification": "_Test Param", + "doctype": "Item Quality Inspection Parameter", + "parentfield": "item_quality_inspection_parameter" + } + ] + } +] diff --git a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py index 7a0f5d08210..846be0b9bdc 100644 --- a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py +++ b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py @@ -12,24 +12,25 @@ from erpnext.stock.utils import get_stock_balance, get_stock_value_on class QuickStockBalance(Document): pass + @frappe.whitelist() def get_stock_item_details(warehouse, date, item=None, barcode=None): out = {} if barcode: out["item"] = frappe.db.get_value( - "Item Barcode", filters={"barcode": barcode}, fieldname=["parent"]) + "Item Barcode", filters={"barcode": barcode}, fieldname=["parent"] + ) if not out["item"]: - frappe.throw( - _("Invalid Barcode. There is no Item attached to this barcode.")) + frappe.throw(_("Invalid Barcode. There is no Item attached to this barcode.")) else: out["item"] = item - barcodes = frappe.db.get_values("Item Barcode", filters={"parent": out["item"]}, - fieldname=["barcode"]) + barcodes = frappe.db.get_values( + "Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"] + ) out["barcodes"] = [x[0] for x in barcodes] out["qty"] = get_stock_balance(out["item"], warehouse, date) out["value"] = get_stock_value_on(warehouse, date, out["item"]) - out["image"] = frappe.db.get_value("Item", - filters={"name": out["item"]}, fieldname=["image"]) + out["image"] = frappe.db.get_value("Item", filters={"name": out["item"]}, fieldname=["image"]) return out diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 0ba97d59a14..8c13149252a 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -23,6 +23,7 @@ "error_section", "error_log", "items_to_be_repost", + "affected_transactions", "distinct_item_and_warehouse", "current_index" ], @@ -172,12 +173,20 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "affected_transactions", + "fieldtype": "Code", + "hidden": 1, + "label": "Affected Transactions", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-01-18 10:57:33.450907", + "modified": "2022-04-18 14:08:08.821602", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", @@ -229,4 +238,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index c6baa46c5eb..b788fd1286b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -4,16 +4,16 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime, today +from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime from frappe.utils.user import get_users_with_role -from rq.timeouts import JobTimeoutException import erpnext -from erpnext.accounts.utils import ( - check_if_stock_and_account_balance_synced, - update_gl_entries_after, +from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers +from erpnext.stock.stock_ledger import ( + get_affected_transactions, + get_items_to_be_repost, + repost_future_sle, ) -from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle class RepostItemValuation(Document): @@ -23,7 +23,7 @@ class RepostItemValuation(Document): self.set_company() def reset_field_values(self): - if self.based_on == 'Transaction': + if self.based_on == "Transaction": self.item_code = None self.warehouse = None @@ -38,29 +38,55 @@ class RepostItemValuation(Document): def set_status(self, status=None, write=True): status = status or self.status if not status: - self.status = 'Queued' + self.status = "Queued" else: self.status = status if write: - self.db_set('status', self.status) + self.db_set("status", self.status) def on_submit(self): - if not frappe.flags.in_test or self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts: + """During tests reposts are executed immediately. + + Exceptions: + 1. "Repost Item Valuation" document has self.flags.dont_run_in_test + 2. global flag frappe.flags.dont_execute_stock_reposts is set + + These flags are useful for asserting real time behaviour like quantity updates. + """ + + if not frappe.flags.in_test: + return + if self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts: return - frappe.enqueue(repost, timeout=1800, queue='long', - job_name='repost_sle', now=frappe.flags.in_test, doc=self) + repost(self) + + def before_cancel(self): + self.check_pending_repost_against_cancelled_transaction() + + def check_pending_repost_against_cancelled_transaction(self): + if self.status not in ("Queued", "In Progress"): + return + + if not (self.voucher_no and self.voucher_no): + return + + transaction_status = frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") + if transaction_status == 2: + msg = _("Cannot cancel as processing of cancelled documents is pending.") + msg += "
    " + _("Please try again in an hour.") + frappe.throw(msg, title=_("Pending processing")) @frappe.whitelist() def restart_reposting(self): - self.set_status('Queued', write=False) + self.set_status("Queued", write=False) self.current_index = 0 self.distinct_item_and_warehouse = None self.items_to_be_repost = None self.db_update() def deduplicate_similar_repost(self): - """ Deduplicate similar reposts based on item-warehouse-posting combination.""" + """Deduplicate similar reposts based on item-warehouse-posting combination.""" if self.based_on != "Item and Warehouse": return @@ -72,7 +98,8 @@ class RepostItemValuation(Document): "posting_time": self.posting_time, } - frappe.db.sql(""" + frappe.db.sql( + """ update `tabRepost Item Valuation` set status = 'Skipped' WHERE item_code = %(item_code)s @@ -83,9 +110,10 @@ class RepostItemValuation(Document): and status = 'Queued' and based_on = 'Item and Warehouse' """, - filters + filters, ) + def on_doctype_update(): frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") @@ -95,63 +123,105 @@ def repost(doc): if not frappe.db.exists("Repost Item Valuation", doc.name): return - doc.set_status('In Progress') + doc.set_status("In Progress") if not frappe.flags.in_test: frappe.db.commit() repost_sl_entries(doc) repost_gl_entries(doc) - doc.set_status('Completed') + doc.set_status("Completed") - except (Exception, JobTimeoutException): + except Exception: frappe.db.rollback() traceback = frappe.get_traceback() frappe.log_error(traceback) - message = frappe.message_log.pop() + message = frappe.message_log.pop() if frappe.message_log else "" if traceback: message += "
    " + "Traceback:
    " + traceback - frappe.db.set_value(doc.doctype, doc.name, 'error_log', message) + frappe.db.set_value(doc.doctype, doc.name, "error_log", message) notify_error_to_stock_managers(doc, message) - doc.set_status('Failed') + doc.set_status("Failed") raise finally: - frappe.db.commit() + if not frappe.flags.in_test: + frappe.db.commit() + def repost_sl_entries(doc): - if doc.based_on == 'Transaction': - repost_future_sle(doc=doc, voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, - allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) + if doc.based_on == "Transaction": + repost_future_sle( + doc=doc, + voucher_type=doc.voucher_type, + voucher_no=doc.voucher_no, + allow_negative_stock=doc.allow_negative_stock, + via_landed_cost_voucher=doc.via_landed_cost_voucher, + ) else: - repost_future_sle(args=[frappe._dict({ - "item_code": doc.item_code, - "warehouse": doc.warehouse, - "posting_date": doc.posting_date, - "posting_time": doc.posting_time - })], allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) + repost_future_sle( + args=[ + frappe._dict( + { + "item_code": doc.item_code, + "warehouse": doc.warehouse, + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + } + ) + ], + allow_negative_stock=doc.allow_negative_stock, + via_landed_cost_voucher=doc.via_landed_cost_voucher, + doc=doc, + ) + def repost_gl_entries(doc): if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)): return - if doc.based_on == 'Transaction': + # directly modified transactions + directly_dependent_transactions = _get_directly_dependent_vouchers(doc) + repost_affected_transaction = get_affected_transactions(doc) + repost_gle_for_stock_vouchers( + directly_dependent_transactions + list(repost_affected_transaction), + doc.posting_date, + doc.company, + ) + + +def _get_directly_dependent_vouchers(doc): + """Get stock vouchers that are directly affected by reposting + i.e. any one item-warehouse is present in the stock transaction""" + + items = set() + warehouses = set() + + if doc.based_on == "Transaction": ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) doc_items, doc_warehouses = ref_doc.get_items_and_warehouses() + items.update(doc_items) + warehouses.update(doc_warehouses) sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no) - sle_items = [sle.item_code for sle in sles] - sle_warehouse = [sle.warehouse for sle in sles] - - items = list(set(doc_items).union(set(sle_items))) - warehouses = list(set(doc_warehouses).union(set(sle_warehouse))) + sle_items = {sle.item_code for sle in sles} + sle_warehouses = {sle.warehouse for sle in sles} + items.update(sle_items) + warehouses.update(sle_warehouses) else: - items = [doc.item_code] - warehouses = [doc.warehouse] + items.add(doc.item_code) + warehouses.add(doc.warehouse) + + affected_vouchers = get_future_stock_vouchers( + posting_date=doc.posting_date, + posting_time=doc.posting_time, + for_warehouses=list(warehouses), + for_items=list(items), + company=doc.company, + ) + return affected_vouchers - update_gl_entries_after(doc.posting_date, doc.posting_time, - for_warehouses=warehouses, for_items=items, company=doc.company) def notify_error_to_stock_managers(doc, traceback): recipients = get_users_with_role("Stock Manager") @@ -159,22 +229,33 @@ def notify_error_to_stock_managers(doc, traceback): get_users_with_role("System Manager") subject = _("Error while reposting item valuation") - message = (_("Hi,") + "
    " - + _("An error has been appeared while reposting item valuation via {0}") - .format(get_link_to_form(doc.doctype, doc.name)) + "
    " - + _("Please check the error message and take necessary actions to fix the error and then restart the reposting again.") + message = ( + _("Hi,") + + "
    " + + _("An error has been appeared while reposting item valuation via {0}").format( + get_link_to_form(doc.doctype, doc.name) + ) + + "
    " + + _( + "Please check the error message and take necessary actions to fix the error and then restart the reposting again." + ) ) frappe.sendmail(recipients=recipients, subject=subject, message=message) + def repost_entries(): + """ + Reposts 'Repost Item Valuation' entries in queue. + Called hourly via hooks.py. + """ if not in_configured_timeslot(): return riv_entries = get_repost_item_valuation_entries() for row in riv_entries: - doc = frappe.get_doc('Repost Item Valuation', row.name) - if doc.status in ('Queued', 'In Progress'): + doc = frappe.get_doc("Repost Item Valuation", row.name) + if doc.status in ("Queued", "In Progress"): repost(doc) doc.deduplicate_similar_repost() @@ -182,14 +263,16 @@ def repost_entries(): if riv_entries: return - for d in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): - check_if_stock_and_account_balance_synced(today(), d.name) def get_repost_item_valuation_entries(): - return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` + return frappe.db.sql( + """ SELECT name from `tabRepost Item Valuation` WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - """, now(), as_dict=1) + """, + now(), + as_dict=1, + ) def in_configured_timeslot(repost_settings=None, current_time=None): diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 78b432d564c..3184f69aa45 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -1,20 +1,25 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from erpnext.controllers.stock_controller import create_item_wise_repost_entries +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( in_configured_timeslot, ) +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.utils import PendingRepostingError -class TestRepostItemValuation(unittest.TestCase): +class TestRepostItemValuation(FrappeTestCase): + def tearDown(self): + frappe.flags.dont_execute_stock_reposts = False + def test_repost_time_slot(self): repost_settings = frappe.get_doc("Stock Reposting Settings") @@ -153,7 +158,7 @@ class TestRepostItemValuation(unittest.TestCase): posting_date=today, posting_time="00:01:00", ) - riv.flags.dont_run_in_test = True # keep it queued + riv.flags.dont_run_in_test = True # keep it queued riv.submit() stock_settings = frappe.get_doc("Stock Settings") @@ -162,3 +167,29 @@ class TestRepostItemValuation(unittest.TestCase): self.assertRaises(PendingRepostingError, stock_settings.save) riv.set_status("Skipped") + + def test_prevention_of_cancelled_transaction_riv(self): + frappe.flags.dont_execute_stock_reposts = True + + item = make_item() + warehouse = "_Test Warehouse - _TC" + old = make_stock_entry(item_code=item.name, to_warehouse=warehouse, qty=2, rate=5) + _new = make_stock_entry(item_code=item.name, to_warehouse=warehouse, qty=5, rate=10) + + old.cancel() + + riv = frappe.get_last_doc( + "Repost Item Valuation", {"voucher_type": old.doctype, "voucher_no": old.name} + ) + self.assertRaises(frappe.ValidationError, riv.cancel) + + riv.db_set("status", "Skipped") + riv.reload() + riv.cancel() # it should cancel now + + def test_queue_progress_serialization(self): + # Make sure set/tuple -> list behaviour is retained. + self.assertEqual( + [["a", "b"], ["c", "d"]], + sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))), + ) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index e300d46db83..cfa5cee453a 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -3,28 +3,66 @@ import json +from typing import List, Optional, Union import frappe from frappe import ValidationError, _ from frappe.model.naming import make_autoname -from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate -from six import string_types -from six.moves import map +from frappe.query_builder.functions import Coalesce +from frappe.utils import ( + add_days, + cint, + cstr, + flt, + get_link_to_form, + getdate, + nowdate, + safe_json_loads, +) from erpnext.controllers.stock_controller import StockController from erpnext.stock.get_item_details import get_reserved_qty_for_so -class SerialNoCannotCreateDirectError(ValidationError): pass -class SerialNoCannotCannotChangeError(ValidationError): pass -class SerialNoNotRequiredError(ValidationError): pass -class SerialNoRequiredError(ValidationError): pass -class SerialNoQtyError(ValidationError): pass -class SerialNoItemError(ValidationError): pass -class SerialNoWarehouseError(ValidationError): pass -class SerialNoBatchError(ValidationError): pass -class SerialNoNotExistsError(ValidationError): pass -class SerialNoDuplicateError(ValidationError): pass +class SerialNoCannotCreateDirectError(ValidationError): + pass + + +class SerialNoCannotCannotChangeError(ValidationError): + pass + + +class SerialNoNotRequiredError(ValidationError): + pass + + +class SerialNoRequiredError(ValidationError): + pass + + +class SerialNoQtyError(ValidationError): + pass + + +class SerialNoItemError(ValidationError): + pass + + +class SerialNoWarehouseError(ValidationError): + pass + + +class SerialNoBatchError(ValidationError): + pass + + +class SerialNoNotExistsError(ValidationError): + pass + + +class SerialNoDuplicateError(ValidationError): + pass + class SerialNo(StockController): def __init__(self, *args, **kwargs): @@ -33,7 +71,12 @@ class SerialNo(StockController): def validate(self): if self.get("__islocal") and self.warehouse and not self.via_stock_ledger: - frappe.throw(_("New Serial No cannot have Warehouse. Warehouse must be set by Stock Entry or Purchase Receipt"), SerialNoCannotCreateDirectError) + frappe.throw( + _( + "New Serial No cannot have Warehouse. Warehouse must be set by Stock Entry or Purchase Receipt" + ), + SerialNoCannotCreateDirectError, + ) self.set_maintenance_status() self.validate_warehouse() @@ -68,22 +111,21 @@ class SerialNo(StockController): def validate_warehouse(self): if not self.get("__islocal"): - item_code, warehouse = frappe.db.get_value("Serial No", - self.name, ["item_code", "warehouse"]) + item_code, warehouse = frappe.db.get_value("Serial No", self.name, ["item_code", "warehouse"]) if not self.via_stock_ledger and item_code != self.item_code: - frappe.throw(_("Item Code cannot be changed for Serial No."), - SerialNoCannotCannotChangeError) + frappe.throw(_("Item Code cannot be changed for Serial No."), SerialNoCannotCannotChangeError) if not self.via_stock_ledger and warehouse != self.warehouse: - frappe.throw(_("Warehouse cannot be changed for Serial No."), - SerialNoCannotCannotChangeError) + frappe.throw(_("Warehouse cannot be changed for Serial No."), SerialNoCannotCannotChangeError) def validate_item(self): """ - Validate whether serial no is required for this item + Validate whether serial no is required for this item """ item = frappe.get_cached_doc("Item", self.item_code) - if item.has_serial_no!=1: - frappe.throw(_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code)) + if item.has_serial_no != 1: + frappe.throw( + _("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code) + ) self.item_group = item.item_group self.description = item.description @@ -99,17 +141,24 @@ class SerialNo(StockController): self.purchase_time = purchase_sle.posting_time self.purchase_rate = purchase_sle.incoming_rate if purchase_sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): - self.supplier, self.supplier_name = \ - frappe.db.get_value(purchase_sle.voucher_type, purchase_sle.voucher_no, - ["supplier", "supplier_name"]) + self.supplier, self.supplier_name = frappe.db.get_value( + purchase_sle.voucher_type, purchase_sle.voucher_no, ["supplier", "supplier_name"] + ) # If sales return entry - if self.purchase_document_type == 'Delivery Note': + if self.purchase_document_type == "Delivery Note": self.sales_invoice = None else: - for fieldname in ("purchase_document_type", "purchase_document_no", - "purchase_date", "purchase_time", "purchase_rate", "supplier", "supplier_name"): - self.set(fieldname, None) + for fieldname in ( + "purchase_document_type", + "purchase_document_no", + "purchase_date", + "purchase_time", + "purchase_rate", + "supplier", + "supplier_name", + ): + self.set(fieldname, None) def set_sales_details(self, delivery_sle): if delivery_sle: @@ -117,18 +166,25 @@ class SerialNo(StockController): self.delivery_document_no = delivery_sle.voucher_no self.delivery_date = delivery_sle.posting_date self.delivery_time = delivery_sle.posting_time - if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"): - self.customer, self.customer_name = \ - frappe.db.get_value(delivery_sle.voucher_type, delivery_sle.voucher_no, - ["customer", "customer_name"]) + if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"): + self.customer, self.customer_name = frappe.db.get_value( + delivery_sle.voucher_type, delivery_sle.voucher_no, ["customer", "customer_name"] + ) if self.warranty_period: - self.warranty_expiry_date = add_days(cstr(delivery_sle.posting_date), - cint(self.warranty_period)) + self.warranty_expiry_date = add_days( + cstr(delivery_sle.posting_date), cint(self.warranty_period) + ) else: - for fieldname in ("delivery_document_type", "delivery_document_no", - "delivery_date", "delivery_time", "customer", "customer_name", - "warranty_expiry_date"): - self.set(fieldname, None) + for fieldname in ( + "delivery_document_type", + "delivery_document_no", + "delivery_date", + "delivery_time", + "customer", + "customer_name", + "warranty_expiry_date", + ): + self.set(fieldname, None) def get_last_sle(self, serial_no=None): entries = {} @@ -150,7 +206,8 @@ class SerialNo(StockController): if not serial_no: serial_no = self.name - for sle in frappe.db.sql(""" + for sle in frappe.db.sql( + """ SELECT voucher_type, voucher_no, posting_date, posting_time, incoming_rate, actual_qty, serial_no FROM @@ -166,25 +223,30 @@ class SerialNo(StockController): ORDER BY posting_date desc, posting_time desc, creation desc""", ( - self.item_code, self.company, + self.item_code, + self.company, serial_no, - serial_no+'\n%', - '%\n'+serial_no, - '%\n'+serial_no+'\n%' + serial_no + "\n%", + "%\n" + serial_no, + "%\n" + serial_no + "\n%", ), - as_dict=1): - if serial_no.upper() in get_serial_nos(sle.serial_no): - if cint(sle.actual_qty) > 0: - sle_dict.setdefault("incoming", []).append(sle) - else: - sle_dict.setdefault("outgoing", []).append(sle) + as_dict=1, + ): + if serial_no.upper() in get_serial_nos(sle.serial_no): + if cint(sle.actual_qty) > 0: + sle_dict.setdefault("incoming", []).append(sle) + else: + sle_dict.setdefault("outgoing", []).append(sle) return sle_dict def on_trash(self): - sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry` + sl_entries = frappe.db.sql( + """select serial_no from `tabStock Ledger Entry` where serial_no like %s and item_code=%s and is_cancelled=0""", - ("%%%s%%" % self.name, self.item_code), as_dict=True) + ("%%%s%%" % self.name, self.item_code), + as_dict=True, + ) # Find the exact match sle_exists = False @@ -194,7 +256,9 @@ class SerialNo(StockController): break if sle_exists: - frappe.throw(_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name)) + frappe.throw( + _("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name) + ) def before_rename(self, old, new, merge=False): if merge: @@ -202,16 +266,24 @@ class SerialNo(StockController): def after_rename(self, old, new, merge=False): """rename serial_no text fields""" - for dt in frappe.db.sql("""select parent from tabDocField - where fieldname='serial_no' and fieldtype in ('Text', 'Small Text', 'Long Text')"""): + for dt in frappe.db.sql( + """select parent from tabDocField + where fieldname='serial_no' and fieldtype in ('Text', 'Small Text', 'Long Text')""" + ): - for item in frappe.db.sql("""select name, serial_no from `tab%s` - where serial_no like %s""" % (dt[0], frappe.db.escape('%' + old + '%'))): + for item in frappe.db.sql( + """select name, serial_no from `tab%s` + where serial_no like %s""" + % (dt[0], frappe.db.escape("%" + old + "%")) + ): - serial_nos = map(lambda i: new if i.upper()==old.upper() else i, item[1].split('\n')) - frappe.db.sql("""update `tab%s` set serial_no = %s - where name=%s""" % (dt[0], '%s', '%s'), - ('\n'.join(list(serial_nos)), item[0])) + serial_nos = map(lambda i: new if i.upper() == old.upper() else i, item[1].split("\n")) + frappe.db.sql( + """update `tab%s` set serial_no = %s + where name=%s""" + % (dt[0], "%s", "%s"), + ("\n".join(list(serial_nos)), item[0]), + ) def update_serial_no_reference(self, serial_no=None): last_sle = self.get_last_sle(serial_no) @@ -220,57 +292,95 @@ class SerialNo(StockController): self.set_maintenance_status() self.set_status() + def process_serial_no(sle): item_det = get_item_details(sle.item_code) validate_serial_no(sle, item_det) update_serial_nos(sle, item_det) + def validate_serial_no(sle, item_det): serial_nos = get_serial_nos(sle.serial_no) if sle.serial_no else [] validate_material_transfer_entry(sle) - if item_det.has_serial_no==0: + if item_det.has_serial_no == 0: if serial_nos: - frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), - SerialNoNotRequiredError) + frappe.throw( + _("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), + SerialNoNotRequiredError, + ) elif not sle.is_cancelled: if serial_nos: if cint(sle.actual_qty) != flt(sle.actual_qty): - frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)) + frappe.throw( + _("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty) + ) if len(serial_nos) and len(serial_nos) != abs(cint(sle.actual_qty)): - frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(abs(sle.actual_qty), sle.item_code, len(serial_nos)), - SerialNoQtyError) + frappe.throw( + _("{0} Serial Numbers required for Item {1}. You have provided {2}.").format( + abs(sle.actual_qty), sle.item_code, len(serial_nos) + ), + SerialNoQtyError, + ) if len(serial_nos) != len(set(serial_nos)): - frappe.throw(_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError) + frappe.throw( + _("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError + ) for serial_no in serial_nos: if frappe.db.exists("Serial No", serial_no): - sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order", - "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_type", - "purchase_document_no", "company", "status"], as_dict=1) + sr = frappe.db.get_value( + "Serial No", + serial_no, + [ + "name", + "item_code", + "batch_no", + "sales_order", + "delivery_document_no", + "delivery_document_type", + "warehouse", + "purchase_document_type", + "purchase_document_no", + "company", + "status", + ], + as_dict=1, + ) - if sr.item_code!=sle.item_code: + if sr.item_code != sle.item_code: if not allow_serial_nos_with_different_item(serial_no, sle): - frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, - sle.item_code), SerialNoItemError) + frappe.throw( + _("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code), + SerialNoItemError, + ) if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle): doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no)) - frappe.throw(_("Serial No {0} has already been received in the {1} #{2}") - .format(frappe.bold(serial_no), sr.purchase_document_type, doc_name), SerialNoDuplicateError) + frappe.throw( + _("Serial No {0} has already been received in the {1} #{2}").format( + frappe.bold(serial_no), sr.purchase_document_type, doc_name + ), + SerialNoDuplicateError, + ) - if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation'] - and sle.voucher_type == sr.delivery_document_type): - return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, 'return_against') + if ( + sr.delivery_document_no + and sle.voucher_type not in ["Stock Entry", "Stock Reconciliation"] + and sle.voucher_type == sr.delivery_document_type + ): + return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, "return_against") if return_against and return_against != sr.delivery_document_no: frappe.throw(_("Serial no {0} has been already returned").format(sr.name)) if cint(sle.actual_qty) < 0: - if sr.warehouse!=sle.warehouse: - frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, - sle.warehouse), SerialNoWarehouseError) + if sr.warehouse != sle.warehouse: + frappe.throw( + _("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse), + SerialNoWarehouseError, + ) if not sr.purchase_document_no: frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) @@ -278,66 +388,100 @@ def validate_serial_no(sle, item_det): if sle.voucher_type in ("Delivery Note", "Sales Invoice"): if sr.batch_no and sr.batch_no != sle.batch_no: - frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no, - sle.batch_no), SerialNoBatchError) + frappe.throw( + _("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no), + SerialNoBatchError, + ) if not sle.is_cancelled and not sr.warehouse: - frappe.throw(_("Serial No {0} does not belong to any Warehouse") - .format(serial_no), SerialNoWarehouseError) + frappe.throw( + _("Serial No {0} does not belong to any Warehouse").format(serial_no), + SerialNoWarehouseError, + ) # if Sales Order reference in Serial No validate the Delivery Note or Invoice is against the same if sr.sales_order: if sle.voucher_type == "Sales Invoice": - if not frappe.db.exists("Sales Invoice Item", {"parent": sle.voucher_no, - "item_code": sle.item_code, "sales_order": sr.sales_order}): + if not frappe.db.exists( + "Sales Invoice Item", + {"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order}, + ): frappe.throw( - _("Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}") - .format(sr.name, sle.item_code, sr.sales_order) + _( + "Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}" + ).format(sr.name, sle.item_code, sr.sales_order) ) elif sle.voucher_type == "Delivery Note": - if not frappe.db.exists("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code, "against_sales_order": sr.sales_order}): - invoice = frappe.db.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_invoice") - if not invoice or frappe.db.exists("Sales Invoice Item", - {"parent": invoice, "item_code": sle.item_code, - "sales_order": sr.sales_order}): + if not frappe.db.exists( + "Delivery Note Item", + { + "parent": sle.voucher_no, + "item_code": sle.item_code, + "against_sales_order": sr.sales_order, + }, + ): + invoice = frappe.db.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_invoice", + ) + if not invoice or frappe.db.exists( + "Sales Invoice Item", + {"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order}, + ): frappe.throw( - _("Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}") - .format(sr.name, sle.item_code, sr.sales_order) + _( + "Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}" + ).format(sr.name, sle.item_code, sr.sales_order) ) # if Sales Order reference in Delivery Note or Invoice validate SO reservations for item if sle.voucher_type == "Sales Invoice": - sales_order = frappe.db.get_value("Sales Invoice Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "sales_order") + sales_order = frappe.db.get_value( + "Sales Invoice Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) elif sle.voucher_type == "Delivery Note": - sales_order = frappe.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_order") + sales_order = frappe.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) else: - sales_invoice = frappe.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_invoice") + sales_invoice = frappe.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_invoice", + ) if sales_invoice: - sales_order = frappe.db.get_value("Sales Invoice Item", { - "parent": sales_invoice, "item_code": sle.item_code}, "sales_order") + sales_order = frappe.db.get_value( + "Sales Invoice Item", + {"parent": sales_invoice, "item_code": sle.item_code}, + "sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) elif cint(sle.actual_qty) < 0: # transfer out frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series: - frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), - SerialNoRequiredError) + frappe.throw( + _("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError + ) elif serial_nos: # SLE is being cancelled and has serial nos for serial_no in serial_nos: check_serial_no_validity_on_cancel(serial_no, sle) + def check_serial_no_validity_on_cancel(serial_no, sle): - sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1) + sr = frappe.db.get_value( + "Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1 + ) sr_link = frappe.utils.get_link_to_form("Serial No", serial_no) doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no) actual_qty = cint(sle.actual_qty) @@ -347,57 +491,65 @@ def check_serial_no_validity_on_cancel(serial_no, sle): if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse): # receipt(inward) is being cancelled msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse) + ) elif sr and actual_qty > 0 and not is_stock_reco: # delivery is being cancelled, check for warehouse. if sr.warehouse: # serial no is active in another warehouse/company. msg = _("Cannot cancel {0} {1} as Serial No {2} is active in warehouse {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse) + ) elif sr.company != sle.company and sr.status == "Delivered": # serial no is inactive (allowed) or delivered from another company (block). msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the company {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company) + ) if msg: frappe.throw(msg, title=_("Cannot cancel")) -def validate_material_transfer_entry(sle_doc): - sle_doc.update({ - "skip_update_serial_no": False, - "skip_serial_no_validaiton": False - }) - if (sle_doc.voucher_type == "Stock Entry" and not sle_doc.is_cancelled and - frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"): +def validate_material_transfer_entry(sle_doc): + sle_doc.update({"skip_update_serial_no": False, "skip_serial_no_validaiton": False}) + + if ( + sle_doc.voucher_type == "Stock Entry" + and not sle_doc.is_cancelled + and frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer" + ): if sle_doc.actual_qty < 0: sle_doc.skip_update_serial_no = True else: sle_doc.skip_serial_no_validaiton = True -def validate_so_serial_no(sr, sales_order): - if not sr.sales_order or sr.sales_order!= sales_order: - msg = (_("Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}.") - .format(sales_order, sr.item_code)) - frappe.throw(_("""{0} Serial No {1} cannot be delivered""") - .format(msg, sr.name)) +def validate_so_serial_no(sr, sales_order): + if not sr.sales_order or sr.sales_order != sales_order: + msg = _( + "Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}." + ).format(sales_order, sr.item_code) + + frappe.throw(_("""{0} Serial No {1} cannot be delivered""").format(msg, sr.name)) + def has_serial_no_exists(sn, sle): - if (sn.warehouse and not sle.skip_serial_no_validaiton - and sle.voucher_type != 'Stock Reconciliation'): + if ( + sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation" + ): return True if sn.company != sle.company: return False + def allow_serial_nos_with_different_item(sle_serial_no, sle): """ - Allows same serial nos for raw materials and finished goods - in Manufacture / Repack type Stock Entry + Allows same serial nos for raw materials and finished goods + in Manufacture / Repack type Stock Entry """ allow_serial_nos = False - if sle.voucher_type=="Stock Entry" and cint(sle.actual_qty) > 0: + if sle.voucher_type == "Stock Entry" and cint(sle.actual_qty) > 0: stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no) if stock_entry.purpose in ("Repack", "Manufacture"): for d in stock_entry.get("items"): @@ -408,16 +560,24 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): return allow_serial_nos + def update_serial_nos(sle, item_det): - if sle.skip_update_serial_no: return - if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \ - and item_det.has_serial_no == 1 and item_det.serial_no_series: + if sle.skip_update_serial_no: + return + if ( + not sle.is_cancelled + and not sle.serial_no + and cint(sle.actual_qty) > 0 + and item_det.has_serial_no == 1 + and item_det.serial_no_series + ): serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty) - frappe.db.set(sle, "serial_no", serial_nos) + sle.db_set("serial_no", serial_nos) validate_serial_no(sle, item_det) if sle.serial_no: auto_make_serial_nos(sle) + def get_auto_serial_nos(serial_no_series, qty): serial_nos = [] for i in range(cint(qty)): @@ -425,22 +585,24 @@ def get_auto_serial_nos(serial_no_series, qty): return "\n".join(serial_nos) + def get_new_serial_number(series): sr_no = make_autoname(series, "Serial No") if frappe.db.exists("Serial No", sr_no): sr_no = get_new_serial_number(series) return sr_no + def auto_make_serial_nos(args): - serial_nos = get_serial_nos(args.get('serial_no')) + serial_nos = get_serial_nos(args.get("serial_no")) created_numbers = [] - voucher_type = args.get('voucher_type') - item_code = args.get('item_code') + voucher_type = args.get("voucher_type") + item_code = args.get("item_code") for serial_no in serial_nos: is_new = False if frappe.db.exists("Serial No", serial_no): sr = frappe.get_cached_doc("Serial No", serial_no) - elif args.get('actual_qty', 0) > 0: + elif args.get("actual_qty", 0) > 0: sr = frappe.new_doc("Serial No") is_new = True @@ -448,7 +610,7 @@ def auto_make_serial_nos(args): if is_new: created_numbers.append(sr.name) - form_links = list(map(lambda d: get_link_to_form('Serial No', d), created_numbers)) + form_links = list(map(lambda d: get_link_to_form("Serial No", d), created_numbers)) # Setting up tranlated title field for all cases singular_title = _("Serial Number Created") @@ -460,29 +622,41 @@ def auto_make_serial_nos(args): if len(form_links) == 1: frappe.msgprint(_("Serial No {0} Created").format(form_links[0]), singular_title) elif len(form_links) > 0: - message = _("The following serial numbers were created:

    {0}").format(get_items_html(form_links, item_code)) + message = _("The following serial numbers were created:

    {0}").format( + get_items_html(form_links, item_code) + ) frappe.msgprint(message, multiple_title) + def get_items_html(serial_nos, item_code): - body = ', '.join(serial_nos) - return '''
    + body = ", ".join(serial_nos) + return """
    {0}: {1} Serial Numbers
    {2}
    - '''.format(item_code, len(serial_nos), body) + """.format( + item_code, len(serial_nos), body + ) def get_item_details(item_code): - return frappe.db.sql("""select name, has_batch_no, docstatus, + return frappe.db.sql( + """select name, has_batch_no, docstatus, is_stock_item, has_serial_no, serial_no_series - from tabItem where name=%s""", item_code, as_dict=True)[0] + from tabItem where name=%s""", + item_code, + as_dict=True, + )[0] + def get_serial_nos(serial_no): if isinstance(serial_no, list): return serial_no - return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n') - if s.strip()] + return [ + s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip() + ] + def clean_serial_no_string(serial_no: str) -> str: if not serial_no: @@ -491,20 +665,23 @@ def clean_serial_no_string(serial_no: str) -> str: serial_no_list = get_serial_nos(serial_no) return "\n".join(serial_no_list) + def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]: if args.get(field): serial_no_doc.set(field, args.get(field)) serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True - serial_no_doc.warehouse = (args.get("warehouse") - if args.get("actual_qty", 0) > 0 else None) + serial_no_doc.warehouse = args.get("warehouse") if args.get("actual_qty", 0) > 0 else None if is_new: serial_no_doc.serial_no = serial_no - if (serial_no_doc.sales_order and args.get("voucher_type") == "Stock Entry" - and not args.get("actual_qty", 0) > 0): + if ( + serial_no_doc.sales_order + and args.get("voucher_type") == "Stock Entry" + and not args.get("actual_qty", 0) > 0 + ): serial_no_doc.sales_order = None serial_no_doc.validate_item() @@ -517,146 +694,224 @@ def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): return serial_no_doc -def update_serial_nos_after_submit(controller, parentfield): - stock_ledger_entries = frappe.db.sql("""select voucher_detail_no, serial_no, actual_qty, warehouse - from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""", - (controller.doctype, controller.name), as_dict=True) - if not stock_ledger_entries: return +def update_serial_nos_after_submit(controller, parentfield): + stock_ledger_entries = frappe.db.sql( + """select voucher_detail_no, serial_no, actual_qty, warehouse + from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""", + (controller.doctype, controller.name), + as_dict=True, + ) + + if not stock_ledger_entries: + return for d in controller.get(parentfield): if d.serial_no: continue - update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") - and d.rejected_qty) else False + update_rejected_serial_nos = ( + True + if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty) + else False + ) accepted_serial_nos_updated = False if controller.doctype == "Stock Entry": warehouse = d.t_warehouse qty = d.transfer_qty + elif controller.doctype in ("Sales Invoice", "Delivery Note"): + warehouse = d.warehouse + qty = d.stock_qty else: warehouse = d.warehouse - qty = (d.qty if controller.doctype == "Stock Reconciliation" - else d.stock_qty) + qty = d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty for sle in stock_ledger_entries: - if sle.voucher_detail_no==d.name: - if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \ - and sle.warehouse == warehouse and sle.serial_no != d.serial_no: - d.serial_no = sle.serial_no - frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no) - accepted_serial_nos_updated = True - if not update_rejected_serial_nos: - break - elif update_rejected_serial_nos and abs(sle.actual_qty)==d.rejected_qty \ - and sle.warehouse == d.rejected_warehouse and sle.serial_no != d.rejected_serial_no: - d.rejected_serial_no = sle.serial_no - frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no) - update_rejected_serial_nos = False - if accepted_serial_nos_updated: - break + if sle.voucher_detail_no == d.name: + if ( + not accepted_serial_nos_updated + and qty + and abs(sle.actual_qty) == abs(qty) + and sle.warehouse == warehouse + and sle.serial_no != d.serial_no + ): + d.serial_no = sle.serial_no + frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no) + accepted_serial_nos_updated = True + if not update_rejected_serial_nos: + break + elif ( + update_rejected_serial_nos + and abs(sle.actual_qty) == d.rejected_qty + and sle.warehouse == d.rejected_warehouse + and sle.serial_no != d.rejected_serial_no + ): + d.rejected_serial_no = sle.serial_no + frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no) + update_rejected_serial_nos = False + if accepted_serial_nos_updated: + break + def update_maintenance_status(): - serial_nos = frappe.db.sql('''select name from `tabSerial No` where (amc_expiry_date<%s or - warranty_expiry_date<%s) and maintenance_status not in ('Out of Warranty', 'Out of AMC')''', - (nowdate(), nowdate())) + serial_nos = frappe.db.sql( + """select name from `tabSerial No` where (amc_expiry_date<%s or + warranty_expiry_date<%s) and maintenance_status not in ('Out of Warranty', 'Out of AMC')""", + (nowdate(), nowdate()), + ) for serial_no in serial_nos: doc = frappe.get_doc("Serial No", serial_no[0]) doc.set_maintenance_status() - frappe.db.set_value('Serial No', doc.name, 'maintenance_status', doc.maintenance_status) + frappe.db.set_value("Serial No", doc.name, "maintenance_status", doc.maintenance_status) + def get_delivery_note_serial_no(item_code, qty, delivery_note): - serial_nos = '' - dn_serial_nos = frappe.db.sql_list(""" select name from `tabSerial No` + serial_nos = "" + dn_serial_nos = frappe.db.sql_list( + """ select name from `tabSerial No` where item_code = %(item_code)s and delivery_document_no = %(delivery_note)s - and sales_invoice is null limit {0}""".format(cint(qty)), { - 'item_code': item_code, - 'delivery_note': delivery_note - }) + and sales_invoice is null limit {0}""".format( + cint(qty) + ), + {"item_code": item_code, "delivery_note": delivery_note}, + ) - if dn_serial_nos and len(dn_serial_nos)>0: - serial_nos = '\n'.join(dn_serial_nos) + if dn_serial_nos and len(dn_serial_nos) > 0: + serial_nos = "\n".join(dn_serial_nos) return serial_nos + @frappe.whitelist() -def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None): - filters = { "item_code": item_code, "warehouse": warehouse } +def auto_fetch_serial_number( + qty: float, + item_code: str, + warehouse: str, + posting_date: Optional[str] = None, + batch_nos: Optional[Union[str, List[str]]] = None, + for_doctype: Optional[str] = None, + exclude_sr_nos: Optional[List[str]] = None, +) -> List[str]: + + filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) + + if exclude_sr_nos is None: + exclude_sr_nos = [] + else: + exclude_sr_nos = safe_json_loads(exclude_sr_nos) + exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos))) if batch_nos: - try: - filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)] - except Exception: - filters["batch_no"] = [batch_nos] + batch_nos_list = safe_json_loads(batch_nos) + if isinstance(batch_nos_list, list): + filters.batch_no = batch_nos_list + else: + filters.batch_no = [batch_nos] if posting_date: - filters["expiry_date"] = posting_date + filters.expiry_date = posting_date serial_numbers = [] - if for_doctype == 'POS Invoice': - reserved_sr_nos = get_pos_reserved_serial_nos(filters) - serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=reserved_sr_nos) - else: - serial_numbers = fetch_serial_numbers(filters, qty) + if for_doctype == "POS Invoice": + exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters)) + + serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos) + + return sorted([d.get("name") for d in serial_numbers]) + + +def get_delivered_serial_nos(serial_nos): + """ + Returns serial numbers that delivered from the list of serial numbers + """ + from frappe.query_builder.functions import Coalesce + + SerialNo = frappe.qb.DocType("Serial No") + serial_nos = get_serial_nos(serial_nos) + query = ( + frappe.qb.select(SerialNo.name) + .from_(SerialNo) + .where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != "")) + ) + + result = query.run() + if result and len(result) > 0: + delivered_serial_nos = [row[0] for row in result] + return delivered_serial_nos - return [d.get('name') for d in serial_numbers] @frappe.whitelist() def get_pos_reserved_serial_nos(filters): - if isinstance(filters, string_types): + if isinstance(filters, str): filters = json.loads(filters) - pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no - from `tabPOS Invoice` p, `tabPOS Invoice Item` item - where p.name = item.parent - and p.consolidated_invoice is NULL - and p.docstatus = 1 - and item.docstatus = 1 - and item.item_code = %(item_code)s - and item.warehouse = %(warehouse)s - and item.serial_no is NOT NULL and item.serial_no != '' - """, filters, as_dict=1) + POSInvoice = frappe.qb.DocType("POS Invoice") + POSInvoiceItem = frappe.qb.DocType("POS Invoice Item") + query = ( + frappe.qb.from_(POSInvoice) + .from_(POSInvoiceItem) + .select(POSInvoice.is_return, POSInvoiceItem.serial_no) + .where( + (POSInvoice.name == POSInvoiceItem.parent) + & (POSInvoice.docstatus == 1) + & (POSInvoiceItem.docstatus == 1) + & (POSInvoiceItem.item_code == filters.get("item_code")) + & (POSInvoiceItem.warehouse == filters.get("warehouse")) + & (POSInvoiceItem.serial_no.isnotnull()) + & (POSInvoiceItem.serial_no != "") + ) + ) + + pos_transacted_sr_nos = query.run(as_dict=True) reserved_sr_nos = [] + returned_sr_nos = [] for d in pos_transacted_sr_nos: - reserved_sr_nos += get_serial_nos(d.serial_no) + if d.is_return == 0: + reserved_sr_nos += get_serial_nos(d.serial_no) + elif d.is_return == 1: + returned_sr_nos += get_serial_nos(d.serial_no) + + for sr_no in returned_sr_nos: + reserved_sr_nos.remove(sr_no) return reserved_sr_nos + def fetch_serial_numbers(filters, qty, do_not_include=None): if do_not_include is None: do_not_include = [] - batch_join_selection = "" - batch_no_condition = "" + batch_nos = filters.get("batch_no") expiry_date = filters.get("expiry_date") + serial_no = frappe.qb.DocType("Serial No") + + query = ( + frappe.qb.from_(serial_no) + .select(serial_no.name) + .where( + (serial_no.item_code == filters["item_code"]) + & (serial_no.warehouse == filters["warehouse"]) + & (Coalesce(serial_no.sales_invoice, "") == "") + & (Coalesce(serial_no.delivery_document_no, "") == "") + ) + .orderby(serial_no.creation) + .limit(qty or 1) + ) + + if do_not_include: + query = query.where(serial_no.name.notin(do_not_include)) + if batch_nos: - batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos)) + query = query.where(serial_no.batch_no.isin(batch_nos)) if expiry_date: - batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name " - expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s " - batch_no_condition += expiry_date_cond - - excluded_sr_nos = ", ".join(["" + frappe.db.escape(sr) + "" for sr in do_not_include]) or "''" - serial_numbers = frappe.db.sql(""" - SELECT sr.name FROM `tabSerial No` sr {batch_join_selection} - WHERE - sr.name not in ({excluded_sr_nos}) AND - sr.item_code = %(item_code)s AND - sr.warehouse = %(warehouse)s AND - ifnull(sr.sales_invoice,'') = '' AND - ifnull(sr.delivery_document_no, '') = '' - {batch_no_condition} - ORDER BY - sr.creation - LIMIT - {qty} - """.format( - excluded_sr_nos=excluded_sr_nos, - qty=qty or 1, - batch_join_selection=batch_join_selection, - batch_no_condition=batch_no_condition - ), filters, as_dict=1) + batch = frappe.qb.DocType("Batch") + query = ( + query.left_join(batch) + .on(serial_no.batch_no == batch.name) + .where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date) + ) + serial_numbers = query.run(as_dict=True) return serial_numbers diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index f8cea717251..68623fba11e 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -6,24 +6,22 @@ import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse test_dependencies = ["Item"] -test_records = frappe.get_test_records('Serial No') - -from erpnext.stock.doctype.serial_no.serial_no import * -from erpnext.tests.utils import ERPNextTestCase +test_records = frappe.get_test_records("Serial No") -class TestSerialNo(ERPNextTestCase): - +class TestSerialNo(FrappeTestCase): def tearDown(self): frappe.db.rollback() @@ -48,7 +46,9 @@ class TestSerialNo(ERPNextTestCase): se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = get_serial_nos(se.get("items")[0].serial_no) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) serial_no = frappe.get_doc("Serial No", serial_nos[0]) @@ -60,8 +60,13 @@ class TestSerialNo(ERPNextTestCase): self.assertEqual(serial_no.delivery_document_no, dn.name) wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0], - company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) serial_no.reload() @@ -74,9 +79,9 @@ class TestSerialNo(ERPNextTestCase): def test_inter_company_transfer_intermediate_cancellation(self): """ - Receive into and Deliver Serial No from one company. - Then Receive into and Deliver from second company. - Try to cancel intermediate receipts/deliveries to test if it is blocked. + Receive into and Deliver Serial No from one company. + Then Receive into and Deliver from second company. + Try to cancel intermediate receipts/deliveries to test if it is blocked. """ se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = get_serial_nos(se.get("items")[0].serial_no) @@ -89,8 +94,9 @@ class TestSerialNo(ERPNextTestCase): self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") self.assertEqual(sn_doc.purchase_document_no, se.name) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) sn_doc.reload() # check Serial No details after delivery from **first** company self.assertEqual(sn_doc.status, "Delivered") @@ -104,8 +110,13 @@ class TestSerialNo(ERPNextTestCase): # receive serial no in second company wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() self.assertEqual(sn_doc.warehouse, wh) @@ -114,8 +125,13 @@ class TestSerialNo(ERPNextTestCase): self.assertRaises(frappe.ValidationError, dn.cancel) # deliver from second company - dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + dn_2 = create_delivery_note( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() # check Serial No details after delivery from **second** company @@ -131,9 +147,9 @@ class TestSerialNo(ERPNextTestCase): def test_inter_company_transfer_fallback_on_cancel(self): """ - Test Serial No state changes on cancellation. - If Delivery cancelled, it should fall back on last Receipt in the same company. - If Receipt is cancelled, it should be Inactive in the same company. + Test Serial No state changes on cancellation. + If Delivery cancelled, it should fall back on last Receipt in the same company. + If Receipt is cancelled, it should be Inactive in the same company. """ # Receipt in **first** company se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") @@ -141,17 +157,28 @@ class TestSerialNo(ERPNextTestCase): sn_doc = frappe.get_doc("Serial No", serial_nos[0]) # Delivery from first company - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) # Receipt in **second** company wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) # Delivery from second company - dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + dn_2 = create_delivery_note( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() self.assertEqual(sn_doc.status, "Delivered") @@ -184,12 +211,11 @@ class TestSerialNo(ERPNextTestCase): def test_auto_creation_of_serial_no(self): """ - Test if auto created Serial No excludes existing serial numbers + Test if auto created Serial No excludes existing serial numbers """ - item_code = make_item("_Test Auto Serial Item ", { - "has_serial_no": 1, - "serial_no_series": "XYZ.###" - }).item_code + item_code = make_item( + "_Test Auto Serial Item ", {"has_serial_no": 1, "serial_no_series": "XYZ.###"} + ).item_code # Reserve XYZ005 pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005") @@ -203,7 +229,7 @@ class TestSerialNo(ERPNextTestCase): def test_serial_no_sanitation(self): "Test if Serial No input is sanitised before entering the DB." item_code = "_Test Serialized Item" - test_records = frappe.get_test_records('Stock Entry') + test_records = frappe.get_test_records("Stock Entry") se = frappe.copy_doc(test_records[0]) se.get("items")[0].item_code = item_code @@ -217,27 +243,94 @@ class TestSerialNo(ERPNextTestCase): self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021") def test_correct_serial_no_incoming_rate(self): - """ Check correct consumption rate based on serial no record. - """ + """Check correct consumption rate based on serial no record.""" item_code = "_Test Serialized Item" warehouse = "_Test Warehouse - _TC" serial_nos = ["LOWVALUATION", "HIGHVALUATION"] - in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, - serial_no=serial_nos[0]) - in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, - serial_no=serial_nos[1]) + in1 = make_stock_entry( + item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0] + ) + in2 = make_stock_entry( + item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=serial_nos[1] + ) - out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True) + out = create_delivery_note( + item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True + ) # change serial no out.items[0].serial_no = serial_nos[1] out.save() out.submit() - value_diff = frappe.db.get_value("Stock Ledger Entry", - {"voucher_no": out.name, "voucher_type": "Delivery Note"}, - "stock_value_difference" - ) + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": out.name, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) self.assertEqual(value_diff, -113) + def test_auto_fetch(self): + item_code = make_item( + properties={ + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "serial_no_series": "TEST.#######", + } + ).name + warehouse = "_Test Warehouse - _TC" + + in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5) + in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5) + + in1.reload() + in2.reload() + + batch1 = in1.items[0].batch_no + batch2 = in2.items[0].batch_no + + batch_wise_serials = { + batch1: get_serial_nos(in1.items[0].serial_no), + batch2: get_serial_nos(in2.items[0].serial_no), + } + + # Test FIFO + first_fetch = auto_fetch_serial_number(5, item_code, warehouse) + self.assertEqual(first_fetch, batch_wise_serials[batch1]) + + # partial FIFO + partial_fetch = auto_fetch_serial_number(2, item_code, warehouse) + self.assertTrue( + set(partial_fetch).issubset(set(first_fetch)), + msg=f"{partial_fetch} should be subset of {first_fetch}", + ) + + # exclusion + remaining = auto_fetch_serial_number( + 3, item_code, warehouse, exclude_sr_nos=json.dumps(partial_fetch) + ) + self.assertEqual(sorted(remaining + partial_fetch), first_fetch) + + # batchwise + for batch, expected_serials in batch_wise_serials.items(): + fetched_sr = auto_fetch_serial_number(5, item_code, warehouse, batch_nos=batch) + self.assertEqual(fetched_sr, sorted(expected_serials)) + + # non existing warehouse + self.assertEqual(auto_fetch_serial_number(10, item_code, warehouse="Nonexisting"), []) + + # multi batch + all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list] + fetched_serials = auto_fetch_serial_number( + 10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys()) + ) + self.assertEqual(sorted(all_serials), fetched_serials) + + # expiry date + frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01") + non_expired_serials = auto_fetch_serial_number( + 5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1 + ) + self.assertEqual(non_expired_serials, []) diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 666de57f34f..42a67f42bec 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -17,22 +17,22 @@ class Shipment(Document): self.validate_pickup_time() self.set_value_of_goods() if self.docstatus == 0: - self.status = 'Draft' + self.status = "Draft" def on_submit(self): if not self.shipment_parcel: - frappe.throw(_('Please enter Shipment Parcel information')) + frappe.throw(_("Please enter Shipment Parcel information")) if self.value_of_goods == 0: - frappe.throw(_('Value of goods cannot be 0')) - self.db_set('status', 'Submitted') + frappe.throw(_("Value of goods cannot be 0")) + self.db_set("status", "Submitted") def on_cancel(self): - self.db_set('status', 'Cancelled') + self.db_set("status", "Cancelled") def validate_weight(self): for parcel in self.shipment_parcel: if flt(parcel.weight) <= 0: - frappe.throw(_('Parcel weight cannot be 0')) + frappe.throw(_("Parcel weight cannot be 0")) def validate_pickup_time(self): if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from): @@ -44,26 +44,34 @@ class Shipment(Document): value_of_goods += flt(entry.get("grand_total")) self.value_of_goods = value_of_goods if value_of_goods else self.value_of_goods + @frappe.whitelist() def get_address_name(ref_doctype, docname): # Return address name return get_party_shipping_address(ref_doctype, docname) + @frappe.whitelist() def get_contact_name(ref_doctype, docname): # Return address name return get_default_contact(ref_doctype, docname) + @frappe.whitelist() def get_company_contact(user): - contact = frappe.db.get_value('User', user, [ - 'first_name', - 'last_name', - 'email', - 'phone', - 'mobile_no', - 'gender', - ], as_dict=1) + contact = frappe.db.get_value( + "User", + user, + [ + "first_name", + "last_name", + "email", + "phone", + "mobile_no", + "gender", + ], + as_dict=1, + ) if not contact.phone: contact.phone = contact.mobile_no return contact diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index afe821845ae..ae97e7af361 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -4,22 +4,23 @@ from datetime import date, timedelta import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment -from erpnext.tests.utils import ERPNextTestCase -class TestShipment(ERPNextTestCase): +class TestShipment(FrappeTestCase): def test_shipment_from_delivery_note(self): delivery_note = create_test_delivery_note() delivery_note.submit() - shipment = create_test_shipment([ delivery_note ]) + shipment = create_test_shipment([delivery_note]) shipment.submit() second_shipment = make_shipment(delivery_note.name) self.assertEqual(second_shipment.value_of_goods, delivery_note.grand_total) self.assertEqual(len(second_shipment.shipment_delivery_note), 1) self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name) + def create_test_delivery_note(): company = get_shipment_company() customer = get_shipment_customer() @@ -30,25 +31,26 @@ def create_test_delivery_note(): delivery_note = frappe.new_doc("Delivery Note") delivery_note.company = company.name delivery_note.posting_date = posting_date.strftime("%Y-%m-%d") - delivery_note.posting_time = '10:00' + delivery_note.posting_time = "10:00" delivery_note.customer = customer.name - delivery_note.append('items', + delivery_note.append( + "items", { "item_code": item.name, "item_name": item.item_name, - "description": 'Test delivery note for shipment', + "description": "Test delivery note for shipment", "qty": 5, - "uom": 'Nos', - "warehouse": 'Stores - _TC', + "uom": "Nos", + "warehouse": "Stores - _TC", "rate": item.standard_rate, - "cost_center": 'Main - _TC' - } + "cost_center": "Main - _TC", + }, ) delivery_note.insert() return delivery_note -def create_test_shipment(delivery_notes = None): +def create_test_shipment(delivery_notes=None): company = get_shipment_company() company_address = get_shipment_company_address(company.name) customer = get_shipment_customer() @@ -57,45 +59,35 @@ def create_test_shipment(delivery_notes = None): posting_date = date.today() + timedelta(days=5) shipment = frappe.new_doc("Shipment") - shipment.pickup_from_type = 'Company' + shipment.pickup_from_type = "Company" shipment.pickup_company = company.name shipment.pickup_address_name = company_address.name - shipment.delivery_to_type = 'Customer' + shipment.delivery_to_type = "Customer" shipment.delivery_customer = customer.name shipment.delivery_address_name = customer_address.name shipment.delivery_contact_name = customer_contact.name - shipment.pallets = 'No' - shipment.shipment_type = 'Goods' + shipment.pallets = "No" + shipment.shipment_type = "Goods" shipment.value_of_goods = 1000 - shipment.pickup_type = 'Pickup' + shipment.pickup_type = "Pickup" shipment.pickup_date = posting_date.strftime("%Y-%m-%d") - shipment.pickup_from = '09:00' - shipment.pickup_to = '17:00' - shipment.description_of_content = 'unit test entry' + shipment.pickup_from = "09:00" + shipment.pickup_to = "17:00" + shipment.description_of_content = "unit test entry" for delivery_note in delivery_notes: - shipment.append('shipment_delivery_note', - { - "delivery_note": delivery_note.name - } - ) - shipment.append('shipment_parcel', - { - "length": 5, - "width": 5, - "height": 5, - "weight": 5, - "count": 5 - } + shipment.append("shipment_delivery_note", {"delivery_note": delivery_note.name}) + shipment.append( + "shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5} ) shipment.insert() return shipment def get_shipment_customer_contact(customer_name): - contact_fname = 'Customer Shipment' - contact_lname = 'Testing' - customer_name = contact_fname + ' ' + contact_lname - contacts = frappe.get_all("Contact", fields=["name"], filters = {"name": customer_name}) + contact_fname = "Customer Shipment" + contact_lname = "Testing" + customer_name = contact_fname + " " + contact_lname + contacts = frappe.get_all("Contact", fields=["name"], filters={"name": customer_name}) if len(contacts): return contacts[0] else: @@ -103,104 +95,106 @@ def get_shipment_customer_contact(customer_name): def get_shipment_customer_address(customer_name): - address_title = customer_name + ' address 123' - customer_address = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + address_title = customer_name + " address 123" + customer_address = frappe.get_all( + "Address", fields=["name"], filters={"address_title": address_title} + ) if len(customer_address): return customer_address[0] else: return create_shipment_address(address_title, customer_name, 81929) + def get_shipment_customer(): - customer_name = 'Shipment Customer' - customer = frappe.get_all("Customer", fields=["name"], filters = {"name": customer_name}) + customer_name = "Shipment Customer" + customer = frappe.get_all("Customer", fields=["name"], filters={"name": customer_name}) if len(customer): return customer[0] else: return create_shipment_customer(customer_name) + def get_shipment_company_address(company_name): - address_title = company_name + ' address 123' - addresses = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + address_title = company_name + " address 123" + addresses = frappe.get_all("Address", fields=["name"], filters={"address_title": address_title}) if len(addresses): return addresses[0] else: return create_shipment_address(address_title, company_name, 80331) + def get_shipment_company(): return frappe.get_doc("Company", "_Test Company") + def get_shipment_item(company_name): - item_name = 'Testing Shipment item' - items = frappe.get_all("Item", + item_name = "Testing Shipment item" + items = frappe.get_all( + "Item", fields=["name", "item_name", "item_code", "standard_rate"], - filters = {"item_name": item_name} + filters={"item_name": item_name}, ) if len(items): return items[0] else: return create_shipment_item(item_name, company_name) + def create_shipment_address(address_title, company_name, postal_code): address = frappe.new_doc("Address") address.address_title = address_title - address.address_type = 'Shipping' - address.address_line1 = company_name + ' address line 1' - address.city = 'Random City' + address.address_type = "Shipping" + address.address_line1 = company_name + " address line 1" + address.city = "Random City" address.postal_code = postal_code - address.country = 'Germany' + address.country = "Germany" address.insert() return address def create_customer_contact(fname, lname): customer = frappe.new_doc("Contact") - customer.customer_name = fname + ' ' + lname + customer.customer_name = fname + " " + lname customer.first_name = fname customer.last_name = lname customer.is_primary_contact = 1 customer.is_billing_contact = 1 - customer.append('email_ids', - { - 'email_id': 'randomme@email.com', - 'is_primary': 1 - } + customer.append("email_ids", {"email_id": "randomme@email.com", "is_primary": 1}) + customer.append( + "phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1} ) - customer.append('phone_nos', - { - 'phone': '123123123', - 'is_primary_phone': 1, - 'is_primary_mobile_no': 1 - } - ) - customer.status = 'Passive' + customer.status = "Passive" customer.insert() return customer + def create_shipment_customer(customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name - customer.customer_type = 'Company' - customer.customer_group = 'All Customer Groups' - customer.territory = 'All Territories' - customer.gst_category = 'Unregistered' + customer.customer_type = "Company" + customer.customer_group = "All Customer Groups" + customer.territory = "All Territories" + customer.gst_category = "Unregistered" customer.insert() return customer + def create_material_receipt(item, company): posting_date = date.today() stock = frappe.new_doc("Stock Entry") stock.company = company - stock.stock_entry_type = 'Material Receipt' + stock.stock_entry_type = "Material Receipt" stock.posting_date = posting_date.strftime("%Y-%m-%d") - stock.append('items', + stock.append( + "items", { - "t_warehouse": 'Stores - _TC', + "t_warehouse": "Stores - _TC", "item_code": item.name, "qty": 5, - "uom": 'Nos', + "uom": "Nos", "basic_rate": item.standard_rate, - "cost_center": 'Main - _TC' - } + "cost_center": "Main - _TC", + }, ) stock.insert() stock.submit() @@ -210,14 +204,9 @@ def create_shipment_item(item_name, company_name): item = frappe.new_doc("Item") item.item_name = item_name item.item_code = item_name - item.item_group = 'All Item Groups' - item.stock_uom = 'Nos' + item.item_group = "All Item Groups" + item.stock_uom = "Nos" item.standard_rate = 50 - item.append('item_defaults', - { - "company": company_name, - "default_warehouse": 'Stores - _TC' - } - ) + item.append("item_defaults", {"company": company_name, "default_warehouse": "Stores - _TC"}) item.insert() return item diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 61466cff032..4ec9f1f220f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -631,7 +631,7 @@ frappe.ui.form.on('Stock Entry Detail', { // set allow_zero_valuation_rate to 0 if s_warehouse is selected. let item = frappe.get_doc(cdt, cdn); if (item.s_warehouse) { - item.allow_zero_valuation_rate = 0; + frappe.model.set_value(cdt, cdn, "allow_zero_valuation_rate", 0); } }, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index c38dfaa1c84..f56e059f81c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -46,9 +46,9 @@ "items", "get_stock_and_rate", "section_break_19", - "total_incoming_value", - "column_break_22", "total_outgoing_value", + "column_break_22", + "total_incoming_value", "value_difference", "additional_costs_section", "additional_costs", @@ -374,7 +374,7 @@ { "fieldname": "total_incoming_value", "fieldtype": "Currency", - "label": "Total Incoming Value", + "label": "Total Incoming Value (Receipt)", "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 @@ -386,7 +386,7 @@ { "fieldname": "total_outgoing_value", "fieldtype": "Currency", - "label": "Total Outgoing Value", + "label": "Total Outgoing Value (Consumption)", "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 @@ -394,7 +394,7 @@ { "fieldname": "value_difference", "fieldtype": "Currency", - "label": "Total Value Difference (Out - In)", + "label": "Total Value Difference (Incoming - Outgoing)", "options": "Company:company:default_currency", "print_hide_if_no_value": 1, "read_only": 1 @@ -619,7 +619,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-07 12:55:14.614077", + "modified": "2022-05-02 05:21:39.060501", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f7109ab6b0d..f0566b08897 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -39,20 +39,28 @@ from erpnext.stock.utils import get_bin, get_incoming_rate class FinishedGoodError(frappe.ValidationError): pass + + class IncorrectValuationRateError(frappe.ValidationError): pass + + class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass + + class OperationsNotCompleteError(frappe.ValidationError): pass + + class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass + from erpnext.controllers.stock_controller import StockController -form_grid_templates = { - "items": "templates/form_grid/stock_entry_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"} + class StockEntry(StockController): def get_feed(self): @@ -64,16 +72,18 @@ class StockEntry(StockController): def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule - apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) + + apply_rule = self.apply_putaway_rule and ( + self.purpose in ["Material Transfer", "Material Receipt"] + ) if self.get("items") and apply_rule: - apply_putaway_rule(self.doctype, self.get("items"), self.company, - purpose=self.purpose) + apply_putaway_rule(self.doctype, self.get("items"), self.company, purpose=self.purpose) def validate(self): self.pro_doc = frappe._dict() if self.work_order: - self.pro_doc = frappe.get_doc('Work Order', self.work_order) + self.pro_doc = frappe.get_doc("Work Order", self.work_order) self.validate_posting_time() self.validate_purpose() @@ -104,10 +114,10 @@ class StockEntry(StockController): if not self.from_bom: self.fg_completed_qty = 0.0 - if self._action == 'submit': - self.make_batches('t_warehouse') + if self._action == "submit": + self.make_batches("t_warehouse") else: - set_batch_nos(self, 's_warehouse') + set_batch_nos(self, "s_warehouse") self.validate_serialized_batch() self.set_actual_qty() @@ -139,10 +149,10 @@ class StockEntry(StockController): if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() - if self.purpose == 'Material Transfer' and self.add_to_transit: - self.set_material_request_transfer_status('In Transit') - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - self.set_material_request_transfer_status('Completed') + if self.purpose == "Material Transfer" and self.add_to_transit: + self.set_material_request_transfer_status("In Transit") + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + self.set_material_request_transfer_status("Completed") def on_cancel(self): self.update_purchase_order_supplied_items() @@ -153,7 +163,7 @@ class StockEntry(StockController): self.update_work_order() self.update_stock_ledger() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() @@ -163,15 +173,16 @@ class StockEntry(StockController): self.delete_auto_created_batches() self.delete_linked_stock_entry() - if self.purpose == 'Material Transfer' and self.add_to_transit: - self.set_material_request_transfer_status('Not Started') - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - self.set_material_request_transfer_status('In Transit') + if self.purpose == "Material Transfer" and self.add_to_transit: + self.set_material_request_transfer_status("Not Started") + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + self.set_material_request_transfer_status("In Transit") def set_job_card_data(self): if self.job_card and not self.work_order: - data = frappe.db.get_value('Job Card', - self.job_card, ['for_quantity', 'work_order', 'bom_no'], as_dict=1) + data = frappe.db.get_value( + "Job Card", self.job_card, ["for_quantity", "work_order", "bom_no"], as_dict=1 + ) self.fg_completed_qty = data.for_quantity self.work_order = data.work_order self.from_bom = 1 @@ -179,107 +190,160 @@ class StockEntry(StockController): def validate_work_order_status(self): pro_doc = frappe.get_doc("Work Order", self.work_order) - if pro_doc.status == 'Completed': + if pro_doc.status == "Completed": frappe.throw(_("Cannot cancel transaction for Completed Work Order.")) def validate_purpose(self): - valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", - "Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor", - "Material Consumption for Manufacture"] + valid_purposes = [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Material Consumption for Manufacture", + ] if self.purpose not in valid_purposes: frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) - if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']: - frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") - .format(self.job_card)) + if self.job_card and self.purpose not in ["Material Transfer for Manufacture", "Repack"]: + frappe.throw( + _( + "For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry" + ).format(self.job_card) + ) def delete_linked_stock_entry(self): if self.purpose == "Send to Warehouse": - for d in frappe.get_all("Stock Entry", filters={"docstatus": 0, - "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}): + for d in frappe.get_all( + "Stock Entry", + filters={"docstatus": 0, "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}, + ): frappe.delete_doc("Stock Entry", d.name) def set_transfer_qty(self): for item in self.get("items"): if not flt(item.qty): - frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx)) + frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx), title=_("Zero quantity")) if not flt(item.conversion_factor): frappe.throw(_("Row {0}: UOM Conversion Factor is mandatory").format(item.idx)) - item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), - self.precision("transfer_qty", item)) + item.transfer_qty = flt( + flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) + ) + if not flt(item.transfer_qty): + frappe.throw( + _("Row {0}: Qty in Stock UOM can not be zero.").format(item.idx), title=_("Zero quantity") + ) def update_cost_in_project(self): - if (self.work_order and not frappe.db.get_value("Work Order", - self.work_order, "update_consumed_material_cost_in_project")): + if self.work_order and not frappe.db.get_value( + "Work Order", self.work_order, "update_consumed_material_cost_in_project" + ): return if self.project: - amount = frappe.db.sql(""" select ifnull(sum(sed.amount), 0) + amount = frappe.db.sql( + """ select ifnull(sum(sed.amount), 0) from `tabStock Entry` se, `tabStock Entry Detail` sed where se.docstatus = 1 and se.project = %s and sed.parent = se.name - and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1) + and (sed.t_warehouse is null or sed.t_warehouse = '')""", + self.project, + as_list=1, + ) amount = amount[0][0] if amount else 0 - additional_costs = frappe.db.sql(""" select ifnull(sum(sed.base_amount), 0) + additional_costs = frappe.db.sql( + """ select ifnull(sum(sed.base_amount), 0) from `tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed where se.docstatus = 1 and se.project = %s and sed.parent = se.name - and se.purpose = 'Manufacture'""", self.project, as_list=1) + and se.purpose = 'Manufacture'""", + self.project, + as_list=1, + ) additional_cost_amt = additional_costs[0][0] if additional_costs else 0 amount += additional_cost_amt - frappe.db.set_value('Project', self.project, 'total_consumed_material_cost', amount) + frappe.db.set_value("Project", self.project, "total_consumed_material_cost", amount) def validate_item(self): stock_items = self.get_stock_items() serialized_items = self.get_serialized_items() for item in self.get("items"): if flt(item.qty) and flt(item.qty) < 0: - frappe.throw(_("Row {0}: The item {1}, quantity must be positive number") - .format(item.idx, frappe.bold(item.item_code))) + frappe.throw( + _("Row {0}: The item {1}, quantity must be positive number").format( + item.idx, frappe.bold(item.item_code) + ) + ) if item.item_code not in stock_items: frappe.throw(_("{0} is not a stock Item").format(item.item_code)) - item_details = self.get_item_details(frappe._dict( - {"item_code": item.item_code, "company": self.company, - "project": self.project, "uom": item.uom, 's_warehouse': item.s_warehouse}), - for_update=True) + item_details = self.get_item_details( + frappe._dict( + { + "item_code": item.item_code, + "company": self.company, + "project": self.project, + "uom": item.uom, + "s_warehouse": item.s_warehouse, + } + ), + for_update=True, + ) - for f in ("uom", "stock_uom", "description", "item_name", "expense_account", - "cost_center", "conversion_factor"): - if f == "stock_uom" or not item.get(f): - item.set(f, item_details.get(f)) - if f == 'conversion_factor' and item.uom == item_details.get('stock_uom'): - item.set(f, item_details.get(f)) + for f in ( + "uom", + "stock_uom", + "description", + "item_name", + "expense_account", + "cost_center", + "conversion_factor", + ): + if f == "stock_uom" or not item.get(f): + item.set(f, item_details.get(f)) + if f == "conversion_factor" and item.uom == item_details.get("stock_uom"): + item.set(f, item_details.get(f)) if not item.transfer_qty and item.qty: - item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), - self.precision("transfer_qty", item)) + item.transfer_qty = flt( + flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) + ) - if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture") + if ( + self.purpose in ("Material Transfer", "Material Transfer for Manufacture") and not item.serial_no - and item.item_code in serialized_items): - frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), - frappe.MandatoryError) + and item.item_code in serialized_items + ): + frappe.throw( + _("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), + frappe.MandatoryError, + ) def validate_qty(self): manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"] if self.purpose in manufacture_purpose and self.work_order: - if not frappe.get_value('Work Order', self.work_order, 'skip_transfer'): + if not frappe.get_value("Work Order", self.work_order, "skip_transfer"): item_code = [] for item in self.items: - if cstr(item.t_warehouse) == '': - req_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order, 'item_code': item.item_code}, fields=["item_code"]) + if cstr(item.t_warehouse) == "": + req_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order, "item_code": item.item_code}, + fields=["item_code"], + ) - transferred_materials = frappe.db.sql(""" + transferred_materials = frappe.db.sql( + """ select sum(qty) as qty from `tabStock Entry` se,`tabStock Entry Detail` sed @@ -287,7 +351,10 @@ class StockEntry(StockController): se.name = sed.parent and se.docstatus=1 and (se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture') and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != '' - """, (item.item_code, self.work_order), as_dict=1) + """, + (item.item_code, self.work_order), + as_dict=1, + ) stock_qty = flt(item.qty) trans_qty = flt(transferred_materials[0].qty) @@ -305,8 +372,11 @@ class StockEntry(StockController): for item_code, qty_list in iteritems(item_wise_qty): total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty")) if self.fg_completed_qty != total: - frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different") - .format(frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty))) + frappe.throw( + _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( + frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty) + ) + ) def validate_difference_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -314,33 +384,53 @@ class StockEntry(StockController): for d in self.get("items"): if not d.expense_account: - frappe.throw(_("Please enter Difference Account or set default Stock Adjustment Account for company {0}") - .format(frappe.bold(self.company))) + frappe.throw( + _( + "Please enter Difference Account or set default Stock Adjustment Account for company {0}" + ).format(frappe.bold(self.company)) + ) - elif self.is_opening == "Yes" and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss": - frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry"), OpeningEntryAccountError) + elif ( + self.is_opening == "Yes" + and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss" + ): + frappe.throw( + _( + "Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry" + ), + OpeningEntryAccountError, + ) def validate_warehouse(self): """perform various (sometimes conditional) validations on warehouse""" - source_mandatory = ["Material Issue", "Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture", - "Material Consumption for Manufacture"] + source_mandatory = [ + "Material Issue", + "Material Transfer", + "Send to Subcontractor", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ] - target_mandatory = ["Material Receipt", "Material Transfer", "Send to Subcontractor", - "Material Transfer for Manufacture"] + target_mandatory = [ + "Material Receipt", + "Material Transfer", + "Send to Subcontractor", + "Material Transfer for Manufacture", + ] validate_for_manufacture = any([d.bom_no for d in self.get("items")]) if self.purpose in source_mandatory and self.purpose not in target_mandatory: self.to_warehouse = None - for d in self.get('items'): + for d in self.get("items"): d.t_warehouse = None elif self.purpose in target_mandatory and self.purpose not in source_mandatory: self.from_warehouse = None - for d in self.get('items'): + for d in self.get("items"): d.s_warehouse = None - for d in self.get('items'): + for d in self.get("items"): if not d.s_warehouse and not d.t_warehouse: d.s_warehouse = self.from_warehouse d.t_warehouse = self.to_warehouse @@ -357,7 +447,6 @@ class StockEntry(StockController): else: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) - if self.purpose == "Manufacture": if validate_for_manufacture: if d.is_finished_item or d.is_scrap_item or d.is_process_loss: @@ -369,18 +458,26 @@ class StockEntry(StockController): if not d.s_warehouse: frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx)) - if cstr(d.s_warehouse) == cstr(d.t_warehouse) and not self.purpose == "Material Transfer for Manufacture": + if ( + cstr(d.s_warehouse) == cstr(d.t_warehouse) + and not self.purpose == "Material Transfer for Manufacture" + ): frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx)) if not (d.s_warehouse or d.t_warehouse): frappe.throw(_("Atleast one warehouse is mandatory")) def validate_work_order(self): - if self.purpose in ("Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture"): + if self.purpose in ( + "Manufacture", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ): # check if work order is entered - if (self.purpose=="Manufacture" or self.purpose=="Material Consumption for Manufacture") \ - and self.work_order: + if ( + self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture" + ) and self.work_order: if not self.fg_completed_qty: frappe.throw(_("For Quantity (Manufactured Qty) is mandatory")) self.check_if_operations_completed() @@ -391,40 +488,66 @@ class StockEntry(StockController): def check_if_operations_completed(self): """Check if Time Sheets are completed against before manufacturing to capture operating costs.""" prod_order = frappe.get_doc("Work Order", self.work_order) - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) for d in prod_order.get("operations"): total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) - completed_qty = d.completed_qty + (allowance_percentage/100 * d.completed_qty) + completed_qty = d.completed_qty + (allowance_percentage / 100 * d.completed_qty) if total_completed_qty > flt(completed_qty): - job_card = frappe.db.get_value('Job Card', {'operation_id': d.name}, 'name') + job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name") if not job_card: - frappe.throw(_("Work Order {0}: Job Card not found for the operation {1}") - .format(self.work_order, d.operation)) + frappe.throw( + _("Work Order {0}: Job Card not found for the operation {1}").format( + self.work_order, d.operation + ) + ) - work_order_link = frappe.utils.get_link_to_form('Work Order', self.work_order) - job_card_link = frappe.utils.get_link_to_form('Job Card', job_card) - frappe.throw(_("Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}.") - .format(d.idx, frappe.bold(d.operation), frappe.bold(total_completed_qty), work_order_link, job_card_link), OperationsNotCompleteError) + work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order) + job_card_link = frappe.utils.get_link_to_form("Job Card", job_card) + frappe.throw( + _( + "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." + ).format( + d.idx, + frappe.bold(d.operation), + frappe.bold(total_completed_qty), + work_order_link, + job_card_link, + ), + OperationsNotCompleteError, + ) def check_duplicate_entry_for_work_order(self): - other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", { - "work_order": self.work_order, - "purpose": self.purpose, - "docstatus": ["!=", 2], - "name": ["!=", self.name] - }, "name")] + other_ste = [ + t[0] + for t in frappe.db.get_values( + "Stock Entry", + { + "work_order": self.work_order, + "purpose": self.purpose, + "docstatus": ["!=", 2], + "name": ["!=", self.name], + }, + "name", + ) + ] if other_ste: - production_item, qty = frappe.db.get_value("Work Order", - self.work_order, ["production_item", "qty"]) + production_item, qty = frappe.db.get_value( + "Work Order", self.work_order, ["production_item", "qty"] + ) args = other_ste + [production_item] - fg_qty_already_entered = frappe.db.sql("""select sum(transfer_qty) + fg_qty_already_entered = frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail` where parent in (%s) and item_code = %s - and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0] + and ifnull(s_warehouse,'')='' """ + % (", ".join(["%s" * len(other_ste)]), "%s"), + args, + )[0][0] if fg_qty_already_entered and fg_qty_already_entered >= qty: frappe.throw( _("Stock Entries already created for Work Order {0}: {1}").format( @@ -434,35 +557,58 @@ class StockEntry(StockController): ) def set_actual_qty(self): - allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) + allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - for d in self.get('items'): - previous_sle = get_previous_sle({ - "item_code": d.item_code, - "warehouse": d.s_warehouse or d.t_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time - }) + for d in self.get("items"): + previous_sle = get_previous_sle( + { + "item_code": d.item_code, + "warehouse": d.s_warehouse or d.t_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) # get actual stock at source warehouse d.actual_qty = previous_sle.get("qty_after_transaction") or 0 # validate qty during submit - if d.docstatus==1 and d.s_warehouse and not allow_negative_stock and flt(d.actual_qty, d.precision("actual_qty")) < flt(d.transfer_qty, d.precision("actual_qty")): - frappe.throw(_("Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})").format(d.idx, - frappe.bold(d.s_warehouse), formatdate(self.posting_date), - format_time(self.posting_time), frappe.bold(d.item_code)) - + '

    ' + _("Available quantity is {0}, you need {1}").format(frappe.bold(d.actual_qty), - frappe.bold(d.transfer_qty)), - NegativeStockError, title=_('Insufficient Stock')) + if ( + d.docstatus == 1 + and d.s_warehouse + and not allow_negative_stock + and flt(d.actual_qty, d.precision("actual_qty")) + < flt(d.transfer_qty, d.precision("actual_qty")) + ): + frappe.throw( + _( + "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})" + ).format( + d.idx, + frappe.bold(d.s_warehouse), + formatdate(self.posting_date), + format_time(self.posting_time), + frappe.bold(d.item_code), + ) + + "

    " + + _("Available quantity is {0}, you need {1}").format( + frappe.bold(d.actual_qty), frappe.bold(d.transfer_qty) + ), + NegativeStockError, + title=_("Insufficient Stock"), + ) def set_serial_nos(self, work_order): - previous_se = frappe.db.get_value("Stock Entry", {"work_order": work_order, - "purpose": "Material Transfer for Manufacture"}, "name") + previous_se = frappe.db.get_value( + "Stock Entry", + {"work_order": work_order, "purpose": "Material Transfer for Manufacture"}, + "name", + ) - for d in self.get('items'): - transferred_serial_no = frappe.db.get_value("Stock Entry Detail",{"parent": previous_se, - "item_code": d.item_code}, "serial_no") + for d in self.get("items"): + transferred_serial_no = frappe.db.get_value( + "Stock Entry Detail", {"parent": previous_se, "item_code": d.item_code}, "serial_no" + ) if transferred_serial_no: d.serial_no = transferred_serial_no @@ -470,8 +616,8 @@ class StockEntry(StockController): @frappe.whitelist() def get_stock_and_rate(self): """ - Updates rate and availability of all the items. - Called from Update Rate and Availability button. + Updates rate and availability of all the items. + Called from Update Rate and Availability button. """ self.set_work_order_details() self.set_transfer_qty() @@ -488,38 +634,51 @@ class StockEntry(StockController): def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): """ - Set rate for outgoing, scrapped and finished items + Set rate for outgoing, scrapped and finished items """ # Set rate for outgoing items - outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss) + outgoing_items_cost = self.set_rate_for_outgoing_items( + reset_outgoing_rate, raise_error_if_no_rate + ) + finished_item_qty = sum( + d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss + ) # Set basic rate for incoming items - for d in self.get('items'): - if d.s_warehouse or d.set_basic_rate_manually: continue + for d in self.get("items"): + if d.s_warehouse or d.set_basic_rate_manually: + continue if d.allow_zero_valuation_rate: d.basic_rate = 0.0 elif d.is_finished_item: if self.purpose == "Manufacture": - d.basic_rate = self.get_basic_rate_for_manufactured_item(finished_item_qty, outgoing_items_cost) + d.basic_rate = self.get_basic_rate_for_manufactured_item( + finished_item_qty, outgoing_items_cost + ) elif self.purpose == "Repack": d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) if not d.basic_rate and not d.allow_zero_valuation_rate: - d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse, - self.doctype, self.name, d.allow_zero_valuation_rate, - currency=erpnext.get_company_currency(self.company), company=self.company, - raise_error_if_no_rate=raise_error_if_no_rate) + d.basic_rate = get_valuation_rate( + d.item_code, + d.t_warehouse, + self.doctype, + self.name, + d.allow_zero_valuation_rate, + currency=erpnext.get_company_currency(self.company), + company=self.company, + raise_error_if_no_rate=raise_error_if_no_rate, + ) d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) if d.is_process_loss: - d.basic_rate = flt(0.) + d.basic_rate = flt(0.0) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): outgoing_items_cost = 0.0 - for d in self.get('items'): + for d in self.get("items"): if d.s_warehouse: if reset_outgoing_rate: args = self.get_args_for_incoming_rate(d) @@ -534,18 +693,20 @@ class StockEntry(StockController): return outgoing_items_cost def get_args_for_incoming_rate(self, item): - return frappe._dict({ - "item_code": item.item_code, - "warehouse": item.s_warehouse or item.t_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty), - "serial_no": item.serial_no, - "voucher_type": self.doctype, - "voucher_no": self.name, - "company": self.company, - "allow_zero_valuation": item.allow_zero_valuation_rate, - }) + return frappe._dict( + { + "item_code": item.item_code, + "warehouse": item.s_warehouse or item.t_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": item.s_warehouse and -1 * flt(item.transfer_qty) or flt(item.transfer_qty), + "serial_no": item.serial_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": item.allow_zero_valuation_rate, + } + ) def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost): finished_items = [d.item_code for d in self.get("items") if d.is_finished_item] @@ -561,9 +722,11 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): + if not outgoing_items_cost and frappe.db.get_single_value( + "Manufacturing Settings", "material_consumption", cache=True + ): bom_items = self.get_bom_raw_materials(finished_item_qty) - outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) + outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()]) return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty) @@ -595,8 +758,10 @@ class StockEntry(StockController): for d in self.get("items"): if d.transfer_qty: d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount")) - d.valuation_rate = flt(flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)), - d.precision("valuation_rate")) + d.valuation_rate = flt( + flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)), + d.precision("valuation_rate"), + ) def set_total_incoming_outgoing_value(self): self.total_incoming_value = self.total_outgoing_value = 0.0 @@ -610,92 +775,120 @@ class StockEntry(StockController): def set_total_amount(self): self.total_amount = None - if self.purpose not in ['Manufacture', 'Repack']: + if self.purpose not in ["Manufacture", "Repack"]: self.total_amount = sum([flt(item.amount) for item in self.get("items")]) def set_stock_entry_type(self): if self.purpose: - self.stock_entry_type = frappe.get_cached_value('Stock Entry Type', - {'purpose': self.purpose}, 'name') + self.stock_entry_type = frappe.get_cached_value( + "Stock Entry Type", {"purpose": self.purpose}, "name" + ) def set_purpose_for_stock_entry(self): if self.stock_entry_type and not self.purpose: - self.purpose = frappe.get_cached_value('Stock Entry Type', - self.stock_entry_type, 'purpose') + self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose") def validate_duplicate_serial_no(self): warehouse_wise_serial_nos = {} # In case of repack the source and target serial nos could be same - for warehouse in ['s_warehouse', 't_warehouse']: + for warehouse in ["s_warehouse", "t_warehouse"]: serial_nos = [] for row in self.items: - if not (row.serial_no and row.get(warehouse)): continue + if not (row.serial_no and row.get(warehouse)): + continue for sn in get_serial_nos(row.serial_no): if sn in serial_nos: - frappe.throw(_('The serial no {0} has added multiple times in the stock entry {1}') - .format(frappe.bold(sn), self.name)) + frappe.throw( + _("The serial no {0} has added multiple times in the stock entry {1}").format( + frappe.bold(sn), self.name + ) + ) serial_nos.append(sn) def validate_purchase_order(self): """Throw exception if more raw material is transferred against Purchase Order than in the raw materials supplied table""" - backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings", - "backflush_raw_materials_of_subcontract_based_on") + backflush_raw_materials_based_on = frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ) - qty_allowance = flt(frappe.db.get_single_value("Buying Settings", - "over_transfer_allowance")) + qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance")) - if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return + if not (self.purpose == "Send to Subcontractor" and self.purchase_order): + return - if (backflush_raw_materials_based_on == 'BOM'): + if backflush_raw_materials_based_on == "BOM": purchase_order = frappe.get_doc("Purchase Order", self.purchase_order) for se_item in self.items: item_code = se_item.original_item or se_item.item_code precision = cint(frappe.db.get_default("float_precision")) or 3 - required_qty = sum([flt(d.required_qty) for d in purchase_order.supplied_items \ - if d.rm_item_code == item_code]) + required_qty = sum( + [flt(d.required_qty) for d in purchase_order.supplied_items if d.rm_item_code == item_code] + ) - total_allowed = required_qty + (required_qty * (qty_allowance/100)) + total_allowed = required_qty + (required_qty * (qty_allowance / 100)) if not required_qty: - bom_no = frappe.db.get_value("Purchase Order Item", + bom_no = frappe.db.get_value( + "Purchase Order Item", {"parent": self.purchase_order, "item_code": se_item.subcontracted_item}, - "bom") + "bom", + ) if se_item.allow_alternative_item: - original_item_code = frappe.get_value("Item Alternative", {"alternative_item_code": item_code}, "item_code") + original_item_code = frappe.get_value( + "Item Alternative", {"alternative_item_code": item_code}, "item_code" + ) - required_qty = sum([flt(d.required_qty) for d in purchase_order.supplied_items \ - if d.rm_item_code == original_item_code]) + required_qty = sum( + [ + flt(d.required_qty) + for d in purchase_order.supplied_items + if d.rm_item_code == original_item_code + ] + ) - total_allowed = required_qty + (required_qty * (qty_allowance/100)) + total_allowed = required_qty + (required_qty * (qty_allowance / 100)) if not required_qty: - frappe.throw(_("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}") - .format(se_item.item_code, self.purchase_order)) - total_supplied = frappe.db.sql("""select sum(transfer_qty) + frappe.throw( + _("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format( + se_item.item_code, self.purchase_order + ) + ) + total_supplied = frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail`, `tabStock Entry` where `tabStock Entry`.purchase_order = %s and `tabStock Entry`.docstatus = 1 and `tabStock Entry Detail`.item_code = %s and `tabStock Entry Detail`.parent = `tabStock Entry`.name""", - (self.purchase_order, se_item.item_code))[0][0] + (self.purchase_order, se_item.item_code), + )[0][0] if flt(total_supplied, precision) > flt(total_allowed, precision): - frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}") - .format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order)) + frappe.throw( + _("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}").format( + se_item.idx, se_item.item_code, total_allowed, self.purchase_order + ) + ) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: - frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}") - .format(row.idx, frappe.bold(row.item_code))) + frappe.throw( + _("Row {0}: Subcontracted Item is mandatory for the raw material {1}").format( + row.idx, frappe.bold(row.item_code) + ) + ) elif not row.po_detail: filters = { - "parent": self.purchase_order, "docstatus": 1, - "rm_item_code": row.item_code, "main_item_code": row.subcontracted_item + "parent": self.purchase_order, + "docstatus": 1, + "rm_item_code": row.item_code, + "main_item_code": row.subcontracted_item, } po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name") @@ -703,7 +896,7 @@ class StockEntry(StockController): row.db_set("po_detail", po_detail) def validate_bom(self): - for d in self.get('items'): + for d in self.get("items"): if d.bom_no and d.is_finished_item: item_code = d.original_item or d.item_code validate_bom_no(item_code, d.bom_no) @@ -721,7 +914,7 @@ class StockEntry(StockController): for d in self.items: if d.t_warehouse and not d.s_warehouse: - if self.purpose=="Repack" or d.item_code == finished_item: + if self.purpose == "Repack" or d.item_code == finished_item: d.is_finished_item = 1 else: d.is_scrap_item = 1 @@ -740,19 +933,17 @@ class StockEntry(StockController): def validate_finished_goods(self): """ - 1. Check if FG exists (mfg, repack) - 2. Check if Multiple FG Items are present (mfg) - 3. Check FG Item and Qty against WO if present (mfg) + 1. Check if FG exists (mfg, repack) + 2. Check if Multiple FG Items are present (mfg) + 3. Check FG Item and Qty against WO if present (mfg) """ production_item, wo_qty, finished_items = None, 0, [] - wo_details = frappe.db.get_value( - "Work Order", self.work_order, ["production_item", "qty"] - ) + wo_details = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) if wo_details: production_item, wo_qty = wo_details - for d in self.get('items'): + for d in self.get("items"): if d.is_finished_item: if not self.work_order: # Independent MFG Entry/ Repack Entry, no WO to match against @@ -760,12 +951,16 @@ class StockEntry(StockController): continue if d.item_code != production_item: - frappe.throw(_("Finished Item {0} does not match with Work Order {1}") - .format(d.item_code, self.work_order) + frappe.throw( + _("Finished Item {0} does not match with Work Order {1}").format( + d.item_code, self.work_order + ) ) elif flt(d.transfer_qty) > flt(self.fg_completed_qty): - frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}") - .format(d.idx, d.transfer_qty, self.fg_completed_qty) + frappe.throw( + _("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format( + d.idx, d.transfer_qty, self.fg_completed_qty + ) ) finished_items.append(d.item_code) @@ -773,28 +968,31 @@ class StockEntry(StockController): if not finished_items: frappe.throw( msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), - title=_("Missing Finished Good"), exc=FinishedGoodError + title=_("Missing Finished Good"), + exc=FinishedGoodError, ) if self.purpose == "Manufacture": if len(set(finished_items)) > 1: frappe.throw( msg=_("Multiple items cannot be marked as finished item"), - title=_("Note"), exc=FinishedGoodError + title=_("Note"), + exc=FinishedGoodError, ) allowance_percentage = flt( frappe.db.get_single_value( - "Manufacturing Settings","overproduction_percentage_for_work_order" + "Manufacturing Settings", "overproduction_percentage_for_work_order" ) ) - allowed_qty = wo_qty + ((allowance_percentage/100) * wo_qty) + allowed_qty = wo_qty + ((allowance_percentage / 100) * wo_qty) # No work order could mean independent Manufacture entry, if so skip validation if self.work_order and self.fg_completed_qty > allowed_qty: frappe.throw( - _("For quantity {0} should not be greater than work order quantity {1}") - .format(flt(self.fg_completed_qty), wo_qty) + _("For quantity {0} should not be greater than work order quantity {1}").format( + flt(self.fg_completed_qty), wo_qty + ) ) def update_stock_ledger(self): @@ -816,35 +1014,38 @@ class StockEntry(StockController): def get_finished_item_row(self): finished_item_row = None if self.purpose in ("Manufacture", "Repack"): - for d in self.get('items'): + for d in self.get("items"): if d.is_finished_item: finished_item_row = d return finished_item_row def get_sle_for_source_warehouse(self, sl_entries, finished_item_row): - for d in self.get('items'): + for d in self.get("items"): if cstr(d.s_warehouse): - sle = self.get_sl_entries(d, { - "warehouse": cstr(d.s_warehouse), - "actual_qty": -flt(d.transfer_qty), - "incoming_rate": 0 - }) + sle = self.get_sl_entries( + d, {"warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0} + ) if cstr(d.t_warehouse): sle.dependant_sle_voucher_detail_no = d.name - elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse): + elif finished_item_row and ( + finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse + ): sle.dependant_sle_voucher_detail_no = finished_item_row.name sl_entries.append(sle) def get_sle_for_target_warehouse(self, sl_entries, finished_item_row): - for d in self.get('items'): + for d in self.get("items"): if cstr(d.t_warehouse): - sle = self.get_sl_entries(d, { - "warehouse": cstr(d.t_warehouse), - "actual_qty": flt(d.transfer_qty), - "incoming_rate": flt(d.valuation_rate) - }) + sle = self.get_sl_entries( + d, + { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate), + }, + ) if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name): sle.recalculate_rate = 1 @@ -874,40 +1075,55 @@ class StockEntry(StockController): continue item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) - item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, { - "amount": 0.0, - "base_amount": 0.0 - }) + item_account_wise_additional_cost[(d.item_code, d.name)].setdefault( + t.expense_account, {"amount": 0.0, "base_amount": 0.0} + ) multiply_based_on = d.basic_amount if total_basic_amount else d.qty - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += ( flt(t.amount * multiply_based_on) / divide_based_on + ) - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += ( flt(t.base_amount * multiply_based_on) / divide_based_on + ) if item_account_wise_additional_cost: for d in self.get("items"): - for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})): - if not amount: continue + for account, amount in iteritems( + item_account_wise_additional_cost.get((d.item_code, d.name), {}) + ): + if not amount: + continue - gl_entries.append(self.get_gl_dict({ - "account": account, - "against": d.expense_account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit_in_account_currency": flt(amount["amount"]), - "credit": flt(amount["base_amount"]) - }, item=d)) + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": d.expense_account, + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit_in_account_currency": flt(amount["amount"]), + "credit": flt(amount["base_amount"]), + }, + item=d, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": d.expense_account, - "against": account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": -1 * amount['base_amount'] # put it as negative credit instead of debit purposefully - }, item=d)) + gl_entries.append( + self.get_gl_dict( + { + "account": d.expense_account, + "against": account, + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": -1 + * amount["base_amount"], # put it as negative credit instead of debit purposefully + }, + item=d, + ) + ) return process_gl_map(gl_entries) @@ -916,11 +1132,13 @@ class StockEntry(StockController): if flt(pro_doc.docstatus) != 1: frappe.throw(_("Work Order {0} must be submitted").format(self.work_order)) - if pro_doc.status == 'Stopped': - frappe.throw(_("Transaction not allowed against stopped Work Order {0}").format(self.work_order)) + if pro_doc.status == "Stopped": + frappe.throw( + _("Transaction not allowed against stopped Work Order {0}").format(self.work_order) + ) if self.job_card: - job_doc = frappe.get_doc('Job Card', self.job_card) + job_doc = frappe.get_doc("Job Card", self.job_card) job_doc.set_transferred_qty(update_status=True) job_doc.set_transferred_qty_in_job_card(self) @@ -940,73 +1158,95 @@ class StockEntry(StockController): @frappe.whitelist() def get_item_details(self, args=None, for_update=False): - item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, + item = frappe.db.sql( + """select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item, id.expense_account, id.buying_cost_center from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s where i.name=%s and i.disabled=0 and (i.end_of_life is null or i.end_of_life='0000-00-00' or i.end_of_life > %s)""", - (self.company, args.get('item_code'), nowdate()), as_dict = 1) + (self.company, args.get("item_code"), nowdate()), + as_dict=1, + ) if not item: - frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code"))) + frappe.throw( + _("Item {0} is not active or end of life has been reached").format(args.get("item_code")) + ) item = item[0] item_group_defaults = get_item_group_defaults(item.name, self.company) brand_defaults = get_brand_defaults(item.name, self.company) - ret = frappe._dict({ - 'uom' : item.stock_uom, - 'stock_uom' : item.stock_uom, - 'description' : item.description, - 'image' : item.image, - 'item_name' : item.item_name, - 'cost_center' : get_default_cost_center(args, item, item_group_defaults, brand_defaults, self.company), - 'qty' : args.get("qty"), - 'transfer_qty' : args.get('qty'), - 'conversion_factor' : 1, - 'batch_no' : '', - 'actual_qty' : 0, - 'basic_rate' : 0, - 'serial_no' : '', - 'has_serial_no' : item.has_serial_no, - 'has_batch_no' : item.has_batch_no, - 'sample_quantity' : item.sample_quantity, - 'expense_account' : item.expense_account - }) + ret = frappe._dict( + { + "uom": item.stock_uom, + "stock_uom": item.stock_uom, + "description": item.description, + "image": item.image, + "item_name": item.item_name, + "cost_center": get_default_cost_center( + args, item, item_group_defaults, brand_defaults, self.company + ), + "qty": args.get("qty"), + "transfer_qty": args.get("qty"), + "conversion_factor": 1, + "batch_no": "", + "actual_qty": 0, + "basic_rate": 0, + "serial_no": "", + "has_serial_no": item.has_serial_no, + "has_batch_no": item.has_batch_no, + "sample_quantity": item.sample_quantity, + "expense_account": item.expense_account, + } + ) - if self.purpose == 'Send to Subcontractor': + if self.purpose == "Send to Subcontractor": ret["allow_alternative_item"] = item.allow_alternative_item # update uom if args.get("uom") and for_update: - ret.update(get_uom_details(args.get('item_code'), args.get('uom'), args.get('qty'))) + ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty"))) - if self.purpose == 'Material Issue': - ret["expense_account"] = (item.get("expense_account") or - item_group_defaults.get("expense_account") or - frappe.get_cached_value('Company', self.company, "default_expense_account")) + if self.purpose == "Material Issue": + ret["expense_account"] = ( + item.get("expense_account") + or item_group_defaults.get("expense_account") + or frappe.get_cached_value("Company", self.company, "default_expense_account") + ) - for company_field, field in {'stock_adjustment_account': 'expense_account', - 'cost_center': 'cost_center'}.items(): + for company_field, field in { + "stock_adjustment_account": "expense_account", + "cost_center": "cost_center", + }.items(): if not ret.get(field): - ret[field] = frappe.get_cached_value('Company', self.company, company_field) + ret[field] = frappe.get_cached_value("Company", self.company, company_field) - args['posting_date'] = self.posting_date - args['posting_time'] = self.posting_time + args["posting_date"] = self.posting_date + args["posting_time"] = self.posting_time - stock_and_rate = get_warehouse_details(args) if args.get('warehouse') else {} + stock_and_rate = get_warehouse_details(args) if args.get("warehouse") else {} ret.update(stock_and_rate) # automatically select batch for outgoing item - if (args.get('s_warehouse', None) and args.get('qty') and - ret.get('has_batch_no') and not args.get('batch_no')): - args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty']) + if ( + args.get("s_warehouse", None) + and args.get("qty") + and ret.get("has_batch_no") + and not args.get("batch_no") + ): + args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"]) - if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'): - subcontract_items = frappe.get_all("Purchase Order Item Supplied", - {"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code") + if ( + self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code") + ): + subcontract_items = frappe.get_all( + "Purchase Order Item Supplied", + {"parent": self.purchase_order, "rm_item_code": args.get("item_code")}, + "main_item_code", + ) if subcontract_items and len(subcontract_items) == 1: ret["subcontracted_item"] = subcontract_items[0].main_item_code @@ -1017,46 +1257,57 @@ class StockEntry(StockController): def set_items_for_stock_in(self): self.items = [] - if self.outgoing_stock_entry and self.purpose == 'Material Transfer': - doc = frappe.get_doc('Stock Entry', self.outgoing_stock_entry) + if self.outgoing_stock_entry and self.purpose == "Material Transfer": + doc = frappe.get_doc("Stock Entry", self.outgoing_stock_entry) if doc.per_transferred == 100: - frappe.throw(_("Goods are already received against the outward entry {0}") - .format(doc.name)) + frappe.throw(_("Goods are already received against the outward entry {0}").format(doc.name)) for d in doc.items: - self.append('items', { - 's_warehouse': d.t_warehouse, - 'item_code': d.item_code, - 'qty': d.qty, - 'uom': d.uom, - 'against_stock_entry': d.parent, - 'ste_detail': d.name, - 'stock_uom': d.stock_uom, - 'conversion_factor': d.conversion_factor, - 'serial_no': d.serial_no, - 'batch_no': d.batch_no - }) + self.append( + "items", + { + "s_warehouse": d.t_warehouse, + "item_code": d.item_code, + "qty": d.qty, + "uom": d.uom, + "against_stock_entry": d.parent, + "ste_detail": d.name, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "serial_no": d.serial_no, + "batch_no": d.batch_no, + }, + ) @frappe.whitelist() def get_items(self): - self.set('items', []) + self.set("items", []) self.validate_work_order() if not self.posting_date or not self.posting_time: frappe.throw(_("Posting date and posting time is mandatory")) self.set_work_order_details() - self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings", - "backflush_raw_materials_based_on") + self.flags.backflush_based_on = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) if self.bom_no: - backflush_based_on = frappe.db.get_single_value("Manufacturing Settings", - "backflush_raw_materials_based_on") + backflush_based_on = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) - if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack", - "Send to Subcontractor", "Material Transfer for Manufacture", "Material Consumption for Manufacture"]: + if self.purpose in [ + "Material Issue", + "Material Transfer", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ]: if self.work_order and self.purpose == "Material Transfer for Manufacture": item_dict = self.get_pending_raw_materials(backflush_based_on) @@ -1065,14 +1316,20 @@ class StockEntry(StockController): item["to_warehouse"] = self.pro_doc.wip_warehouse self.add_to_stock_entry_detail(item_dict) - elif (self.work_order and (self.purpose == "Manufacture" - or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer - and self.flags.backflush_based_on == "Material Transferred for Manufacture"): + elif ( + self.work_order + and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + and not self.pro_doc.skip_transfer + and self.flags.backflush_based_on == "Material Transferred for Manufacture" + ): self.get_transfered_raw_materials() - elif (self.work_order and (self.purpose == "Manufacture" or - self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM" - and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1): + elif ( + self.work_order + and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + and self.flags.backflush_based_on == "BOM" + and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1 + ): self.get_unconsumed_raw_materials() else: @@ -1081,31 +1338,36 @@ class StockEntry(StockController): item_dict = self.get_bom_raw_materials(self.fg_completed_qty) - #Get PO Supplied Items Details + # Get PO Supplied Items Details if self.purchase_order and self.purpose == "Send to Subcontractor": - #Get PO Supplied Items Details - item_wh = frappe._dict(frappe.db.sql(""" + # Get PO Supplied Items Details + item_wh = frappe._dict( + frappe.db.sql( + """ SELECT rm_item_code, reserve_warehouse FROM `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup WHERE - po.name = poitemsup.parent and po.name = %s """,self.purchase_order)) + po.name = poitemsup.parent and po.name = %s """, + self.purchase_order, + ) + ) for item in itervalues(item_dict): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): item["from_warehouse"] = self.pro_doc.wip_warehouse - #Get Reserve Warehouse from PO - if self.purchase_order and self.purpose=="Send to Subcontractor": + # Get Reserve Warehouse from PO + if self.purchase_order and self.purpose == "Send to Subcontractor": item["from_warehouse"] = item_wh.get(item.item_code) - item["to_warehouse"] = self.to_warehouse if self.purpose=="Send to Subcontractor" else "" + item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else "" self.add_to_stock_entry_detail(item_dict) # fetch the serial_no of the first stock entry for the second stock entry if self.work_order and self.purpose == "Manufacture": self.set_serial_nos(self.work_order) - work_order = frappe.get_doc('Work Order', self.work_order) + work_order = frappe.get_doc("Work Order", self.work_order) add_additional_cost(self, work_order) # add finished goods item @@ -1121,8 +1383,8 @@ class StockEntry(StockController): def set_scrap_items(self): if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) - for item in itervalues(scrap_item_dict): - item.idx = '' + + for item in scrap_item_dict.values(): if self.pro_doc and self.pro_doc.scrap_warehouse: item["to_warehouse"] = self.pro_doc.scrap_warehouse @@ -1135,7 +1397,7 @@ class StockEntry(StockController): if self.work_order: # common validations if not self.pro_doc: - self.pro_doc = frappe.get_doc('Work Order', self.work_order) + self.pro_doc = frappe.get_doc("Work Order", self.work_order) if self.pro_doc: self.bom_no = self.pro_doc.bom_no @@ -1166,11 +1428,18 @@ class StockEntry(StockController): "stock_uom": item.stock_uom, "expense_account": item.get("expense_account"), "cost_center": item.get("buying_cost_center"), - "is_finished_item": 1 + "is_finished_item": 1, } - if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings', - 'make_serial_no_batch_from_work_order', cache=True)): + if ( + self.work_order + and self.pro_doc.has_batch_no + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True + ) + ) + ): self.set_batchwise_finished_goods(args, item) else: self.add_finished_goods(args, item) @@ -1179,12 +1448,12 @@ class StockEntry(StockController): filters = { "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, - "qty_to_produce": (">", 0) + "qty_to_produce": (">", 0), } fields = ["qty_to_produce as qty", "produced_qty", "name"] - data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc") + data = frappe.get_all("Batch", filters=filters, fields=fields, order_by="creation asc") if not data: self.add_finished_goods(args, item) @@ -1199,7 +1468,7 @@ class StockEntry(StockController): if not batch_qty: continue - if qty <=0: + if qty <= 0: break fg_qty = batch_qty @@ -1213,23 +1482,27 @@ class StockEntry(StockController): self.add_finished_goods(args, item) def add_finished_goods(self, args, item): - self.add_to_stock_entry_detail({ - item.name: args - }, bom_no = self.bom_no) + self.add_to_stock_entry_detail({item.name: args}, bom_no=self.bom_no) def get_bom_raw_materials(self, qty): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict # item dict = { item_code: {qty, description, stock_uom} } - item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, - fetch_exploded = self.use_multi_level_bom, fetch_qty_in_stock_uom=False) + item_dict = get_bom_items_as_dict( + self.bom_no, + self.company, + qty=qty, + fetch_exploded=self.use_multi_level_bom, + fetch_qty_in_stock_uom=False, + ) - used_alternative_items = get_used_alternative_items(work_order = self.work_order) + used_alternative_items = get_used_alternative_items(work_order=self.work_order) for item in itervalues(item_dict): # if source warehouse presents in BOM set from_warehouse as bom source_warehouse if item["allow_alternative_item"]: - item["allow_alternative_item"] = frappe.db.get_value('Work Order', - self.work_order, "allow_alternative_item") + item["allow_alternative_item"] = frappe.db.get_value( + "Work Order", self.work_order, "allow_alternative_item" + ) item.from_warehouse = self.from_warehouse or item.source_warehouse or item.default_warehouse if item.item_code in used_alternative_items: @@ -1247,8 +1520,10 @@ class StockEntry(StockController): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict # item dict = { item_code: {qty, description, stock_uom} } - item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, - fetch_exploded = 0, fetch_scrap_items = 1) or {} + item_dict = ( + get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) + or {} + ) for item in itervalues(item_dict): item.from_warehouse = "" @@ -1262,16 +1537,18 @@ class StockEntry(StockController): if not item_row: item_row = frappe._dict({}) - item_row.update({ - 'uom': row.stock_uom, - 'from_warehouse': '', - 'qty': row.stock_qty + flt(item_row.stock_qty), - 'converison_factor': 1, - 'is_scrap_item': 1, - 'item_name': row.item_name, - 'description': row.description, - 'allow_zero_valuation_rate': 1 - }) + item_row.update( + { + "uom": row.stock_uom, + "from_warehouse": "", + "qty": row.stock_qty + flt(item_row.stock_qty), + "converison_factor": 1, + "is_scrap_item": 1, + "item_name": row.item_name, + "description": row.description, + "allow_zero_valuation_rate": 1, + } + ) item_dict[row.item_code] = item_row @@ -1284,21 +1561,25 @@ class StockEntry(StockController): if not self.pro_doc.operations: return [] - job_card = frappe.qb.DocType('Job Card') - job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item') + job_card = frappe.qb.DocType("Job Card") + job_card_scrap_item = frappe.qb.DocType("Job Card Scrap Item") scrap_items = ( frappe.qb.from_(job_card) .select( - Sum(job_card_scrap_item.stock_qty).as_('stock_qty'), - job_card_scrap_item.item_code, job_card_scrap_item.item_name, - job_card_scrap_item.description, job_card_scrap_item.stock_uom) + Sum(job_card_scrap_item.stock_qty).as_("stock_qty"), + job_card_scrap_item.item_code, + job_card_scrap_item.item_name, + job_card_scrap_item.description, + job_card_scrap_item.stock_uom, + ) .join(job_card_scrap_item) .on(job_card_scrap_item.parent == job_card.name) .where( (job_card_scrap_item.item_code.isnotnull()) & (job_card.work_order == self.work_order) - & (job_card.docstatus == 1)) + & (job_card.docstatus == 1) + ) .groupby(job_card_scrap_item.item_code) ).run(as_dict=1) @@ -1312,7 +1593,7 @@ class StockEntry(StockController): if used_scrap_items.get(row.item_code): used_scrap_items[row.item_code] -= row.stock_qty - if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')): + if cint(frappe.get_cached_value("UOM", row.stock_uom, "must_be_whole_number")): row.stock_qty = frappe.utils.ceil(row.stock_qty) return scrap_items @@ -1323,16 +1604,14 @@ class StockEntry(StockController): def get_used_scrap_items(self): used_scrap_items = defaultdict(float) data = frappe.get_all( - 'Stock Entry', - fields = [ - '`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`' + "Stock Entry", + fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"], + filters=[ + ["Stock Entry", "work_order", "=", self.work_order], + ["Stock Entry Detail", "is_scrap_item", "=", 1], + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purpose", "in", ["Repack", "Manufacture"]], ], - filters = [ - ['Stock Entry', 'work_order', '=', self.work_order], - ['Stock Entry Detail', 'is_scrap_item', '=', 1], - ['Stock Entry', 'docstatus', '=', 1], - ['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']] - ] ) for row in data: @@ -1342,10 +1621,11 @@ class StockEntry(StockController): def get_unconsumed_raw_materials(self): wo = frappe.get_doc("Work Order", self.work_order) - wo_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order}, - fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"] - ) + wo_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order}, + fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"], + ) work_order_qty = wo.material_transferred_for_manufacturing or wo.qty for item in wo_items: @@ -1362,21 +1642,24 @@ class StockEntry(StockController): qty = req_qty_each * flt(self.fg_completed_qty) if qty > 0: - self.add_to_stock_entry_detail({ - item.item_code: { - "from_warehouse": wo.wip_warehouse or item.source_warehouse, - "to_warehouse": "", - "qty": qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item_account_details.stock_uom, - "expense_account": item_account_details.get("expense_account"), - "cost_center": item_account_details.get("buying_cost_center"), + self.add_to_stock_entry_detail( + { + item.item_code: { + "from_warehouse": wo.wip_warehouse or item.source_warehouse, + "to_warehouse": "", + "qty": qty, + "item_name": item.item_name, + "description": item.description, + "stock_uom": item_account_details.stock_uom, + "expense_account": item_account_details.get("expense_account"), + "cost_center": item_account_details.get("buying_cost_center"), + } } - }) + ) def get_transfered_raw_materials(self): - transferred_materials = frappe.db.sql(""" + transferred_materials = frappe.db.sql( + """ select item_name, original_item, item_code, sum(qty) as qty, sed.t_warehouse as warehouse, description, stock_uom, expense_account, cost_center @@ -1385,9 +1668,13 @@ class StockEntry(StockController): se.name = sed.parent and se.docstatus=1 and se.purpose='Material Transfer for Manufacture' and se.work_order= %s and ifnull(sed.t_warehouse, '') != '' group by sed.item_code, sed.t_warehouse - """, self.work_order, as_dict=1) + """, + self.work_order, + as_dict=1, + ) - materials_already_backflushed = frappe.db.sql(""" + materials_already_backflushed = frappe.db.sql( + """ select item_code, sed.s_warehouse as warehouse, sum(qty) as qty from @@ -1397,26 +1684,34 @@ class StockEntry(StockController): and (se.purpose='Manufacture' or se.purpose='Material Consumption for Manufacture') and se.work_order= %s and ifnull(sed.s_warehouse, '') != '' group by sed.item_code, sed.s_warehouse - """, self.work_order, as_dict=1) + """, + self.work_order, + as_dict=1, + ) - backflushed_materials= {} + backflushed_materials = {} for d in materials_already_backflushed: - backflushed_materials.setdefault(d.item_code,[]).append({d.warehouse: d.qty}) + backflushed_materials.setdefault(d.item_code, []).append({d.warehouse: d.qty}) - po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from - `tabWork Order` where name=%s""", self.work_order, as_dict=1)[0] + po_qty = frappe.db.sql( + """select qty, produced_qty, material_transferred_for_manufacturing from + `tabWork Order` where name=%s""", + self.work_order, + as_dict=1, + )[0] manufacturing_qty = flt(po_qty.qty) or 1 produced_qty = flt(po_qty.produced_qty) trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1 for item in transferred_materials: - qty= item.qty + qty = item.qty item_code = item.original_item or item.item_code - req_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order, 'item_code': item_code}, - fields=["required_qty", "consumed_qty"] - ) + req_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order, "item_code": item_code}, + fields=["required_qty", "consumed_qty"], + ) req_qty = flt(req_items[0].required_qty) if req_items else flt(4) req_qty_each = flt(req_qty / manufacturing_qty) @@ -1424,23 +1719,23 @@ class StockEntry(StockController): if trans_qty and manufacturing_qty > (produced_qty + flt(self.fg_completed_qty)): if qty >= req_qty: - qty = (req_qty/trans_qty) * flt(self.fg_completed_qty) + qty = (req_qty / trans_qty) * flt(self.fg_completed_qty) else: qty = qty - consumed_qty - if self.purpose == 'Manufacture': + if self.purpose == "Manufacture": # If Material Consumption is booked, must pull only remaining components to finish product if consumed_qty != 0: remaining_qty = consumed_qty - (produced_qty * req_qty_each) exhaust_qty = req_qty_each * produced_qty - if remaining_qty > exhaust_qty : - if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1: - qty =0 + if remaining_qty > exhaust_qty: + if (remaining_qty / (req_qty_each * flt(self.fg_completed_qty))) >= 1: + qty = 0 else: qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty else: if self.flags.backflush_based_on == "Material Transferred for Manufacture": - qty = (item.qty/trans_qty) * flt(self.fg_completed_qty) + qty = (item.qty / trans_qty) * flt(self.fg_completed_qty) else: qty = req_qty_each * flt(self.fg_completed_qty) @@ -1448,45 +1743,51 @@ class StockEntry(StockController): precision = frappe.get_precision("Stock Entry Detail", "qty") for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse) > 0: - if (qty > req_qty): - qty = ((flt(qty, precision) - flt(d.get(item.warehouse), precision)) + if qty > req_qty: + qty = ( + (flt(qty, precision) - flt(d.get(item.warehouse), precision)) / (flt(trans_qty, precision) - flt(produced_qty, precision)) ) * flt(self.fg_completed_qty) d[item.warehouse] -= qty - if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): + if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")): qty = frappe.utils.ceil(qty) if qty > 0: - self.add_to_stock_entry_detail({ - item.item_code: { - "from_warehouse": item.warehouse, - "to_warehouse": "", - "qty": qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item.stock_uom, - "expense_account": item.expense_account, - "cost_center": item.buying_cost_center, - "original_item": item.original_item + self.add_to_stock_entry_detail( + { + item.item_code: { + "from_warehouse": item.warehouse, + "to_warehouse": "", + "qty": qty, + "item_name": item.item_name, + "description": item.description, + "stock_uom": item.stock_uom, + "expense_account": item.expense_account, + "cost_center": item.buying_cost_center, + "original_item": item.original_item, + } } - }) + ) def get_pending_raw_materials(self, backflush_based_on=None): """ - issue (item quantity) that is pending to issue or desire to transfer, - whichever is less + issue (item quantity) that is pending to issue or desire to transfer, + whichever is less """ item_dict = self.get_pro_order_required_items(backflush_based_on) max_qty = flt(self.pro_doc.qty) allow_overproduction = False - overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + overproduction_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) - to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty) + to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt( + self.fg_completed_qty + ) transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100) if transfer_limit_qty >= to_transfer_qty: @@ -1496,10 +1797,14 @@ class StockEntry(StockController): pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty) desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty - if (desire_to_transfer <= pending_to_issue + if ( + desire_to_transfer <= pending_to_issue or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture") - or allow_overproduction): - item_dict[item]["qty"] = desire_to_transfer + or allow_overproduction + ): + # "No need for transfer but qty still pending to transfer" case can occur + # when transferring multiple RM in different Stock Entries + item_dict[item]["qty"] = desire_to_transfer if (desire_to_transfer > 0) else pending_to_issue elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue else: @@ -1519,7 +1824,7 @@ class StockEntry(StockController): def get_pro_order_required_items(self, backflush_based_on=None): """ - Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**. + Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**. """ item_dict, job_card_items = frappe._dict(), [] work_order = frappe.get_doc("Work Order", self.work_order) @@ -1538,7 +1843,9 @@ class StockEntry(StockController): continue transfer_pending = flt(d.required_qty) > flt(d.transferred_qty) - can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture") + can_transfer = transfer_pending or ( + backflush_based_on == "Material Transferred for Manufacture" + ) if not can_transfer: continue @@ -1549,11 +1856,7 @@ class StockEntry(StockController): if consider_job_card: job_card_item = frappe.db.get_value( - "Job Card Item", - { - "item_code": d.item_code, - "parent": self.get("job_card") - } + "Job Card Item", {"item_code": d.item_code, "parent": self.get("job_card")} ) item_row["job_card_item"] = job_card_item or None @@ -1573,12 +1876,7 @@ class StockEntry(StockController): return [] job_card_items = frappe.get_all( - "Job Card Item", - filters={ - "parent": job_card - }, - fields=["item_code"], - distinct=True + "Job Card Item", filters={"parent": job_card}, fields=["item_code"], distinct=True ) return [d.item_code for d in job_card_items] @@ -1587,60 +1885,86 @@ class StockEntry(StockController): item_row = item_dict[d] stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") - se_child = self.append('items') + se_child = self.append("items") se_child.s_warehouse = item_row.get("from_warehouse") se_child.t_warehouse = item_row.get("to_warehouse") - se_child.item_code = item_row.get('item_code') or cstr(d) + se_child.item_code = item_row.get("item_code") or cstr(d) se_child.uom = item_row["uom"] if item_row.get("uom") else stock_uom se_child.stock_uom = stock_uom se_child.qty = flt(item_row["qty"], se_child.precision("qty")) se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0) se_child.subcontracted_item = item_row.get("main_item_code") - se_child.cost_center = (item_row.get("cost_center") or - get_default_cost_center(item_row, company = self.company)) + se_child.cost_center = item_row.get("cost_center") or get_default_cost_center( + item_row, company=self.company + ) se_child.is_finished_item = item_row.get("is_finished_item", 0) se_child.is_scrap_item = item_row.get("is_scrap_item", 0) se_child.is_process_loss = item_row.get("is_process_loss", 0) - for field in ["idx", "po_detail", "original_item", "expense_account", - "description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]: + for field in [ + "po_detail", + "original_item", + "expense_account", + "description", + "item_name", + "serial_no", + "batch_no", + "allow_zero_valuation_rate", + ]: if item_row.get(field): se_child.set(field, item_row.get(field)) - if se_child.s_warehouse==None: + if se_child.s_warehouse == None: se_child.s_warehouse = self.from_warehouse - if se_child.t_warehouse==None: + if se_child.t_warehouse == None: se_child.t_warehouse = self.to_warehouse # in stock uom se_child.conversion_factor = flt(item_row.get("conversion_factor")) or 1 - se_child.transfer_qty = flt(item_row["qty"]*se_child.conversion_factor, se_child.precision("qty")) + se_child.transfer_qty = flt( + item_row["qty"] * se_child.conversion_factor, se_child.precision("qty") + ) - se_child.bom_no = bom_no # to be assigned for finished item + se_child.bom_no = bom_no # to be assigned for finished item se_child.job_card_item = item_row.get("job_card_item") if self.get("job_card") else None def validate_with_material_request(self): for item in self.get("items"): material_request = item.material_request or None material_request_item = item.material_request_item or None - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - parent_se = frappe.get_value("Stock Entry Detail", item.ste_detail, ['material_request','material_request_item'],as_dict=True) + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + parent_se = frappe.get_value( + "Stock Entry Detail", + item.ste_detail, + ["material_request", "material_request_item"], + as_dict=True, + ) if parent_se: material_request = parent_se.material_request material_request_item = parent_se.material_request_item if material_request: - mreq_item = frappe.db.get_value("Material Request Item", + mreq_item = frappe.db.get_value( + "Material Request Item", {"name": material_request_item, "parent": material_request}, - ["item_code", "warehouse", "idx"], as_dict=True) + ["item_code", "warehouse", "idx"], + as_dict=True, + ) if mreq_item.item_code != item.item_code: - frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx), - frappe.MappingMismatchError) + frappe.throw( + _("Item for row {0} does not match Material Request").format(item.idx), + frappe.MappingMismatchError, + ) elif self.purpose == "Material Transfer" and self.add_to_transit: continue def validate_batch(self): - if self.purpose in ["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"]: + if self.purpose in [ + "Material Transfer for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + ]: for item in self.get("items"): if item.batch_no: disabled = frappe.db.get_value("Batch", item.batch_no, "disabled") @@ -1648,30 +1972,34 @@ class StockEntry(StockController): expiry_date = frappe.db.get_value("Batch", item.batch_no, "expiry_date") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): - frappe.throw(_("Batch {0} of Item {1} has expired.") - .format(item.batch_no, item.item_code)) + frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code)) else: - frappe.throw(_("Batch {0} of Item {1} is disabled.") - .format(item.batch_no, item.item_code)) + frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)) def update_purchase_order_supplied_items(self): - if (self.purchase_order and - (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)): + if self.purchase_order and ( + self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return + ): - #Get PO Supplied Items Details - item_wh = frappe._dict(frappe.db.sql(""" + # Get PO Supplied Items Details + item_wh = frappe._dict( + frappe.db.sql( + """ select rm_item_code, reserve_warehouse from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup where po.name = poitemsup.parent - and po.name = %s""", self.purchase_order)) + and po.name = %s""", + self.purchase_order, + ) + ) supplied_items = get_supplied_items(self.purchase_order) for name, item in supplied_items.items(): - frappe.db.set_value('Purchase Order Item Supplied', name, item) + frappe.db.set_value("Purchase Order Item Supplied", name, item) - #Update reserved sub contracted quantity in bin based on Supplied Item Details and + # Update reserved sub contracted quantity in bin based on Supplied Item Details and for d in self.get("items"): - item_code = d.get('original_item') or d.get('item_code') + item_code = d.get("original_item") or d.get("item_code") reserve_warehouse = item_wh.get(item_code) if not (reserve_warehouse and item_code): continue @@ -1679,12 +2007,17 @@ class StockEntry(StockController): stock_bin.update_reserved_qty_for_sub_contracting() def update_so_in_serial_number(self): - so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"]) + so_name, item_code = frappe.db.get_value( + "Work Order", self.work_order, ["sales_order", "production_item"] + ) if so_name and item_code: qty_to_reserve = get_reserved_qty_for_so(so_name, item_code) if qty_to_reserve: - reserved_qty = frappe.db.sql("""select count(name) from `tabSerial No` where item_code=%s and - sales_order=%s""", (item_code, so_name)) + reserved_qty = frappe.db.sql( + """select count(name) from `tabSerial No` where item_code=%s and + sales_order=%s""", + (item_code, so_name), + ) if reserved_qty and reserved_qty[0][0]: qty_to_reserve -= reserved_qty[0][0] if qty_to_reserve > 0: @@ -1695,7 +2028,7 @@ class StockEntry(StockController): for serial_no in serial_nos: if qty_to_reserve > 0: frappe.db.set_value("Serial No", serial_no, "sales_order", so_name) - qty_to_reserve -=1 + qty_to_reserve -= 1 def validate_reserved_serial_no_consumption(self): for item in self.items: @@ -1703,13 +2036,14 @@ class StockEntry(StockController): for sr in get_serial_nos(item.serial_no): sales_order = frappe.db.get_value("Serial No", sr, "sales_order") if sales_order: - msg = (_("(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}.") - .format(sr, sales_order)) + msg = _( + "(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}." + ).format(sr, sales_order) frappe.throw(_("Item {0} {1}").format(item.item_code, msg)) def update_transferred_qty(self): - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: stock_entries = {} stock_entries_child_list = [] for d in self.items: @@ -1717,70 +2051,87 @@ class StockEntry(StockController): continue stock_entries_child_list.append(d.ste_detail) - transferred_qty = frappe.get_all("Stock Entry Detail", fields = ["sum(qty) as qty"], - filters = { 'against_stock_entry': d.against_stock_entry, - 'ste_detail': d.ste_detail,'docstatus': 1}) + transferred_qty = frappe.get_all( + "Stock Entry Detail", + fields=["sum(qty) as qty"], + filters={ + "against_stock_entry": d.against_stock_entry, + "ste_detail": d.ste_detail, + "docstatus": 1, + }, + ) - stock_entries[(d.against_stock_entry, d.ste_detail)] = (transferred_qty[0].qty - if transferred_qty and transferred_qty[0] else 0.0) or 0.0 + stock_entries[(d.against_stock_entry, d.ste_detail)] = ( + transferred_qty[0].qty if transferred_qty and transferred_qty[0] else 0.0 + ) or 0.0 - if not stock_entries: return None + if not stock_entries: + return None - cond = '' + cond = "" for data, transferred_qty in stock_entries.items(): cond += """ WHEN (parent = %s and name = %s) THEN %s - """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty) + """ % ( + frappe.db.escape(data[0]), + frappe.db.escape(data[1]), + transferred_qty, + ) if stock_entries_child_list: - frappe.db.sql(""" UPDATE `tabStock Entry Detail` + frappe.db.sql( + """ UPDATE `tabStock Entry Detail` SET transferred_qty = CASE {cond} END WHERE - name in ({ste_details}) """.format(cond=cond, - ste_details = ','.join(['%s'] * len(stock_entries_child_list))), - tuple(stock_entries_child_list)) + name in ({ste_details}) """.format( + cond=cond, ste_details=",".join(["%s"] * len(stock_entries_child_list)) + ), + tuple(stock_entries_child_list), + ) args = { - 'source_dt': 'Stock Entry Detail', - 'target_field': 'transferred_qty', - 'target_ref_field': 'qty', - 'target_dt': 'Stock Entry Detail', - 'join_field': 'ste_detail', - 'target_parent_dt': 'Stock Entry', - 'target_parent_field': 'per_transferred', - 'source_field': 'qty', - 'percent_join_field': 'against_stock_entry' + "source_dt": "Stock Entry Detail", + "target_field": "transferred_qty", + "target_ref_field": "qty", + "target_dt": "Stock Entry Detail", + "join_field": "ste_detail", + "target_parent_dt": "Stock Entry", + "target_parent_field": "per_transferred", + "source_field": "qty", + "percent_join_field": "against_stock_entry", } self._update_percent_field_in_targets(args, update_modified=True) def update_quality_inspection(self): if self.inspection_required: - reference_type = reference_name = '' + reference_type = reference_name = "" if self.docstatus == 1: reference_name = self.name - reference_type = 'Stock Entry' + reference_type = "Stock Entry" for d in self.items: if d.quality_inspection: - frappe.db.set_value("Quality Inspection", d.quality_inspection, { - 'reference_type': reference_type, - 'reference_name': reference_name - }) + frappe.db.set_value( + "Quality Inspection", + d.quality_inspection, + {"reference_type": reference_type, "reference_name": reference_name}, + ) + def set_material_request_transfer_status(self, status): material_requests = [] if self.outgoing_stock_entry: - parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, 'add_to_transit') + parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, "add_to_transit") for item in self.items: material_request = item.material_request or None if self.purpose == "Material Transfer" and material_request not in material_requests: if self.outgoing_stock_entry and parent_se: - material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, 'material_request') + material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, "material_request") if material_request and material_request not in material_requests: material_requests.append(material_request) - frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + frappe.db.set_value("Material Request", material_request, "transfer_status", status) def update_items_for_process_loss(self): process_loss_dict = {} @@ -1788,7 +2139,9 @@ class StockEntry(StockController): if not d.is_process_loss: continue - scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse") + scrap_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_scrap_warehouse" + ) if scrap_warehouse is not None: d.t_warehouse = scrap_warehouse d.is_scrap_item = 0 @@ -1805,7 +2158,6 @@ class StockEntry(StockController): d.transfer_qty -= process_loss_dict[d.item_code][0] d.qty -= process_loss_dict[d.item_code][1] - def set_serial_no_batch_for_finished_good(self): args = {} if self.pro_doc.serial_no: @@ -1814,14 +2166,22 @@ class StockEntry(StockController): for row in self.items: if row.is_finished_item and row.item_code == self.pro_doc.production_item: if args.get("serial_no"): - row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)]) + row.serial_no = "\n".join(args["serial_no"][0 : cint(row.qty)]) def get_serial_nos_for_fg(self, args): - fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`", - "`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"] + fields = [ + "`tabStock Entry`.`name`", + "`tabStock Entry Detail`.`qty`", + "`tabStock Entry Detail`.`serial_no`", + "`tabStock Entry Detail`.`batch_no`", + ] - filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"], - ["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]] + filters = [ + ["Stock Entry", "work_order", "=", self.work_order], + ["Stock Entry", "purpose", "=", "Manufacture"], + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item], + ] stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) @@ -1836,85 +2196,98 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): items = json.loads(items) - retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse') + retention_warehouse = frappe.db.get_single_value("Stock Settings", "sample_retention_warehouse") stock_entry = frappe.new_doc("Stock Entry") stock_entry.company = company stock_entry.purpose = "Material Transfer" stock_entry.set_stock_entry_type() for item in items: - if item.get('sample_quantity') and item.get('batch_no'): - sample_quantity = validate_sample_quantity(item.get('item_code'), item.get('sample_quantity'), - item.get('transfer_qty') or item.get('qty'), item.get('batch_no')) + if item.get("sample_quantity") and item.get("batch_no"): + sample_quantity = validate_sample_quantity( + item.get("item_code"), + item.get("sample_quantity"), + item.get("transfer_qty") or item.get("qty"), + item.get("batch_no"), + ) if sample_quantity: - sample_serial_nos = '' - if item.get('serial_no'): - serial_nos = (item.get('serial_no')).split() - if serial_nos and len(serial_nos) > item.get('sample_quantity'): - serial_no_list = serial_nos[:-(len(serial_nos)-item.get('sample_quantity'))] - sample_serial_nos = '\n'.join(serial_no_list) + sample_serial_nos = "" + if item.get("serial_no"): + serial_nos = (item.get("serial_no")).split() + if serial_nos and len(serial_nos) > item.get("sample_quantity"): + serial_no_list = serial_nos[: -(len(serial_nos) - item.get("sample_quantity"))] + sample_serial_nos = "\n".join(serial_no_list) - stock_entry.append("items", { - "item_code": item.get('item_code'), - "s_warehouse": item.get('t_warehouse'), - "t_warehouse": retention_warehouse, - "qty": item.get('sample_quantity'), - "basic_rate": item.get('valuation_rate'), - 'uom': item.get('uom'), - 'stock_uom': item.get('stock_uom'), - "conversion_factor": 1.0, - "serial_no": sample_serial_nos, - 'batch_no': item.get('batch_no') - }) - if stock_entry.get('items'): + stock_entry.append( + "items", + { + "item_code": item.get("item_code"), + "s_warehouse": item.get("t_warehouse"), + "t_warehouse": retention_warehouse, + "qty": item.get("sample_quantity"), + "basic_rate": item.get("valuation_rate"), + "uom": item.get("uom"), + "stock_uom": item.get("stock_uom"), + "conversion_factor": 1.0, + "serial_no": sample_serial_nos, + "batch_no": item.get("batch_no"), + }, + ) + if stock_entry.get("items"): return stock_entry.as_dict() + @frappe.whitelist() def make_stock_in_entry(source_name, target_doc=None): - def set_missing_values(source, target): target.set_stock_entry_type() def update_item(source_doc, target_doc, source_parent): - target_doc.t_warehouse = '' + target_doc.t_warehouse = "" - if source_doc.material_request_item and source_doc.material_request : - add_to_transit = frappe.db.get_value('Stock Entry', source_name, 'add_to_transit') + if source_doc.material_request_item and source_doc.material_request: + add_to_transit = frappe.db.get_value("Stock Entry", source_name, "add_to_transit") if add_to_transit: - warehouse = frappe.get_value('Material Request Item', source_doc.material_request_item, 'warehouse') + warehouse = frappe.get_value( + "Material Request Item", source_doc.material_request_item, "warehouse" + ) target_doc.t_warehouse = warehouse target_doc.s_warehouse = source_doc.t_warehouse target_doc.qty = source_doc.qty - source_doc.transferred_qty - doclist = get_mapped_doc("Stock Entry", source_name, { - "Stock Entry": { - "doctype": "Stock Entry", - "field_map": { - "name": "outgoing_stock_entry" + doclist = get_mapped_doc( + "Stock Entry", + source_name, + { + "Stock Entry": { + "doctype": "Stock Entry", + "field_map": {"name": "outgoing_stock_entry"}, + "validation": {"docstatus": ["=", 1]}, }, - "validation": { - "docstatus": ["=", 1] - } - }, - "Stock Entry Detail": { - "doctype": "Stock Entry Detail", - "field_map": { - "name": "ste_detail", - "parent": "against_stock_entry", - "serial_no": "serial_no", - "batch_no": "batch_no" + "Stock Entry Detail": { + "doctype": "Stock Entry Detail", + "field_map": { + "name": "ste_detail", + "parent": "against_stock_entry", + "serial_no": "serial_no", + "batch_no": "batch_no", + }, + "postprocess": update_item, + "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01, }, - "postprocess": update_item, - "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01 }, - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def get_work_order_details(work_order, company): work_order = frappe.get_doc("Work Order", work_order) @@ -1926,9 +2299,10 @@ def get_work_order_details(work_order, company): "use_multi_level_bom": work_order.use_multi_level_bom, "wip_warehouse": work_order.wip_warehouse, "fg_warehouse": work_order.fg_warehouse, - "fg_completed_qty": pending_qty_to_produce + "fg_completed_qty": pending_qty_to_produce, } + def get_operating_cost_per_unit(work_order=None, bom_no=None): operating_cost_per_unit = 0 if work_order: @@ -1947,54 +2321,78 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): if bom.quantity: operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) - if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings', - 'add_corrective_operation_cost_in_finished_good_valuation')): - operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty) + if ( + work_order + and work_order.produced_qty + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" + ) + ) + ): + operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt( + work_order.produced_qty + ) return operating_cost_per_unit + def get_used_alternative_items(purchase_order=None, work_order=None): cond = "" if purchase_order: - cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format(purchase_order) + cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format( + purchase_order + ) elif work_order: - cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(work_order) + cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format( + work_order + ) - if not cond: return {} + if not cond: + return {} used_alternative_items = {} - data = frappe.db.sql(""" select sted.original_item, sted.uom, sted.conversion_factor, + data = frappe.db.sql( + """ select sted.original_item, sted.uom, sted.conversion_factor, sted.item_code, sted.item_name, sted.conversion_factor,sted.stock_uom, sted.description from `tabStock Entry` ste, `tabStock Entry Detail` sted where sted.parent = ste.name and ste.docstatus = 1 and sted.original_item != sted.item_code - {0} """.format(cond), as_dict=1) + {0} """.format( + cond + ), + as_dict=1, + ) for d in data: used_alternative_items[d.original_item] = d return used_alternative_items + def get_valuation_rate_for_finished_good_entry(work_order): - work_order_qty = flt(frappe.get_cached_value("Work Order", - work_order, 'material_transferred_for_manufacturing')) + work_order_qty = flt( + frappe.get_cached_value("Work Order", work_order, "material_transferred_for_manufacturing") + ) field = "(SUM(total_outgoing_value) / %s) as valuation_rate" % (work_order_qty) - stock_data = frappe.get_all("Stock Entry", - fields = field, - filters = { + stock_data = frappe.get_all( + "Stock Entry", + fields=field, + filters={ "docstatus": 1, "purpose": "Material Transfer for Manufacture", - "work_order": work_order - } + "work_order": work_order, + }, ) if stock_data: return stock_data[0].valuation_rate + @frappe.whitelist() def get_uom_details(item_code, uom, qty): """Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}` @@ -2003,24 +2401,31 @@ def get_uom_details(item_code, uom, qty): conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor") if not conversion_factor: - frappe.msgprint(_("UOM coversion factor required for UOM: {0} in Item: {1}") - .format(uom, item_code)) - ret = {'uom' : ''} + frappe.msgprint( + _("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) + ) + ret = {"uom": ""} else: ret = { - 'conversion_factor' : flt(conversion_factor), - 'transfer_qty' : flt(qty) * flt(conversion_factor) + "conversion_factor": flt(conversion_factor), + "transfer_qty": flt(qty) * flt(conversion_factor), } return ret + @frappe.whitelist() def get_expired_batch_items(): - return frappe.db.sql("""select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\ + return frappe.db.sql( + """select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\ from `tabBatch` b, `tabStock Ledger Entry` sle where b.expiry_date <= %s and b.expiry_date is not NULL and b.batch_id = sle.batch_no and sle.is_cancelled = 0 - group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1) + group by sle.warehouse, sle.item_code, sle.batch_no""", + (nowdate()), + as_dict=1, + ) + @frappe.whitelist() def get_warehouse_details(args): @@ -2031,51 +2436,73 @@ def get_warehouse_details(args): ret = {} if args.warehouse and args.item_code: - args.update({ - "posting_date": args.posting_date, - "posting_time": args.posting_time, - }) + args.update( + { + "posting_date": args.posting_date, + "posting_time": args.posting_time, + } + ) ret = { - "actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0, - "basic_rate" : get_incoming_rate(args) + "actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0, + "basic_rate": get_incoming_rate(args), } return ret + @frappe.whitelist() -def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None): +def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None): if cint(qty) < cint(sample_quantity): - frappe.throw(_("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty)) - retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse') + frappe.throw( + _("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty) + ) + retention_warehouse = frappe.db.get_single_value("Stock Settings", "sample_retention_warehouse") retainted_qty = 0 if batch_no: retainted_qty = get_batch_qty(batch_no, retention_warehouse, item_code) - max_retain_qty = frappe.get_value('Item', item_code, 'sample_quantity') + max_retain_qty = frappe.get_value("Item", item_code, "sample_quantity") if retainted_qty >= max_retain_qty: - frappe.msgprint(_("Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}."). - format(retainted_qty, batch_no, item_code, batch_no), alert=True) + frappe.msgprint( + _( + "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." + ).format(retainted_qty, batch_no, item_code, batch_no), + alert=True, + ) sample_quantity = 0 - qty_diff = max_retain_qty-retainted_qty + qty_diff = max_retain_qty - retainted_qty if cint(sample_quantity) > cint(qty_diff): - frappe.msgprint(_("Maximum Samples - {0} can be retained for Batch {1} and Item {2}."). - format(max_retain_qty, batch_no, item_code), alert=True) + frappe.msgprint( + _("Maximum Samples - {0} can be retained for Batch {1} and Item {2}.").format( + max_retain_qty, batch_no, item_code + ), + alert=True, + ) sample_quantity = qty_diff return sample_quantity -def get_supplied_items(purchase_order): - fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`', - '`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`'] - filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]] +def get_supplied_items(purchase_order): + fields = [ + "`tabStock Entry Detail`.`transfer_qty`", + "`tabStock Entry`.`is_return`", + "`tabStock Entry Detail`.`po_detail`", + "`tabStock Entry Detail`.`item_code`", + ] + + filters = [ + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purchase_order", "=", purchase_order], + ] supplied_item_details = {} - for row in frappe.get_all('Stock Entry', fields = fields, filters = filters): + for row in frappe.get_all("Stock Entry", fields=fields, filters=filters): if not row.po_detail: continue key = row.po_detail if key not in supplied_item_details: - supplied_item_details.setdefault(key, - frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0})) + supplied_item_details.setdefault( + key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}) + ) supplied_item = supplied_item_details[key] @@ -2084,6 +2511,8 @@ def get_supplied_items(purchase_order): else: supplied_item.supplied_qty += row.transfer_qty - supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty) + supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt( + supplied_item.returned_qty + ) return supplied_item_details diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 5832fe49b2d..85ccc5b13fc 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -11,7 +11,7 @@ import erpnext @frappe.whitelist() def make_stock_entry(**args): - '''Helper function to make a Stock Entry + """Helper function to make a Stock Entry :item_code: Item to be moved :qty: Qty to be moved @@ -26,16 +26,16 @@ def make_stock_entry(**args): :purpose: Optional :do_not_save: Optional flag :do_not_submit: Optional flag - ''' + """ def process_serial_numbers(serial_nos_list): serial_nos_list = [ - '\n'.join(serial_num['serial_no'] for serial_num in serial_nos_list if serial_num.serial_no) + "\n".join(serial_num["serial_no"] for serial_num in serial_nos_list if serial_num.serial_no) ] - uniques = list(set(serial_nos_list[0].split('\n'))) + uniques = list(set(serial_nos_list[0].split("\n"))) - return '\n'.join(uniques) + return "\n".join(uniques) s = frappe.new_doc("Stock Entry") args = frappe._dict(args) @@ -61,7 +61,7 @@ def make_stock_entry(**args): s.apply_putaway_rule = args.apply_putaway_rule if isinstance(args.qty, string_types): - if '.' in args.qty: + if "." in args.qty: args.qty = flt(args.qty) else: args.qty = cint(args.qty) @@ -80,16 +80,16 @@ def make_stock_entry(**args): # company if not args.company: if args.source: - args.company = frappe.db.get_value('Warehouse', args.source, 'company') + args.company = frappe.db.get_value("Warehouse", args.source, "company") elif args.target: - args.company = frappe.db.get_value('Warehouse', args.target, 'company') + args.company = frappe.db.get_value("Warehouse", args.target, "company") # set vales from test if frappe.flags.in_test: if not args.company: - args.company = '_Test Company' + args.company = "_Test Company" if not args.item: - args.item = '_Test Item' + args.item = "_Test Item" s.company = args.company or erpnext.get_default_company() s.purchase_receipt_no = args.purchase_receipt_no @@ -97,40 +97,40 @@ def make_stock_entry(**args): s.sales_invoice_no = args.sales_invoice_no s.is_opening = args.is_opening or "No" if not args.cost_center: - args.cost_center = frappe.get_value('Company', s.company, 'cost_center') + args.cost_center = frappe.get_value("Company", s.company, "cost_center") if not args.expense_account and s.is_opening == "No": - args.expense_account = frappe.get_value('Company', s.company, 'stock_adjustment_account') + args.expense_account = frappe.get_value("Company", s.company, "stock_adjustment_account") # We can find out the serial number using the batch source document serial_number = args.serial_no if not args.serial_no and args.qty and args.batch_no: serial_number_list = frappe.get_list( - doctype='Stock Ledger Entry', - fields=['serial_no'], - filters={ - 'batch_no': args.batch_no, - 'warehouse': args.from_warehouse - } + doctype="Stock Ledger Entry", + fields=["serial_no"], + filters={"batch_no": args.batch_no, "warehouse": args.from_warehouse}, ) serial_number = process_serial_numbers(serial_number_list) args.serial_no = serial_number - s.append("items", { - "item_code": args.item, - "s_warehouse": args.source, - "t_warehouse": args.target, - "qty": args.qty, - "basic_rate": args.rate or args.basic_rate, - "conversion_factor": args.conversion_factor or 1.0, - "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), - "serial_no": args.serial_no, - 'batch_no': args.batch_no, - 'cost_center': args.cost_center, - 'expense_account': args.expense_account - }) + s.append( + "items", + { + "item_code": args.item, + "s_warehouse": args.source, + "t_warehouse": args.target, + "qty": args.qty, + "basic_rate": args.rate or args.basic_rate, + "conversion_factor": args.conversion_factor or 1.0, + "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), + "serial_no": args.serial_no, + "batch_no": args.batch_no, + "cost_center": args.cost_center, + "expense_account": args.expense_account, + }, + ) s.set_stock_entry_type() if not args.do_not_save: diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 3c34d4795cb..323ab6af819 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.permissions import add_user_permission, remove_user_permission +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import flt, nowdate, nowtime from six import iteritems @@ -29,7 +30,6 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle -from erpnext.tests.utils import ERPNextTestCase, change_settings def get_sle(**args): @@ -39,51 +39,56 @@ def get_sle(**args): condition += "`{0}`=%s".format(key) values.append(value) - return frappe.db.sql("""select * from `tabStock Ledger Entry` %s - order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, - values, as_dict=1) + return frappe.db.sql( + """select * from `tabStock Ledger Entry` %s + order by timestamp(posting_date, posting_time) desc, creation desc limit 1""" + % condition, + values, + as_dict=1, + ) -class TestStockEntry(ERPNextTestCase): + +class TestStockEntry(FrappeTestCase): def tearDown(self): frappe.db.rollback() frappe.set_user("Administrator") - frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") def test_fifo(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) item_code = "_Test Item 2" warehouse = "_Test Warehouse - _TC" - create_stock_reconciliation(item_code="_Test Item 2", warehouse="_Test Warehouse - _TC", - qty=0, rate=100) + create_stock_reconciliation( + item_code="_Test Item 2", warehouse="_Test Warehouse - _TC", qty=0, rate=100 + ) make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=10) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[1, 10]], frappe.safe_eval(sle.stock_queue)) # negative qty make_stock_entry(item_code=item_code, source=warehouse, qty=2, basic_rate=10) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[-1, 10]], frappe.safe_eval(sle.stock_queue)) # further negative make_stock_entry(item_code=item_code, source=warehouse, qty=1) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[-2, 10]], frappe.safe_eval(sle.stock_queue)) # move stock to positive make_stock_entry(item_code=item_code, target=warehouse, qty=3, basic_rate=20) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[1, 20]], frappe.safe_eval(sle.stock_queue)) # incoming entry with diff rate make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=30) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] - self.assertEqual([[1, 20],[1, 30]], frappe.safe_eval(sle.stock_queue)) + self.assertEqual([[1, 20], [1, 30]], frappe.safe_eval(sle.stock_queue)) frappe.db.set_default("allow_negative_stock", 0) @@ -93,37 +98,48 @@ class TestStockEntry(ERPNextTestCase): self._test_auto_material_request("_Test Item", material_request_type="Transfer") def test_auto_material_request_for_variant(self): - fields = [{'field_name': 'reorder_levels'}] + fields = [{"field_name": "reorder_levels"}] set_item_variant_settings(fields) make_item_variant() template = frappe.get_doc("Item", "_Test Variant Item") if not template.reorder_levels: - template.append('reorder_levels', { - "material_request_type": "Purchase", - "warehouse": "_Test Warehouse - _TC", - "warehouse_reorder_level": 20, - "warehouse_reorder_qty": 20 - }) + template.append( + "reorder_levels", + { + "material_request_type": "Purchase", + "warehouse": "_Test Warehouse - _TC", + "warehouse_reorder_level": 20, + "warehouse_reorder_qty": 20, + }, + ) template.save() self._test_auto_material_request("_Test Variant Item-S") def test_auto_material_request_for_warehouse_group(self): - self._test_auto_material_request("_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC") + self._test_auto_material_request( + "_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC" + ) - def _test_auto_material_request(self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC"): + def _test_auto_material_request( + self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC" + ): variant = frappe.get_doc("Item", item_code) - projected_qty, actual_qty = frappe.db.get_value("Bin", {"item_code": item_code, - "warehouse": warehouse}, ["projected_qty", "actual_qty"]) or [0, 0] + projected_qty, actual_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, ["projected_qty", "actual_qty"] + ) or [0, 0] # stock entry reqd for auto-reorder - create_stock_reconciliation(item_code=item_code, warehouse=warehouse, - qty = actual_qty + abs(projected_qty) + 10, rate=100) + create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=actual_qty + abs(projected_qty) + 10, rate=100 + ) - projected_qty = frappe.db.get_value("Bin", {"item_code": item_code, - "warehouse": warehouse}, "projected_qty") or 0 + projected_qty = ( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty") + or 0 + ) frappe.db.set_value("Stock Settings", None, "auto_indent", 1) @@ -134,6 +150,7 @@ class TestStockEntry(ERPNextTestCase): variant.save() from erpnext.stock.reorder_item import reorder_item + mr_list = reorder_item() frappe.db.set_value("Stock Settings", None, "auto_indent", 0) @@ -146,65 +163,113 @@ class TestStockEntry(ERPNextTestCase): self.assertTrue(item_code in items) def test_material_receipt_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + mr = make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) stock_in_hand_account = get_inventory_account(mr.company, mr.get("items")[0].t_warehouse) - self.check_stock_ledger_entries("Stock Entry", mr.name, - [["_Test Item", "Stores - TCP1", 50.0]]) + self.check_stock_ledger_entries("Stock Entry", mr.name, [["_Test Item", "Stores - TCP1", 50.0]]) - self.check_gl_entries("Stock Entry", mr.name, - sorted([ - [stock_in_hand_account, 5000.0, 0.0], - ["Stock Adjustment - TCP1", 0.0, 5000.0] - ]) + self.check_gl_entries( + "Stock Entry", + mr.name, + sorted([[stock_in_hand_account, 5000.0, 0.0], ["Stock Adjustment - TCP1", 0.0, 5000.0]]), ) mr.cancel() - self.assertTrue(frappe.db.sql("""select * from `tabStock Ledger Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mr.name)) + self.assertTrue( + frappe.db.sql( + """select * from `tabStock Ledger Entry` + where voucher_type='Stock Entry' and voucher_no=%s""", + mr.name, + ) + ) - self.assertTrue(frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mr.name)) + self.assertTrue( + frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Stock Entry' and voucher_no=%s""", + mr.name, + ) + ) def test_material_issue_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") + make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) - mi = make_stock_entry(item_code="_Test Item", source="Stores - TCP1", company=company, - qty=40, expense_account="Stock Adjustment - TCP1") + mi = make_stock_entry( + item_code="_Test Item", + source="Stores - TCP1", + company=company, + qty=40, + expense_account="Stock Adjustment - TCP1", + ) - self.check_stock_ledger_entries("Stock Entry", mi.name, - [["_Test Item", "Stores - TCP1", -40.0]]) + self.check_stock_ledger_entries("Stock Entry", mi.name, [["_Test Item", "Stores - TCP1", -40.0]]) stock_in_hand_account = get_inventory_account(mi.company, "Stores - TCP1") - stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": mi.name}, "stock_value_difference")) + stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": mi.name}, + "stock_value_difference", + ) + ) - self.check_gl_entries("Stock Entry", mi.name, - sorted([ - [stock_in_hand_account, 0.0, stock_value_diff], - ["Stock Adjustment - TCP1", stock_value_diff, 0.0] - ]) + self.check_gl_entries( + "Stock Entry", + mi.name, + sorted( + [ + [stock_in_hand_account, 0.0, stock_value_diff], + ["Stock Adjustment - TCP1", stock_value_diff, 0.0], + ] + ), ) mi.cancel() def test_material_transfer_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - item_code = 'Hand Sanitizer - 001' - create_item(item_code =item_code, is_stock_item = 1, - is_purchase_item=1, opening_stock=1000, valuation_rate=10, company=company, warehouse="Stores - TCP1") + item_code = "Hand Sanitizer - 001" + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + opening_stock=1000, + valuation_rate=10, + company=company, + warehouse="Stores - TCP1", + ) - mtn = make_stock_entry(item_code=item_code, source="Stores - TCP1", - target="Finished Goods - TCP1", qty=45, company=company) + mtn = make_stock_entry( + item_code=item_code, + source="Stores - TCP1", + target="Finished Goods - TCP1", + qty=45, + company=company, + ) - self.check_stock_ledger_entries("Stock Entry", mtn.name, - [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]]) + self.check_stock_ledger_entries( + "Stock Entry", + mtn.name, + [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]], + ) source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse) @@ -212,18 +277,33 @@ class TestStockEntry(ERPNextTestCase): if source_warehouse_account == target_warehouse_account: # no gl entry as both source and target warehouse has linked to same account. - self.assertFalse(frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name, as_dict=1)) + self.assertFalse( + frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Stock Entry' and voucher_no=%s""", + mtn.name, + as_dict=1, + ) + ) else: - stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": mtn.name, "warehouse": "Stores - TCP1"}, "stock_value_difference")) + stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": mtn.name, "warehouse": "Stores - TCP1"}, + "stock_value_difference", + ) + ) - self.check_gl_entries("Stock Entry", mtn.name, - sorted([ - [source_warehouse_account, 0.0, stock_value_diff], - [target_warehouse_account, stock_value_diff, 0.0], - ]) + self.check_gl_entries( + "Stock Entry", + mtn.name, + sorted( + [ + [source_warehouse_account, 0.0, stock_value_diff], + [target_warehouse_account, stock_value_diff, 0.0], + ] + ), ) mtn.cancel() @@ -240,20 +320,23 @@ class TestStockEntry(ERPNextTestCase): repack.items[0].transfer_qty = 100.0 repack.items[1].qty = 50.0 - repack.append("items", { - "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - _TC", - "basic_rate": 150, - "item_code": "_Test Item 2", - "parentfield": "items", - "qty": 50.0, - "stock_uom": "_Test UOM", - "t_warehouse": "_Test Warehouse - _TC", - "transfer_qty": 50.0, - "uom": "_Test UOM" - }) + repack.append( + "items", + { + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - _TC", + "basic_rate": 150, + "item_code": "_Test Item 2", + "parentfield": "items", + "qty": 50.0, + "stock_uom": "_Test UOM", + "t_warehouse": "_Test Warehouse - _TC", + "transfer_qty": 50.0, + "uom": "_Test UOM", + }, + ) repack.set_stock_entry_type() repack.insert() @@ -266,12 +349,13 @@ class TestStockEntry(ERPNextTestCase): # must raise error if 0 fg in repack entry self.assertRaises(FinishedGoodError, repack.validate_finished_goods) - repack.delete() # teardown + repack.delete() # teardown def test_repack_no_change_in_valuation(self): make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", - qty=50, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=50, basic_rate=100 + ) repack = frappe.copy_doc(test_records[3]) repack.posting_date = nowdate() @@ -280,76 +364,113 @@ class TestStockEntry(ERPNextTestCase): repack.insert() repack.submit() - self.check_stock_ledger_entries("Stock Entry", repack.name, - [["_Test Item", "_Test Warehouse - _TC", -50.0], - ["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1]]) + self.check_stock_ledger_entries( + "Stock Entry", + repack.name, + [ + ["_Test Item", "_Test Warehouse - _TC", -50.0], + ["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1], + ], + ) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Stock Entry' and voucher_no=%s - order by account desc""", repack.name, as_dict=1) + order by account desc""", + repack.name, + as_dict=1, + ) self.assertFalse(gl_entries) def test_repack_with_additional_costs(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) - - repack = make_stock_entry(company = company, purpose="Repack", do_not_save=True) + repack = make_stock_entry(company=company, purpose="Repack", do_not_save=True) repack.posting_date = nowdate() repack.posting_time = nowtime() - expenses_included_in_valuation = frappe.get_value("Company", company, "expenses_included_in_valuation") + expenses_included_in_valuation = frappe.get_value( + "Company", company, "expenses_included_in_valuation" + ) items = get_multiple_items() repack.items = [] for item in items: repack.append("items", item) - repack.set("additional_costs", [ - { - "expense_account": expenses_included_in_valuation, - "description": "Actual Operating Cost", - "amount": 1000 - }, - { - "expense_account": expenses_included_in_valuation, - "description": "Additional Operating Cost", - "amount": 200 - }, - ]) + repack.set( + "additional_costs", + [ + { + "expense_account": expenses_included_in_valuation, + "description": "Actual Operating Cost", + "amount": 1000, + }, + { + "expense_account": expenses_included_in_valuation, + "description": "Additional Operating Cost", + "amount": 200, + }, + ], + ) repack.set_stock_entry_type() repack.insert() repack.submit() stock_in_hand_account = get_inventory_account(repack.company, repack.get("items")[1].t_warehouse) - rm_stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": repack.name, "item_code": "_Test Item"}, "stock_value_difference")) + rm_stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": repack.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + ) - fg_stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": repack.name, "item_code": "_Test Item Home Desktop 100"}, "stock_value_difference")) + fg_stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + "item_code": "_Test Item Home Desktop 100", + }, + "stock_value_difference", + ) + ) stock_value_diff = flt(fg_stock_value_diff - rm_stock_value_diff, 2) self.assertEqual(stock_value_diff, 1200) - self.check_gl_entries("Stock Entry", repack.name, - sorted([ - [stock_in_hand_account, 1200, 0.0], - ["Expenses Included In Valuation - TCP1", 0.0, 1200.0] - ]) + self.check_gl_entries( + "Stock Entry", + repack.name, + sorted( + [[stock_in_hand_account, 1200, 0.0], ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]] + ), ) def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle): expected_sle.sort(key=lambda x: x[1]) # check stock ledger entries - sle = frappe.db.sql("""select item_code, warehouse, actual_qty + sle = frappe.db.sql( + """select item_code, warehouse, actual_qty from `tabStock Ledger Entry` where voucher_type = %s and voucher_no = %s order by item_code, warehouse, actual_qty""", - (voucher_type, voucher_no), as_list=1) + (voucher_type, voucher_no), + as_list=1, + ) self.assertTrue(sle) sle.sort(key=lambda x: x[1]) @@ -361,9 +482,13 @@ class TestStockEntry(ERPNextTestCase): def check_gl_entries(self, voucher_type, voucher_no, expected_gl_entries): expected_gl_entries.sort(key=lambda x: x[0]) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account asc, debit asc""", (voucher_type, voucher_no), as_list=1) + order by account asc, debit asc""", + (voucher_type, voucher_no), + as_list=1, + ) self.assertTrue(gl_entries) gl_entries.sort(key=lambda x: x[0]) @@ -464,7 +589,7 @@ class TestStockEntry(ERPNextTestCase): def test_serial_item_error(self): se, serial_nos = self.test_serial_by_series() - if not frappe.db.exists('Serial No', 'ABCD'): + if not frappe.db.exists("Serial No", "ABCD"): make_serialized_item(item_code="_Test Serialized Item", serial_no="ABCD\nEFGH") se = frappe.copy_doc(test_records[0]) @@ -494,10 +619,14 @@ class TestStockEntry(ERPNextTestCase): se.set_stock_entry_type() se.insert() se.submit() - self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC") + self.assertTrue( + frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC" + ) se.cancel() - self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC") + self.assertTrue( + frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC" + ) def test_serial_warehouse_error(self): make_serialized_item(target_warehouse="_Test Warehouse 1 - _TC") @@ -525,14 +654,16 @@ class TestStockEntry(ERPNextTestCase): self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) def test_warehouse_company_validation(self): - company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company') - frappe.get_doc("User", "test2@example.com")\ - .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager") + company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") + frappe.get_doc("User", "test2@example.com").add_roles( + "Sales User", "Sales Manager", "Stock User", "Stock Manager" + ) frappe.set_user("test2@example.com") from erpnext.stock.utils import InvalidWarehouseCompany + st1 = frappe.copy_doc(test_records[0]) - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" st1.set_stock_entry_type() st1.insert() self.assertRaises(InvalidWarehouseCompany, st1.submit) @@ -546,14 +677,15 @@ class TestStockEntry(ERPNextTestCase): test_user.add_roles("Sales User", "Sales Manager", "Stock User") test_user.remove_roles("Stock Manager", "System Manager") - frappe.get_doc("User", "test2@example.com")\ - .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager") + frappe.get_doc("User", "test2@example.com").add_roles( + "Sales User", "Sales Manager", "Stock User", "Stock Manager" + ) st1 = frappe.copy_doc(test_records[0]) st1.company = "_Test Company 1" frappe.set_user("test@example.com") - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" self.assertRaises(frappe.PermissionError, st1.insert) test_user.add_roles("System Manager") @@ -561,7 +693,7 @@ class TestStockEntry(ERPNextTestCase): frappe.set_user("test2@example.com") st1 = frappe.copy_doc(test_records[0]) st1.company = "_Test Company 1" - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" st1.get("items")[0].expense_account = "Stock Adjustment - _TC1" st1.get("items")[0].cost_center = "Main - _TC1" st1.set_stock_entry_type() @@ -575,14 +707,14 @@ class TestStockEntry(ERPNextTestCase): remove_user_permission("Company", "_Test Company 1", "test2@example.com") def test_freeze_stocks(self): - frappe.db.set_value('Stock Settings', None,'stock_auth_role', '') + frappe.db.set_value("Stock Settings", None, "stock_auth_role", "") # test freeze_stocks_upto frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", add_days(nowdate(), 5)) se = frappe.copy_doc(test_records[0]).insert() self.assertRaises(StockFreezeError, se.submit) - frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", '') + frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", "") # test freeze_stocks_upto_days frappe.db.set_value("Stock Settings", None, "stock_frozen_upto_days", -1) @@ -598,20 +730,24 @@ class TestStockEntry(ERPNextTestCase): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) - bom_no, bom_operation_cost = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", - "is_default": 1, "docstatus": 1}, ["name", "operating_cost"]) + + bom_no, bom_operation_cost = frappe.db.get_value( + "BOM", {"item": "_Test FG Item 2", "is_default": 1, "docstatus": 1}, ["name", "operating_cost"] + ) work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test FG Item 2", - "bom_no": bom_no, - "qty": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC", - "additional_operating_cost": 1000 - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item 2", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + "additional_operating_cost": 1000, + } + ) work_order.insert() work_order.submit() @@ -624,37 +760,40 @@ class TestStockEntry(ERPNextTestCase): for d in stock_entry.get("items"): if d.item_code != "_Test FG Item 2": rm_cost += flt(d.amount) - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item 2", stock_entry.get("items")))[0].amount - self.assertEqual(fg_cost, - flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)) + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items")))[ + 0 + ].amount + self.assertEqual( + fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2) + ) + @change_settings("Manufacturing Settings", {"material_consumption": 1}) def test_work_order_manufacture_with_material_consumption(self): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) - frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1") - bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", - "is_default": 1, "docstatus": 1}) + bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_default": 1, "docstatus": 1}) work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test FG Item", - "bom_no": bom_no, - "qty": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC" - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + } + ) work_order.insert() work_order.submit() - make_stock_entry(item_code="_Test Item", - target="Stores - _TC", qty=10, basic_rate=5000.0) - make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - _TC", qty=10, basic_rate=1000.0) - + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0 + ) s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) for d in s.get("items"): @@ -666,13 +805,12 @@ class TestStockEntry(ERPNextTestCase): s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1)) s.save() rm_cost = 0 - for d in s.get('items'): + for d in s.get("items"): if d.s_warehouse: rm_cost += d.amount - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount - self.assertEqual(fg_cost, - flt(rm_cost - scrap_cost, 2)) + self.assertEqual(fg_cost, flt(rm_cost - scrap_cost, 2)) # When Stock Entry has only FG + Scrap s.items.pop(0) @@ -680,31 +818,34 @@ class TestStockEntry(ERPNextTestCase): s.submit() rm_cost = 0 - for d in s.get('items'): + for d in s.get("items"): if d.s_warehouse: rm_cost += d.amount self.assertEqual(rm_cost, 0) expected_fg_cost = s.get_basic_rate_for_manufactured_item(1) - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2)) def test_variant_work_order(self): - bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", - "is_default": 1, "docstatus": 1}) + bom_no = frappe.db.get_value( + "BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1} + ) - make_item_variant() # make variant of _Test Variant Item if absent + make_item_variant() # make variant of _Test Variant Item if absent work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test Variant Item-S", - "bom_no": bom_no, - "qty": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC", - "skip_transfer": 1 - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test Variant Item-S", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + "skip_transfer": 1, + } + ) work_order.insert() work_order.submit() @@ -718,19 +859,29 @@ class TestStockEntry(ERPNextTestCase): s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = s1.get("items")[0].serial_no - s2 = make_stock_entry(item_code="_Test Serialized Item With Series", source="_Test Warehouse - _TC", - qty=2, basic_rate=100, purpose="Repack", serial_no=serial_nos, do_not_save=True) + s2 = make_stock_entry( + item_code="_Test Serialized Item With Series", + source="_Test Warehouse - _TC", + qty=2, + basic_rate=100, + purpose="Repack", + serial_no=serial_nos, + do_not_save=True, + ) - s2.append("items", { - "item_code": "_Test Serialized Item", - "t_warehouse": "_Test Warehouse - _TC", - "qty": 2, - "basic_rate": 120, - "expense_account": "Stock Adjustment - _TC", - "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC", - "serial_no": serial_nos - }) + s2.append( + "items", + { + "item_code": "_Test Serialized Item", + "t_warehouse": "_Test Warehouse - _TC", + "qty": 2, + "basic_rate": 120, + "expense_account": "Stock Adjustment - _TC", + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "serial_no": serial_nos, + }, + ) s2.submit() s2.cancel() @@ -740,10 +891,15 @@ class TestStockEntry(ERPNextTestCase): from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse create_warehouse("Test Warehouse for Sample Retention") - frappe.db.set_value("Stock Settings", None, "sample_retention_warehouse", "Test Warehouse for Sample Retention - _TC") + frappe.db.set_value( + "Stock Settings", + None, + "sample_retention_warehouse", + "Test Warehouse for Sample Retention - _TC", + ) test_item_code = "Retain Sample Item" - if not frappe.db.exists('Item', test_item_code): + if not frappe.db.exists("Item", test_item_code): item = frappe.new_doc("Item") item.item_code = test_item_code item.item_name = "Retain Sample Item" @@ -759,44 +915,58 @@ class TestStockEntry(ERPNextTestCase): receipt_entry = frappe.new_doc("Stock Entry") receipt_entry.company = "_Test Company" receipt_entry.purpose = "Material Receipt" - receipt_entry.append("items", { - "item_code": test_item_code, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 40, - "basic_rate": 12, - "cost_center": "_Test Cost Center - _TC", - "sample_quantity": 4 - }) + receipt_entry.append( + "items", + { + "item_code": test_item_code, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 40, + "basic_rate": 12, + "cost_center": "_Test Cost Center - _TC", + "sample_quantity": 4, + }, + ) receipt_entry.set_stock_entry_type() receipt_entry.insert() receipt_entry.submit() - retention_data = move_sample_to_retention_warehouse(receipt_entry.company, receipt_entry.get("items")) + retention_data = move_sample_to_retention_warehouse( + receipt_entry.company, receipt_entry.get("items") + ) retention_entry = frappe.new_doc("Stock Entry") retention_entry.company = retention_data.company retention_entry.purpose = retention_data.purpose - retention_entry.append("items", { - "item_code": test_item_code, - "t_warehouse": "Test Warehouse for Sample Retention - _TC", - "s_warehouse": "_Test Warehouse - _TC", - "qty": 4, - "basic_rate": 12, - "cost_center": "_Test Cost Center - _TC", - "batch_no": receipt_entry.get("items")[0].batch_no - }) + retention_entry.append( + "items", + { + "item_code": test_item_code, + "t_warehouse": "Test Warehouse for Sample Retention - _TC", + "s_warehouse": "_Test Warehouse - _TC", + "qty": 4, + "basic_rate": 12, + "cost_center": "_Test Cost Center - _TC", + "batch_no": receipt_entry.get("items")[0].batch_no, + }, + ) retention_entry.set_stock_entry_type() retention_entry.insert() retention_entry.submit() - qty_in_usable_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item") - qty_in_retention_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "Test Warehouse for Sample Retention - _TC", "_Test Item") + qty_in_usable_warehouse = get_batch_qty( + receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item" + ) + qty_in_retention_warehouse = get_batch_qty( + receipt_entry.get("items")[0].batch_no, + "Test Warehouse for Sample Retention - _TC", + "_Test Item", + ) self.assertEqual(qty_in_usable_warehouse, 36) self.assertEqual(qty_in_retention_warehouse, 4) def test_quality_check(self): item_code = "_Test Item For QC" - if not frappe.db.exists('Item', item_code): + if not frappe.db.exists("Item", item_code): create_item(item_code) repack = frappe.copy_doc(test_records[3]) @@ -812,82 +982,64 @@ class TestStockEntry(ERPNextTestCase): repack.insert() self.assertRaises(frappe.ValidationError, repack.submit) - # def test_material_consumption(self): - # frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") - # frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") - - # from erpnext.manufacturing.doctype.work_order.work_order \ - # import make_stock_entry as _make_stock_entry - # bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", - # "is_default": 1, "docstatus": 1}) - - # work_order = frappe.new_doc("Work Order") - # work_order.update({ - # "company": "_Test Company", - # "fg_warehouse": "_Test Warehouse 1 - _TC", - # "production_item": "_Test FG Item 2", - # "bom_no": bom_no, - # "qty": 4.0, - # "stock_uom": "_Test UOM", - # "wip_warehouse": "_Test Warehouse - _TC", - # "additional_operating_cost": 1000, - # "use_multi_level_bom": 1 - # }) - # work_order.insert() - # work_order.submit() - - # make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100) - # make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20) - - # item_quantity = { - # '_Test Item': 2.0, - # '_Test Item 2': 12.0, - # '_Test Serialized Item With Series': 6.0 - # } - - # stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2)) - # for d in stock_entry.get('items'): - # self.assertEqual(item_quantity.get(d.item_code), d.qty) - def test_customer_provided_parts_se(self): - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) - se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', - qty=4, to_warehouse = "_Test Warehouse - _TC") + create_item( + "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) + se = make_stock_entry( + item_code="CUST-0987", purpose="Material Receipt", qty=4, to_warehouse="_Test Warehouse - _TC" + ) self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) def test_zero_incoming_rate(self): - """ Make sure incoming rate of 0 is allowed while consuming. + """Make sure incoming rate of 0 is allowed while consuming. - qty | rate | valuation rate - 1 | 100 | 100 - 1 | 0 | 50 - -1 | 100 | 0 - -1 | 0 <--- assert this + qty | rate | valuation rate + 1 | 100 | 100 + 1 | 0 | 50 + -1 | 100 | 0 + -1 | 0 <--- assert this """ item_code = "_TestZeroVal" warehouse = "_Test Warehouse - _TC" - create_item('_TestZeroVal') + create_item("_TestZeroVal") _receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100) - receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True) + receipt2 = make_stock_entry( + item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True + ) receipt2.items[0].allow_zero_valuation_rate = 1 receipt2.save() receipt2.submit() issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) - value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) self.assertEqual(value_diff, -100) issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) - value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) self.assertEqual(value_diff, 0) - def test_gle_for_opening_stock_entry(self): - mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", - company="_Test Company with perpetual inventory", qty=50, basic_rate=100, - expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True) + mr = make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company="_Test Company with perpetual inventory", + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + is_opening="Yes", + do_not_save=True, + ) self.assertRaises(OpeningEntryAccountError, mr.save) @@ -896,52 +1048,61 @@ class TestStockEntry(ERPNextTestCase): mr.save() mr.submit() - is_opening = frappe.db.get_value("GL Entry", - filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, fieldname="is_opening") + is_opening = frappe.db.get_value( + "GL Entry", + filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, + fieldname="is_opening", + ) self.assertEqual(is_opening, "Yes") def test_total_basic_amount_zero(self): - se = frappe.get_doc({"doctype":"Stock Entry", - "purpose":"Material Receipt", - "stock_entry_type":"Material Receipt", - "posting_date": nowdate(), - "company":"_Test Company with perpetual inventory", - "items":[ - { - "item_code":"_Test Item", - "description":"_Test Item", - "qty": 1, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1" - }, - { - "item_code":"_Test Item", - "description":"_Test Item", - "qty": 2, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1" - }, - ], - "additional_costs":[ - {"expense_account":"Miscellaneous Expenses - TCP1", - "amount":100, - "description": "miscellanous" - }] - }) + se = frappe.get_doc( + { + "doctype": "Stock Entry", + "purpose": "Material Receipt", + "stock_entry_type": "Material Receipt", + "posting_date": nowdate(), + "company": "_Test Company with perpetual inventory", + "items": [ + { + "item_code": "_Test Item", + "description": "_Test Item", + "qty": 1, + "basic_rate": 0, + "uom": "Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1", + }, + { + "item_code": "_Test Item", + "description": "_Test Item", + "qty": 2, + "basic_rate": 0, + "uom": "Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1", + }, + ], + "additional_costs": [ + { + "expense_account": "Miscellaneous Expenses - TCP1", + "amount": 100, + "description": "miscellanous", + } + ], + } + ) se.insert() se.submit() - self.check_gl_entries("Stock Entry", se.name, - sorted([ - ["Stock Adjustment - TCP1", 100.0, 0.0], - ["Miscellaneous Expenses - TCP1", 0.0, 100.0] - ]) + self.check_gl_entries( + "Stock Entry", + se.name, + sorted( + [["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]] + ), ) def test_conversion_factor_change(self): @@ -972,15 +1133,15 @@ class TestStockEntry(ERPNextTestCase): def test_additional_cost_distribution_manufacture(self): se = frappe.get_doc( - doctype="Stock Entry", - purpose="Manufacture", - additional_costs=[frappe._dict(base_amount=100)], - items=[ - frappe._dict(item_code="RM", basic_amount=10), - frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1), - frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X") - ], - ) + doctype="Stock Entry", + purpose="Manufacture", + additional_costs=[frappe._dict(base_amount=100)], + items=[ + frappe._dict(item_code="RM", basic_amount=10), + frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1), + frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X"), + ], + ) se.distribute_additional_costs() @@ -989,14 +1150,14 @@ class TestStockEntry(ERPNextTestCase): def test_additional_cost_distribution_non_manufacture(self): se = frappe.get_doc( - doctype="Stock Entry", - purpose="Material Receipt", - additional_costs=[frappe._dict(base_amount=100)], - items=[ - frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"), - frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X") - ], - ) + doctype="Stock Entry", + purpose="Material Receipt", + additional_costs=[frappe._dict(base_amount=100)], + items=[ + frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"), + frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X"), + ], + ) se.distribute_additional_costs() @@ -1006,40 +1167,42 @@ class TestStockEntry(ERPNextTestCase): @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle(self): # Initialize item, batch, warehouse, opening qty - item_code = '_Test Future Neg Item' - batch_no = '_Test Future Neg Batch' - warehouses = [ - '_Test Future Neg Warehouse Source', - '_Test Future Neg Warehouse Destination' - ] + item_code = "_Test Future Neg Item" + batch_no = "_Test Future Neg Batch" + warehouses = ["_Test Future Neg Warehouse Source", "_Test Future Neg Warehouse Destination"] warehouse_names = initialize_records_for_future_negative_sle_test( - item_code, batch_no, warehouses, - opening_qty=2, posting_date='2021-07-01' + item_code, batch_no, warehouses, opening_qty=2, posting_date="2021-07-01" ) # Executing an illegal sequence should raise an error sequence_of_entries = [ - dict(item_code=item_code, + dict( + item_code=item_code, qty=2, from_warehouse=warehouse_names[0], to_warehouse=warehouse_names[1], batch_no=batch_no, - posting_date='2021-07-03', - purpose='Material Transfer'), - dict(item_code=item_code, + posting_date="2021-07-03", + purpose="Material Transfer", + ), + dict( + item_code=item_code, qty=2, from_warehouse=warehouse_names[1], to_warehouse=warehouse_names[0], batch_no=batch_no, - posting_date='2021-07-04', - purpose='Material Transfer'), - dict(item_code=item_code, + posting_date="2021-07-04", + purpose="Material Transfer", + ), + dict( + item_code=item_code, qty=2, from_warehouse=warehouse_names[0], to_warehouse=warehouse_names[1], batch_no=batch_no, - posting_date='2021-07-02', # Illegal SE - purpose='Material Transfer') + posting_date="2021-07-02", # Illegal SE + purpose="Material Transfer", + ), ] self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) @@ -1049,36 +1212,38 @@ class TestStockEntry(ERPNextTestCase): from erpnext.stock.doctype.batch.test_batch import TestBatch # Initialize item, batch, warehouse, opening qty - item_code = '_Test MultiBatch Item' + item_code = "_Test MultiBatch Item" TestBatch.make_batch_item(item_code) - batch_nos = [] # store generate batches - warehouse = '_Test Warehouse - _TC' + batch_nos = [] # store generate batches + warehouse = "_Test Warehouse - _TC" se1 = make_stock_entry( - item_code=item_code, - qty=2, - to_warehouse=warehouse, - posting_date='2021-09-01', - purpose='Material Receipt' - ) + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date="2021-09-01", + purpose="Material Receipt", + ) batch_nos.append(se1.items[0].batch_no) se2 = make_stock_entry( - item_code=item_code, - qty=2, - to_warehouse=warehouse, - posting_date='2021-09-03', - purpose='Material Receipt' - ) + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date="2021-09-03", + purpose="Material Receipt", + ) batch_nos.append(se2.items[0].batch_no) with self.assertRaises(NegativeStockError) as nse: - make_stock_entry(item_code=item_code, + make_stock_entry( + item_code=item_code, qty=1, from_warehouse=warehouse, batch_no=batch_nos[1], - posting_date='2021-09-02', # backdated consumption of 2nd batch - purpose='Material Issue') + posting_date="2021-09-02", # backdated consumption of 2nd batch + purpose="Material Issue", + ) def test_independent_manufacture_entry(self): "Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked." @@ -1088,9 +1253,11 @@ class TestStockEntry(ERPNextTestCase): stock_entry_type="Manufacture", company="_Test Company", items=[ - frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), - frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC") - ] + frappe._dict( + item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC" + ), + frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC"), + ], ) # SE must have atleast one FG self.assertRaises(FinishedGoodError, se.save) @@ -1105,10 +1272,18 @@ class TestStockEntry(ERPNextTestCase): # Check if FG cost is calculated based on RM total cost # RM total cost = 200, FG rate = 200/4(FG qty) = 50 - self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4)) + self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate / 4)) self.assertEqual(se.value_difference, 0.0) self.assertEqual(se.total_incoming_value, se.total_outgoing_value) + def test_transfer_qty_validation(self): + se = make_stock_entry(item_code="_Test Item", do_not_save=True, qty=0.001, rate=100) + se.items[0].uom = "Kg" + se.items[0].conversion_factor = 0.002 + + self.assertRaises(frappe.ValidationError, se.save) + + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) @@ -1138,50 +1313,57 @@ def make_serialized_item(**args): se.submit() return se + def get_qty_after_transaction(**args): args = frappe._dict(args) - last_sle = get_previous_sle({ - "item_code": args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "posting_date": args.posting_date or nowdate(), - "posting_time": args.posting_time or nowtime() - }) + last_sle = get_previous_sle( + { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "posting_date": args.posting_date or nowdate(), + "posting_time": args.posting_time or nowtime(), + } + ) return flt(last_sle.get("qty_after_transaction")) + def get_multiple_items(): return [ - { - "conversion_factor": 1.0, - "cost_center": "Main - TCP1", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - TCP1", - "basic_rate": 100, - "item_code": "_Test Item", - "qty": 50.0, - "s_warehouse": "Stores - TCP1", - "stock_uom": "_Test UOM", - "transfer_qty": 50.0, - "uom": "_Test UOM" - }, - { - "conversion_factor": 1.0, - "cost_center": "Main - TCP1", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - TCP1", - "basic_rate": 5000, - "item_code": "_Test Item Home Desktop 100", - "qty": 1, - "stock_uom": "_Test UOM", - "t_warehouse": "Stores - TCP1", - "transfer_qty": 1, - "uom": "_Test UOM" - } - ] + { + "conversion_factor": 1.0, + "cost_center": "Main - TCP1", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - TCP1", + "basic_rate": 100, + "item_code": "_Test Item", + "qty": 50.0, + "s_warehouse": "Stores - TCP1", + "stock_uom": "_Test UOM", + "transfer_qty": 50.0, + "uom": "_Test UOM", + }, + { + "conversion_factor": 1.0, + "cost_center": "Main - TCP1", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - TCP1", + "basic_rate": 5000, + "item_code": "_Test Item Home Desktop 100", + "qty": 1, + "stock_uom": "_Test UOM", + "t_warehouse": "Stores - TCP1", + "transfer_qty": 1, + "uom": "_Test UOM", + }, + ] + + +test_records = frappe.get_test_records("Stock Entry") -test_records = frappe.get_test_records('Stock Entry') def initialize_records_for_future_negative_sle_test( - item_code, batch_no, warehouses, opening_qty, posting_date): + item_code, batch_no, warehouses, opening_qty, posting_date +): from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, @@ -1192,9 +1374,9 @@ def initialize_records_for_future_negative_sle_test( make_new_batch(item_code=item_code, batch_id=batch_no) warehouse_names = [create_warehouse(w) for w in warehouses] create_stock_reconciliation( - purpose='Opening Stock', + purpose="Opening Stock", posting_date=posting_date, - posting_time='20:00:20', + posting_time="20:00:20", item_code=item_code, warehouse=warehouse_names[0], valuation_rate=100, diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index efd97c04ac6..7258cfbe2c9 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -8,5 +8,5 @@ from frappe.model.document import Document class StockEntryType(Document): def validate(self): - if self.add_to_transit and self.purpose != 'Material Transfer': + if self.add_to_transit and self.purpose != "Material Transfer": self.add_to_transit = 0 diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 2651407d16f..6de40984cd7 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -317,7 +317,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-10-08 13:42:51.857631", + "modified": "2021-10-08 13:44:51.857631", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 39ca97a47bc..5c1da420e24 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -1,4 +1,3 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt @@ -15,11 +14,17 @@ from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock -class StockFreezeError(frappe.ValidationError): pass -class BackDatedStockTransaction(frappe.ValidationError): pass +class StockFreezeError(frappe.ValidationError): + pass + + +class BackDatedStockTransaction(frappe.ValidationError): + pass + exclude_from_linked_with = True + class StockLedgerEntry(Document): def autoname(self): """ @@ -27,10 +32,13 @@ class StockLedgerEntry(Document): name will be changed using autoname options (in a scheduled job) """ self.name = frappe.generate_hash(txt="", length=10) + if self.meta.autoname == "hash": + self.to_rename = 0 def validate(self): self.flags.ignore_submit_comment = True from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company + self.validate_mandatory() self.validate_item() self.validate_batch() @@ -41,24 +49,29 @@ class StockLedgerEntry(Document): self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() - def on_submit(self): self.check_stock_frozen_date() self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): from erpnext.stock.doctype.serial_no.serial_no import process_serial_no + process_serial_no(self) def calculate_batch_qty(self): if self.batch_no: - batch_qty = frappe.db.get_value("Stock Ledger Entry", - {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0}, - "sum(actual_qty)") or 0 + batch_qty = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0}, + "sum(actual_qty)", + ) + or 0 + ) frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) def validate_mandatory(self): - mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] + mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"] for k in mandatory: if not self.get(k): frappe.throw(_("{0} is required").format(self.meta.get_label(k))) @@ -67,9 +80,13 @@ class StockLedgerEntry(Document): frappe.throw(_("Actual Qty is mandatory")) def validate_item(self): - item_det = frappe.db.sql("""select name, item_name, has_batch_no, docstatus, + item_det = frappe.db.sql( + """select name, item_name, has_batch_no, docstatus, is_stock_item, has_variants, stock_uom, create_new_batch - from tabItem where name=%s""", self.item_code, as_dict=True) + from tabItem where name=%s""", + self.item_code, + as_dict=True, + ) if not item_det: frappe.throw(_("Item {0} not found").format(self.item_code)) @@ -81,39 +98,58 @@ class StockLedgerEntry(Document): # check if batch number is valid if item_det.has_batch_no == 1: - batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name + batch_item = ( + self.item_code + if self.item_code == item_det.item_name + else self.item_code + ":" + item_det.item_name + ) if not self.batch_no: frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) - elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): - frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) + elif not frappe.db.get_value("Batch", {"item": self.item_code, "name": self.batch_no}): + frappe.throw( + _("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item) + ) elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: - frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), - ItemTemplateCannotHaveStock) + frappe.throw( + _("Stock cannot exist for Item {0} since has variants").format(self.item_code), + ItemTemplateCannotHaveStock, + ) self.stock_uom = item_det.stock_uom def check_stock_frozen_date(self): - stock_settings = frappe.get_cached_doc('Stock Settings') + stock_settings = frappe.get_cached_doc("Stock Settings") if stock_settings.stock_frozen_upto: - if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) - and stock_settings.stock_auth_role not in frappe.get_roles()): - frappe.throw(_("Stock transactions before {0} are frozen") - .format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError) + if ( + getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) + and stock_settings.stock_auth_role not in frappe.get_roles() + ): + frappe.throw( + _("Stock transactions before {0} are frozen").format( + formatdate(stock_settings.stock_frozen_upto) + ), + StockFreezeError, + ) stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days) if stock_frozen_upto_days: - older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today()) + older_than_x_days_ago = ( + add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today() + ) if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles(): - frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError) + frappe.throw( + _("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), + StockFreezeError, + ) def scrub_posting_time(self): - if not self.posting_time or self.posting_time == '00:0': - self.posting_time = '00:00' + if not self.posting_time or self.posting_time == "00:0": + self.posting_time = "00:00" def validate_batch(self): if self.batch_no and self.voucher_type != "Stock Entry": @@ -127,43 +163,61 @@ class StockLedgerEntry(Document): self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] else: from erpnext.accounts.utils import validate_fiscal_year - validate_fiscal_year(self.posting_date, self.fiscal_year, self.company, - self.meta.get_label("posting_date"), self) + + validate_fiscal_year( + self.posting_date, self.fiscal_year, self.company, self.meta.get_label("posting_date"), self + ) def block_transactions_against_group_warehouse(self): from erpnext.stock.utils import is_group_warehouse + is_group_warehouse(self.warehouse) def validate_with_last_transaction_posting_time(self): - authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions") + authorized_role = frappe.db.get_single_value( + "Stock Settings", "role_allowed_to_create_edit_back_dated_transactions" + ) if authorized_role: authorized_users = get_users(authorized_role) if authorized_users and frappe.session.user not in authorized_users: - last_transaction_time = frappe.db.sql(""" + last_transaction_time = frappe.db.sql( + """ select MAX(timestamp(posting_date, posting_time)) as posting_time from `tabStock Ledger Entry` where docstatus = 1 and is_cancelled = 0 and item_code = %s - and warehouse = %s""", (self.item_code, self.warehouse))[0][0] + and warehouse = %s""", + (self.item_code, self.warehouse), + )[0][0] - cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") + cur_doc_posting_datetime = "%s %s" % ( + self.posting_date, + self.get("posting_time") or "00:00:00", + ) - if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time): - msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code), - frappe.bold(self.warehouse), frappe.bold(last_transaction_time)) + if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime( + last_transaction_time + ): + msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format( + frappe.bold(self.item_code), frappe.bold(self.warehouse), frappe.bold(last_transaction_time) + ) - msg += "

    " + _("You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.").format( - frappe.bold(self.item_code), frappe.bold(self.warehouse)) + msg += "

    " + _( + "You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time." + ).format(frappe.bold(self.item_code), frappe.bold(self.warehouse)) msg += "

    " + _("Please contact any of the following users to {} this transaction.") msg += "
    " + "
    ".join(authorized_users) frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry")) + def on_doctype_update(): - if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'): + if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): frappe.db.commit() - frappe.db.add_index("Stock Ledger Entry", + frappe.db.add_index( + "Stock Ledger Entry", fields=["posting_date", "posting_time", "name"], - index_name="posting_sort_index") + index_name="posting_sort_index", + ) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) 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 6d113ba4eb6..74775b98e39 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -5,12 +5,13 @@ import json import frappe from frappe.core.page.permission_manager.permission_manager import reset +from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today +from frappe.utils.data import add_to_date -from erpnext.stock.doctype.delivery_note.test_delivery_note import ( - create_delivery_note, - create_return_delivery_note, -) +from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( create_landed_cost_voucher, @@ -22,27 +23,35 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.stock.stock_ledger import get_previous_sle -from erpnext.tests.utils import ERPNextTestCase -class TestStockLedgerEntry(ERPNextTestCase): +class TestStockLedgerEntry(FrappeTestCase): def setUp(self): items = create_items() - reset('Stock Entry') + 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) - frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items) - + frappe.db.sql( + "delete from `tabStock Ledger Entry` where item_code in (%s)" + % (", ".join(["%s"] * len(items))), + items, + ) + frappe.db.sql( + "delete from `tabBin` where item_code in (%s)" % (", ".join(["%s"] * len(items))), items + ) def assertSLEs(self, doc, expected_sles, sle_filters=None): - """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" + """Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} if sle_filters: filters.update(sle_filters) - sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, - order_by="timestamp(posting_date, posting_time), creation") + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["*"], + filters=filters, + order_by="timestamp(posting_date, posting_time), creation", + ) for exp_sle, act_sle in zip(expected_sles, sles): for k, v in exp_sle.items(): @@ -65,9 +74,11 @@ class TestStockLedgerEntry(ERPNextTestCase): qty=50, rate=100, company=company, - expense_account = "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-10', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-10", + posting_time="14:00", ) # _Test Item for Reposting at FG warehouse on 20-04-2020: Qty = 10, Rate = 200 @@ -77,9 +88,11 @@ class TestStockLedgerEntry(ERPNextTestCase): qty=10, rate=200, company=company, - expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-20', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-20", + posting_time="14:00", ) # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020 @@ -89,28 +102,40 @@ class TestStockLedgerEntry(ERPNextTestCase): target="Finished Goods - _TC", company=company, qty=10, - expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-30', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-30", + posting_time="14:00", + ) + target_wh_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": se.name, + }, + ["valuation_rate"], + as_dict=1, ) - target_wh_sle = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": se.name - }, ["valuation_rate"], as_dict=1) self.assertEqual(target_wh_sle.get("valuation_rate"), 150) # Repack entry on 5-5-2020 - repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00') + repack = create_repack_entry(company=company, posting_date="2020-05-05", posting_time="14:00") - finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Finished Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": repack.name - }, ["incoming_rate", "valuation_rate"], as_dict=1) + finished_item_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Finished Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + }, + ["incoming_rate", "valuation_rate"], + as_dict=1, + ) self.assertEqual(finished_item_sle.get("incoming_rate"), 540) self.assertEqual(finished_item_sle.get("valuation_rate"), 540) @@ -121,29 +146,37 @@ class TestStockLedgerEntry(ERPNextTestCase): qty=50, rate=150, company=company, - expense_account ="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-12', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-12", + posting_time="14:00", ) - # Check valuation rate of finished goods warehouse after back-dated entry at Stores - target_wh_sle = get_previous_sle({ - "item_code": "_Test Item for Reposting", - "warehouse": "Finished Goods - _TC", - "posting_date": '2020-04-30', - "posting_time": '14:00' - }) + target_wh_sle = get_previous_sle( + { + "item_code": "_Test Item for Reposting", + "warehouse": "Finished Goods - _TC", + "posting_date": "2020-04-30", + "posting_time": "14:00", + } + ) self.assertEqual(target_wh_sle.get("incoming_rate"), 150) self.assertEqual(target_wh_sle.get("valuation_rate"), 175) # Check valuation rate of repacked item after back-dated entry at Stores - finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Finished Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": repack.name - }, ["incoming_rate", "valuation_rate"], as_dict=1) + finished_item_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Finished Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + }, + ["incoming_rate", "valuation_rate"], + as_dict=1, + ) self.assertEqual(finished_item_sle.get("incoming_rate"), 790) self.assertEqual(finished_item_sle.get("valuation_rate"), 790) @@ -153,76 +186,133 @@ class TestStockLedgerEntry(ERPNextTestCase): self.assertEqual(repack.items[1].get("basic_rate"), 750) def test_purchase_return_valuation_reposting(self): - pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10', - warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100) + pr = make_purchase_receipt( + company="_Test Company", + posting_date="2020-04-10", + warehouse="Stores - _TC", + item_code="_Test Item for Reposting", + qty=5, + rate=100, + ) - return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15', - warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2) + return_pr = make_purchase_receipt( + company="_Test Company", + posting_date="2020-04-15", + warehouse="Stores - _TC", + item_code="_Test Item for Reposting", + is_return=1, + return_against=pr.name, + qty=-2, + ) # check sle - outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"]) + outgoing_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, + ["outgoing_rate", "stock_value_difference"], + ) self.assertEqual(outgoing_rate, 100) self.assertEqual(stock_value_difference, -200) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"]) + outgoing_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, + ["outgoing_rate", "stock_value_difference"], + ) self.assertEqual(outgoing_rate, 110) self.assertEqual(stock_value_difference, -220) def test_sales_return_valuation_reposting(self): company = "_Test Company" - item_code="_Test Item for Reposting" + item_code = "_Test Item for Reposting" # Purchase Return: Qty = 5, Rate = 100 - pr = make_purchase_receipt(company=company, posting_date='2020-04-10', - warehouse="Stores - _TC", item_code=item_code, qty=5, rate=100) + pr = make_purchase_receipt( + company=company, + posting_date="2020-04-10", + warehouse="Stores - _TC", + item_code=item_code, + qty=5, + rate=100, + ) - #Delivery Note: Qty = 5, Rate = 150 - dn = create_delivery_note(item_code=item_code, qty=5, rate=150, warehouse="Stores - _TC", - company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + # Delivery Note: Qty = 5, Rate = 150 + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=150, + warehouse="Stores - _TC", + company=company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + ) # check outgoing_rate for DN - outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 5) + outgoing_rate = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 5 + ) self.assertEqual(dn.items[0].incoming_rate, 100) self.assertEqual(outgoing_rate, 100) # Return Entry: Qty = -2, Rate = 150 - return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=item_code, qty=-2, rate=150, - company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + return_dn = create_delivery_note( + is_return=1, + return_against=dn.name, + item_code=item_code, + qty=-2, + rate=150, + company=company, + warehouse="Stores - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + ) # check incoming rate for Return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(return_dn.items[0].incoming_rate, 100) self.assertEqual(incoming_rate, 100) self.assertEqual(stock_value_difference, 200) - #------------------------------- + # ------------------------------- # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50 lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) # check outgoing_rate for DN after reposting - outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 5) + outgoing_rate = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 5 + ) self.assertEqual(outgoing_rate, 110) dn.reload() self.assertEqual(dn.items[0].incoming_rate, 110) # check incoming rate for Return entry after reposting - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 110) self.assertEqual(stock_value_difference, 220) @@ -238,54 +328,93 @@ class TestStockLedgerEntry(ERPNextTestCase): def test_reposting_of_sales_return_for_packed_item(self): company = "_Test Company" - packed_item_code="_Test Item for Reposting" + packed_item_code = "_Test Item for Reposting" bundled_item = "_Test Bundled Item for Reposting" create_product_bundle_item(bundled_item, [[packed_item_code, 4]]) # Purchase Return: Qty = 50, Rate = 100 - pr = make_purchase_receipt(company=company, posting_date='2020-04-10', - warehouse="Stores - _TC", item_code=packed_item_code, qty=50, rate=100) + pr = make_purchase_receipt( + company=company, + posting_date="2020-04-10", + warehouse="Stores - _TC", + item_code=packed_item_code, + qty=50, + rate=100, + ) - #Delivery Note: Qty = 5, Rate = 150 - dn = create_delivery_note(item_code=bundled_item, qty=5, rate=150, warehouse="Stores - _TC", - company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + # Delivery Note: Qty = 5, Rate = 150 + dn = create_delivery_note( + item_code=bundled_item, + qty=5, + rate=150, + warehouse="Stores - _TC", + company=company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + ) # check outgoing_rate for DN - outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 20) + outgoing_rate = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 20 + ) self.assertEqual(dn.packed_items[0].incoming_rate, 100) self.assertEqual(outgoing_rate, 100) # Return Entry: Qty = -2, Rate = 150 - return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2) + return_dn = create_delivery_note( + is_return=1, + return_against=dn.name, + item_code=bundled_item, + qty=-2, + rate=150, + company=company, + warehouse="Stores - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + ) # check incoming rate for Return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(return_dn.packed_items[0].incoming_rate, 100) self.assertEqual(incoming_rate, 100) self.assertEqual(stock_value_difference, 800) - #------------------------------- + # ------------------------------- # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50 lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) # check outgoing_rate for DN after reposting - outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 20) + outgoing_rate = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 20 + ) self.assertEqual(outgoing_rate, 101) dn.reload() self.assertEqual(dn.packed_items[0].incoming_rate, 101) # check incoming rate for Return entry after reposting - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 101) self.assertEqual(stock_value_difference, 808) @@ -303,20 +432,35 @@ class TestStockLedgerEntry(ERPNextTestCase): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom company = "_Test Company" - rm_item_code="_Test Item for Reposting" + rm_item_code = "_Test Item for Reposting" subcontracted_item = "_Test Subcontracted Item for Reposting" - frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") - make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR") + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) + make_bom(item=subcontracted_item, raw_materials=[rm_item_code], currency="INR") # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100 - pr = make_purchase_receipt(company=company, posting_date='2020-04-10', - warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100) + pr = make_purchase_receipt( + company=company, + posting_date="2020-04-10", + warehouse="Stores - _TC", + item_code=rm_item_code, + qty=10, + rate=100, + ) # Purchase Receipt for subcontracted item - pr1 = make_purchase_receipt(company=company, posting_date='2020-04-20', - warehouse="Finished Goods - _TC", supplier_warehouse="Stores - _TC", - item_code=subcontracted_item, qty=10, rate=20, is_subcontracted="Yes") + pr1 = make_purchase_receipt( + company=company, + posting_date="2020-04-20", + warehouse="Finished Goods - _TC", + supplier_warehouse="Stores - _TC", + item_code=subcontracted_item, + qty=10, + rate=20, + is_subcontracted="Yes", + ) self.assertEqual(pr1.items[0].valuation_rate, 120) @@ -327,8 +471,11 @@ class TestStockLedgerEntry(ERPNextTestCase): self.assertEqual(pr1.items[0].valuation_rate, 125) # check outgoing_rate for DN after reposting - incoming_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": pr1.name, "item_code": subcontracted_item}, "incoming_rate") + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "item_code": subcontracted_item}, + "incoming_rate", + ) self.assertEqual(incoming_rate, 125) # cleanup data @@ -338,8 +485,9 @@ class TestStockLedgerEntry(ERPNextTestCase): def test_back_dated_entry_not_allowed(self): # Back dated stock transactions are only allowed to stock managers - frappe.db.set_value("Stock Settings", None, - "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager") + frappe.db.set_value( + "Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager" + ) # Set User with Stock User role but not Stock Manager try: @@ -350,8 +498,13 @@ class TestStockLedgerEntry(ERPNextTestCase): 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) + 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, + ) # Block back-dated entry self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit) @@ -361,14 +514,17 @@ class TestStockLedgerEntry(ERPNextTestCase): frappe.set_user(user.name) # Back dated entry allowed to Stock Manager - back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, - posting_date=add_days(today(), -1)) + back_dated_se_2 = make_stock_entry( + target="_Test Warehouse - _TC", qty=10, basic_rate=100, posting_date=add_days(today(), -1) + ) back_dated_se_2.cancel() stock_entry_on_today.cancel() finally: - frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None) + frappe.db.set_value( + "Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None + ) frappe.set_user("Administrator") user.remove_roles("Stock Manager") @@ -390,12 +546,12 @@ class TestStockLedgerEntry(ERPNextTestCase): expected_queues = [] for idx, rate in enumerate(rates, start=1): - expected_queues.append( - {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]} - ) + expected_queues.append({"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}) self.assertSLEs(receipt, expected_queues) - transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10) + transfer = make_stock_entry( + item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10 + ) for rate in rates[1:]: row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False) transfer.append("items", row) @@ -413,7 +569,9 @@ class TestStockLedgerEntry(ERPNextTestCase): rates = [10 * i for i in range(1, 5)] - receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10) + receipt = make_stock_entry( + item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10 + ) for rate in rates[1:]: row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) row.basic_rate = rate @@ -422,26 +580,108 @@ class TestStockLedgerEntry(ERPNextTestCase): receipt.save() receipt.submit() - repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10, - do_not_save=True, rate=10, purpose="Repack") + repack = make_stock_entry( + item_code=rm.name, source=warehouse, qty=10, do_not_save=True, rate=10, purpose="Repack" + ) for rate in rates[1:]: row = frappe.copy_doc(repack.items[0], ignore_no_copy=False) repack.append("items", row) - repack.append("items", { - "item_code": packed.name, - "t_warehouse": warehouse, - "qty": 1, - "transfer_qty": 1, - }) + repack.append( + "items", + { + "item_code": packed.name, + "t_warehouse": warehouse, + "qty": 1, + "transfer_qty": 1, + }, + ) repack.save() repack.submit() # same exact queue should be transferred - self.assertSLEs(repack, [ - {"incoming_rate": sum(rates) * 10} - ], sle_filters={"item_code": packed.name}) + self.assertSLEs( + repack, [{"incoming_rate": sum(rates) * 10}], sle_filters={"item_code": packed.name} + ) + + @change_settings("Stock Settings", {"allow_negative_stock": 1}) + def test_negative_fifo_valuation(self): + """ + When stock goes negative discard FIFO queue. + Only pervailing valuation rate should be used for making transactions in such cases. + """ + item = make_item(properties={"allow_negative_stock": 1}).name + warehouse = "_Test Warehouse - _TC" + + receipt = make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10) + consume1 = make_stock_entry(item_code=item, source=warehouse, qty=15) + + self.assertSLEs(consume1, [{"stock_value": -5 * 10, "stock_queue": [[-5, 10]]}]) + + consume2 = make_stock_entry(item_code=item, source=warehouse, qty=5) + self.assertSLEs(consume2, [{"stock_value": -10 * 10, "stock_queue": [[-10, 10]]}]) + + receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15) + self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}]) + + def test_dependent_gl_entry_reposting(self): + def _get_stock_credit(doc): + return frappe.db.get_value( + "GL Entry", + { + "voucher_no": doc.name, + "voucher_type": doc.doctype, + "is_cancelled": 0, + "account": "Stock In Hand - TCP1", + }, + "sum(credit)", + ) + + def _day(days): + return add_to_date(date=today(), days=days) + + item = make_item().name + A = "Stores - TCP1" + B = "Work In Progress - TCP1" + C = "Finished Goods - TCP1" + + make_stock_entry(item_code=item, to_warehouse=A, qty=5, rate=10, posting_date=_day(0)) + make_stock_entry(item_code=item, from_warehouse=A, to_warehouse=B, qty=5, posting_date=_day(1)) + depdendent_consumption = make_stock_entry( + item_code=item, from_warehouse=B, qty=5, posting_date=_day(2) + ) + self.assertEqual(50, _get_stock_credit(depdendent_consumption)) + + # backdated receipt - should trigger GL repost of all previous stock entries + bd_receipt = make_stock_entry( + item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1) + ) + self.assertEqual(100, _get_stock_credit(depdendent_consumption)) + + # cancelling receipt should reset it back + bd_receipt.cancel() + self.assertEqual(50, _get_stock_credit(depdendent_consumption)) + + bd_receipt2 = make_stock_entry( + item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2) + ) + # total as per FIFO -> 2 * 20 + 3 * 10 = 70 + self.assertEqual(70, _get_stock_credit(depdendent_consumption)) + + # transfer WIP material to final destination and consume it all + depdendent_consumption.cancel() + make_stock_entry(item_code=item, from_warehouse=B, to_warehouse=C, qty=5, posting_date=_day(3)) + final_consumption = make_stock_entry( + item_code=item, from_warehouse=C, qty=5, posting_date=_day(4) + ) + # exact amount gets consumed + self.assertEqual(70, _get_stock_credit(final_consumption)) + + # cancel original backdated receipt - should repost A -> B -> C + bd_receipt2.cancel() + # original amount + self.assertEqual(50, _get_stock_credit(final_consumption)) def create_repack_entry(**args): @@ -451,51 +691,63 @@ def create_repack_entry(**args): repack.company = args.company or "_Test Company" repack.posting_date = args.posting_date repack.set_posting_time = 1 - repack.append("items", { - "item_code": "_Test Item for Reposting", - "s_warehouse": "Stores - _TC", - "qty": 5, - "conversion_factor": 1, - "expense_account": "Stock Adjustment - _TC", - "cost_center": "Main - _TC" - }) + repack.append( + "items", + { + "item_code": "_Test Item for Reposting", + "s_warehouse": "Stores - _TC", + "qty": 5, + "conversion_factor": 1, + "expense_account": "Stock Adjustment - _TC", + "cost_center": "Main - _TC", + }, + ) - repack.append("items", { - "item_code": "_Test Finished Item for Reposting", - "t_warehouse": "Finished Goods - _TC", - "qty": 1, - "conversion_factor": 1, - "expense_account": "Stock Adjustment - _TC", - "cost_center": "Main - _TC" - }) + repack.append( + "items", + { + "item_code": "_Test Finished Item for Reposting", + "t_warehouse": "Finished Goods - _TC", + "qty": 1, + "conversion_factor": 1, + "expense_account": "Stock Adjustment - _TC", + "cost_center": "Main - _TC", + }, + ) - repack.append("additional_costs", { - "expense_account": "Freight and Forwarding Charges - _TC", - "description": "transport cost", - "amount": 40 - }) + repack.append( + "additional_costs", + { + "expense_account": "Freight and Forwarding Charges - _TC", + "description": "transport cost", + "amount": 40, + }, + ) repack.save() repack.submit() return repack + def create_product_bundle_item(new_item_code, packed_items): if not frappe.db.exists("Product Bundle", new_item_code): item = frappe.new_doc("Product Bundle") item.new_item_code = new_item_code for d in packed_items: - item.append("items", { - "item_code": d[0], - "qty": d[1] - }) + item.append("items", {"item_code": d[0], "qty": d[1]}) item.save() + def create_items(): - items = ["_Test Item for Reposting", "_Test Finished Item for Reposting", - "_Test Subcontracted Item for Reposting", "_Test Bundled Item for Reposting"] + items = [ + "_Test Item for Reposting", + "_Test Finished Item for Reposting", + "_Test Subcontracted Item for Reposting", + "_Test Bundled Item for Reposting", + ] for d in items: properties = {"valuation_method": "FIFO"} if d == "_Test Bundled Item for Reposting": @@ -506,3 +758,81 @@ def create_items(): make_item(d, properties=properties) return items + + +class TestDeferredNaming(FrappeTestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.gle_autoname = frappe.get_meta("GL Entry").autoname + cls.sle_autoname = frappe.get_meta("Stock Ledger Entry").autoname + + def setUp(self) -> None: + self.item = make_item().name + self.warehouse = "Stores - TCP1" + self.company = "_Test Company with perpetual inventory" + + def tearDown(self) -> None: + make_property_setter( + doctype="GL Entry", + for_doctype=True, + property="autoname", + value=self.gle_autoname, + property_type="Data", + fieldname=None, + ) + make_property_setter( + doctype="Stock Ledger Entry", + for_doctype=True, + property="autoname", + value=self.sle_autoname, + property_type="Data", + fieldname=None, + ) + + # since deferred naming autocommits, commit all changes to avoid flake + frappe.db.commit() # nosemgrep + + @staticmethod + def get_gle_sles(se): + filters = {"voucher_type": se.doctype, "voucher_no": se.name} + gle = set(frappe.get_list("GL Entry", filters, pluck="name")) + sle = set(frappe.get_list("Stock Ledger Entry", filters, pluck="name")) + return gle, sle + + def test_deferred_naming(self): + se = make_stock_entry( + item_code=self.item, to_warehouse=self.warehouse, qty=10, rate=100, company=self.company + ) + + gle, sle = self.get_gle_sles(se) + rename_gle_sle_docs() + renamed_gle, renamed_sle = self.get_gle_sles(se) + + self.assertFalse(gle & renamed_gle, msg="GLEs not renamed") + self.assertFalse(sle & renamed_sle, msg="SLEs not renamed") + se.cancel() + + def test_hash_naming(self): + # disable naming series + for doctype in ("GL Entry", "Stock Ledger Entry"): + make_property_setter( + doctype=doctype, + for_doctype=True, + property="autoname", + value="hash", + property_type="Data", + fieldname=None, + ) + + se = make_stock_entry( + item_code=self.item, to_warehouse=self.warehouse, qty=10, rate=100, company=self.company + ) + + gle, sle = self.get_gle_sles(se) + rename_gle_sle_docs() + renamed_gle, renamed_sle = self.get_gle_sles(se) + + self.assertEqual(gle, renamed_gle, msg="GLEs are renamed while using hash naming") + self.assertEqual(sle, renamed_sle, msg="SLEs are renamed while using hash naming") + se.cancel() diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 8f65287c4e8..16ca88bd810 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -14,8 +14,13 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance -class OpeningEntryAccountError(frappe.ValidationError): pass -class EmptyStockReconciliationItemsError(frappe.ValidationError): pass +class OpeningEntryAccountError(frappe.ValidationError): + pass + + +class EmptyStockReconciliationItemsError(frappe.ValidationError): + pass + class StockReconciliation(StockController): def __init__(self, *args, **kwargs): @@ -24,9 +29,11 @@ class StockReconciliation(StockController): def validate(self): if not self.expense_account: - self.expense_account = frappe.get_cached_value('Company', self.company, "stock_adjustment_account") + self.expense_account = frappe.get_cached_value( + "Company", self.company, "stock_adjustment_account" + ) if not self.cost_center: - self.cost_center = frappe.get_cached_value('Company', self.company, "cost_center") + self.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") self.validate_posting_time() self.remove_items_with_no_change() self.validate_data() @@ -37,8 +44,8 @@ class StockReconciliation(StockController): self.set_total_qty_and_amount() self.validate_putaway_capacity() - if self._action=="submit": - self.make_batches('warehouse') + if self._action == "submit": + self.make_batches("warehouse") def on_submit(self): self.update_stock_ledger() @@ -46,10 +53,11 @@ class StockReconciliation(StockController): self.repost_future_sle_and_gle() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.make_sle_on_cancel() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() @@ -57,13 +65,17 @@ class StockReconciliation(StockController): def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 - def _changed(item): - item_dict = get_stock_balance_for(item.item_code, item.warehouse, - self.posting_date, self.posting_time, batch_no=item.batch_no) - if ((item.qty is None or item.qty==item_dict.get("qty")) and - (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and - (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")) )): + def _changed(item): + item_dict = get_stock_balance_for( + item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no + ) + + if ( + (item.qty is None or item.qty == item_dict.get("qty")) + and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")) + and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos"))) + ): return False else: # set default as current rates @@ -80,16 +92,20 @@ class StockReconciliation(StockController): item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") - self.difference_amount += (flt(item.qty, item.precision("qty")) * \ - flt(item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")) \ - - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate"))) + self.difference_amount += flt(item.qty, item.precision("qty")) * flt( + item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate") + ) - flt(item_dict.get("qty"), item.precision("qty")) * flt( + item_dict.get("rate"), item.precision("valuation_rate") + ) return True items = list(filter(lambda d: _changed(d), self.items)) if not items: - frappe.throw(_("None of the items have any change in quantity or value."), - EmptyStockReconciliationItemsError) + frappe.throw( + _("None of the items have any change in quantity or value."), + EmptyStockReconciliationItemsError, + ) elif len(items) != len(self.items): self.items = items @@ -99,7 +115,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 = [] @@ -109,7 +125,7 @@ class StockReconciliation(StockController): for row_num, row in enumerate(self.items): # find duplicates key = [row.item_code, row.warehouse] - for field in ['serial_no', 'batch_no']: + for field in ["serial_no", "batch_no"]: if row.get(field): key.append(row.get(field)) @@ -126,32 +142,35 @@ class StockReconciliation(StockController): # if both not specified if row.qty in ["", None] and row.valuation_rate in ["", None]: - self.validation_messages.append(_get_msg(row_num, - _("Please specify either Quantity or Valuation Rate or both"))) + self.validation_messages.append( + _get_msg(row_num, _("Please specify either Quantity or Valuation Rate or both")) + ) # do not allow negative quantity if flt(row.qty) < 0: - self.validation_messages.append(_get_msg(row_num, - _("Negative Quantity is not allowed"))) + self.validation_messages.append(_get_msg(row_num, _("Negative Quantity is not allowed"))) # do not allow negative valuation if flt(row.valuation_rate) < 0: - self.validation_messages.append(_get_msg(row_num, - _("Negative Valuation Rate is not allowed"))) + self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed"))) if row.qty and row.valuation_rate in ["", None]: - row.valuation_rate = get_stock_balance(row.item_code, row.warehouse, - self.posting_date, self.posting_time, with_valuation_rate=True)[1] + row.valuation_rate = get_stock_balance( + row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True + )[1] if not row.valuation_rate: # try if there is a buying price list in default currency - buying_rate = frappe.db.get_value("Item Price", {"item_code": row.item_code, - "buying": 1, "currency": default_currency}, "price_list_rate") + buying_rate = frappe.db.get_value( + "Item Price", + {"item_code": row.item_code, "buying": 1, "currency": default_currency}, + "price_list_rate", + ) if buying_rate: row.valuation_rate = buying_rate else: # get valuation rate from Item - row.valuation_rate = frappe.get_value('Item', row.item_code, 'valuation_rate') + row.valuation_rate = frappe.get_value("Item", row.item_code, "valuation_rate") # throw all validation messages if self.validation_messages: @@ -178,7 +197,9 @@ class StockReconciliation(StockController): # item should not be serialized if item.has_serial_no and not row.serial_no and not item.serial_no_series: - raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code)) + raise frappe.ValidationError( + _("Serial no(s) required for serialized item {0}").format(item_code) + ) # item managed batch-wise not allowed if item.has_batch_no and not row.batch_no and not item.create_new_batch: @@ -191,8 +212,8 @@ class StockReconciliation(StockController): self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e)) def update_stock_ledger(self): - """ find difference between current and expected entries - and create stock ledger entries based on the difference""" + """find difference between current and expected entries + and create stock ledger entries based on the difference""" from erpnext.stock.stock_ledger import get_previous_sle sl_entries = [] @@ -208,15 +229,20 @@ class StockReconciliation(StockController): self.get_sle_for_serialized_items(row, sl_entries) else: if row.serial_no or row.batch_no: - frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ - .format(row.idx, frappe.bold(row.item_code))) + frappe.throw( + _( + "Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it." + ).format(row.idx, frappe.bold(row.item_code)) + ) - previous_sle = get_previous_sle({ - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time - }) + previous_sle = get_previous_sle( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) if previous_sle: if row.qty in ("", None): @@ -226,12 +252,16 @@ class StockReconciliation(StockController): row.valuation_rate = previous_sle.get("valuation_rate", 0) if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate: - frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)) + frappe.throw( + _("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx) + ) - if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") - and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)) - or (not previous_sle and not row.qty)): - continue + if ( + previous_sle + and row.qty == previous_sle.get("qty_after_transaction") + and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0) + ) or (not previous_sle and not row.qty): + continue sl_entries.append(self.get_sle_for_items(row)) @@ -253,21 +283,24 @@ class StockReconciliation(StockController): serial_nos = get_serial_nos(row.serial_no) - # To issue existing serial nos if row.current_qty and (row.current_serial_no or row.batch_no): args = self.get_sle_for_items(row) - args.update({ - 'actual_qty': -1 * row.current_qty, - 'serial_no': row.current_serial_no, - 'batch_no': row.batch_no, - 'valuation_rate': row.current_valuation_rate - }) + args.update( + { + "actual_qty": -1 * row.current_qty, + "serial_no": row.current_serial_no, + "batch_no": row.batch_no, + "valuation_rate": row.current_valuation_rate, + } + ) if row.current_serial_no: - args.update({ - 'qty_after_transaction': 0, - }) + args.update( + { + "qty_after_transaction": 0, + } + ) sl_entries.append(args) @@ -275,42 +308,49 @@ class StockReconciliation(StockController): for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) - previous_sle = get_previous_sle({ - "item_code": row.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "serial_no": serial_no - }) + previous_sle = get_previous_sle( + { + "item_code": row.item_code, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "serial_no": serial_no, + } + ) if previous_sle and row.warehouse != previous_sle.get("warehouse"): # If serial no exists in different warehouse - warehouse = previous_sle.get("warehouse", '') or row.warehouse + warehouse = previous_sle.get("warehouse", "") or row.warehouse if not qty_after_transaction: - qty_after_transaction = get_stock_balance(row.item_code, - warehouse, self.posting_date, self.posting_time) + qty_after_transaction = get_stock_balance( + row.item_code, warehouse, self.posting_date, self.posting_time + ) qty_after_transaction -= 1 new_args = args.copy() - new_args.update({ - 'actual_qty': -1, - 'qty_after_transaction': qty_after_transaction, - 'warehouse': warehouse, - 'valuation_rate': previous_sle.get("valuation_rate") - }) + new_args.update( + { + "actual_qty": -1, + "qty_after_transaction": qty_after_transaction, + "warehouse": warehouse, + "valuation_rate": previous_sle.get("valuation_rate"), + } + ) sl_entries.append(new_args) if row.qty: args = self.get_sle_for_items(row) - args.update({ - 'actual_qty': row.qty, - 'incoming_rate': row.valuation_rate, - 'valuation_rate': row.valuation_rate - }) + args.update( + { + "actual_qty": row.qty, + "incoming_rate": row.valuation_rate, + "valuation_rate": row.valuation_rate, + } + ) sl_entries.append(args) @@ -320,7 +360,8 @@ class StockReconciliation(StockController): def update_valuation_rate_for_serial_no(self): for d in self.items: - if not d.serial_no: continue + if not d.serial_no: + continue serial_nos = get_serial_nos(d.serial_no) self.update_valuation_rate_for_serial_nos(d, serial_nos) @@ -331,7 +372,7 @@ class StockReconciliation(StockController): return for d in serial_nos: - frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate) + frappe.db.set_value("Serial No", d, "purchase_rate", valuation_rate) def get_sle_for_items(self, row, serial_nos=None): """Insert Stock Ledger Entries""" @@ -339,22 +380,24 @@ class StockReconciliation(StockController): if not serial_nos and row.serial_no: serial_nos = get_serial_nos(row.serial_no) - data = frappe._dict({ - "doctype": "Stock Ledger Entry", - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "voucher_type": self.doctype, - "voucher_no": self.name, - "voucher_detail_no": row.name, - "company": self.company, - "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), - "is_cancelled": 1 if self.docstatus == 2 else 0, - "serial_no": '\n'.join(serial_nos) if serial_nos else '', - "batch_no": row.batch_no, - "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")) - }) + data = frappe._dict( + { + "doctype": "Stock Ledger Entry", + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "company": self.company, + "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), + "is_cancelled": 1 if self.docstatus == 2 else 0, + "serial_no": "\n".join(serial_nos) if serial_nos else "", + "batch_no": row.batch_no, + "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")), + } + ) if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) @@ -382,7 +425,7 @@ class StockReconciliation(StockController): for row in self.items: if row.serial_no or row.batch_no or row.current_serial_no: has_serial_no = True - serial_nos = '' + serial_nos = "" if row.current_serial_no: serial_nos = get_serial_nos(row.current_serial_no) @@ -395,10 +438,11 @@ class StockReconciliation(StockController): sl_entries = self.merge_similar_item_serial_nos(sl_entries) sl_entries.reverse() - allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + allow_negative_stock = cint( + frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + ) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) - def merge_similar_item_serial_nos(self, sl_entries): # If user has put the same item in multiple row with different serial no new_sl_entries = [] @@ -411,16 +455,16 @@ class StockReconciliation(StockController): key = (d.item_code, d.warehouse) if key not in merge_similar_entries: - d.total_amount = (d.actual_qty * d.valuation_rate) + d.total_amount = d.actual_qty * d.valuation_rate merge_similar_entries[key] = d elif d.serial_no: data = merge_similar_entries[key] data.actual_qty += d.actual_qty data.qty_after_transaction += d.qty_after_transaction - data.total_amount += (d.actual_qty * d.valuation_rate) + data.total_amount += d.actual_qty * d.valuation_rate data.valuation_rate = (data.total_amount) / data.actual_qty - data.serial_no += '\n' + d.serial_no + data.serial_no += "\n" + d.serial_no data.incoming_rate = (data.total_amount) / data.actual_qty @@ -433,8 +477,9 @@ class StockReconciliation(StockController): if not self.cost_center: msgprint(_("Please enter Cost Center"), raise_exception=1) - return super(StockReconciliation, self).get_gl_entries(warehouse_account, - self.expense_account, self.cost_center) + return super(StockReconciliation, self).get_gl_entries( + warehouse_account, self.expense_account, self.cost_center + ) def validate_expense_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -442,29 +487,39 @@ class StockReconciliation(StockController): if not self.expense_account: frappe.throw(_("Please enter Expense Account")) - elif self.purpose == "Opening Stock" or not frappe.db.sql("""select name from `tabStock Ledger Entry` limit 1"""): + elif self.purpose == "Opening Stock" or not frappe.db.sql( + """select name from `tabStock Ledger Entry` limit 1""" + ): if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss": - frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError) + frappe.throw( + _( + "Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry" + ), + OpeningEntryAccountError, + ) def set_zero_value_for_customer_provided_items(self): changed_any_values = False - for d in self.get('items'): - is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item') + for d in self.get("items"): + is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item") if is_customer_item and d.valuation_rate: d.valuation_rate = 0.0 changed_any_values = True if changed_any_values: - msgprint(_("Valuation rate for customer provided items has been set to zero."), - title=_("Note"), indicator="blue") - + msgprint( + _("Valuation rate for customer provided items has been set to zero."), + title=_("Note"), + indicator="blue", + ) def set_total_qty_and_amount(self): for d in self.get("items"): d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate")) - d.current_amount = (flt(d.current_qty, - d.precision("current_qty")) * flt(d.current_valuation_rate, d.precision("current_valuation_rate"))) + d.current_amount = flt(d.current_qty, d.precision("current_qty")) * flt( + d.current_valuation_rate, d.precision("current_valuation_rate") + ) d.quantity_difference = flt(d.qty) - flt(d.current_qty) d.amount_difference = flt(d.amount) - flt(d.current_amount) @@ -476,25 +531,33 @@ class StockReconciliation(StockController): def submit(self): if len(self.items) > 100: - msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage")) - self.queue_action('submit', timeout=2000) + msgprint( + _( + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" + ) + ) + self.queue_action("submit", timeout=2000) else: self._submit() def cancel(self): if len(self.items) > 100: - msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage")) - self.queue_action('cancel', timeout=2000) + msgprint( + _( + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" + ) + ) + self.queue_action("cancel", timeout=2000) else: self._cancel() + @frappe.whitelist() -def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False): +def get_items( + warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False +): ignore_empty_stock = cint(ignore_empty_stock) - items = [frappe._dict({ - 'item_code': item_code, - 'warehouse': warehouse - })] + items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})] if not item_code: items = get_items_for_stock_reco(warehouse, company) @@ -504,8 +567,9 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig for d in items: if d.item_code in itemwise_batch_data: - valuation_rate = get_stock_balance(d.item_code, d.warehouse, - posting_date, posting_time, with_valuation_rate=True)[1] + valuation_rate = get_stock_balance( + d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True + )[1] for row in itemwise_batch_data.get(d.item_code): if ignore_empty_stock and not row.qty: @@ -514,12 +578,22 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig args = get_item_data(row, row.qty, valuation_rate) res.append(args) else: - stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time, - with_valuation_rate=True , with_serial_no=cint(d.has_serial_no)) - qty, valuation_rate, serial_no = stock_bal[0], stock_bal[1], stock_bal[2] if cint(d.has_serial_no) else '' + stock_bal = get_stock_balance( + d.item_code, + d.warehouse, + posting_date, + posting_time, + with_valuation_rate=True, + with_serial_no=cint(d.has_serial_no), + ) + qty, valuation_rate, serial_no = ( + stock_bal[0], + stock_bal[1], + stock_bal[2] if cint(d.has_serial_no) else "", + ) if ignore_empty_stock and not stock_bal[0]: - continue + continue args = get_item_data(d, qty, valuation_rate, serial_no) @@ -527,9 +601,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig return res + def get_items_for_stock_reco(warehouse, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - items = frappe.db.sql(f""" + items = frappe.db.sql( + f""" select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no from @@ -542,9 +618,12 @@ def get_items_for_stock_reco(warehouse, company): and exists( select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse ) - """, as_dict=1) + """, + as_dict=1, + ) - items += frappe.db.sql(""" + items += frappe.db.sql( + """ select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no from @@ -559,40 +638,50 @@ def get_items_for_stock_reco(warehouse, company): and IFNULL(i.disabled, 0) = 0 and id.company = %s group by i.name - """, (lft, rgt, company), as_dict=1) + """, + (lft, rgt, company), + as_dict=1, + ) # remove duplicates # check if item-warehouse key extracted from each entry exists in set iw_keys # and update iw_keys iw_keys = set() - items = [item for item in items if [(item.item_code, item.warehouse) not in iw_keys, iw_keys.add((item.item_code, item.warehouse))][0]] + items = [ + item + for item in items + if [ + (item.item_code, item.warehouse) not in iw_keys, + iw_keys.add((item.item_code, item.warehouse)), + ][0] + ] return items + def get_item_data(row, qty, valuation_rate, serial_no=None): return { - 'item_code': row.item_code, - 'warehouse': row.warehouse, - 'qty': qty, - 'item_name': row.item_name, - 'valuation_rate': valuation_rate, - 'current_qty': qty, - 'current_valuation_rate': valuation_rate, - 'current_serial_no': serial_no, - 'serial_no': serial_no, - 'batch_no': row.get('batch_no') + "item_code": row.item_code, + "warehouse": row.warehouse, + "qty": qty, + "item_name": row.item_name, + "valuation_rate": valuation_rate, + "current_qty": qty, + "current_valuation_rate": valuation_rate, + "current_serial_no": serial_no, + "serial_no": serial_no, + "batch_no": row.get("batch_no"), } + def get_itemwise_batch(warehouse, posting_date, company, item_code=None): from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute + itemwise_batch_data = {} - filters = frappe._dict({ - 'warehouse': warehouse, - 'from_date': posting_date, - 'to_date': posting_date, - 'company': company - }) + filters = frappe._dict( + {"warehouse": warehouse, "from_date": posting_date, "to_date": posting_date, "company": company} + ) if item_code: filters.item_code = item_code @@ -600,23 +689,28 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): columns, data = execute(filters) for row in data: - itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({ - 'item_code': row[0], - 'warehouse': warehouse, - 'qty': row[8], - 'item_name': row[1], - 'batch_no': row[4] - })) + itemwise_batch_data.setdefault(row[0], []).append( + frappe._dict( + { + "item_code": row[0], + "warehouse": warehouse, + "qty": row[8], + "item_name": row[1], + "batch_no": row[4], + } + ) + ) return itemwise_batch_data -@frappe.whitelist() -def get_stock_balance_for(item_code, warehouse, - posting_date, posting_time, batch_no=None, with_valuation_rate= True): - frappe.has_permission("Stock Reconciliation", "write", throw = True) - item_dict = frappe.db.get_value("Item", item_code, - ["has_serial_no", "has_batch_no"], as_dict=1) +@frappe.whitelist() +def get_stock_balance_for( + item_code, warehouse, posting_date, posting_time, batch_no=None, with_valuation_rate=True +): + frappe.has_permission("Stock Reconciliation", "write", throw=True) + + item_dict = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1) if not item_dict: # In cases of data upload to Items table @@ -625,8 +719,14 @@ def get_stock_balance_for(item_code, warehouse, serial_nos = "" with_serial_no = True if item_dict.get("has_serial_no") else False - data = get_stock_balance(item_code, warehouse, posting_date, posting_time, - with_valuation_rate=with_valuation_rate, with_serial_no=with_serial_no) + data = get_stock_balance( + item_code, + warehouse, + posting_date, + posting_time, + with_valuation_rate=with_valuation_rate, + with_serial_no=with_serial_no, + ) if with_serial_no: qty, rate, serial_nos = data @@ -634,20 +734,20 @@ def get_stock_balance_for(item_code, warehouse, qty, rate = data if item_dict.get("has_batch_no"): - qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 + qty = ( + get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 + ) + + return {"qty": qty, "rate": rate, "serial_nos": serial_nos} - return { - 'qty': qty, - 'rate': rate, - 'serial_nos': serial_nos - } @frappe.whitelist() def get_difference_account(purpose, company): - if purpose == 'Stock Reconciliation': + if purpose == "Stock Reconciliation": account = get_company_default(company, "stock_adjustment_account") else: - account = frappe.db.get_value('Account', {'is_group': 0, - 'company': company, 'account_type': 'Temporary'}, 'name') + account = frappe.db.get_value( + "Account", {"is_group": 0, "company": company, "account_type": "Temporary"}, "name" + ) return account diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 86af0a0cf3b..cb9b5abaa23 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -6,10 +6,11 @@ import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string from erpnext.accounts.utils import get_stock_and_account_balance -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( @@ -19,10 +20,9 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestStockReconciliation(ERPNextTestCase): +class TestStockReconciliation(FrappeTestCase): @classmethod def setUpClass(cls): create_batch_or_serial_no_items() @@ -30,8 +30,7 @@ class TestStockReconciliation(ERPNextTestCase): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) def tearDown(self): - frappe.flags.dont_execute_stock_reposts = None - + frappe.local.future_sle = {} def test_reco_for_fifo(self): self._test_reco_sle_gle("FIFO") @@ -40,55 +39,73 @@ class TestStockReconciliation(ERPNextTestCase): self._test_reco_sle_gle("Moving Average") def _test_reco_sle_gle(self, valuation_method): - se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1') - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + item_code = make_item(properties={"valuation_method": valuation_method}).name + + se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code) + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") # [[qty, valuation_rate, posting_date, - # posting_time, expected_stock_value, bin_qty, bin_valuation]] + # posting_time, expected_stock_value, bin_qty, bin_valuation]] input_data = [ [50, 1000, "2012-12-26", "12:00"], [25, 900, "2012-12-26", "12:00"], ["", 1000, "2012-12-20", "12:05"], [20, "", "2012-12-26", "12:05"], - [0, "", "2012-12-31", "12:10"] + [0, "", "2012-12-31", "12:10"], ] for d in input_data: - set_valuation_method("_Test Item", valuation_method) - - last_sle = get_previous_sle({ - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "posting_date": d[2], - "posting_time": d[3] - }) + last_sle = get_previous_sle( + { + "item_code": item_code, + "warehouse": "Stores - TCP1", + "posting_date": d[2], + "posting_time": d[3], + } + ) # submit stock reconciliation - stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1], - posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1", - company=company, expense_account = "Stock Adjustment - TCP1") + stock_reco = create_stock_reconciliation( + item_code=item_code, + qty=d[0], + rate=d[1], + posting_date=d[2], + posting_time=d[3], + warehouse="Stores - TCP1", + company=company, + expense_account="Stock Adjustment - TCP1", + ) # check stock value - sle = frappe.db.sql("""select * from `tabStock Ledger Entry` - where voucher_type='Stock Reconciliation' and voucher_no=%s""", stock_reco.name, as_dict=1) + sle = frappe.db.sql( + """select * from `tabStock Ledger Entry` + where voucher_type='Stock Reconciliation' and voucher_no=%s""", + stock_reco.name, + as_dict=1, + ) qty_after_transaction = flt(d[0]) if d[0] != "" else flt(last_sle.get("qty_after_transaction")) valuation_rate = flt(d[1]) if d[1] != "" else flt(last_sle.get("valuation_rate")) - if qty_after_transaction == last_sle.get("qty_after_transaction") \ - and valuation_rate == last_sle.get("valuation_rate"): - self.assertFalse(sle) + if qty_after_transaction == last_sle.get( + "qty_after_transaction" + ) and valuation_rate == last_sle.get("valuation_rate"): + self.assertFalse(sle) else: self.assertEqual(flt(sle[0].qty_after_transaction, 1), flt(qty_after_transaction, 1)) self.assertEqual(flt(sle[0].stock_value, 1), flt(qty_after_transaction * valuation_rate, 1)) # no gl entries - self.assertTrue(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name})) + self.assertTrue( + frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name} + ) + ) - acc_bal, stock_bal, wh_list = get_stock_and_account_balance("Stock In Hand - TCP1", - stock_reco.posting_date, stock_reco.company) + acc_bal, stock_bal, wh_list = get_stock_and_account_balance( + "Stock In Hand - TCP1", stock_reco.posting_date, stock_reco.company + ) self.assertEqual(flt(acc_bal, 1), flt(stock_bal, 1)) stock_reco.cancel() @@ -98,18 +115,33 @@ class TestStockReconciliation(ERPNextTestCase): se1.cancel() def test_get_items(self): - create_warehouse("_Test Warehouse Group 1", - {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"}) - create_warehouse("_Test Warehouse Ledger 1", - {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"}) + create_warehouse( + "_Test Warehouse Group 1", + {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"}, + ) + create_warehouse( + "_Test Warehouse Ledger 1", + { + "is_group": 0, + "parent_warehouse": "_Test Warehouse Group 1 - _TC", + "company": "_Test Company", + }, + ) - create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100, - warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100) + create_item( + "_Test Stock Reco Item", + is_stock_item=1, + valuation_rate=100, + warehouse="_Test Warehouse Ledger 1 - _TC", + opening_stock=100, + ) items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime(), "_Test Company") - self.assertEqual(["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100], - [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]]) + self.assertEqual( + ["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100], + [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]], + ) def test_stock_reco_for_serialized_item(self): to_delete_records = [] @@ -119,8 +151,9 @@ class TestStockReconciliation(ERPNextTestCase): serial_item_code = "Stock-Reco-Serial-Item-1" serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" - sr = create_stock_reconciliation(item_code=serial_item_code, - warehouse = serial_warehouse, qty=5, rate=200) + sr = create_stock_reconciliation( + item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=200 + ) serial_nos = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos), 5) @@ -130,7 +163,7 @@ class TestStockReconciliation(ERPNextTestCase): "warehouse": serial_warehouse, "posting_date": nowdate(), "posting_time": nowtime(), - "serial_no": sr.items[0].serial_no + "serial_no": sr.items[0].serial_no, } valuation_rate = get_incoming_rate(args) @@ -138,8 +171,9 @@ class TestStockReconciliation(ERPNextTestCase): to_delete_records.append(sr.name) - sr = create_stock_reconciliation(item_code=serial_item_code, - warehouse = serial_warehouse, qty=5, rate=300) + sr = create_stock_reconciliation( + item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=300 + ) serial_nos1 = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos1), 5) @@ -149,7 +183,7 @@ class TestStockReconciliation(ERPNextTestCase): "warehouse": serial_warehouse, "posting_date": nowdate(), "posting_time": nowtime(), - "serial_no": sr.items[0].serial_no + "serial_no": sr.items[0].serial_no, } valuation_rate = get_incoming_rate(args) @@ -162,7 +196,6 @@ class TestStockReconciliation(ERPNextTestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() - def test_stock_reco_for_merge_serialized_item(self): to_delete_records = [] @@ -170,23 +203,34 @@ class TestStockReconciliation(ERPNextTestCase): serial_item_code = "Stock-Reco-Serial-Item-2" serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" - sr = create_stock_reconciliation(item_code=serial_item_code, serial_no=random_string(6), - warehouse = serial_warehouse, qty=1, rate=100, do_not_submit=True, purpose='Opening Stock') + sr = create_stock_reconciliation( + item_code=serial_item_code, + serial_no=random_string(6), + warehouse=serial_warehouse, + qty=1, + rate=100, + do_not_submit=True, + purpose="Opening Stock", + ) for i in range(3): - sr.append('items', { - 'item_code': serial_item_code, - 'warehouse': serial_warehouse, - 'qty': 1, - 'valuation_rate': 100, - 'serial_no': random_string(6) - }) + sr.append( + "items", + { + "item_code": serial_item_code, + "warehouse": serial_warehouse, + "qty": 1, + "valuation_rate": 100, + "serial_no": random_string(6), + }, + ) sr.save() sr.submit() - sle_entries = frappe.get_all('Stock Ledger Entry', filters= {'voucher_no': sr.name}, - fields = ['name', 'incoming_rate']) + sle_entries = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": sr.name}, fields=["name", "incoming_rate"] + ) self.assertEqual(len(sle_entries), 1) self.assertEqual(sle_entries[0].incoming_rate, 100) @@ -206,16 +250,18 @@ class TestStockReconciliation(ERPNextTestCase): item_code = "Stock-Reco-batch-Item-1" warehouse = "_Test Warehouse for Stock Reco2 - _TC" - sr = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=5, rate=200, do_not_submit=1) + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1 + ) sr.save(ignore_permissions=True) sr.submit() self.assertTrue(sr.items[0].batch_no) to_delete_records.append(sr.name) - sr1 = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no) + sr1 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no + ) args = { "item_code": item_code, @@ -228,9 +274,9 @@ class TestStockReconciliation(ERPNextTestCase): self.assertEqual(valuation_rate, 300) to_delete_records.append(sr1.name) - - sr2 = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no) + sr2 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no + ) stock_value = get_stock_value_on(warehouse, nowdate(), item_code) self.assertEqual(stock_value, 0) @@ -242,11 +288,12 @@ class TestStockReconciliation(ERPNextTestCase): stock_doc.cancel() def test_customer_provided_items(self): - item_code = 'Stock-Reco-customer-Item-100' - create_item(item_code, is_customer_provided_item = 1, - customer = '_Test Customer', is_purchase_item = 0) + item_code = "Stock-Reco-customer-Item-100" + create_item( + item_code, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) - sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420) + sr = create_stock_reconciliation(item_code=item_code, qty=10, rate=420) self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(sr.get("items")[0].valuation_rate, 0) @@ -254,65 +301,78 @@ class TestStockReconciliation(ERPNextTestCase): def test_backdated_stock_reco_qty_reposting(self): """ - Test if a backdated stock reco recalculates future qty until next reco. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] - PR1 | PR | 10 | 18 (posting date: today-3) - PR2 | PR | 1 | 19 (posting date: today-2) - SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] - PR3 | PR | 1 | 7 (posting date: today) # can't post future PR + Test if a backdated stock reco recalculates future qty until next reco. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] + PR1 | PR | 10 | 18 (posting date: today-3) + PR2 | PR | 1 | 19 (posting date: today-2) + SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] + PR3 | PR | 1 | 7 (posting date: today) # can't post future PR """ - item_code = "Backdated-Reco-Item" + item_code = make_item().name warehouse = "_Test Warehouse - _TC" - create_item(item_code) - pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), -3)) - pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=add_days(nowdate(), -2)) - pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=nowdate()) + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) + ) + pr2 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=add_days(nowdate(), -2) + ) + pr3 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() + ) - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr3_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(pr3_balance, 12) # post backdated stock reco in between - sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100, - posting_date=add_days(nowdate(), -1)) - pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, - "qty_after_transaction") + sr4 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1) + ) + pr3_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr3_balance, 7) # post backdated stock reco at the start - sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100, - posting_date=add_days(nowdate(), -4)) - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, - "qty_after_transaction") - sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, - "qty_after_transaction") + sr5 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4) + ) + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" + ) + sr4_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 18) self.assertEqual(pr2_balance, 19) - self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected # cancel backdated stock reco and check future impact sr5.cancel() - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, - "qty_after_transaction") - sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" + ) + sr4_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(pr2_balance, 11) - self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected # teardown sr4.cancel() @@ -323,37 +383,45 @@ class TestStockReconciliation(ERPNextTestCase): @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_future_negative_stock(self): """ - Test if a backdated stock reco causes future negative stock and is blocked. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - PR1 | PR | 10 | 10 (posting date: today-2) - SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] - DN2 | DN | -2 | 8(-1) (posting date: today) + Test if a backdated stock reco causes future negative stock and is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + PR1 | PR | 10 | 10 (posting date: today-2) + SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] + DN2 | DN | -2 | 8(-1) (posting date: today) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError - item_code = "Backdated-Reco-Item" + item_code = make_item().name warehouse = "_Test Warehouse - _TC" - create_item(item_code) + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2) + ) + dn2 = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=2, rate=120, posting_date=nowdate() + ) - pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), -2)) - dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120, - posting_date=nowdate()) - - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + dn2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(dn2_balance, 8) # check if stock reco is blocked - sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=add_days(nowdate(), -1), do_not_submit=True) + sr3 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=1, + rate=100, + posting_date=add_days(nowdate(), -1), + do_not_submit=True, + ) self.assertRaises(NegativeStockError, sr3.submit) # teardown @@ -361,33 +429,37 @@ class TestStockReconciliation(ERPNextTestCase): dn2.cancel() pr1.cancel() - @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_cancellation_future_negative_stock(self): """ - Test if a backdated stock reco cancellation that causes future negative stock is blocked. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN) - DN | DN | 100 | 0 (posting date: today) + Test if a backdated stock reco cancellation that causes future negative stock is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN) + DN | DN | 100 | 0 (posting date: today) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError - item_code = "Backdated-Reco-Cancellation-Item" + item_code = make_item().name warehouse = "_Test Warehouse - _TC" - create_item(item_code) + sr = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=100, + rate=100, + posting_date=add_days(nowdate(), -1), + ) - sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=100, rate=100, - posting_date=add_days(nowdate(), -1)) + dn = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=100, rate=120, posting_date=nowdate() + ) - dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=100, rate=120, - posting_date=nowdate()) - - dn_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, - "qty_after_transaction") + dn_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(dn_balance, 0) # check if cancellation of stock reco is blocked @@ -399,43 +471,51 @@ class TestStockReconciliation(ERPNextTestCase): def test_intermediate_sr_bin_update(self): """Bin should show correct qty even for backdated entries. - ------------------------------------------- - | creation | Var | Doc | Qty | balance qty - ------------------------------------------- - | 1 | SR | Reco | 10 | 10 (posting date: today+10) - | 3 | SR2 | Reco | 11 | 11 (posting date: today+11) - | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12) + ------------------------------------------- + | creation | Var | Doc | Qty | balance qty + ------------------------------------------- + | 1 | SR | Reco | 10 | 10 (posting date: today+10) + | 3 | SR2 | Reco | 11 | 11 (posting date: today+11) + | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - # repost will make this test useless, qty should update in realtime without reposts - frappe.flags.dont_execute_stock_reposts = True frappe.db.rollback() - item_code = "Backdated-Reco-Cancellation-Item" + # repost will make this test useless, qty should update in realtime without reposts + frappe.flags.dont_execute_stock_reposts = True + self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") + + item_code = make_item().name warehouse = "_Test Warehouse - _TC" - create_item(item_code) - sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), 10)) + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10) + ) - dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=5, rate=120, - posting_date=add_days(nowdate(), 12)) - old_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") + dn = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=5, rate=120, posting_date=add_days(nowdate(), 12) + ) + old_bin_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty" + ) - sr2 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=11, rate=100, - posting_date=add_days(nowdate(), 11)) - new_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") + sr2 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=11, rate=100, posting_date=add_days(nowdate(), 11) + ) + new_bin_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty" + ) self.assertEqual(old_bin_qty + 1, new_bin_qty) frappe.db.rollback() - def test_valid_batch(self): create_batch_item_with_batch("Testing Batch Item 1", "001") create_batch_item_with_batch("Testing Batch Item 2", "002") - sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002" - , do_not_submit=True) + sr = create_stock_reconciliation( + item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002", do_not_submit=True + ) self.assertRaises(frappe.ValidationError, sr.submit) def test_serial_no_cancellation(self): @@ -457,15 +537,17 @@ class TestStockReconciliation(ERPNextTestCase): serial_nos.pop() new_serial_nos = "\n".join(serial_nos) - sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9) + sr = create_stock_reconciliation( + item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9 + ) sr.cancel() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 10) - def test_serial_no_creation_and_inactivation(self): item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1) if not item.has_serial_no: @@ -475,19 +557,27 @@ class TestStockReconciliation(ERPNextTestCase): item_code = item.name warehouse = "_Test Warehouse - _TC" - sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, - serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100) + sr = create_stock_reconciliation( + item_code=item.name, + warehouse=warehouse, + serial_no="SR-CREATED-SR-NO", + qty=1, + do_not_submit=True, + rate=100, + ) sr.save() self.assertEqual(cstr(sr.items[0].current_serial_no), "") sr.submit() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 1) sr.cancel() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 0) @@ -498,32 +588,51 @@ def create_batch_item_with_batch(item_name, batch_id): batch_item_doc.create_new_batch = 1 batch_item_doc.save(ignore_permissions=True) - if not frappe.db.exists('Batch', batch_id): - b = frappe.new_doc('Batch') + if not frappe.db.exists("Batch", batch_id): + b = frappe.new_doc("Batch") b.item = item_name b.batch_id = batch_id b.save() -def insert_existing_sle(warehouse): + +def insert_existing_sle(warehouse, item_code="_Test Item"): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item", - target=warehouse, qty=10, basic_rate=700) + se1 = make_stock_entry( + posting_date="2012-12-15", + posting_time="02:00", + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=700, + ) - se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", - source=warehouse, qty=15) + se2 = make_stock_entry( + posting_date="2012-12-25", posting_time="03:00", item_code=item_code, source=warehouse, qty=15 + ) - se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", - target=warehouse, qty=15, basic_rate=1200) + se3 = make_stock_entry( + posting_date="2013-01-05", + posting_time="07:00", + item_code=item_code, + target=warehouse, + qty=15, + basic_rate=1200, + ) return se1, se2, se3 -def create_batch_or_serial_no_items(): - create_warehouse("_Test Warehouse for Stock Reco1", - {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) - create_warehouse("_Test Warehouse for Stock Reco2", - {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) +def create_batch_or_serial_no_items(): + create_warehouse( + "_Test Warehouse for Stock Reco1", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}, + ) + + create_warehouse( + "_Test Warehouse for Stock Reco2", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}, + ) serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1) if not serial_item_doc.has_serial_no: @@ -544,6 +653,7 @@ def create_batch_or_serial_no_items(): serial_item_doc.batch_number_series = "BASR.#####" batch_item_doc.save(ignore_permissions=True) + def create_stock_reconciliation(**args): args = frappe._dict(args) sr = frappe.new_doc("Stock Reconciliation") @@ -552,20 +662,26 @@ def create_stock_reconciliation(**args): sr.posting_time = args.posting_time or nowtime() sr.set_posting_time = 1 sr.company = args.company or "_Test Company" - sr.expense_account = args.expense_account or \ - ("Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC") - sr.cost_center = args.cost_center \ - or frappe.get_cached_value("Company", sr.company, "cost_center") \ + sr.expense_account = args.expense_account or ( + "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC" + ) + sr.cost_center = ( + args.cost_center + or frappe.get_cached_value("Company", sr.company, "cost_center") or "_Test Cost Center - _TC" + ) - sr.append("items", { - "item_code": args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty, - "valuation_rate": args.rate, - "serial_no": args.serial_no, - "batch_no": args.batch_no - }) + sr.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty, + "valuation_rate": args.rate, + "serial_no": args.serial_no, + "batch_no": args.batch_no, + }, + ) try: if not args.do_not_submit: @@ -574,6 +690,7 @@ def create_stock_reconciliation(**args): pass return sr + def set_valuation_method(item_code, valuation_method): existing_valuation_method = get_valuation_method(item_code) if valuation_method == existing_valuation_method: @@ -581,11 +698,13 @@ def set_valuation_method(item_code, valuation_method): frappe.db.set_value("Item", item_code, "valuation_method", valuation_method) - for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]): + for warehouse in frappe.get_all( + "Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"] + ): if not warehouse.is_group: - update_entries_after({ - "item_code": item_code, - "warehouse": warehouse.name - }, allow_negative_stock=1) + update_entries_after( + {"item_code": item_code, "warehouse": warehouse.name}, allow_negative_stock=1 + ) + test_dependencies = ["Item", "Warehouse"] diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py index bab521d69fc..e0c8ed12e7d 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -6,8 +6,6 @@ from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_h class StockRepostingSettings(Document): - - def validate(self): self.set_minimum_reposting_time_slot() diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 6167becdaac..66da215dbbe 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -13,6 +13,25 @@ frappe.ui.form.on('Stock Settings', { frm.set_query("default_warehouse", filters); frm.set_query("sample_retention_warehouse", filters); + }, + allow_negative_stock: function(frm) { + if (!frm.doc.allow_negative_stock) { + return; + } + + let msg = __("Using negative stock disables FIFO/Moving average valuation when inventory is negative."); + msg += " "; + msg += __("This is considered dangerous from accounting point of view.") + msg += "
    "; + msg += ("Do you still want to enable negative inventory?"); + + frappe.confirm( + msg, + () => {}, + () => { + frm.set_value("allow_negative_stock", 0); + } + ); } }); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index c1293cbf0fa..e592a4be3c6 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -16,24 +16,40 @@ from erpnext.stock.utils import check_pending_reposting class StockSettings(Document): def validate(self): - for key in ["item_naming_by", "item_group", "stock_uom", - "allow_negative_stock", "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input"]: - frappe.db.set_default(key, self.get(key, "")) + for key in [ + "item_naming_by", + "item_group", + "stock_uom", + "allow_negative_stock", + "default_warehouse", + "set_qty_in_transactions_based_on_serial_no_input", + ]: + frappe.db.set_default(key, self.get(key, "")) from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Item", "item_code", - self.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0) + + set_by_naming_series( + "Item", + "item_code", + self.get("item_naming_by") == "Naming Series", + hide_name_field=True, + make_mandatory=0, + ) stock_frozen_limit = 356 submitted_stock_frozen = self.stock_frozen_upto_days or 0 if submitted_stock_frozen > stock_frozen_limit: self.stock_frozen_upto_days = stock_frozen_limit - frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit) + frappe.msgprint( + _("`Freeze Stocks Older Than` should be smaller than %d days.") % stock_frozen_limit + ) # show/hide barcode field for name in ["barcode", "barcodes", "scan_barcode"]: - frappe.make_property_setter({'fieldname': name, 'property': 'hidden', - 'value': 0 if self.show_barcode_field else 1}, validate_fields_for_doctype=False) + frappe.make_property_setter( + {"fieldname": name, "property": "hidden", "value": 0 if self.show_barcode_field else 1}, + validate_fields_for_doctype=False, + ) self.validate_warehouses() self.cant_change_valuation_method() @@ -44,8 +60,12 @@ class StockSettings(Document): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] for field in warehouse_fields: if frappe.db.get_value("Warehouse", self.get(field), "is_group"): - frappe.throw(_("Group Warehouses cannot be used in transactions. Please change the value of {0}") \ - .format(frappe.bold(self.meta.get_field(field).label)), title =_("Incorrect Warehouse")) + frappe.throw( + _("Group Warehouses cannot be used in transactions. Please change the value of {0}").format( + frappe.bold(self.meta.get_field(field).label) + ), + title=_("Incorrect Warehouse"), + ) def cant_change_valuation_method(self): db_valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") @@ -53,38 +73,73 @@ class StockSettings(Document): if db_valuation_method and db_valuation_method != self.valuation_method: # check if there are any stock ledger entries against items # which does not have it's own valuation method - sle = frappe.db.sql("""select name from `tabStock Ledger Entry` sle + sle = frappe.db.sql( + """select name from `tabStock Ledger Entry` sle where exists(select name from tabItem where name=sle.item_code and (valuation_method is null or valuation_method='')) limit 1 - """) + """ + ) if sle: - frappe.throw(_("Can't change the valuation method, as there are transactions against some items which do not have its own valuation method")) + frappe.throw( + _( + "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" + ) + ) def validate_clean_description_html(self): - if int(self.clean_description_html or 0) \ - and not int(self.db_get('clean_description_html') or 0): + if int(self.clean_description_html or 0) and not int(self.db_get("clean_description_html") or 0): # changed to text - frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test) + frappe.enqueue( + "erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions", + now=frappe.flags.in_test, + ) def validate_pending_reposts(self): if self.stock_frozen_upto: check_pending_reposting(self.stock_frozen_upto) - def on_update(self): self.toggle_warehouse_field_for_inter_warehouse_transfer() def toggle_warehouse_field_for_inter_warehouse_transfer(self): - make_property_setter("Sales Invoice Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False) - make_property_setter("Delivery Note Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False) - make_property_setter("Purchase Invoice Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False) - make_property_setter("Purchase Receipt Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False) + make_property_setter( + "Sales Invoice Item", + "target_warehouse", + "hidden", + 1 - cint(self.allow_from_dn), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Delivery Note Item", + "target_warehouse", + "hidden", + 1 - cint(self.allow_from_dn), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Purchase Invoice Item", + "from_warehouse", + "hidden", + 1 - cint(self.allow_from_pr), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Purchase Receipt Item", + "from_warehouse", + "hidden", + 1 - cint(self.allow_from_pr), + "Check", + validate_fields_for_doctype=False, + ) def clean_all_descriptions(): - for item in frappe.get_all('Item', ['name', 'description']): + for item in frappe.get_all("Item", ["name", "description"]): if item.description: clean_description = clean_html(item.description) if item.description != clean_description: - frappe.db.set_value('Item', item.name, 'description', clean_description) + frappe.db.set_value("Item", item.name, "description", clean_description) diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py index 072b54b8205..974e16339b7 100644 --- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py @@ -4,45 +4,54 @@ import unittest import frappe - -from erpnext.tests.utils import ERPNextTestCase +from frappe.tests.utils import FrappeTestCase -class TestStockSettings(ERPNextTestCase): +class TestStockSettings(FrappeTestCase): def setUp(self): super().setUp() frappe.db.set_value("Stock Settings", None, "clean_description_html", 0) def test_settings(self): - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = 'Item for description test', - item_group = 'Products', - description = '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ' - )).insert() + item = frappe.get_doc( + dict( + doctype="Item", + item_code="Item for description test", + item_group="Products", + description='

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ', + ) + ).insert() - settings = frappe.get_single('Stock Settings') + settings = frappe.get_single("Stock Settings") settings.clean_description_html = 1 settings.save() item.reload() - self.assertEqual(item.description, '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ') + self.assertEqual( + item.description, + "

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ", + ) item.delete() def test_clean_html(self): - settings = frappe.get_single('Stock Settings') + settings = frappe.get_single("Stock Settings") settings.clean_description_html = 1 settings.save() - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = 'Item for description test', - item_group = 'Products', - description = '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ' - )).insert() + item = frappe.get_doc( + dict( + doctype="Item", + item_code="Item for description test", + item_group="Products", + description='

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ', + ) + ).insert() - self.assertEqual(item.description, '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ') + self.assertEqual( + item.description, + "

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ", + ) item.delete() diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 26db2642e4b..5a7228a5068 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -3,21 +3,22 @@ import frappe from frappe.test_runner import make_test_records -from frappe.utils import cint +from frappe.tests.utils import FrappeTestCase import erpnext -from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account +from erpnext.accounts.doctype.account.test_account import create_account from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase +from erpnext.stock.doctype.warehouse.warehouse import convert_to_group_or_ledger, get_children -test_records = frappe.get_test_records('Warehouse') +test_records = frappe.get_test_records("Warehouse") -class TestWarehouse(ERPNextTestCase): + +class TestWarehouse(FrappeTestCase): def setUp(self): super().setUp() - if not frappe.get_value('Item', '_Test Item'): - make_test_records('Item') + if not frappe.get_value("Item", "_Test Item"): + make_test_records("Item") def test_parent_warehouse(self): parent_warehouse = frappe.get_doc("Warehouse", "_Test Warehouse Group - _TC") @@ -26,23 +27,37 @@ class TestWarehouse(ERPNextTestCase): def test_warehouse_hierarchy(self): p_warehouse = frappe.get_doc("Warehouse", "_Test Warehouse Group - _TC") - child_warehouses = frappe.db.sql("""select name, is_group, parent_warehouse from `tabWarehouse` wh - where wh.lft > %s and wh.rgt < %s""", (p_warehouse.lft, p_warehouse.rgt), as_dict=1) + child_warehouses = frappe.db.sql( + """select name, is_group, parent_warehouse from `tabWarehouse` wh + where wh.lft > %s and wh.rgt < %s""", + (p_warehouse.lft, p_warehouse.rgt), + as_dict=1, + ) for child_warehouse in child_warehouses: self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(child_warehouse.is_group, 0) + def test_naming(self): + company = "Wind Power LLC" + warehouse_name = "Named Warehouse - WP" + wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert() + self.assertEqual(wh.name, warehouse_name) + + warehouse_name = "Unnamed Warehouse" + wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert() + self.assertIn(warehouse_name, wh.name) + def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" - warehouse_names = [f'_Test Warehouse {i} for Unlinking' for i in range(2)] + warehouse_names = [f"_Test Warehouse {i} for Unlinking" for i in range(2)] warehouse_ids = [] for warehouse in warehouse_names: warehouse_id = create_warehouse(warehouse, company=company) warehouse_ids.append(warehouse_id) - item_names = [f'_Test Item {i} for Unlinking' for i in range(2)] + item_names = [f"_Test Item {i} for Unlinking" for i in range(2)] for item, warehouse in zip(item_names, warehouse_ids): create_item(item, warehouse=warehouse, company=company) @@ -52,19 +67,43 @@ class TestWarehouse(ERPNextTestCase): # Check Item existance for item in item_names: - self.assertTrue( - bool(frappe.db.exists("Item", item)), - f"{item} doesn't exist" - ) + self.assertTrue(bool(frappe.db.exists("Item", item)), f"{item} doesn't exist") item_doc = frappe.get_doc("Item", item) for item_default in item_doc.item_defaults: self.assertNotIn( item_default.default_warehouse, warehouse_ids, - f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}." + f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}.", ) + def test_group_non_group_conversion(self): + + warehouse = frappe.get_doc("Warehouse", create_warehouse("TestGroupConversion")) + + convert_to_group_or_ledger(warehouse.name) + warehouse.reload() + self.assertEqual(warehouse.is_group, 1) + + child = create_warehouse("GroupWHChild", {"parent_warehouse": warehouse.name}) + # chid exists + self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name) + frappe.delete_doc("Warehouse", child) + + convert_to_group_or_ledger(warehouse.name) + warehouse.reload() + self.assertEqual(warehouse.is_group, 0) + + make_stock_entry(item_code="_Test Item", target=warehouse.name, qty=1) + # SLE exists + self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name) + + def test_get_children(self): + company = "_Test Company" + + children = get_children("Warehouse", parent=company, company=company, is_root=True) + self.assertTrue(any(wh["value"] == "_Test Warehouse - _TC" for wh in children)) + def create_warehouse(warehouse_name, properties=None, company=None): if not company: @@ -84,40 +123,46 @@ def create_warehouse(warehouse_name, properties=None, company=None): else: return warehouse_id + def get_warehouse(**args): args = frappe._dict(args) - if(frappe.db.exists("Warehouse", args.warehouse_name + " - " + args.abbr)): + if frappe.db.exists("Warehouse", args.warehouse_name + " - " + args.abbr): return frappe.get_doc("Warehouse", args.warehouse_name + " - " + args.abbr) else: - w = frappe.get_doc({ - "company": args.company or "_Test Company", - "doctype": "Warehouse", - "warehouse_name": args.warehouse_name, - "is_group": 0, - "account": get_warehouse_account(args.warehouse_name, args.company, args.abbr) - }) + w = frappe.get_doc( + { + "company": args.company or "_Test Company", + "doctype": "Warehouse", + "warehouse_name": args.warehouse_name, + "is_group": 0, + "account": get_warehouse_account(args.warehouse_name, args.company, args.abbr), + } + ) w.insert() return w + def get_warehouse_account(warehouse_name, company, company_abbr=None): if not company_abbr: - company_abbr = frappe.get_cached_value("Company", company, 'abbr') + company_abbr = frappe.get_cached_value("Company", company, "abbr") if not frappe.db.exists("Account", warehouse_name + " - " + company_abbr): return create_account( account_name=warehouse_name, parent_account=get_group_stock_account(company, company_abbr), - account_type='Stock', - company=company) + account_type="Stock", + company=company, + ) else: return warehouse_name + " - " + company_abbr def get_group_stock_account(company, company_abbr=None): - group_stock_account = frappe.db.get_value("Account", - filters={'account_type': 'Stock', 'is_group': 1, 'company': company}, fieldname='name') + group_stock_account = frappe.db.get_value( + "Account", filters={"account_type": "Stock", "is_group": 1, "company": company}, fieldname="name" + ) if not group_stock_account: if not company_abbr: - company_abbr = frappe.get_cached_value("Company", company, 'abbr') + company_abbr = frappe.get_cached_value("Company", company, "abbr") group_stock_account = "Current Assets - " + company_abbr return group_stock_account diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 9cfad86f142..df16643d460 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -14,25 +14,31 @@ from erpnext.stock import get_warehouse_account class Warehouse(NestedSet): - nsm_parent_field = 'parent_warehouse' + nsm_parent_field = "parent_warehouse" def autoname(self): if self.company: - suffix = " - " + frappe.get_cached_value('Company', self.company, "abbr") + suffix = " - " + frappe.get_cached_value("Company", self.company, "abbr") if not self.warehouse_name.endswith(suffix): self.name = self.warehouse_name + suffix - else: - self.name = self.warehouse_name + return + + self.name = self.warehouse_name def onload(self): - '''load account name for General Ledger Report''' - if self.company and cint(frappe.db.get_value("Company", self.company, "enable_perpetual_inventory")): + """load account name for General Ledger Report""" + if self.company and cint( + frappe.db.get_value("Company", self.company, "enable_perpetual_inventory") + ): account = self.account or get_warehouse_account(self) if account: - self.set_onload('account', account) + self.set_onload("account", account) load_address_and_contact(self) + def validate(self): + self.warn_about_multiple_warehouse_account() + def on_update(self): self.update_nsm_model() @@ -41,14 +47,21 @@ class Warehouse(NestedSet): def on_trash(self): # delete bin - bins = frappe.db.sql("select * from `tabBin` where warehouse = %s", - self.name, as_dict=1) + bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name}) for d in bins: - if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \ - d['indented_qty'] or d['projected_qty'] or d['planned_qty']: - throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code'])) - else: - frappe.db.sql("delete from `tabBin` where name = %s", d['name']) + if ( + d["actual_qty"] + or d["reserved_qty"] + or d["ordered_qty"] + or d["indented_qty"] + or d["projected_qty"] + or d["planned_qty"] + ): + throw( + _("Warehouse {0} can not be deleted as quantity exists for Item {1}").format( + self.name, d["item_code"] + ) + ) if self.check_if_sle_exists(): throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse.")) @@ -56,16 +69,62 @@ class Warehouse(NestedSet): if self.check_if_child_exists(): throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse.")) + frappe.db.delete("Bin", filters={"warehouse": self.name}) self.update_nsm_model() self.unlink_from_items() + def warn_about_multiple_warehouse_account(self): + "If Warehouse value is split across multiple accounts, warn." + + def get_accounts_where_value_is_booked(name): + sle = frappe.qb.DocType("Stock Ledger Entry") + gle = frappe.qb.DocType("GL Entry") + ac = frappe.qb.DocType("Account") + + return ( + frappe.qb.from_(sle) + .join(gle) + .on(sle.voucher_no == gle.voucher_no) + .join(ac) + .on(ac.name == gle.account) + .select(gle.account) + .distinct() + .where((sle.warehouse == name) & (ac.account_type == "Stock")) + .orderby(sle.creation) + .run(as_dict=True) + ) + + if self.is_new(): + return + + old_wh_account = frappe.db.get_value("Warehouse", self.name, "account") + + # WH account is being changed or set get all accounts against which wh value is booked + if self.account != old_wh_account: + accounts = get_accounts_where_value_is_booked(self.name) + accounts = [d.account for d in accounts] + + if not accounts or (len(accounts) == 1 and self.account in accounts): + # if same singular account has stock value booked ignore + return + + warning = _("Warehouse's Stock Value has already been booked in the following accounts:") + account_str = "
    " + ", ".join(frappe.bold(ac) for ac in accounts) + reason = "

    " + _( + "Booking stock value across multiple accounts will make it harder to track stock and account value." + ) + + frappe.msgprint( + warning + account_str + reason, + title=_("Multiple Warehouse Accounts"), + indicator="orange", + ) + def check_if_sle_exists(self): - return frappe.db.sql("""select name from `tabStock Ledger Entry` - where warehouse = %s limit 1""", self.name) + return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name}) def check_if_child_exists(self): - return frappe.db.sql("""select name from `tabWarehouse` - where parent_warehouse = %s limit 1""", self.name) + return frappe.db.exists("Warehouse", {"parent_warehouse": self.name}) def convert_to_group_or_ledger(self): if self.is_group: @@ -92,28 +151,26 @@ class Warehouse(NestedSet): return 1 def unlink_from_items(self): - frappe.db.sql(""" - update `tabItem Default` - set default_warehouse=NULL - where default_warehouse=%s""", self.name) + frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None) + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): if is_root: parent = "" - fields = ['name as value', 'is_group as expandable'] + fields = ["name as value", "is_group as expandable"] filters = [ - ['docstatus', '<', '2'], - ['ifnull(`parent_warehouse`, "")', '=', parent], - ['company', 'in', (company, None,'')] + ["docstatus", "<", "2"], + ['ifnull(`parent_warehouse`, "")', "=", parent], + ["company", "in", (company, None, "")], ] - warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by='name') + warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by="name") - company_currency = '' + company_currency = "" if company: - company_currency = frappe.get_cached_value('Company', company, 'default_currency') + company_currency = frappe.get_cached_value("Company", company, "default_currency") warehouse_wise_value = get_warehouse_wise_stock_value(company) @@ -124,14 +181,20 @@ def get_children(doctype, parent=None, company=None, is_root=False): wh["company_currency"] = company_currency return warehouses -def get_warehouse_wise_stock_value(company): - warehouses = frappe.get_all('Warehouse', - fields = ['name', 'parent_warehouse'], filters = {'company': company}) - parent_warehouse = {d.name : d.parent_warehouse for d in warehouses} - filters = {'warehouse': ('in', [data.name for data in warehouses])} - bin_data = frappe.get_all('Bin', fields = ['sum(stock_value) as stock_value', 'warehouse'], - filters = filters, group_by = 'warehouse') +def get_warehouse_wise_stock_value(company): + warehouses = frappe.get_all( + "Warehouse", fields=["name", "parent_warehouse"], filters={"company": company} + ) + parent_warehouse = {d.name: d.parent_warehouse for d in warehouses} + + filters = {"warehouse": ("in", [data.name for data in warehouses])} + bin_data = frappe.get_all( + "Bin", + fields=["sum(stock_value) as stock_value", "warehouse"], + filters=filters, + group_by="warehouse", + ) warehouse_wise_stock_value = defaultdict(float) for row in bin_data: @@ -139,23 +202,30 @@ def get_warehouse_wise_stock_value(company): continue warehouse_wise_stock_value[row.warehouse] = row.stock_value - update_value_in_parent_warehouse(warehouse_wise_stock_value, - parent_warehouse, row.warehouse, row.stock_value) + update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse, row.warehouse, row.stock_value + ) return warehouse_wise_stock_value -def update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value): + +def update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value +): parent_warehouse = parent_warehouse_dict.get(warehouse) if not parent_warehouse: return warehouse_wise_stock_value[parent_warehouse] += flt(stock_value) - update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, - parent_warehouse, stock_value) + update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse_dict, parent_warehouse, stock_value + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = make_tree_args(**frappe.form_dict) if cint(args.is_root): @@ -163,32 +233,37 @@ def add_node(): frappe.get_doc(args).insert() + @frappe.whitelist() -def convert_to_group_or_ledger(): - args = frappe.form_dict - return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger() +def convert_to_group_or_ledger(docname=None): + if not docname: + docname = frappe.form_dict.docname + return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger() + def get_child_warehouses(warehouse): - lft, rgt = frappe.get_cached_value("Warehouse", warehouse, ["lft", "rgt"]) + from frappe.utils.nestedset import get_descendants_of + + children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft") + return children + [warehouse] # append self for backward compatibility - return frappe.db.sql_list("""select name from `tabWarehouse` - where lft >= %s and rgt <= %s""", (lft, rgt)) def get_warehouses_based_on_account(account, company=None): warehouses = [] - for d in frappe.get_all("Warehouse", fields = ["name", "is_group"], - filters = {"account": account}): + for d in frappe.get_all("Warehouse", fields=["name", "is_group"], filters={"account": account}): if d.is_group: warehouses.extend(get_child_warehouses(d.name)) else: warehouses.append(d.name) - if (not warehouses and company and - frappe.get_cached_value("Company", company, "default_inventory_account") == account): - warehouses = [d.name for d in frappe.get_all("Warehouse", filters={'is_group': 0})] + if ( + not warehouses + and company + and frappe.get_cached_value("Company", company, "default_inventory_account") == account + ): + warehouses = [d.name for d in frappe.get_all("Warehouse", filters={"is_group": 0})] if not warehouses: - frappe.throw(_("Warehouse not found against the account {0}") - .format(account)) + frappe.throw(_("Warehouse not found against the account {0}").format(account)) return warehouses diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e7b4ca2de38..384dd7d94f4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -23,31 +23,38 @@ from erpnext.stock.doctype.item.item import get_item_defaults, get_uom_conv_fact from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_manufacturer_part_no from erpnext.stock.doctype.price_list.price_list import get_price_list_details -sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice', 'POS Invoice'] -purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] +sales_doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"] +purchase_doctypes = [ + "Material Request", + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", +] + @frappe.whitelist() def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): """ - args = { - "item_code": "", - "warehouse": None, - "customer": "", - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "is_subcontracted": "Yes" / "No", - "ignore_pricing_rule": 0/1 - "project": "" - "set_warehouse": "" - } + args = { + "item_code": "", + "warehouse": None, + "customer": "", + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "is_subcontracted": "Yes" / "No", + "ignore_pricing_rule": 0/1 + "project": "" + "set_warehouse": "" + } """ args = process_args(args) @@ -61,16 +68,21 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if isinstance(doc, string_types): doc = json.loads(doc) - if doc and doc.get('doctype') == 'Purchase Invoice': - args['bill_date'] = doc.get('bill_date') + if doc and doc.get("doctype") == "Purchase Invoice": + args["bill_date"] = doc.get("bill_date") if doc: - args['posting_date'] = doc.get('posting_date') - args['transaction_date'] = doc.get('transaction_date') + args["posting_date"] = doc.get("posting_date") + args["transaction_date"] = doc.get("transaction_date") get_item_tax_template(args, item, out) - out["item_tax_rate"] = get_item_tax_map(args.company, args.get("item_tax_template") if out.get("item_tax_template") is None \ - else out.get("item_tax_template"), as_json=True) + out["item_tax_rate"] = get_item_tax_map( + args.company, + args.get("item_tax_template") + if out.get("item_tax_template") is None + else out.get("item_tax_template"), + as_json=True, + ) get_party_item_code(args, item, out) @@ -83,12 +95,14 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) - if (args.get("doctype") == "Material Request" and - args.get("material_request_type") == "Material Transfer"): + if ( + args.get("doctype") == "Material Request" + and args.get("material_request_type") == "Material Transfer" + ): out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) elif out.get("warehouse"): - if doc and doc.get('doctype') == 'Purchase Order': + if doc and doc.get("doctype") == "Purchase Order": # calculate company_total_stock only for po bin_details = get_bin_details(args.item_code, out.warehouse, args.company) else: @@ -101,31 +115,35 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.get(key) is None: args[key] = value - data = get_pricing_rule_for_item(args, out.price_list_rate, - doc, for_validate=for_validate) + data = get_pricing_rule_for_item(args, out.price_list_rate, doc, for_validate=for_validate) out.update(data) update_stock(args, out) if args.transaction_date and item.lead_time_days: - out.schedule_date = out.lead_time_date = add_days(args.transaction_date, - item.lead_time_days) + out.schedule_date = out.lead_time_date = add_days(args.transaction_date, item.lead_time_days) if args.get("is_subcontracted") == "Yes": - out.bom = args.get('bom') or get_default_bom(args.item_code) + out.bom = args.get("bom") or get_default_bom(args.item_code) get_gross_profit(out) - if args.doctype == 'Material Request': + if args.doctype == "Material Request": out.rate = args.rate or out.price_list_rate out.amount = flt(args.qty) * flt(out.rate) return out + def update_stock(args, out): - if (args.get("doctype") == "Delivery Note" or - (args.get("doctype") == "Sales Invoice" and args.get('update_stock'))) \ - and out.warehouse and out.stock_qty > 0: + if ( + ( + args.get("doctype") == "Delivery Note" + or (args.get("doctype") == "Sales Invoice" and args.get("update_stock")) + ) + and out.warehouse + and out.stock_qty > 0 + ): if out.has_batch_no and not args.get("batch_no"): out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty) @@ -133,9 +151,9 @@ def update_stock(args, out): if actual_batch_qty: out.update(actual_batch_qty) - if out.has_serial_no and args.get('batch_no'): + if out.has_serial_no and args.get("batch_no"): reserved_so = get_so_reservation_for_item(args) - out.batch_no = args.get('batch_no') + out.batch_no = args.get("batch_no") out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so) elif out.has_serial_no: @@ -149,13 +167,14 @@ def set_valuation_rate(out, args): bundled_items = frappe.get_doc("Product Bundle", args.item_code) for bundle_item in bundled_items.items: - valuation_rate += \ - flt(get_valuation_rate(bundle_item.item_code, args.company, out.get("warehouse")).get("valuation_rate") \ - * bundle_item.qty) + valuation_rate += flt( + get_valuation_rate(bundle_item.item_code, args.company, out.get("warehouse")).get( + "valuation_rate" + ) + * bundle_item.qty + ) - out.update({ - "valuation_rate": valuation_rate - }) + out.update({"valuation_rate": valuation_rate}) else: out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse"))) @@ -178,11 +197,13 @@ def process_args(args): set_transaction_type(args) return args + def process_string_args(args): if isinstance(args, string_types): args = json.loads(args) return args + @frappe.whitelist() def get_item_code(barcode=None, serial_no=None): if barcode: @@ -202,6 +223,7 @@ def validate_item_details(args, item): throw(_("Please specify Company")) from erpnext.stock.doctype.item.item import validate_end_of_life + validate_end_of_life(item.name, item.end_of_life, item.disabled) if args.transaction_type == "selling" and cint(item.has_variants): @@ -215,37 +237,37 @@ def validate_item_details(args, item): def get_basic_details(args, item, overwrite_warehouse=True): """ :param args: { - "item_code": "", - "warehouse": None, - "customer": "", - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "price_list_uom_dependant": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "is_subcontracted": "Yes" / "No", - "ignore_pricing_rule": 0/1 - "project": "", - barcode: "", - serial_no: "", - currency: "", - update_stock: "", - price_list: "", - company: "", - order_type: "", - is_pos: "", - project: "", - qty: "", - stock_qty: "", - conversion_factor: "", - against_blanket_order: 0/1 - } + "item_code": "", + "warehouse": None, + "customer": "", + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "price_list_uom_dependant": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "is_subcontracted": "Yes" / "No", + "ignore_pricing_rule": 0/1 + "project": "", + barcode: "", + serial_no: "", + currency: "", + update_stock: "", + price_list: "", + company: "", + order_type: "", + is_pos: "", + project: "", + qty: "", + stock_qty: "", + conversion_factor: "", + against_blanket_order: 0/1 + } :param item: `item_code` of Item object :return: frappe._dict """ @@ -260,77 +282,99 @@ def get_basic_details(args, item, overwrite_warehouse=True): item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - defaults = frappe._dict({ - 'item_defaults': item_defaults, - 'item_group_defaults': item_group_defaults, - 'brand_defaults': brand_defaults - }) + defaults = frappe._dict( + { + "item_defaults": item_defaults, + "item_group_defaults": item_group_defaults, + "brand_defaults": brand_defaults, + } + ) warehouse = get_item_warehouse(item, args, overwrite_warehouse, defaults) - if args.get('doctype') == "Material Request" and not args.get('material_request_type'): - args['material_request_type'] = frappe.db.get_value('Material Request', - args.get('name'), 'material_request_type', cache=True) + if args.get("doctype") == "Material Request" and not args.get("material_request_type"): + args["material_request_type"] = frappe.db.get_value( + "Material Request", args.get("name"), "material_request_type", cache=True + ) expense_account = None - if args.get('doctype') == 'Purchase Invoice' and item.is_fixed_asset: + if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset: from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account - expense_account = get_asset_category_account(fieldname = "fixed_asset_account", item = args.item_code, company= args.company) - #Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master - if not args.get('uom'): - if args.get('doctype') in sales_doctypes: + expense_account = get_asset_category_account( + fieldname="fixed_asset_account", item=args.item_code, company=args.company + ) + + # Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master + if not args.get("uom"): + if args.get("doctype") in sales_doctypes: args.uom = item.sales_uom if item.sales_uom else item.stock_uom - elif (args.get('doctype') in ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']) or \ - (args.get('doctype') == 'Material Request' and args.get('material_request_type') == 'Purchase'): + elif (args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) or ( + args.get("doctype") == "Material Request" and args.get("material_request_type") == "Purchase" + ): args.uom = item.purchase_uom if item.purchase_uom else item.stock_uom else: args.uom = item.stock_uom - if (args.get("batch_no") and - item.name != frappe.get_cached_value('Batch', args.get("batch_no"), 'item')): - args['batch_no'] = '' + if args.get("batch_no") and item.name != frappe.get_cached_value( + "Batch", args.get("batch_no"), "item" + ): + args["batch_no"] = "" - out = frappe._dict({ - "item_code": item.name, - "item_name": item.item_name, - "description": cstr(item.description).strip(), - "image": cstr(item.image).strip(), - "warehouse": warehouse, - "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), - "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , - "discount_account": get_default_discount_account(args, item_defaults), - "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), - 'has_serial_no': item.has_serial_no, - 'has_batch_no': item.has_batch_no, - "batch_no": args.get("batch_no"), - "uom": args.uom, - "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", - "qty": flt(args.qty) or 1.0, - "stock_qty": flt(args.qty) or 1.0, - "price_list_rate": 0.0, - "base_price_list_rate": 0.0, - "rate": 0.0, - "base_rate": 0.0, - "amount": 0.0, - "base_amount": 0.0, - "net_rate": 0.0, - "net_amount": 0.0, - "discount_percentage": 0.0, - "discount_amount": 0.0, - "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), - "update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0, - "delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0, - "is_fixed_asset": item.is_fixed_asset, - "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0, - "transaction_date": args.get("transaction_date"), - "against_blanket_order": args.get("against_blanket_order"), - "bom_no": item.get("default_bom"), - "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom"), - "grant_commission": item.get("grant_commission") - }) + out = frappe._dict( + { + "item_code": item.name, + "item_name": item.item_name, + "description": cstr(item.description).strip(), + "image": cstr(item.image).strip(), + "warehouse": warehouse, + "income_account": get_default_income_account( + args, item_defaults, item_group_defaults, brand_defaults + ), + "expense_account": expense_account + or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), + "discount_account": get_default_discount_account(args, item_defaults), + "provisional_expense_account": get_provisional_account(args, item_defaults), + "cost_center": get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ), + "has_serial_no": item.has_serial_no, + "has_batch_no": item.has_batch_no, + "batch_no": args.get("batch_no"), + "uom": args.uom, + "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", + "qty": flt(args.qty) or 1.0, + "stock_qty": flt(args.qty) or 1.0, + "price_list_rate": 0.0, + "base_price_list_rate": 0.0, + "rate": 0.0, + "base_rate": 0.0, + "amount": 0.0, + "base_amount": 0.0, + "net_rate": 0.0, + "net_amount": 0.0, + "discount_percentage": 0.0, + "discount_amount": 0.0, + "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), + "update_stock": args.get("update_stock") + if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"] + else 0, + "delivered_by_supplier": item.delivered_by_supplier + if args.get("doctype") in ["Sales Order", "Sales Invoice"] + else 0, + "is_fixed_asset": item.is_fixed_asset, + "last_purchase_rate": item.last_purchase_rate + if args.get("doctype") in ["Purchase Order"] + else 0, + "transaction_date": args.get("transaction_date"), + "against_blanket_order": args.get("against_blanket_order"), + "bom_no": item.get("default_bom"), + "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), + "weight_uom": args.get("weight_uom") or item.get("weight_uom"), + "grant_commission": item.get("grant_commission"), + } + ) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): out.update(calculate_service_end_date(args, item)) @@ -339,28 +383,33 @@ def get_basic_details(args, item, overwrite_warehouse=True): if item.stock_uom == args.uom: out.conversion_factor = 1.0 else: - out.conversion_factor = args.conversion_factor or \ - get_conversion_factor(item.name, args.uom).get("conversion_factor") + out.conversion_factor = args.conversion_factor or get_conversion_factor(item.name, args.uom).get( + "conversion_factor" + ) args.conversion_factor = out.conversion_factor out.stock_qty = out.qty * out.conversion_factor args.stock_qty = out.stock_qty # calculate last purchase rate - if args.get('doctype') in purchase_doctypes: + if args.get("doctype") in purchase_doctypes: from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate - out.last_purchase_rate = item_last_purchase_rate(args.name, args.conversion_rate, item.name, out.conversion_factor) + + out.last_purchase_rate = item_last_purchase_rate( + args.name, args.conversion_rate, item.name, out.conversion_factor + ) # if default specified in item is for another company, fetch from company for d in [ ["Account", "income_account", "default_income_account"], ["Account", "expense_account", "default_expense_account"], ["Cost Center", "cost_center", "cost_center"], - ["Warehouse", "warehouse", ""]]: - if not out[d[1]]: - out[d[1]] = frappe.get_cached_value('Company', args.company, d[2]) if d[2] else None + ["Warehouse", "warehouse", ""], + ]: + if not out[d[1]]: + out[d[1]] = frappe.get_cached_value("Company", args.company, d[2]) if d[2] else None - for fieldname in ("item_name", "item_group", "barcodes", "brand", "stock_uom"): + for fieldname in ("item_name", "item_group", "brand", "stock_uom"): out[fieldname] = item.get(fieldname) if args.get("manufacturer"): @@ -371,53 +420,58 @@ def get_basic_details(args, item, overwrite_warehouse=True): out["manufacturer_part_no"] = None out["manufacturer"] = None else: - data = frappe.get_value("Item", item.name, - ["default_item_manufacturer", "default_manufacturer_part_no"] , as_dict=1) + data = frappe.get_value( + "Item", item.name, ["default_item_manufacturer", "default_manufacturer_part_no"], as_dict=1 + ) if data: - out.update({ - "manufacturer": data.default_item_manufacturer, - "manufacturer_part_no": data.default_manufacturer_part_no - }) + out.update( + { + "manufacturer": data.default_item_manufacturer, + "manufacturer_part_no": data.default_manufacturer_part_no, + } + ) - child_doctype = args.doctype + ' Item' + child_doctype = args.doctype + " Item" meta = frappe.get_meta(child_doctype) if meta.get_field("barcode"): update_barcode_value(out) if out.get("weight_per_unit"): - out['total_weight'] = out.weight_per_unit * out.stock_qty + out["total_weight"] = out.weight_per_unit * out.stock_qty return out + def get_item_warehouse(item, args, overwrite_warehouse, defaults=None): if not defaults: - defaults = frappe._dict({ - 'item_defaults' : get_item_defaults(item.name, args.company), - 'item_group_defaults' : get_item_group_defaults(item.name, args.company), - 'brand_defaults' : get_brand_defaults(item.name, args.company) - }) + defaults = frappe._dict( + { + "item_defaults": get_item_defaults(item.name, args.company), + "item_group_defaults": get_item_group_defaults(item.name, args.company), + "brand_defaults": get_brand_defaults(item.name, args.company), + } + ) if overwrite_warehouse or not args.warehouse: warehouse = ( - args.get("set_warehouse") or - defaults.item_defaults.get("default_warehouse") or - defaults.item_group_defaults.get("default_warehouse") or - defaults.brand_defaults.get("default_warehouse") or - args.get('warehouse') + args.get("set_warehouse") + or defaults.item_defaults.get("default_warehouse") + or defaults.item_group_defaults.get("default_warehouse") + or defaults.brand_defaults.get("default_warehouse") + or args.get("warehouse") ) if not warehouse: defaults = frappe.defaults.get_defaults() or {} - warehouse_exists = frappe.db.exists("Warehouse", { - 'name': defaults.default_warehouse, - 'company': args.company - }) + warehouse_exists = frappe.db.exists( + "Warehouse", {"name": defaults.default_warehouse, "company": args.company} + ) if defaults.get("default_warehouse") and warehouse_exists: warehouse = defaults.default_warehouse else: - warehouse = args.get('warehouse') + warehouse = args.get("warehouse") if not warehouse: default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") @@ -426,12 +480,14 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults=None): return warehouse + def update_barcode_value(out): barcode_data = get_barcode_data([out]) # If item has one barcode then update the value of the barcode field if barcode_data and len(barcode_data.get(out.item_code)) == 1: - out['barcode'] = barcode_data.get(out.item_code)[0] + out["barcode"] = barcode_data.get(out.item_code)[0] + def get_barcode_data(items_list): # get itemwise batch no data @@ -440,9 +496,13 @@ def get_barcode_data(items_list): itemwise_barcode = {} for item in items_list: - barcodes = frappe.db.sql(""" + barcodes = frappe.db.sql( + """ select barcode from `tabItem Barcode` where parent = %s - """, item.item_code, as_dict=1) + """, + item.item_code, + as_dict=1, + ) for barcode in barcodes: if item.item_code not in itemwise_barcode: @@ -451,6 +511,7 @@ def get_barcode_data(items_list): return itemwise_barcode + @frappe.whitelist() def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} @@ -476,22 +537,29 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])} + args = { + "company": company, + "tax_category": tax_category, + "net_rate": item_rates.get(item_code[1]), + } if item_tax_templates: args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) get_item_tax_template(args, item, out[item_code[1]]) - out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) + out[item_code[1]]["item_tax_rate"] = get_item_tax_map( + company, out[item_code[1]].get("item_tax_template"), as_json=True + ) return out + def get_item_tax_template(args, item, out): """ - args = { - "tax_category": None - "item_tax_template": None - } + args = { + "tax_category": None + "item_tax_template": None + } """ item_tax_template = None if item.taxes: @@ -504,6 +572,7 @@ def get_item_tax_template(args, item, out): item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_group = item_group_doc.parent_item_group + def _get_item_tax_template(args, taxes, out=None, for_validate=False): if out is None: out = {} @@ -511,36 +580,43 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity = [] for tax in taxes: - tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, 'company') - if tax_company == args['company']: - if (tax.valid_from or tax.maximum_net_rate): + tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company") + if tax_company == args["company"]: + if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date - validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date') + validation_date = ( + args.get("transaction_date") or args.get("bill_date") or args.get("posting_date") + ) - if getdate(tax.valid_from) <= getdate(validation_date) \ - and is_within_valid_range(args, tax): + if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax): taxes_with_validity.append(tax) else: taxes_with_no_validity.append(tax) if taxes_with_validity: - taxes = sorted(taxes_with_validity, key = lambda i: i.valid_from, reverse=True) + taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from, reverse=True) else: taxes = taxes_with_no_validity if for_validate: - return [tax.item_tax_template for tax in taxes if (cstr(tax.tax_category) == cstr(args.get('tax_category')) \ - and (tax.item_tax_template not in taxes))] + return [ + tax.item_tax_template + for tax in taxes + if ( + cstr(tax.tax_category) == cstr(args.get("tax_category")) + and (tax.item_tax_template not in taxes) + ) + ] # all templates have validity and no template is valid if not taxes_with_validity and (not taxes_with_no_validity): return None # do not change if already a valid template - if args.get('item_tax_template') in {t.item_tax_template for t in taxes}: - out["item_tax_template"] = args.get('item_tax_template') - return args.get('item_tax_template') + if args.get("item_tax_template") in {t.item_tax_template for t in taxes}: + out["item_tax_template"] = args.get("item_tax_template") + return args.get("item_tax_template") for tax in taxes: if cstr(tax.tax_category) == cstr(args.get("tax_category")): @@ -548,15 +624,17 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return tax.item_tax_template return None + def is_within_valid_range(args, tax): if not flt(tax.maximum_net_rate): # No range specified, just ignore return True - elif flt(tax.minimum_net_rate) <= flt(args.get('net_rate')) <= flt(tax.maximum_net_rate): + elif flt(tax.minimum_net_rate) <= flt(args.get("net_rate")) <= flt(tax.maximum_net_rate): return True return False + @frappe.whitelist() def get_item_tax_map(company, item_tax_template, as_json=True): item_tax_map = {} @@ -568,6 +646,7 @@ def get_item_tax_map(company, item_tax_template, as_json=True): return json.dumps(item_tax_map) if as_json else item_tax_map + @frappe.whitelist() def calculate_service_end_date(args, item=None): args = process_args(args) @@ -586,53 +665,72 @@ def calculate_service_end_date(args, item=None): service_start_date = args.service_start_date if args.service_start_date else args.transaction_date service_end_date = add_months(service_start_date, item.get(no_of_months)) - deferred_detail = { - "service_start_date": service_start_date, - "service_end_date": service_end_date - } + deferred_detail = {"service_start_date": service_start_date, "service_end_date": service_end_date} deferred_detail[enable_deferred] = item.get(enable_deferred) deferred_detail[account] = get_default_deferred_account(args, item, fieldname=account) return deferred_detail + def get_default_income_account(args, item, item_group, brand): - return (item.get("income_account") + return ( + item.get("income_account") or item_group.get("income_account") or brand.get("income_account") - or args.income_account) + or args.income_account + ) + def get_default_expense_account(args, item, item_group, brand): - return (item.get("expense_account") + return ( + item.get("expense_account") or item_group.get("expense_account") or brand.get("expense_account") - or args.expense_account) + or args.expense_account + ) + + +def get_provisional_account(args, item): + return item.get("default_provisional_account") or args.default_provisional_account + def get_default_discount_account(args, item): - return (item.get("default_discount_account") - or args.discount_account) + return item.get("default_discount_account") or args.discount_account + def get_default_deferred_account(args, item, fieldname=None): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): - return (item.get(fieldname) + return ( + item.get(fieldname) or args.get(fieldname) - or frappe.get_cached_value('Company', args.company, "default_"+fieldname)) + or frappe.get_cached_value("Company", args.company, "default_" + fieldname) + ) else: return None + def get_default_cost_center(args, item=None, item_group=None, brand=None, company=None): cost_center = None if not company and args.get("company"): company = args.get("company") - if args.get('project'): + if args.get("project"): cost_center = frappe.db.get_value("Project", args.get("project"), "cost_center", cache=True) if not cost_center and (item and item_group and brand): - if args.get('customer'): - cost_center = item.get('selling_cost_center') or item_group.get('selling_cost_center') or brand.get('selling_cost_center') + if args.get("customer"): + cost_center = ( + item.get("selling_cost_center") + or item_group.get("selling_cost_center") + or brand.get("selling_cost_center") + ) else: - cost_center = item.get('buying_cost_center') or item_group.get('buying_cost_center') or brand.get('buying_cost_center') + cost_center = ( + item.get("buying_cost_center") + or item_group.get("buying_cost_center") + or brand.get("buying_cost_center") + ) elif not cost_center and args.get("item_code") and company: for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]: @@ -645,20 +743,26 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan if not cost_center and args.get("cost_center"): cost_center = args.get("cost_center") - if (company and cost_center - and frappe.get_cached_value("Cost Center", cost_center, "company") != company): + if ( + company + and cost_center + and frappe.get_cached_value("Cost Center", cost_center, "company") != company + ): return None if not cost_center and company: - cost_center = frappe.get_cached_value("Company", - company, "cost_center") + cost_center = frappe.get_cached_value("Company", company, "cost_center") return cost_center + def get_default_supplier(args, item, item_group, brand): - return (item.get("default_supplier") + return ( + item.get("default_supplier") or item_group.get("default_supplier") - or brand.get("default_supplier")) + or brand.get("default_supplier") + ) + def get_price_list_rate(args, item_doc, out=None): if out is None: @@ -666,7 +770,7 @@ def get_price_list_rate(args, item_doc, out=None): meta = frappe.get_meta(args.parenttype or args.doctype) - if meta.get_field("currency") or args.get('currency'): + if meta.get_field("currency") or args.get("currency"): if not args.get("price_list_currency") or not args.get("plc_conversion_rate"): # if currency and plc_conversion_rate exist then # `get_price_list_currency_and_exchange_rate` has already been called @@ -688,54 +792,72 @@ def get_price_list_rate(args, item_doc, out=None): insert_item_price(args) return out - out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \ - / flt(args.conversion_rate) + out.price_list_rate = ( + flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) + ) - if not out.price_list_rate and args.transaction_type=="buying": + if not out.price_list_rate and args.transaction_type == "buying": from erpnext.stock.doctype.item.item import get_last_purchase_details - out.update(get_last_purchase_details(item_doc.name, - args.name, args.conversion_rate)) + + out.update(get_last_purchase_details(item_doc.name, args.name, args.conversion_rate)) return out + def insert_item_price(args): """Insert Item Price if Price List and Price List Rate are specified and currency is the same""" - if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency \ - and cint(frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")): + if frappe.db.get_value( + "Price List", args.price_list, "currency", cache=True + ) == args.currency and cint( + frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing") + ): if frappe.has_permission("Item Price", "write"): - price_list_rate = (args.rate / args.get('conversion_factor') - if args.get("conversion_factor") else args.rate) + price_list_rate = ( + args.rate / args.get("conversion_factor") if args.get("conversion_factor") else args.rate + ) - item_price = frappe.db.get_value('Item Price', - {'item_code': args.item_code, 'price_list': args.price_list, 'currency': args.currency}, - ['name', 'price_list_rate'], as_dict=1) + item_price = frappe.db.get_value( + "Item Price", + {"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency}, + ["name", "price_list_rate"], + as_dict=1, + ) if item_price and item_price.name: - if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value('Stock Settings', 'update_existing_price_list_rate'): - frappe.db.set_value('Item Price', item_price.name, "price_list_rate", price_list_rate) - frappe.msgprint(_("Item Price updated for {0} in Price List {1}").format(args.item_code, - args.price_list), alert=True) + if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value( + "Stock Settings", "update_existing_price_list_rate" + ): + frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate) + frappe.msgprint( + _("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list), + alert=True, + ) else: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": args.price_list, - "item_code": args.item_code, - "currency": args.currency, - "price_list_rate": price_list_rate - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": args.price_list, + "item_code": args.item_code, + "currency": args.currency, + "price_list_rate": price_list_rate, + } + ) item_price.insert() - frappe.msgprint(_("Item Price added for {0} in Price List {1}").format(args.item_code, - args.price_list), alert=True) + frappe.msgprint( + _("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list), + alert=True, + ) + def get_item_price(args, item_code, ignore_party=False): """ - Get name, price_list_rate from Item Price based on conditions - Check if the desired qty is within the increment of the packing list. - :param args: dict (or frappe._dict) with mandatory fields price_list, uom - optional fields transaction_date, customer, supplier - :param item_code: str, Item Doctype field item_code + Get name, price_list_rate from Item Price based on conditions + Check if the desired qty is within the increment of the packing list. + :param args: dict (or frappe._dict) with mandatory fields price_list, uom + optional fields transaction_date, customer, supplier + :param item_code: str, Item Doctype field item_code """ - args['item_code'] = item_code + args["item_code"] = item_code conditions = """where item_code=%(item_code)s and price_list=%(price_list)s @@ -751,36 +873,42 @@ def get_item_price(args, item_code, ignore_party=False): else: conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" - if args.get('transaction_date'): + if args.get("transaction_date"): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" - if args.get('posting_date'): + if args.get("posting_date"): conditions += """ and %(posting_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" - return frappe.db.sql(""" select name, price_list_rate, uom + return frappe.db.sql( + """ select name, price_list_rate, uom from `tabItem Price` {conditions} - order by valid_from desc, batch_no desc, uom desc """.format(conditions=conditions), args) + order by valid_from desc, batch_no desc, uom desc """.format( + conditions=conditions + ), + args, + ) + def get_price_list_rate_for(args, item_code): """ - :param customer: link to Customer DocType - :param supplier: link to Supplier DocType - :param price_list: str (Standard Buying or Standard Selling) - :param item_code: str, Item Doctype field item_code - :param qty: Desired Qty - :param transaction_date: Date of the price + :param customer: link to Customer DocType + :param supplier: link to Supplier DocType + :param price_list: str (Standard Buying or Standard Selling) + :param item_code: str, Item Doctype field item_code + :param qty: Desired Qty + :param transaction_date: Date of the price """ item_price_args = { - "item_code": item_code, - "price_list": args.get('price_list'), - "customer": args.get('customer'), - "supplier": args.get('supplier'), - "uom": args.get('uom'), - "transaction_date": args.get('transaction_date'), - "posting_date": args.get('posting_date'), - "batch_no": args.get('batch_no') + "item_code": item_code, + "price_list": args.get("price_list"), + "customer": args.get("customer"), + "supplier": args.get("supplier"), + "uom": args.get("uom"), + "transaction_date": args.get("transaction_date"), + "posting_date": args.get("posting_date"), + "batch_no": args.get("batch_no"), } item_price_data = 0 @@ -793,12 +921,15 @@ def get_price_list_rate_for(args, item_code): for field in ["customer", "supplier"]: del item_price_args[field] - general_price_list_rate = get_item_price(item_price_args, item_code, - ignore_party=args.get("ignore_party")) + general_price_list_rate = get_item_price( + item_price_args, item_code, ignore_party=args.get("ignore_party") + ) if not general_price_list_rate and args.get("uom") != args.get("stock_uom"): item_price_args["uom"] = args.get("stock_uom") - general_price_list_rate = get_item_price(item_price_args, item_code, ignore_party=args.get("ignore_party")) + general_price_list_rate = get_item_price( + item_price_args, item_code, ignore_party=args.get("ignore_party") + ) if general_price_list_rate: item_price_data = general_price_list_rate @@ -806,18 +937,19 @@ def get_price_list_rate_for(args, item_code): if item_price_data: if item_price_data[0][2] == args.get("uom"): return item_price_data[0][1] - elif not args.get('price_list_uom_dependant'): + elif not args.get("price_list_uom_dependant"): return flt(item_price_data[0][1] * flt(args.get("conversion_factor", 1))) else: return item_price_data[0][1] + def check_packing_list(price_list_rate_name, desired_qty, item_code): """ - Check if the desired qty is within the increment of the packing list. - :param price_list_rate_name: Name of Item Price - :param desired_qty: Desired Qt - :param item_code: str, Item Doctype field item_code - :param qty: Desired Qt + Check if the desired qty is within the increment of the packing list. + :param price_list_rate_name: Name of Item Price + :param desired_qty: Desired Qt + :param item_code: str, Item Doctype field item_code + :param qty: Desired Qt """ flag = True @@ -830,47 +962,62 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code): return flag + def validate_conversion_rate(args, meta): from erpnext.controllers.accounts_controller import validate_conversion_rate - company_currency = frappe.get_cached_value('Company', args.company, "default_currency") - if (not args.conversion_rate and args.currency==company_currency): + company_currency = frappe.get_cached_value("Company", args.company, "default_currency") + if not args.conversion_rate and args.currency == company_currency: args.conversion_rate = 1.0 - if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency): - args.conversion_rate = get_exchange_rate(args.currency, - company_currency, args.transaction_date, "for_buying") or 1.0 + if ( + not args.ignore_conversion_rate + and args.conversion_rate == 1 + and args.currency != company_currency + ): + args.conversion_rate = ( + get_exchange_rate(args.currency, company_currency, args.transaction_date, "for_buying") or 1.0 + ) # validate currency conversion rate - validate_conversion_rate(args.currency, args.conversion_rate, - meta.get_label("conversion_rate"), args.company) + validate_conversion_rate( + args.currency, args.conversion_rate, meta.get_label("conversion_rate"), args.company + ) - args.conversion_rate = flt(args.conversion_rate, - get_field_precision(meta.get_field("conversion_rate"), - frappe._dict({"fields": args}))) + args.conversion_rate = flt( + args.conversion_rate, + get_field_precision(meta.get_field("conversion_rate"), frappe._dict({"fields": args})), + ) if args.price_list: - if (not args.plc_conversion_rate - and args.price_list_currency==frappe.db.get_value("Price List", args.price_list, "currency", cache=True)): + if not args.plc_conversion_rate and args.price_list_currency == frappe.db.get_value( + "Price List", args.price_list, "currency", cache=True + ): args.plc_conversion_rate = 1.0 # validate price list currency conversion rate if not args.get("price_list_currency"): throw(_("Price List Currency not selected")) else: - validate_conversion_rate(args.price_list_currency, args.plc_conversion_rate, - meta.get_label("plc_conversion_rate"), args.company) + validate_conversion_rate( + args.price_list_currency, + args.plc_conversion_rate, + meta.get_label("plc_conversion_rate"), + args.company, + ) if meta.get_field("plc_conversion_rate"): - args.plc_conversion_rate = flt(args.plc_conversion_rate, - get_field_precision(meta.get_field("plc_conversion_rate"), - frappe._dict({"fields": args}))) + args.plc_conversion_rate = flt( + args.plc_conversion_rate, + get_field_precision(meta.get_field("plc_conversion_rate"), frappe._dict({"fields": args})), + ) + def get_party_item_code(args, item_doc, out): - if args.transaction_type=="selling" and args.customer: + if args.transaction_type == "selling" and args.customer: out.customer_item_code = None - if args.quotation_to and args.quotation_to != 'Customer': + if args.quotation_to and args.quotation_to != "Customer": return customer_item_code = item_doc.get("customer_items", {"customer_name": args.customer}) @@ -883,15 +1030,16 @@ def get_party_item_code(args, item_doc, out): if customer_group_item_code and not customer_group_item_code[0].customer_name: out.customer_item_code = customer_group_item_code[0].ref_code - if args.transaction_type=="buying" and args.supplier: + if args.transaction_type == "buying" and args.supplier: item_supplier = item_doc.get("supplier_items", {"supplier": args.supplier}) out.supplier_part_no = item_supplier[0].supplier_part_no if item_supplier else None + def get_pos_profile_item_details(company, args, pos_profile=None, update_data=False): res = frappe._dict() if not frappe.flags.pos_profile and not pos_profile: - pos_profile = frappe.flags.pos_profile = get_pos_profile(company, args.get('pos_profile')) + pos_profile = frappe.flags.pos_profile = get_pos_profile(company, args.get("pos_profile")) if pos_profile: for fieldname in ("income_account", "cost_center", "warehouse", "expense_account"): @@ -903,70 +1051,89 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa return res + @frappe.whitelist() def get_pos_profile(company, pos_profile=None, user=None): - if pos_profile: return frappe.get_cached_doc('POS Profile', pos_profile) + if pos_profile: + return frappe.get_cached_doc("POS Profile", pos_profile) if not user: - user = frappe.session['user'] + user = frappe.session["user"] condition = "pfu.user = %(user)s AND pfu.default=1" if user and company: condition = "pfu.user = %(user)s AND pf.company = %(company)s AND pfu.default=1" - pos_profile = frappe.db.sql("""SELECT pf.* + pos_profile = frappe.db.sql( + """SELECT pf.* FROM `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu ON pf.name = pfu.parent WHERE {cond} AND pf.disabled = 0 - """.format(cond = condition), { - 'user': user, - 'company': company - }, as_dict=1) + """.format( + cond=condition + ), + {"user": user, "company": company}, + as_dict=1, + ) if not pos_profile and company: - pos_profile = frappe.db.sql("""SELECT pf.* + pos_profile = frappe.db.sql( + """SELECT pf.* FROM `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu ON pf.name = pfu.parent WHERE pf.company = %(company)s AND pf.disabled = 0 - """, { - 'company': company - }, as_dict=1) + """, + {"company": company}, + as_dict=1, + ) return pos_profile and pos_profile[0] or None + def get_serial_nos_by_fifo(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join(frappe.db.sql_list("""select name from `tabSerial No` + return "\n".join( + frappe.db.sql_list( + """select name from `tabSerial No` where item_code=%(item_code)s and warehouse=%(warehouse)s and sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) order by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", - { - "item_code": args.item_code, - "warehouse": args.warehouse, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order - })) + { + "item_code": args.item_code, + "warehouse": args.warehouse, + "qty": abs(cint(args.stock_qty)), + "sales_order": sales_order, + }, + ) + ) + def get_serial_no_batchwise(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join(frappe.db.sql_list("""select name from `tabSerial No` + return "\n".join( + frappe.db.sql_list( + """select name from `tabSerial No` where item_code=%(item_code)s and warehouse=%(warehouse)s and sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) and batch_no=IF(%(batch_no)s IS NULL, batch_no, %(batch_no)s) order - by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", { - "item_code": args.item_code, - "warehouse": args.warehouse, - "batch_no": args.batch_no, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order - })) + by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", + { + "item_code": args.item_code, + "warehouse": args.warehouse, + "batch_no": args.batch_no, + "qty": abs(cint(args.stock_qty)), + "sales_order": sales_order, + }, + ) + ) + @frappe.whitelist() def get_conversion_factor(item_code, uom): @@ -974,69 +1141,94 @@ def get_conversion_factor(item_code, uom): filters = {"parent": item_code, "uom": uom} if variant_of: filters["parent"] = ("in", (item_code, variant_of)) - conversion_factor = frappe.db.get_value("UOM Conversion Detail", - filters, "conversion_factor") + conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor") if not conversion_factor: stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") conversion_factor = get_uom_conv_factor(uom, stock_uom) return {"conversion_factor": conversion_factor or 1.0} + @frappe.whitelist() def get_projected_qty(item_code, warehouse): - return {"projected_qty": frappe.db.get_value("Bin", - {"item_code": item_code, "warehouse": warehouse}, "projected_qty")} + return { + "projected_qty": frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty" + ) + } + @frappe.whitelist() def get_bin_details(item_code, warehouse, company=None): - bin_details = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - ["projected_qty", "actual_qty", "reserved_qty"], as_dict=True, cache=True) \ - or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} + bin_details = frappe.db.get_value( + "Bin", + {"item_code": item_code, "warehouse": warehouse}, + ["projected_qty", "actual_qty", "reserved_qty"], + as_dict=True, + cache=True, + ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} if company: - bin_details['company_total_stock'] = get_company_total_stock(item_code, company) + bin_details["company_total_stock"] = get_company_total_stock(item_code, company) return bin_details + def get_company_total_stock(item_code, company): - return frappe.db.sql("""SELECT sum(actual_qty) from + return frappe.db.sql( + """SELECT sum(actual_qty) from (`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name) WHERE `tabWarehouse`.company = %s and `tabBin`.item_code = %s""", - (company, item_code))[0][0] + (company, item_code), + )[0][0] + @frappe.whitelist() def get_serial_no_details(item_code, warehouse, stock_qty, serial_no): - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty, "serial_no":serial_no}) + args = frappe._dict( + {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no} + ) serial_no = get_serial_no(args) - return {'serial_no': serial_no} + return {"serial_no": serial_no} + @frappe.whitelist() -def get_bin_details_and_serial_nos(item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None): +def get_bin_details_and_serial_nos( + item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None +): bin_details_and_serial_nos = {} bin_details_and_serial_nos.update(get_bin_details(item_code, warehouse)) if flt(stock_qty) > 0: if has_batch_no: - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty}) + args = frappe._dict({"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty}) serial_no = get_serial_no(args) - bin_details_and_serial_nos.update({'serial_no': serial_no}) + bin_details_and_serial_nos.update({"serial_no": serial_no}) return bin_details_and_serial_nos - bin_details_and_serial_nos.update(get_serial_no_details(item_code, warehouse, stock_qty, serial_no)) + bin_details_and_serial_nos.update( + get_serial_no_details(item_code, warehouse, stock_qty, serial_no) + ) return bin_details_and_serial_nos + @frappe.whitelist() def get_batch_qty_and_serial_no(batch_no, stock_qty, warehouse, item_code, has_serial_no): batch_qty_and_serial_no = {} batch_qty_and_serial_no.update(get_batch_qty(batch_no, warehouse, item_code)) - if (flt(batch_qty_and_serial_no.get('actual_batch_qty')) >= flt(stock_qty)) and has_serial_no: - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty, "batch_no":batch_no}) + if (flt(batch_qty_and_serial_no.get("actual_batch_qty")) >= flt(stock_qty)) and has_serial_no: + args = frappe._dict( + {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "batch_no": batch_no} + ) serial_no = get_serial_no(args) - batch_qty_and_serial_no.update({'serial_no': serial_no}) + batch_qty_and_serial_no.update({"serial_no": serial_no}) return batch_qty_and_serial_no + @frappe.whitelist() def get_batch_qty(batch_no, warehouse, item_code): from erpnext.stock.doctype.batch import batch + if batch_no: - return {'actual_batch_qty': batch.get_batch_qty(batch_no, warehouse)} + return {"actual_batch_qty": batch.get_batch_qty(batch_no, warehouse)} + @frappe.whitelist() def apply_price_list(args, as_doc=False): @@ -1046,23 +1238,23 @@ def apply_price_list(args, as_doc=False): :param args: See below :param as_doc: Updates value in the passed dict - args = { - "doctype": "", - "name": "", - "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...], - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "price_list_uom_dependant": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "ignore_pricing_rule": 0/1 - } + args = { + "doctype": "", + "name": "", + "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...], + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "price_list_uom_dependant": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "ignore_pricing_rule": 0/1 + } """ args = process_args(args) @@ -1082,10 +1274,10 @@ def apply_price_list(args, as_doc=False): children.append(item_details) if as_doc: - args.price_list_currency = parent.price_list_currency, + args.price_list_currency = (parent.price_list_currency,) args.plc_conversion_rate = parent.plc_conversion_rate - if args.get('items'): - for i, item in enumerate(args.get('items')): + if args.get("items"): + for i, item in enumerate(args.get("items")): for fieldname in children[i]: # if the field exists in the original doc # update the value @@ -1093,26 +1285,25 @@ def apply_price_list(args, as_doc=False): item[fieldname] = children[i][fieldname] return args else: - return { - "parent": parent, - "children": children - } + return {"parent": parent, "children": children} + def apply_price_list_on_item(args): - item_doc = frappe.db.get_value("Item", args.item_code, ['name', 'variant_of'], as_dict=1) + item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1) item_details = get_price_list_rate(args, item_doc) item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate)) return item_details + def get_price_list_currency_and_exchange_rate(args): if not args.price_list: return {} - if args.doctype in ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']: + if args.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: args.update({"exchange_rate": "for_selling"}) - elif args.doctype in ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']: + elif args.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: args.update({"exchange_rate": "for_buying"}) price_list_details = get_price_list_details(args.price_list) @@ -1123,25 +1314,38 @@ def get_price_list_currency_and_exchange_rate(args): plc_conversion_rate = args.plc_conversion_rate company_currency = get_company_currency(args.company) - if (not plc_conversion_rate) or (price_list_currency and args.price_list_currency \ - and price_list_currency != args.price_list_currency): - # cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate - plc_conversion_rate = get_exchange_rate(price_list_currency, company_currency, - args.transaction_date, args.exchange_rate) or plc_conversion_rate + if (not plc_conversion_rate) or ( + price_list_currency + and args.price_list_currency + and price_list_currency != args.price_list_currency + ): + # cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate + plc_conversion_rate = ( + get_exchange_rate( + price_list_currency, company_currency, args.transaction_date, args.exchange_rate + ) + or plc_conversion_rate + ) + + return frappe._dict( + { + "price_list_currency": price_list_currency, + "price_list_uom_dependant": price_list_uom_dependant, + "plc_conversion_rate": plc_conversion_rate or 1, + } + ) - return frappe._dict({ - "price_list_currency": price_list_currency, - "price_list_uom_dependant": price_list_uom_dependant, - "plc_conversion_rate": plc_conversion_rate or 1 - }) @frappe.whitelist() def get_default_bom(item_code=None): if item_code: - bom = frappe.db.get_value("BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code}) + bom = frappe.db.get_value( + "BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code} + ) if bom: return bom + @frappe.whitelist() def get_valuation_rate(item_code, company, warehouse=None): item = get_item_defaults(item_code, company) @@ -1150,43 +1354,57 @@ def get_valuation_rate(item_code, company, warehouse=None): # item = frappe.get_doc("Item", item_code) if item.get("is_stock_item"): if not warehouse: - warehouse = item.get("default_warehouse") or item_group.get("default_warehouse") or brand.get("default_warehouse") + warehouse = ( + item.get("default_warehouse") + or item_group.get("default_warehouse") + or brand.get("default_warehouse") + ) - return frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - ["valuation_rate"], as_dict=True) or {"valuation_rate": 0} + return frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, ["valuation_rate"], as_dict=True + ) or {"valuation_rate": 0} elif not item.get("is_stock_item"): - valuation_rate =frappe.db.sql("""select sum(base_net_amount) / sum(qty*conversion_factor) + valuation_rate = frappe.db.sql( + """select sum(base_net_amount) / sum(qty*conversion_factor) from `tabPurchase Invoice Item` - where item_code = %s and docstatus=1""", item_code) + where item_code = %s and docstatus=1""", + item_code, + ) if valuation_rate: return {"valuation_rate": valuation_rate[0][0] or 0.0} else: return {"valuation_rate": 0.0} + def get_gross_profit(out): if out.valuation_rate: - out.update({ - "gross_profit": ((out.base_rate - out.valuation_rate) * out.stock_qty) - }) + out.update({"gross_profit": ((out.base_rate - out.valuation_rate) * out.stock_qty)}) return out + @frappe.whitelist() def get_serial_no(args, serial_nos=None, sales_order=None): serial_no = None if isinstance(args, string_types): args = json.loads(args) args = frappe._dict(args) - if args.get('doctype') == 'Sales Invoice' and not args.get('update_stock'): + if args.get("doctype") == "Sales Invoice" and not args.get("update_stock"): return "" - if args.get('warehouse') and args.get('stock_qty') and args.get('item_code'): - has_serial_no = frappe.get_value('Item', {'item_code': args.item_code}, "has_serial_no") - if args.get('batch_no') and has_serial_no == 1: + if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"): + has_serial_no = frappe.get_value("Item", {"item_code": args.item_code}, "has_serial_no") + if args.get("batch_no") and has_serial_no == 1: return get_serial_no_batchwise(args, sales_order) elif has_serial_no == 1: - args = json.dumps({"item_code": args.get('item_code'),"warehouse": args.get('warehouse'),"stock_qty": args.get('stock_qty')}) + args = json.dumps( + { + "item_code": args.get("item_code"), + "warehouse": args.get("warehouse"), + "stock_qty": args.get("stock_qty"), + } + ) args = process_args(args) serial_no = get_serial_nos_by_fifo(args, sales_order) @@ -1203,53 +1421,68 @@ def update_party_blanket_order(args, out): if blanket_order_details: out.update(blanket_order_details) + @frappe.whitelist() def get_blanket_order_details(args): if isinstance(args, string_types): args = frappe._dict(json.loads(args)) blanket_order_details = None - condition = '' + condition = "" if args.item_code: if args.customer and args.doctype == "Sales Order": - condition = ' and bo.customer=%(customer)s' + condition = " and bo.customer=%(customer)s" elif args.supplier and args.doctype == "Purchase Order": - condition = ' and bo.supplier=%(supplier)s' + condition = " and bo.supplier=%(supplier)s" if args.blanket_order: - condition += ' and bo.name =%(blanket_order)s' + condition += " and bo.name =%(blanket_order)s" if args.transaction_date: - condition += ' and bo.to_date>=%(transaction_date)s' + condition += " and bo.to_date>=%(transaction_date)s" - blanket_order_details = frappe.db.sql(''' + blanket_order_details = frappe.db.sql( + """ select boi.rate as blanket_order_rate, bo.name as blanket_order from `tabBlanket Order` bo, `tabBlanket Order Item` boi where bo.company=%(company)s and boi.item_code=%(item_code)s and bo.docstatus=1 and bo.name = boi.parent {0} - '''.format(condition), args, as_dict=True) + """.format( + condition + ), + args, + as_dict=True, + ) - blanket_order_details = blanket_order_details[0] if blanket_order_details else '' + blanket_order_details = blanket_order_details[0] if blanket_order_details else "" return blanket_order_details + def get_so_reservation_for_item(args): reserved_so = None - if args.get('against_sales_order'): - if get_reserved_qty_for_so(args.get('against_sales_order'), args.get('item_code')): - reserved_so = args.get('against_sales_order') - elif args.get('against_sales_invoice'): - sales_order = frappe.db.sql("""select sales_order from `tabSales Invoice Item` where - parent=%s and item_code=%s""", (args.get('against_sales_invoice'), args.get('item_code'))) + if args.get("against_sales_order"): + if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")): + reserved_so = args.get("against_sales_order") + elif args.get("against_sales_invoice"): + sales_order = frappe.db.sql( + """select sales_order from `tabSales Invoice Item` where + parent=%s and item_code=%s""", + (args.get("against_sales_invoice"), args.get("item_code")), + ) if sales_order and sales_order[0]: - if get_reserved_qty_for_so(sales_order[0][0], args.get('item_code')): + if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")): reserved_so = sales_order[0] elif args.get("sales_order"): - if get_reserved_qty_for_so(args.get('sales_order'), args.get('item_code')): - reserved_so = args.get('sales_order') + if get_reserved_qty_for_so(args.get("sales_order"), args.get("item_code")): + reserved_so = args.get("sales_order") return reserved_so + def get_reserved_qty_for_so(sales_order, item_code): - reserved_qty = frappe.db.sql("""select sum(qty) from `tabSales Order Item` + reserved_qty = frappe.db.sql( + """select sum(qty) from `tabSales Order Item` where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1 - """, (sales_order, item_code)) + """, + (sales_order, item_code), + ) if reserved_qty and reserved_qty[0][0]: return reserved_qty[0][0] else: diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 21f2573a279..a96ffefd474 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -13,22 +13,29 @@ import erpnext def reorder_item(): - """ Reorder item if stock reaches reorder level""" + """Reorder item if stock reaches reorder level""" # if initial setup not completed, return if not (frappe.db.a_row_exists("Company") and frappe.db.a_row_exists("Fiscal Year")): return - if cint(frappe.db.get_value('Stock Settings', None, 'auto_indent')): + if cint(frappe.db.get_value("Stock Settings", None, "auto_indent")): return _reorder_item() + def _reorder_item(): material_requests = {"Purchase": {}, "Transfer": {}, "Material Issue": {}, "Manufacture": {}} - warehouse_company = frappe._dict(frappe.db.sql("""select name, company from `tabWarehouse` - where disabled=0""")) - default_company = (erpnext.get_default_company() or - frappe.db.sql("""select name from tabCompany limit 1""")[0][0]) + warehouse_company = frappe._dict( + frappe.db.sql( + """select name, company from `tabWarehouse` + where disabled=0""" + ) + ) + default_company = ( + erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] + ) - items_to_consider = frappe.db.sql_list("""select name from `tabItem` item + items_to_consider = frappe.db.sql_list( + """select name from `tabItem` item where is_stock_item=1 and has_variants=0 and disabled=0 and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s) @@ -36,14 +43,17 @@ def _reorder_item(): or (variant_of is not null and variant_of != '' and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of)) )""", - {"today": nowdate()}) + {"today": nowdate()}, + ) if not items_to_consider: return item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider) - def add_to_material_request(item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None): + def add_to_material_request( + item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None + ): if warehouse not in warehouse_company: # a disabled warehouse return @@ -64,11 +74,9 @@ def _reorder_item(): company = warehouse_company.get(warehouse) or default_company - material_requests[material_request_type].setdefault(company, []).append({ - "item_code": item_code, - "warehouse": warehouse, - "reorder_qty": reorder_qty - }) + material_requests[material_request_type].setdefault(company, []).append( + {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty} + ) for item_code in items_to_consider: item = frappe.get_doc("Item", item_code) @@ -78,19 +86,30 @@ def _reorder_item(): if item.get("reorder_levels"): for d in item.get("reorder_levels"): - add_to_material_request(item_code, d.warehouse, d.warehouse_reorder_level, - d.warehouse_reorder_qty, d.material_request_type, warehouse_group=d.warehouse_group) + add_to_material_request( + item_code, + d.warehouse, + d.warehouse_reorder_level, + d.warehouse_reorder_qty, + d.material_request_type, + warehouse_group=d.warehouse_group, + ) if material_requests: return create_material_request(material_requests) + def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} - for item_code, warehouse, projected_qty in frappe.db.sql("""select item_code, warehouse, projected_qty + for item_code, warehouse, projected_qty in frappe.db.sql( + """select item_code, warehouse, projected_qty from tabBin where item_code in ({0}) - and (warehouse != "" and warehouse is not null)"""\ - .format(", ".join(["%s"] * len(items_to_consider))), items_to_consider): + and (warehouse != "" and warehouse is not null)""".format( + ", ".join(["%s"] * len(items_to_consider)) + ), + items_to_consider, + ): if item_code not in item_warehouse_projected_qty: item_warehouse_projected_qty.setdefault(item_code, {}) @@ -102,15 +121,18 @@ def get_item_warehouse_projected_qty(items_to_consider): while warehouse_doc.parent_warehouse: if not item_warehouse_projected_qty.get(item_code, {}).get(warehouse_doc.parent_warehouse): - item_warehouse_projected_qty.setdefault(item_code, {})[warehouse_doc.parent_warehouse] = flt(projected_qty) + item_warehouse_projected_qty.setdefault(item_code, {})[warehouse_doc.parent_warehouse] = flt( + projected_qty + ) else: item_warehouse_projected_qty[item_code][warehouse_doc.parent_warehouse] += flt(projected_qty) warehouse_doc = frappe.get_doc("Warehouse", warehouse_doc.parent_warehouse) return item_warehouse_projected_qty + def create_material_request(material_requests): - """ Create indent on reaching reorder level """ + """Create indent on reaching reorder level""" mr_list = [] exceptions_list = [] @@ -131,11 +153,13 @@ def create_material_request(material_requests): continue mr = frappe.new_doc("Material Request") - mr.update({ - "company": company, - "transaction_date": nowdate(), - "material_request_type": "Material Transfer" if request_type=="Transfer" else request_type - }) + mr.update( + { + "company": company, + "transaction_date": nowdate(), + "material_request_type": "Material Transfer" if request_type == "Transfer" else request_type, + } + ) for d in items: d = frappe._dict(d) @@ -143,30 +167,37 @@ def create_material_request(material_requests): uom = item.stock_uom conversion_factor = 1.0 - if request_type == 'Purchase': + if request_type == "Purchase": uom = item.purchase_uom or item.stock_uom if uom != item.stock_uom: - conversion_factor = frappe.db.get_value("UOM Conversion Detail", - {'parent': item.name, 'uom': uom}, 'conversion_factor') or 1.0 + conversion_factor = ( + frappe.db.get_value( + "UOM Conversion Detail", {"parent": item.name, "uom": uom}, "conversion_factor" + ) + or 1.0 + ) must_be_whole_number = frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) qty = d.reorder_qty / conversion_factor if must_be_whole_number: qty = ceil(qty) - mr.append("items", { - "doctype": "Material Request Item", - "item_code": d.item_code, - "schedule_date": add_days(nowdate(),cint(item.lead_time_days)), - "qty": qty, - "uom": uom, - "stock_uom": item.stock_uom, - "warehouse": d.warehouse, - "item_name": item.item_name, - "description": item.description, - "item_group": item.item_group, - "brand": item.brand, - }) + mr.append( + "items", + { + "doctype": "Material Request Item", + "item_code": d.item_code, + "schedule_date": add_days(nowdate(), cint(item.lead_time_days)), + "qty": qty, + "uom": uom, + "stock_uom": item.stock_uom, + "warehouse": d.warehouse, + "item_name": item.item_name, + "description": item.description, + "item_group": item.item_group, + "brand": item.brand, + }, + ) schedule_dates = [d.schedule_date for d in mr.items] mr.schedule_date = max(schedule_dates or [nowdate()]) @@ -180,10 +211,11 @@ def create_material_request(material_requests): if mr_list: if getattr(frappe.local, "reorder_email_notify", None) is None: - frappe.local.reorder_email_notify = cint(frappe.db.get_value('Stock Settings', None, - 'reorder_email_notify')) + frappe.local.reorder_email_notify = cint( + frappe.db.get_value("Stock Settings", None, "reorder_email_notify") + ) - if(frappe.local.reorder_email_notify): + if frappe.local.reorder_email_notify: send_email_notification(mr_list) if exceptions_list: @@ -191,33 +223,44 @@ def create_material_request(material_requests): return mr_list -def send_email_notification(mr_list): - """ Notify user about auto creation of indent""" - email_list = frappe.db.sql_list("""select distinct r.parent +def send_email_notification(mr_list): + """Notify user about auto creation of indent""" + + email_list = frappe.db.sql_list( + """select distinct r.parent from `tabHas Role` r, tabUser p where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 and r.role in ('Purchase Manager','Stock Manager') - and p.name not in ('Administrator', 'All', 'Guest')""") + and p.name not in ('Administrator', 'All', 'Guest')""" + ) - msg = frappe.render_template("templates/emails/reorder_item.html", { - "mr_list": mr_list - }) + msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + + frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg) - frappe.sendmail(recipients=email_list, - subject=_('Auto Material Requests Generated'), message = msg) def notify_errors(exceptions_list): subject = _("[Important] [ERPNext] Auto Reorder Errors") - content = _("Dear System Manager,") + "
    " + _("An error occured for certain Items while creating Material Requests based on Re-order level. \ - Please rectify these issues :") + "
    " + content = ( + _("Dear System Manager,") + + "
    " + + _( + "An error occured for certain Items while creating Material Requests based on Re-order level. \ + Please rectify these issues :" + ) + + "
    " + ) for exception in exceptions_list: exception = json.loads(exception) - error_message = """
    {0}

    """.format(_(exception.get("message"))) + error_message = """
    {0}

    """.format( + _(exception.get("message")) + ) content += error_message content += _("Regards,") + "
    " + _("Administrator") from frappe.email import sendmail_to_system_managers + sendmail_to_system_managers(subject, content) diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index 87097c72fa4..3d9b0461977 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -8,7 +8,8 @@ from frappe.utils import cint, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} float_precision = cint(frappe.db.get_default("float_precision")) or 3 @@ -22,22 +23,37 @@ def execute(filters=None): for batch in sorted(iwb_map[item][wh]): qty_dict = iwb_map[item][wh][batch] - data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, - frappe.db.get_value('Batch', batch, 'expiry_date'), qty_dict.expiry_status - ]) - + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["description"], + wh, + batch, + frappe.db.get_value("Batch", batch, "expiry_date"), + qty_dict.expiry_status, + ] + ) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \ - [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Expires On") + ":Date:90"] + \ - [_("Expiry (In Days)") + ":Int:120"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::150"] + + [_("Description") + "::150"] + + [_("Warehouse") + ":Link/Warehouse:100"] + + [_("Batch") + ":Link/Batch:100"] + + [_("Expires On") + ":Date:90"] + + [_("Expiry (In Days)") + ":Int:120"] + ) return columns + def get_conditions(filters): conditions = "" if not filters.get("from_date"): @@ -50,14 +66,19 @@ def get_conditions(filters): return conditions + def get_stock_ledger_entries(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select item_code, batch_no, warehouse, + return frappe.db.sql( + """select item_code, batch_no, warehouse, posting_date, actual_qty from `tabStock Ledger Entry` where is_cancelled = 0 - and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % - conditions, as_dict=1) + and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" + % conditions, + as_dict=1, + ) + def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) @@ -67,13 +88,13 @@ def get_item_warehouse_batch_map(filters, float_precision): to_date = getdate(filters["to_date"]) for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {})\ - .setdefault(d.batch_no, frappe._dict({ - "expires_on": None, "expiry_status": None})) + iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( + d.batch_no, frappe._dict({"expires_on": None, "expiry_status": None}) + ) qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] - expiry_date_unicode = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') + expiry_date_unicode = frappe.db.get_value("Batch", d.batch_no, "expiry_date") qty_dict.expires_on = expiry_date_unicode exp_date = frappe.utils.data.getdate(expiry_date_unicode) @@ -88,6 +109,7 @@ def get_item_warehouse_batch_map(filters, float_precision): return iwb_map + def get_item_details(filters): item_map = {} for d in frappe.db.sql("select name, item_name, description from tabItem", as_dict=1): diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 9b21deabcd4..8a13300dc83 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -8,7 +8,8 @@ from frappe.utils import cint, flt, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) @@ -26,11 +27,20 @@ def execute(filters=None): for batch in sorted(iwb_map[item][wh]): qty_dict = iwb_map[item][wh][batch] if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty: - data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, - flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision), - flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision), - item_map[item]["stock_uom"] - ]) + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["description"], + wh, + batch, + flt(qty_dict.opening_qty, float_precision), + flt(qty_dict.in_qty, float_precision), + flt(qty_dict.out_qty, float_precision), + flt(qty_dict.bal_qty, float_precision), + item_map[item]["stock_uom"], + ] + ) return columns, data @@ -38,10 +48,18 @@ def execute(filters=None): def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \ - [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \ - [_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \ - [_("UOM") + "::90"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::150"] + + [_("Description") + "::150"] + + [_("Warehouse") + ":Link/Warehouse:100"] + + [_("Batch") + ":Link/Batch:100"] + + [_("Opening Qty") + ":Float:90"] + + [_("In Qty") + ":Float:80"] + + [_("Out Qty") + ":Float:80"] + + [_("Balance Qty") + ":Float:90"] + + [_("UOM") + "::90"] + ) return columns @@ -66,13 +84,16 @@ def get_conditions(filters): # get all details def get_stock_ledger_entries(filters): conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty from `tabStock Ledger Entry` where is_cancelled = 0 and docstatus < 2 and ifnull(batch_no, '') != '' %s group by voucher_no, batch_no, item_code, warehouse - order by item_code, warehouse""" % - conditions, as_dict=1) + order by item_code, warehouse""" + % conditions, + as_dict=1, + ) def get_item_warehouse_batch_map(filters, float_precision): @@ -83,20 +104,21 @@ def get_item_warehouse_batch_map(filters, float_precision): to_date = getdate(filters["to_date"]) for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {})\ - .setdefault(d.batch_no, frappe._dict({ - "opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0 - })) + iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( + d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0}) + ) qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] if d.posting_date < from_date: - qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) \ - + flt(d.actual_qty, float_precision) + qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) + flt( + d.actual_qty, float_precision + ) elif d.posting_date >= from_date and d.posting_date <= to_date: if flt(d.actual_qty) > 0: qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt(d.actual_qty, float_precision) else: - qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) \ - + abs(flt(d.actual_qty, float_precision)) + qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) + abs( + flt(d.actual_qty, float_precision) + ) qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision) diff --git a/erpnext/stock/report/bom_search/bom_search.py b/erpnext/stock/report/bom_search/bom_search.py index 960396d5345..ec5da6236d6 100644 --- a/erpnext/stock/report/bom_search/bom_search.py +++ b/erpnext/stock/report/bom_search/bom_search.py @@ -11,11 +11,13 @@ def execute(filters=None): parents = { "Product Bundle Item": "Product Bundle", "BOM Explosion Item": "BOM", - "BOM Item": "BOM" + "BOM Item": "BOM", } - for doctype in ("Product Bundle Item", - "BOM Explosion Item" if filters.search_sub_assemblies else "BOM Item"): + for doctype in ( + "Product Bundle Item", + "BOM Explosion Item" if filters.search_sub_assemblies else "BOM Item", + ): all_boms = {} for d in frappe.get_all(doctype, fields=["parent", "item_code"]): all_boms.setdefault(d.parent, []).append(d.item_code) @@ -30,16 +32,13 @@ def execute(filters=None): if valid: data.append((parent, parents[doctype])) - return [{ - "fieldname": "parent", - "label": "BOM", - "width": 200, - "fieldtype": "Dynamic Link", - "options": "doctype" - }, - { - "fieldname": "doctype", - "label": "Type", - "width": 200, - "fieldtype": "Data" - }], data + return [ + { + "fieldname": "parent", + "label": "BOM", + "width": 200, + "fieldtype": "Dynamic Link", + "options": "doctype", + }, + {"fieldname": "doctype", "label": "Type", "width": 200, "fieldtype": "Data"}, + ], data diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py index 058af77aa21..4642a535b63 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -41,18 +41,8 @@ def validate_filters(filters: Filters) -> None: def get_columns() -> Columns: return [ - { - 'label': _('Item Group'), - 'fieldname': 'item_group', - 'fieldtype': 'Data', - 'width': '200' - }, - { - 'label': _('COGS Debit'), - 'fieldname': 'cogs_debit', - 'fieldtype': 'Currency', - 'width': '200' - } + {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Data", "width": "200"}, + {"label": _("COGS Debit"), "fieldname": "cogs_debit", "fieldtype": "Currency", "width": "200"}, ] @@ -67,11 +57,11 @@ def get_data(filters: Filters) -> Data: data = [] for item in leveled_dict.items(): i = item[1] - if i['agg_value'] == 0: + if i["agg_value"] == 0: continue - data.append(get_row(i['name'], i['agg_value'], i['is_group'], i['level'])) - if i['self_value'] < i['agg_value'] and i['self_value'] > 0: - data.append(get_row(i['name'], i['self_value'], 0, i['level'] + 1)) + data.append(get_row(i["name"], i["agg_value"], i["is_group"], i["level"])) + if i["self_value"] < i["agg_value"] and i["self_value"] > 0: + data.append(get_row(i["name"], i["self_value"], 0, i["level"] + 1)) return data @@ -79,8 +69,8 @@ def get_filtered_entries(filters: Filters) -> FilteredEntries: gl_entries = get_gl_entries(filters, []) filtered_entries = [] for entry in gl_entries: - posting_date = entry.get('posting_date') - from_date = filters.get('from_date') + posting_date = entry.get("posting_date") + from_date = filters.get("from_date") if date_diff(from_date, posting_date) > 0: continue filtered_entries.append(entry) @@ -88,10 +78,11 @@ def get_filtered_entries(filters: Filters) -> FilteredEntries: def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDList: - voucher_nos = [fe.get('voucher_no') for fe in filtered_entries] + voucher_nos = [fe.get("voucher_no") for fe in filtered_entries] svd_list = frappe.get_list( - 'Stock Ledger Entry', fields=['item_code','stock_value_difference'], - filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)] + "Stock Ledger Entry", + fields=["item_code", "stock_value_difference"], + filters=[("voucher_no", "in", voucher_nos), ("is_cancelled", "=", 0)], ) assign_item_groups_to_svd_list(svd_list) return svd_list @@ -99,7 +90,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis def get_leveled_dict() -> OrderedDict: item_groups_dict = get_item_groups_dict() - lr_list = sorted(item_groups_dict, key=lambda x : int(x[0])) + lr_list = sorted(item_groups_dict, key=lambda x: int(x[0])) leveled_dict = OrderedDict() current_level = 0 nesting_r = [] @@ -108,10 +99,10 @@ def get_leveled_dict() -> OrderedDict: nesting_r.pop() current_level -= 1 - leveled_dict[(l,r)] = { - 'level' : current_level, - 'name' : item_groups_dict[(l,r)]['name'], - 'is_group' : item_groups_dict[(l,r)]['is_group'] + leveled_dict[(l, r)] = { + "level": current_level, + "name": item_groups_dict[(l, r)]["name"], + "is_group": item_groups_dict[(l, r)]["is_group"], } if int(r) - int(l) > 1: @@ -123,38 +114,38 @@ def get_leveled_dict() -> OrderedDict: def assign_self_values(leveled_dict: OrderedDict, svd_list: SVDList) -> None: - key_dict = {v['name']:k for k, v in leveled_dict.items()} + key_dict = {v["name"]: k for k, v in leveled_dict.items()} for item in svd_list: key = key_dict[item.get("item_group")] - leveled_dict[key]['self_value'] += -item.get("stock_value_difference") + leveled_dict[key]["self_value"] += -item.get("stock_value_difference") def assign_agg_values(leveled_dict: OrderedDict) -> None: keys = list(leveled_dict.keys())[::-1] - prev_level = leveled_dict[keys[-1]]['level'] + prev_level = leveled_dict[keys[-1]]["level"] accu = [0] for k in keys[:-1]: - curr_level = leveled_dict[k]['level'] + curr_level = leveled_dict[k]["level"] if curr_level == prev_level: - accu[-1] += leveled_dict[k]['self_value'] - leveled_dict[k]['agg_value'] = leveled_dict[k]['self_value'] + accu[-1] += leveled_dict[k]["self_value"] + leveled_dict[k]["agg_value"] = leveled_dict[k]["self_value"] elif curr_level > prev_level: - accu.append(leveled_dict[k]['self_value']) - leveled_dict[k]['agg_value'] = accu[-1] + accu.append(leveled_dict[k]["self_value"]) + leveled_dict[k]["agg_value"] = accu[-1] elif curr_level < prev_level: - accu[-1] += leveled_dict[k]['self_value'] - leveled_dict[k]['agg_value'] = accu[-1] + accu[-1] += leveled_dict[k]["self_value"] + leveled_dict[k]["agg_value"] = accu[-1] prev_level = curr_level # root node rk = keys[-1] - leveled_dict[rk]['agg_value'] = sum(accu) + leveled_dict[rk]['self_value'] + leveled_dict[rk]["agg_value"] = sum(accu) + leveled_dict[rk]["self_value"] -def get_row(name:str, value:float, is_bold:int, indent:int) -> Row: +def get_row(name: str, value: float, is_bold: int, indent: int) -> Row: item_group = name if is_bold: item_group = frappe.bold(item_group) @@ -168,20 +159,20 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None: def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]: - item_codes = set(i['item_code'] for i in svd_list) + item_codes = set(i["item_code"] for i in svd_list) ig_list = frappe.get_list( - 'Item', fields=['item_code','item_group'], - filters=[('item_code', 'in', item_codes)] + "Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)] ) - return {i['item_code']:i['item_group'] for i in ig_list} + return {i["item_code"]: i["item_group"] for i in ig_list} def get_item_groups_dict() -> ItemGroupsDict: item_groups_list = frappe.get_all("Item Group", fields=("name", "is_group", "lft", "rgt")) - return {(i['lft'],i['rgt']):{'name':i['name'], 'is_group':i['is_group']} - for i in item_groups_list} + return { + (i["lft"], i["rgt"]): {"name": i["name"], "is_group": i["is_group"]} for i in item_groups_list + } def update_leveled_dict(leveled_dict: OrderedDict) -> None: for k in leveled_dict: - leveled_dict[k].update({'self_value':0, 'agg_value':0}) + leveled_dict[k].update({"self_value": 0, "agg_value": 0}) diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 4ec36ea417f..9df24d65596 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -7,11 +7,12 @@ from frappe import _ from frappe.utils import date_diff -def execute(filters=None, consolidated = False): +def execute(filters=None, consolidated=False): data, columns = DelayedItemReport(filters).run() return data, columns + class DelayedItemReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -23,28 +24,38 @@ class DelayedItemReport(object): conditions = "" doctype = self.filters.get("based_on") - child_doc= "%s Item" % doctype + child_doc = "%s Item" % doctype if doctype == "Sales Invoice": conditions = " and `tabSales Invoice`.update_stock = 1 and `tabSales Invoice`.is_pos = 0" if self.filters.get("item_group"): - conditions += " and `tab%s`.item_group = %s" % (child_doc, - frappe.db.escape(self.filters.get("item_group"))) + conditions += " and `tab%s`.item_group = %s" % ( + child_doc, + frappe.db.escape(self.filters.get("item_group")), + ) for field in ["customer", "customer_group", "company"]: if self.filters.get(field): - conditions += " and `tab%s`.%s = %s" % (doctype, - field, frappe.db.escape(self.filters.get(field))) + conditions += " and `tab%s`.%s = %s" % ( + doctype, + field, + frappe.db.escape(self.filters.get(field)), + ) sales_order_field = "against_sales_order" if doctype == "Sales Invoice": sales_order_field = "sales_order" if self.filters.get("sales_order"): - conditions = " and `tab%s`.%s = '%s'" %(child_doc, sales_order_field, self.filters.get("sales_order")) + conditions = " and `tab%s`.%s = '%s'" % ( + child_doc, + sales_order_field, + self.filters.get("sales_order"), + ) - self.transactions = frappe.db.sql(""" SELECT `tab{child_doc}`.item_code, `tab{child_doc}`.item_name, + self.transactions = frappe.db.sql( + """ SELECT `tab{child_doc}`.item_code, `tab{child_doc}`.item_name, `tab{child_doc}`.item_group, `tab{child_doc}`.qty, `tab{child_doc}`.rate, `tab{child_doc}`.amount, `tab{child_doc}`.so_detail, `tab{child_doc}`.{so_field} as sales_order, `tab{doctype}`.shipping_address_name, `tab{doctype}`.po_no, `tab{doctype}`.customer, @@ -54,10 +65,12 @@ class DelayedItemReport(object): `tab{child_doc}`.parent = `tab{doctype}`.name and `tab{doctype}`.docstatus = 1 and `tab{doctype}`.posting_date between %(from_date)s and %(to_date)s and `tab{child_doc}`.{so_field} is not null and `tab{child_doc}`.{so_field} != '' {cond} - """.format(cond=conditions, doctype=doctype, child_doc=child_doc, so_field=sales_order_field), { - 'from_date': self.filters.get('from_date'), - 'to_date': self.filters.get('to_date') - }, as_dict=1) + """.format( + cond=conditions, doctype=doctype, child_doc=child_doc, so_field=sales_order_field + ), + {"from_date": self.filters.get("from_date"), "to_date": self.filters.get("to_date")}, + as_dict=1, + ) if self.transactions: self.filter_transactions_data(consolidated) @@ -67,112 +80,85 @@ class DelayedItemReport(object): def filter_transactions_data(self, consolidated=False): sales_orders = [d.sales_order for d in self.transactions] doctype = "Sales Order" - filters = {'name': ('in', sales_orders)} + filters = {"name": ("in", sales_orders)} if not consolidated: sales_order_items = [d.so_detail for d in self.transactions] doctype = "Sales Order Item" - filters = {'parent': ('in', sales_orders), 'name': ('in', sales_order_items)} + filters = {"parent": ("in", sales_orders), "name": ("in", sales_order_items)} so_data = {} - for d in frappe.get_all(doctype, filters = filters, - fields = ["delivery_date", "parent", "name"]): + for d in frappe.get_all(doctype, filters=filters, fields=["delivery_date", "parent", "name"]): key = d.name if consolidated else (d.parent, d.name) if key not in so_data: so_data.setdefault(key, d.delivery_date) for row in self.transactions: key = row.sales_order if consolidated else (row.sales_order, row.so_detail) - row.update({ - 'delivery_date': so_data.get(key), - 'delayed_days': date_diff(row.posting_date, so_data.get(key)) - }) + row.update( + { + "delivery_date": so_data.get(key), + "delayed_days": date_diff(row.posting_date, so_data.get(key)), + } + ) return self.transactions def get_columns(self): based_on = self.filters.get("based_on") - return [{ - "label": _(based_on), - "fieldname": "name", - "fieldtype": "Link", - "options": based_on, - "width": 100 - }, - { - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Link", - "options": "Customer", - "width": 200 - }, - { - "label": _("Shipping Address"), - "fieldname": "shipping_address_name", - "fieldtype": "Link", - "options": "Address", - "width": 120 - }, - { - "label": _("Expected Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Actual Delivery Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Quantity"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Delayed Days"), - "fieldname": "delayed_days", - "fieldtype": "Int", - "width": 100 - }, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 100 - }, - { - "label": _("Customer PO"), - "fieldname": "po_no", - "fieldtype": "Data", - "width": 100 - }] + return [ + { + "label": _(based_on), + "fieldname": "name", + "fieldtype": "Link", + "options": based_on, + "width": 100, + }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 200, + }, + { + "label": _("Shipping Address"), + "fieldname": "shipping_address_name", + "fieldtype": "Link", + "options": "Address", + "width": 120, + }, + { + "label": _("Expected Delivery Date"), + "fieldname": "delivery_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Actual Delivery Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 100}, + {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 100}, + {"label": _("Delayed Days"), "fieldname": "delayed_days", "fieldtype": "Int", "width": 100}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 100, + }, + {"label": _("Customer PO"), "fieldname": "po_no", "fieldtype": "Data", "width": 100}, + ] diff --git a/erpnext/stock/report/delayed_order_report/delayed_order_report.py b/erpnext/stock/report/delayed_order_report/delayed_order_report.py index 26090ab8ffb..197218d7ff4 100644 --- a/erpnext/stock/report/delayed_order_report/delayed_order_report.py +++ b/erpnext/stock/report/delayed_order_report/delayed_order_report.py @@ -14,6 +14,7 @@ def execute(filters=None): return columns, data + class DelayedOrderReport(DelayedItemReport): def run(self): return self.get_columns(), self.get_data(consolidated=True) or [] @@ -33,60 +34,48 @@ class DelayedOrderReport(DelayedItemReport): def get_columns(self): based_on = self.filters.get("based_on") - return [{ - "label": _(based_on), - "fieldname": "name", - "fieldtype": "Link", - "options": based_on, - "width": 100 - },{ - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Link", - "options": "Customer", - "width": 200 - }, - { - "label": _("Shipping Address"), - "fieldname": "shipping_address_name", - "fieldtype": "Link", - "options": "Address", - "width": 140 - }, - { - "label": _("Expected Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Actual Delivery Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "grand_total", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Delayed Days"), - "fieldname": "delayed_days", - "fieldtype": "Int", - "width": 100 - }, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 150 - }, - { - "label": _("Customer PO"), - "fieldname": "po_no", - "fieldtype": "Data", - "width": 110 - }] + return [ + { + "label": _(based_on), + "fieldname": "name", + "fieldtype": "Link", + "options": based_on, + "width": 100, + }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 200, + }, + { + "label": _("Shipping Address"), + "fieldname": "shipping_address_name", + "fieldtype": "Link", + "options": "Address", + "width": 140, + }, + { + "label": _("Expected Delivery Date"), + "fieldname": "delivery_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Actual Delivery Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + {"label": _("Amount"), "fieldname": "grand_total", "fieldtype": "Currency", "width": 100}, + {"label": _("Delayed Days"), "fieldname": "delayed_days", "fieldtype": "Int", "width": 100}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 150, + }, + {"label": _("Customer PO"), "fieldname": "po_no", "fieldtype": "Data", "width": 110}, + ] diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py index b7ac7ff6a53..7a1b8c0cee9 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Delivery Note") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, filters): if not data: return [] @@ -27,7 +29,7 @@ def get_chart_data(data, filters): # consider only consolidated row data = [row for row in data if row[0]] - data = sorted(data, key = lambda i: i[-1],reverse=True) + data = sorted(data, key=lambda i: i[-1], reverse=True) if len(data) > 10: # get top 10 if data too long @@ -39,13 +41,8 @@ def get_chart_data(data, filters): return { "data": { - "labels" : labels, - "datasets" : [ - { - "name": _("Total Delivered Amount"), - "values": datapoints - } - ] + "labels": labels, + "datasets": [{"name": _("Total Delivered Amount"), "values": datapoints}], }, - "type" : "bar" + "type": "bar", } diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py index 7d7e9644854..af91b248b8c 100644 --- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py @@ -13,6 +13,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_data(filters): data = get_stock_ledger_entries(filters) itewise_balance_qty = {} @@ -24,6 +25,7 @@ def get_data(filters): res = validate_data(itewise_balance_qty) return res + def validate_data(itewise_balance_qty): res = [] for key, data in iteritems(itewise_balance_qty): @@ -34,6 +36,7 @@ def validate_data(itewise_balance_qty): return res + def get_incorrect_data(data): balance_qty = 0.0 for row in data: @@ -46,67 +49,84 @@ def get_incorrect_data(data): row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) return row + def get_stock_ledger_entries(report_filters): filters = {"is_cancelled": 0} - fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty', - 'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no'] + fields = [ + "name", + "voucher_type", + "voucher_no", + "item_code", + "actual_qty", + "posting_date", + "posting_time", + "company", + "warehouse", + "qty_after_transaction", + "batch_no", + ] - for field in ['warehouse', 'item_code', 'company']: + for field in ["warehouse", "item_code", "company"]: if report_filters.get(field): filters[field] = report_filters.get(field) - return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, - order_by = 'timestamp(posting_date, posting_time) asc, creation asc') + return frappe.get_all( + "Stock Ledger Entry", + fields=fields, + filters=filters, + order_by="timestamp(posting_date, posting_time) asc, creation asc", + ) + def get_columns(): - return [{ - 'label': _('Id'), - 'fieldtype': 'Link', - 'fieldname': 'name', - 'options': 'Stock Ledger Entry', - 'width': 120 - }, { - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date', - 'width': 110 - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 120 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 120 - }, { - 'label': _('Item Code'), - 'fieldtype': 'Link', - 'fieldname': 'item_code', - 'options': 'Item', - 'width': 120 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 120 - }, { - 'label': _('Expected Balance Qty'), - 'fieldtype': 'Float', - 'fieldname': 'expected_balance_qty', - 'width': 170 - }, { - 'label': _('Actual Balance Qty'), - 'fieldtype': 'Float', - 'fieldname': 'qty_after_transaction', - 'width': 150 - }, { - 'label': _('Difference'), - 'fieldtype': 'Float', - 'fieldname': 'differnce', - 'width': 110 - }] + return [ + { + "label": _("Id"), + "fieldtype": "Link", + "fieldname": "name", + "options": "Stock Ledger Entry", + "width": 120, + }, + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 110}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 120, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 120, + }, + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 120, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("Expected Balance Qty"), + "fieldtype": "Float", + "fieldname": "expected_balance_qty", + "width": 170, + }, + { + "label": _("Actual Balance Qty"), + "fieldtype": "Float", + "fieldname": "qty_after_transaction", + "width": 150, + }, + {"label": _("Difference"), "fieldtype": "Float", "fieldname": "differnce", "width": 110}, + ] diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py index e4ea9947166..26892407d99 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -16,6 +16,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_data(filters): data = get_stock_ledger_entries(filters) serial_nos_data = prepare_serial_nos(data) @@ -23,6 +24,7 @@ def get_data(filters): return data + def prepare_serial_nos(data): serial_no_wise_data = {} for row in data: @@ -38,13 +40,16 @@ def prepare_serial_nos(data): return serial_no_wise_data + def get_incorrect_serial_nos(serial_nos_data): result = [] - total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))}) + total_value = frappe._dict( + {"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Balance"))} + ) for serial_no, data in iteritems(serial_nos_data): - total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))}) + total_dict = frappe._dict({"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Total"))}) if check_incorrect_serial_data(data, total_dict): result.extend(data) @@ -59,93 +64,111 @@ def get_incorrect_serial_nos(serial_nos_data): return result + def check_incorrect_serial_data(data, total_dict): incorrect_data = False for row in data: total_dict.qty += row.qty total_dict.valuation_rate += row.valuation_rate - if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0): + if (total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0: incorrect_data = True return incorrect_data + def get_stock_ledger_entries(report_filters): - fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty', - 'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate'] + fields = [ + "name", + "voucher_type", + "voucher_no", + "item_code", + "serial_no as serial_nos", + "actual_qty", + "posting_date", + "posting_time", + "company", + "warehouse", + "(stock_value_difference / actual_qty) as valuation_rate", + ] - filters = {'serial_no': ("is", "set"), "is_cancelled": 0} + filters = {"serial_no": ("is", "set"), "is_cancelled": 0} - if report_filters.get('item_code'): - filters['item_code'] = report_filters.get('item_code') + if report_filters.get("item_code"): + filters["item_code"] = report_filters.get("item_code") - if report_filters.get('from_date') and report_filters.get('to_date'): - filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')]) + if report_filters.get("from_date") and report_filters.get("to_date"): + filters["posting_date"] = ( + "between", + [report_filters.get("from_date"), report_filters.get("to_date")], + ) + + return frappe.get_all( + "Stock Ledger Entry", + fields=fields, + filters=filters, + order_by="timestamp(posting_date, posting_time) asc, creation asc", + ) - return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, - order_by = 'timestamp(posting_date, posting_time) asc, creation asc') def get_columns(): - return [{ - 'label': _('Company'), - 'fieldtype': 'Link', - 'fieldname': 'company', - 'options': 'Company', - 'width': 120 - }, { - 'label': _('Id'), - 'fieldtype': 'Link', - 'fieldname': 'name', - 'options': 'Stock Ledger Entry', - 'width': 120 - }, { - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date', - 'width': 90 - }, { - 'label': _('Posting Time'), - 'fieldtype': 'Time', - 'fieldname': 'posting_time', - 'width': 90 - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 100 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 110 - }, { - 'label': _('Item Code'), - 'fieldtype': 'Link', - 'fieldname': 'item_code', - 'options': 'Item', - 'width': 120 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 120 - }, { - 'label': _('Serial No'), - 'fieldtype': 'Link', - 'fieldname': 'serial_no', - 'options': 'Serial No', - 'width': 100 - }, { - 'label': _('Qty'), - 'fieldtype': 'Float', - 'fieldname': 'qty', - 'width': 80 - }, { - 'label': _('Valuation Rate (In / Out)'), - 'fieldtype': 'Currency', - 'fieldname': 'valuation_rate', - 'width': 110 - }] + return [ + { + "label": _("Company"), + "fieldtype": "Link", + "fieldname": "company", + "options": "Company", + "width": 120, + }, + { + "label": _("Id"), + "fieldtype": "Link", + "fieldname": "name", + "options": "Stock Ledger Entry", + "width": 120, + }, + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 90}, + {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 100, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 110, + }, + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 120, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("Serial No"), + "fieldtype": "Link", + "fieldname": "serial_no", + "options": "Serial No", + "width": 100, + }, + {"label": _("Qty"), "fieldtype": "Float", "fieldname": "qty", "width": 80}, + { + "label": _("Valuation Rate (In / Out)"), + "fieldtype": "Currency", + "fieldname": "valuation_rate", + "width": 110, + }, + ] diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index 7cce4a7d22e..15f211127d7 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -14,14 +14,18 @@ from erpnext.stock.utils import get_stock_value_on def execute(filters=None): if not erpnext.is_perpetual_inventory_enabled(filters.company): - frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") - .format(filters.company)) + frappe.throw( + _("Perpetual inventory required for the company {0} to view this report.").format( + filters.company + ) + ) data = get_data(filters) columns = get_columns(filters) return columns, data + def get_unsync_date(filters): date = filters.from_date if not date: @@ -32,14 +36,16 @@ def get_unsync_date(filters): return while getdate(date) < getdate(today()): - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date, - company=filters.company, account = filters.account) + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( + posting_date=date, company=filters.company, account=filters.account + ) if abs(account_bal - stock_bal) > 0.1: return date date = add_days(date, 1) + def get_data(report_filters): from_date = get_unsync_date(report_filters) @@ -49,7 +55,8 @@ def get_data(report_filters): result = [] voucher_wise_dict = {} - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT name, posting_date, posting_time, voucher_type, voucher_no, stock_value_difference, stock_value, warehouse, item_code @@ -60,14 +67,19 @@ def get_data(report_filters): = %s and company = %s and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - ''', (from_date, report_filters.company), as_dict=1) + """, + (from_date, report_filters.company), + as_dict=1, + ) for d in data: voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) closing_date = add_days(from_date, -1) for key, stock_data in iteritems(voucher_wise_dict): - prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1]) + prev_stock_value = get_stock_value_on( + posting_date=closing_date, item_code=key[0], warehouse=key[1] + ) for data in stock_data: expected_stock_value = prev_stock_value + data.stock_value_difference if abs(data.stock_value - expected_stock_value) > 0.1: @@ -77,6 +89,7 @@ def get_data(report_filters): return result + def get_columns(filters): return [ { @@ -84,60 +97,43 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Stock Ledger Entry", - "width": "80" - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date" - }, - { - "label": _("Posting Time"), - "fieldname": "posting_time", - "fieldtype": "Time" - }, - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": "110" + "width": "80", }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"}, + {"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"}, { "label": _("Voucher No"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": "110" + "width": "110", }, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": "110" + "width": "110", }, { "label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": "110" + "width": "110", }, { "label": _("Expected Stock Value"), "fieldname": "expected_stock_value", "fieldtype": "Currency", - "width": "150" - }, - { - "label": _("Stock Value"), - "fieldname": "stock_value", - "fieldtype": "Currency", - "width": "120" + "width": "150", }, + {"label": _("Stock Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": "120"}, { "label": _("Difference Value"), "fieldname": "difference_value", "fieldtype": "Currency", - "width": "150" - } + "width": "150", + }, ] diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py index 65af9f51cdf..15218e63a87 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.py +++ b/erpnext/stock/report/item_price_stock/item_price_stock.py @@ -7,10 +7,11 @@ from frappe import _ def execute(filters=None): columns, data = [], [] - columns=get_columns() - data=get_data(filters,columns) + columns = get_columns() + data = get_data(filters, columns) return columns, data + def get_columns(): return [ { @@ -18,77 +19,64 @@ def get_columns(): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 120 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Data", "width": 100}, { "label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": 120 + "width": 120, }, { "label": _("Stock Available"), "fieldname": "stock_available", "fieldtype": "Float", - "width": 120 + "width": 120, }, { "label": _("Buying Price List"), "fieldname": "buying_price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 - }, - { - "label": _("Buying Rate"), - "fieldname": "buying_rate", - "fieldtype": "Currency", - "width": 120 + "width": 120, }, + {"label": _("Buying Rate"), "fieldname": "buying_rate", "fieldtype": "Currency", "width": 120}, { "label": _("Selling Price List"), "fieldname": "selling_price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 + "width": 120, }, - { - "label": _("Selling Rate"), - "fieldname": "selling_rate", - "fieldtype": "Currency", - "width": 120 - } + {"label": _("Selling Rate"), "fieldname": "selling_rate", "fieldtype": "Currency", "width": 120}, ] + def get_data(filters, columns): item_price_qty_data = [] item_price_qty_data = get_item_price_qty_data(filters) return item_price_qty_data + def get_item_price_qty_data(filters): conditions = "" if filters.get("item_code"): conditions += "where a.item_code=%(item_code)s" - item_results = frappe.db.sql("""select a.item_code, a.item_name, a.name as price_list_name, + item_results = frappe.db.sql( + """select a.item_code, a.item_name, a.name as price_list_name, a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty from `tabItem Price` a left join `tabBin` b ON a.item_code = b.item_code - {conditions}""" - .format(conditions=conditions), filters, as_dict=1) + {conditions}""".format( + conditions=conditions + ), + filters, + as_dict=1, + ) price_list_names = list(set(item.price_list_name for item in item_results)) @@ -99,15 +87,15 @@ def get_item_price_qty_data(filters): if item_results: for item_dict in item_results: data = { - 'item_code': item_dict.item_code, - 'item_name': item_dict.item_name, - 'brand': item_dict.brand, - 'warehouse': item_dict.warehouse, - 'stock_available': item_dict.actual_qty or 0, - 'buying_price_list': "", - 'buying_rate': 0.0, - 'selling_price_list': "", - 'selling_rate': 0.0 + "item_code": item_dict.item_code, + "item_name": item_dict.item_name, + "brand": item_dict.brand, + "warehouse": item_dict.warehouse, + "stock_available": item_dict.actual_qty or 0, + "buying_price_list": "", + "buying_rate": 0.0, + "selling_price_list": "", + "selling_rate": 0.0, } price_list = item_dict["price_list_name"] @@ -122,6 +110,7 @@ def get_item_price_qty_data(filters): return result + def get_price_map(price_list_names, buying=0, selling=0): price_map = {} @@ -137,14 +126,12 @@ def get_price_map(price_list_names, buying=0, selling=0): else: filters["selling"] = 1 - pricing_details = frappe.get_all("Item Price", - fields = ["name", "price_list", "price_list_rate"], filters=filters) + pricing_details = frappe.get_all( + "Item Price", fields=["name", "price_list", "price_list_rate"], filters=filters + ) for d in pricing_details: name = d["name"] - price_map[name] = { - price_list_key :d["price_list"], - rate_key :d["price_list_rate"] - } + price_map[name] = {price_list_key: d["price_list"], rate_key: d["price_list_rate"]} return price_map diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index 0d0e8d22924..87f1a42e2b2 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -8,7 +8,8 @@ from frappe.utils import flt def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) conditions = get_condition(filters) @@ -19,64 +20,95 @@ def execute(filters=None): val_rate_map = get_valuation_rate() from erpnext.accounts.utils import get_currency_precision + precision = get_currency_precision() or 2 data = [] for item in sorted(item_map): - data.append([item, item_map[item]["item_name"],item_map[item]["item_group"], - item_map[item]["brand"], item_map[item]["description"], item_map[item]["stock_uom"], - flt(last_purchase_rate.get(item, 0), precision), - flt(val_rate_map.get(item, 0), precision), - pl.get(item, {}).get("Selling"), - pl.get(item, {}).get("Buying"), - flt(bom_rate.get(item, 0), precision) - ]) + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["item_group"], + item_map[item]["brand"], + item_map[item]["description"], + item_map[item]["stock_uom"], + flt(last_purchase_rate.get(item, 0), precision), + flt(val_rate_map.get(item, 0), precision), + pl.get(item, {}).get("Selling"), + pl.get(item, {}).get("Buying"), + flt(bom_rate.get(item, 0), precision), + ] + ) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100", _("Item Name") + "::150",_("Item Group") + ":Link/Item Group:125", - _("Brand") + "::100", _("Description") + "::150", _("UOM") + ":Link/UOM:80", - _("Last Purchase Rate") + ":Currency:90", _("Valuation Rate") + ":Currency:80", _("Sales Price List") + "::180", - _("Purchase Price List") + "::180", _("BOM Rate") + ":Currency:90"] + columns = [ + _("Item") + ":Link/Item:100", + _("Item Name") + "::150", + _("Item Group") + ":Link/Item Group:125", + _("Brand") + "::100", + _("Description") + "::150", + _("UOM") + ":Link/UOM:80", + _("Last Purchase Rate") + ":Currency:90", + _("Valuation Rate") + ":Currency:80", + _("Sales Price List") + "::180", + _("Purchase Price List") + "::180", + _("BOM Rate") + ":Currency:90", + ] return columns + def get_item_details(conditions): """returns all items details""" item_map = {} - for i in frappe.db.sql("""select name, item_group, item_name, description, + for i in frappe.db.sql( + """select name, item_group, item_name, description, brand, stock_uom from tabItem %s - order by item_code, item_group""" % (conditions), as_dict=1): - item_map.setdefault(i.name, i) + order by item_code, item_group""" + % (conditions), + as_dict=1, + ): + item_map.setdefault(i.name, i) return item_map + def get_price_list(): """Get selling & buying price list of every item""" rate = {} - price_list = frappe.db.sql("""select ip.item_code, ip.buying, ip.selling, + price_list = frappe.db.sql( + """select ip.item_code, ip.buying, ip.selling, concat(ifnull(cu.symbol,ip.currency), " ", round(ip.price_list_rate,2), " - ", ip.price_list) as price from `tabItem Price` ip, `tabPrice List` pl, `tabCurrency` cu - where ip.price_list=pl.name and pl.currency=cu.name and pl.enabled=1""", as_dict=1) + where ip.price_list=pl.name and pl.currency=cu.name and pl.enabled=1""", + as_dict=1, + ) for j in price_list: if j.price: - rate.setdefault(j.item_code, {}).setdefault("Buying" if j.buying else "Selling", []).append(j.price) + rate.setdefault(j.item_code, {}).setdefault("Buying" if j.buying else "Selling", []).append( + j.price + ) item_rate_map = {} for item in rate: for buying_or_selling in rate[item]: - item_rate_map.setdefault(item, {}).setdefault(buying_or_selling, - ", ".join(rate[item].get(buying_or_selling, []))) + item_rate_map.setdefault(item, {}).setdefault( + buying_or_selling, ", ".join(rate[item].get(buying_or_selling, [])) + ) return item_rate_map + def get_last_purchase_rate(): item_last_purchase_rate_map = {} @@ -108,29 +140,38 @@ def get_last_purchase_rate(): return item_last_purchase_rate_map + def get_item_bom_rate(): """Get BOM rate of an item from BOM""" item_bom_map = {} - for b in frappe.db.sql("""select item, (total_cost/quantity) as bom_rate - from `tabBOM` where is_active=1 and is_default=1""", as_dict=1): - item_bom_map.setdefault(b.item, flt(b.bom_rate)) + for b in frappe.db.sql( + """select item, (total_cost/quantity) as bom_rate + from `tabBOM` where is_active=1 and is_default=1""", + as_dict=1, + ): + item_bom_map.setdefault(b.item, flt(b.bom_rate)) return item_bom_map + def get_valuation_rate(): """Get an average valuation rate of an item from all warehouses""" item_val_rate_map = {} - for d in frappe.db.sql("""select item_code, + for d in frappe.db.sql( + """select item_code, sum(actual_qty*valuation_rate)/sum(actual_qty) as val_rate - from tabBin where actual_qty > 0 group by item_code""", as_dict=1): - item_val_rate_map.setdefault(d.item_code, d.val_rate) + from tabBin where actual_qty > 0 group by item_code""", + as_dict=1, + ): + item_val_rate_map.setdefault(d.item_code, d.val_rate) return item_val_rate_map + def get_condition(filters): """Get Filter Items""" diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py index 30c761421fd..03a3a6a0b83 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -18,6 +18,7 @@ def execute(filters=None): return columns, data, None, chart_data + def get_conditions(filters): conditions = "" @@ -28,8 +29,10 @@ def get_conditions(filters): return conditions + def get_data(conditions, filters): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT bin.warehouse, bin.item_code, @@ -51,10 +54,16 @@ def get_data(conditions, filters): AND warehouse.name = bin.warehouse AND bin.item_code=item.name {0} - ORDER BY bin.projected_qty;""".format(conditions), filters, as_dict=1) + ORDER BY bin.projected_qty;""".format( + conditions + ), + filters, + as_dict=1, + ) return data + def get_chart_data(data): labels, datapoints = [], [] @@ -67,18 +76,11 @@ def get_chart_data(data): datapoints = datapoints[:10] return { - "data": { - "labels": labels, - "datasets":[ - { - "name": _("Projected Qty"), - "values": datapoints - } - ] - }, - "type": "bar" + "data": {"labels": labels, "datasets": [{"name": _("Projected Qty"), "values": datapoints}]}, + "type": "bar", } + def get_columns(): columns = [ { @@ -86,76 +88,66 @@ def get_columns(): "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": 150 + "width": 150, }, { "label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 150 + "width": 150, }, { "label": _("Actual Quantity"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Ordered Quantity"), "fieldname": "ordered_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Planned Quantity"), "fieldname": "planned_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Reserved Quantity"), "fieldname": "reserved_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Reserved Quantity for Production"), "fieldname": "reserved_qty_for_production", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Projected Quantity"), "fieldname": "projected_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 + "width": 120, }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 120 - } + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 120}, ] return columns diff --git a/erpnext/stock/report/item_variant_details/item_variant_details.py b/erpnext/stock/report/item_variant_details/item_variant_details.py index 10cef70215b..d1bf2203f17 100644 --- a/erpnext/stock/report/item_variant_details/item_variant_details.py +++ b/erpnext/stock/report/item_variant_details/item_variant_details.py @@ -11,25 +11,21 @@ def execute(filters=None): data = get_data(filters.item) return columns, data + def get_data(item): if not item: return [] item_dicts = [] variant_results = frappe.db.get_all( - "Item", - fields=["name"], - filters={ - "variant_of": ["=", item], - "disabled": 0 - } + "Item", fields=["name"], filters={"variant_of": ["=", item], "disabled": 0} ) if not variant_results: frappe.msgprint(_("There aren't any item variants for the selected item")) return [] else: - variant_list = [variant['name'] for variant in variant_results] + variant_list = [variant["name"] for variant in variant_results] order_count_map = get_open_sales_orders_count(variant_list) stock_details_map = get_stock_details_map(variant_list) @@ -40,15 +36,13 @@ def get_data(item): attributes = frappe.db.get_all( "Item Variant Attribute", fields=["attribute"], - filters={ - "parent": ["in", variant_list] - }, - group_by="attribute" + filters={"parent": ["in", variant_list]}, + group_by="attribute", ) attribute_list = [row.get("attribute") for row in attributes] # Prepare dicts - variant_dicts = [{"variant_name": d['name']} for d in variant_results] + variant_dicts = [{"variant_name": d["name"]} for d in variant_results] for item_dict in variant_dicts: name = item_dict.get("variant_name") @@ -72,73 +66,66 @@ def get_data(item): return item_dicts + def get_columns(item): - columns = [{ - "fieldname": "variant_name", - "label": "Variant", - "fieldtype": "Link", - "options": "Item", - "width": 200 - }] + columns = [ + { + "fieldname": "variant_name", + "label": "Variant", + "fieldtype": "Link", + "options": "Item", + "width": 200, + } + ] item_doc = frappe.get_doc("Item", item) for entry in item_doc.attributes: - columns.append({ - "fieldname": frappe.scrub(entry.attribute), - "label": entry.attribute, - "fieldtype": "Data", - "width": 100 - }) + columns.append( + { + "fieldname": frappe.scrub(entry.attribute), + "label": entry.attribute, + "fieldtype": "Data", + "width": 100, + } + ) additional_columns = [ { "fieldname": "avg_buying_price_list_rate", "label": _("Avg. Buying Price List Rate"), "fieldtype": "Currency", - "width": 150 + "width": 150, }, { "fieldname": "avg_selling_price_list_rate", "label": _("Avg. Selling Price List Rate"), "fieldtype": "Currency", - "width": 150 - }, - { - "fieldname": "current_stock", - "label": _("Current Stock"), - "fieldtype": "Float", - "width": 120 - }, - { - "fieldname": "in_production", - "label": _("In Production"), - "fieldtype": "Float", - "width": 150 + "width": 150, }, + {"fieldname": "current_stock", "label": _("Current Stock"), "fieldtype": "Float", "width": 120}, + {"fieldname": "in_production", "label": _("In Production"), "fieldtype": "Float", "width": 150}, { "fieldname": "open_orders", "label": _("Open Sales Orders"), "fieldtype": "Float", - "width": 150 - } + "width": 150, + }, ] columns.extend(additional_columns) return columns + def get_open_sales_orders_count(variants_list): open_sales_orders = frappe.db.get_list( "Sales Order", - fields=[ - "name", - "`tabSales Order Item`.item_code" - ], + fields=["name", "`tabSales Order Item`.item_code"], filters=[ ["Sales Order", "docstatus", "=", 1], - ["Sales Order Item", "item_code", "in", variants_list] + ["Sales Order Item", "item_code", "in", variants_list], ], - distinct=1 + distinct=1, ) order_count_map = {} @@ -151,6 +138,7 @@ def get_open_sales_orders_count(variants_list): return order_count_map + def get_stock_details_map(variant_list): stock_details = frappe.db.get_all( "Bin", @@ -160,10 +148,8 @@ def get_stock_details_map(variant_list): "sum(projected_qty) as projected_qty", "item_code", ], - filters={ - "item_code": ["in", variant_list] - }, - group_by="item_code" + filters={"item_code": ["in", variant_list]}, + group_by="item_code", ) stock_details_map = {} @@ -171,11 +157,12 @@ def get_stock_details_map(variant_list): name = row.get("item_code") stock_details_map[name] = { "Inventory": row.get("actual_qty"), - "In Production": row.get("planned_qty") + "In Production": row.get("planned_qty"), } return stock_details_map + def get_buying_price_map(variant_list): buying = frappe.db.get_all( "Item Price", @@ -183,11 +170,8 @@ def get_buying_price_map(variant_list): "avg(price_list_rate) as avg_rate", "item_code", ], - filters={ - "item_code": ["in", variant_list], - "buying": 1 - }, - group_by="item_code" + filters={"item_code": ["in", variant_list], "buying": 1}, + group_by="item_code", ) buying_price_map = {} @@ -196,6 +180,7 @@ def get_buying_price_map(variant_list): return buying_price_map + def get_selling_price_map(variant_list): selling = frappe.db.get_all( "Item Price", @@ -203,11 +188,8 @@ def get_selling_price_map(variant_list): "avg(price_list_rate) as avg_rate", "item_code", ], - filters={ - "item_code": ["in", variant_list], - "selling": 1 - }, - group_by="item_code" + filters={"item_code": ["in", variant_list], "selling": 1}, + group_by="item_code", ) selling_price_map = {} @@ -216,17 +198,12 @@ def get_selling_price_map(variant_list): return selling_price_map + def get_attribute_values_map(variant_list): attribute_list = frappe.db.get_all( "Item Variant Attribute", - fields=[ - "attribute", - "attribute_value", - "parent" - ], - filters={ - "parent": ["in", variant_list] - } + fields=["attribute", "attribute_value", "parent"], + filters={"parent": ["in", variant_list]}, ) attr_val_map = {} diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index cfa1e474c7b..f308e9e41f1 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -7,13 +7,14 @@ from frappe.utils import flt, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} float_precision = frappe.db.get_default("float_precision") condition = get_condition(filters) avg_daily_outgoing = 0 - diff = ((getdate(filters.get("to_date")) - getdate(filters.get("from_date"))).days)+1 + diff = ((getdate(filters.get("to_date")) - getdate(filters.get("from_date"))).days) + 1 if diff <= 0: frappe.throw(_("'From Date' must be after 'To Date'")) @@ -24,42 +25,72 @@ def execute(filters=None): data = [] for item in items: - total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(delivered_item_map.get(item.name,0)) + total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt( + delivered_item_map.get(item.name, 0) + ) avg_daily_outgoing = flt(total_outgoing / diff, float_precision) reorder_level = (avg_daily_outgoing * flt(item.lead_time_days)) + flt(item.safety_stock) - data.append([item.name, item.item_name, item.item_group, item.brand, item.description, - item.safety_stock, item.lead_time_days, consumed_item_map.get(item.name, 0), - delivered_item_map.get(item.name,0), total_outgoing, avg_daily_outgoing, reorder_level]) + data.append( + [ + item.name, + item.item_name, + item.item_group, + item.brand, + item.description, + item.safety_stock, + item.lead_time_days, + consumed_item_map.get(item.name, 0), + delivered_item_map.get(item.name, 0), + total_outgoing, + avg_daily_outgoing, + reorder_level, + ] + ) + + return columns, data - return columns , data def get_columns(): - return[ - _("Item") + ":Link/Item:120", _("Item Name") + ":Data:120", _("Item Group") + ":Link/Item Group:100", - _("Brand") + ":Link/Brand:100", _("Description") + "::160", - _("Safety Stock") + ":Float:160", _("Lead Time Days") + ":Float:120", _("Consumed") + ":Float:120", - _("Delivered") + ":Float:120", _("Total Outgoing") + ":Float:120", _("Avg Daily Outgoing") + ":Float:160", - _("Reorder Level") + ":Float:120" + return [ + _("Item") + ":Link/Item:120", + _("Item Name") + ":Data:120", + _("Item Group") + ":Link/Item Group:100", + _("Brand") + ":Link/Brand:100", + _("Description") + "::160", + _("Safety Stock") + ":Float:160", + _("Lead Time Days") + ":Float:120", + _("Consumed") + ":Float:120", + _("Delivered") + ":Float:120", + _("Total Outgoing") + ":Float:120", + _("Avg Daily Outgoing") + ":Float:160", + _("Reorder Level") + ":Float:120", ] + def get_item_info(filters): from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition + conditions = [get_item_group_condition(filters.get("item_group"))] if filters.get("brand"): conditions.append("item.brand=%(brand)s") conditions.append("is_stock_item = 1") - return frappe.db.sql("""select name, item_name, description, brand, item_group, - safety_stock, lead_time_days from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters, as_dict=1) + return frappe.db.sql( + """select name, item_name, description, brand, item_group, + safety_stock, lead_time_days from `tabItem` item where {}""".format( + " and ".join(conditions) + ), + filters, + as_dict=1, + ) def get_consumed_items(condition): purpose_to_exclude = [ "Material Transfer for Manufacture", "Material Transfer", - "Send to Subcontractor" + "Send to Subcontractor", ] condition += """ @@ -67,10 +98,13 @@ def get_consumed_items(condition): purpose is NULL or purpose not in ({}) ) - """.format(', '.join(f"'{p}'" for p in purpose_to_exclude)) + """.format( + ", ".join(f"'{p}'" for p in purpose_to_exclude) + ) condition = condition.replace("posting_date", "sle.posting_date") - consumed_items = frappe.db.sql(""" + consumed_items = frappe.db.sql( + """ select item_code, abs(sum(actual_qty)) as consumed_qty from `tabStock Ledger Entry` as sle left join `tabStock Entry` as se on sle.voucher_no = se.name @@ -79,22 +113,34 @@ def get_consumed_items(condition): and is_cancelled = 0 and voucher_type not in ('Delivery Note', 'Sales Invoice') %s - group by item_code""" % condition, as_dict=1) + group by item_code""" + % condition, + as_dict=1, + ) - consumed_items_map = {item.item_code : item.consumed_qty for item in consumed_items} + consumed_items_map = {item.item_code: item.consumed_qty for item in consumed_items} return consumed_items_map + def get_delivered_items(condition): - dn_items = frappe.db.sql("""select dn_item.item_code, sum(dn_item.stock_qty) as dn_qty + dn_items = frappe.db.sql( + """select dn_item.item_code, sum(dn_item.stock_qty) as dn_qty from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item where dn.name = dn_item.parent and dn.docstatus = 1 %s - group by dn_item.item_code""" % (condition), as_dict=1) + group by dn_item.item_code""" + % (condition), + as_dict=1, + ) - si_items = frappe.db.sql("""select si_item.item_code, sum(si_item.stock_qty) as si_qty + si_items = frappe.db.sql( + """select si_item.item_code, sum(si_item.stock_qty) as si_qty from `tabSales Invoice` si, `tabSales Invoice Item` si_item where si.name = si_item.parent and si.docstatus = 1 and si.update_stock = 1 %s - group by si_item.item_code""" % (condition), as_dict=1) + group by si_item.item_code""" + % (condition), + as_dict=1, + ) dn_item_map = {} for item in dn_items: @@ -105,10 +151,14 @@ def get_delivered_items(condition): return dn_item_map + def get_condition(filters): conditions = "" if filters.get("from_date") and filters.get("to_date"): - conditions += " and posting_date between '%s' and '%s'" % (filters["from_date"],filters["to_date"]) + conditions += " and posting_date between '%s' and '%s'" % ( + filters["from_date"], + filters["to_date"], + ) else: frappe.throw(_("From and To dates required")) return conditions diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 3c4dbce73a6..88b29e60130 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -45,7 +45,9 @@ def execute(filters=None): child_rows = [] for child_item_detail in required_items: - child_item_balance = stock_balance.get(child_item_detail.item_code, frappe._dict()).get(warehouse, frappe._dict()) + child_item_balance = stock_balance.get(child_item_detail.item_code, frappe._dict()).get( + warehouse, frappe._dict() + ) child_row = { "indent": 1, "parent_item": parent_item, @@ -74,16 +76,46 @@ def execute(filters=None): def get_columns(): columns = [ - {"fieldname": "item_code", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 300}, - {"fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Link", "options": "Warehouse", "width": 100}, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", + "width": 300, + }, + { + "fieldname": "warehouse", + "label": _("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 70}, {"fieldname": "bundle_qty", "label": _("Bundle Qty"), "fieldtype": "Float", "width": 100}, {"fieldname": "actual_qty", "label": _("Actual Qty"), "fieldtype": "Float", "width": 100}, {"fieldname": "minimum_qty", "label": _("Minimum Qty"), "fieldtype": "Float", "width": 100}, - {"fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"fieldname": "brand", "label": _("Brand"), "fieldtype": "Link", "options": "Brand", "width": 100}, + { + "fieldname": "item_group", + "label": _("Item Group"), + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "fieldname": "brand", + "label": _("Brand"), + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, {"fieldname": "description", "label": _("Description"), "width": 140}, - {"fieldname": "company", "label": _("Company"), "fieldtype": "Link", "options": "Company", "width": 100} + { + "fieldname": "company", + "label": _("Company"), + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, ] return columns @@ -93,12 +125,18 @@ def get_items(filters): item_details = frappe._dict() conditions = get_parent_item_conditions(filters) - parent_item_details = frappe.db.sql(""" + parent_item_details = frappe.db.sql( + """ select item.name as item_code, item.item_name, pb.description, item.item_group, item.brand, item.stock_uom from `tabItem` item inner join `tabProduct Bundle` pb on pb.new_item_code = item.name where ifnull(item.disabled, 0) = 0 {0} - """.format(conditions), filters, as_dict=1) # nosec + """.format( + conditions + ), + filters, + as_dict=1, + ) # nosec parent_items = [] for d in parent_item_details: @@ -106,7 +144,8 @@ def get_items(filters): item_details[d.item_code] = d if parent_items: - child_item_details = frappe.db.sql(""" + child_item_details = frappe.db.sql( + """ select pb.new_item_code as parent_item, pbi.item_code, item.item_name, pbi.description, item.item_group, item.brand, item.stock_uom, pbi.uom, pbi.qty @@ -114,7 +153,12 @@ def get_items(filters): inner join `tabProduct Bundle` pb on pb.name = pbi.parent inner join `tabItem` item on item.name = pbi.item_code where pb.new_item_code in ({0}) - """.format(", ".join(["%s"] * len(parent_items))), parent_items, as_dict=1) # nosec + """.format( + ", ".join(["%s"] * len(parent_items)) + ), + parent_items, + as_dict=1, + ) # nosec else: child_item_details = [] @@ -141,12 +185,14 @@ def get_stock_ledger_entries(filters, items): if not items: return [] - item_conditions_sql = ' and sle.item_code in ({})' \ - .format(', '.join(frappe.db.escape(i) for i in items)) + item_conditions_sql = " and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i) for i in items) + ) conditions = get_sle_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company from @@ -154,7 +200,10 @@ def get_stock_ledger_entries(filters, items): left join `tabStock Ledger Entry` sle2 on sle.item_code = sle2.item_code and sle.warehouse = sle2.warehouse and (sle.posting_date, sle.posting_time, sle.name) < (sle2.posting_date, sle2.posting_time, sle2.name) - where sle2.name is null and sle.docstatus < 2 %s %s""" % (item_conditions_sql, conditions), as_dict=1) # nosec + where sle2.name is null and sle.docstatus < 2 %s %s""" + % (item_conditions_sql, conditions), + as_dict=1, + ) # nosec def get_parent_item_conditions(filters): @@ -180,9 +229,14 @@ def get_sle_conditions(filters): conditions += " and sle.posting_date <= %s" % frappe.db.escape(filters.get("date")) if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" % (warehouse_details.lft, warehouse_details.rgt) # nosec + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) # nosec return conditions diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index 97384427fa4..fe2d55a3913 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Purchase Receipt") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, filters): if not data: return [] @@ -27,7 +29,7 @@ def get_chart_data(data, filters): # consider only consolidated row data = [row for row in data if row[0]] - data = sorted(data, key = lambda i: i[-1], reverse=True) + data = sorted(data, key=lambda i: i[-1], reverse=True) if len(data) > 10: # get top 10 if data too long @@ -39,14 +41,9 @@ def get_chart_data(data, filters): return { "data": { - "labels" : labels, - "datasets" : [ - { - "name": _("Total Received Amount"), - "values": datapoints - } - ] + "labels": labels, + "datasets": [{"name": _("Total Received Amount"), "values": datapoints}], }, - "type" : "bar", - "colors":["#5e64ff"] + "type": "bar", + "colors": ["#5e64ff"], } diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py index 80ec848e5b6..e439f51dd69 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -12,42 +12,43 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): - columns = [{ - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date' - }, { - 'label': _('Posting Time'), - 'fieldtype': 'Time', - 'fieldname': 'posting_time' - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 220 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 220 - }, { - 'label': _('Company'), - 'fieldtype': 'Link', - 'fieldname': 'company', - 'options': 'Company', - 'width': 220 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 220 - }] + columns = [ + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date"}, + {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time"}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 220, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 220, + }, + { + "label": _("Company"), + "fieldtype": "Link", + "fieldname": "company", + "options": "Company", + "width": 220, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 220, + }, + ] return columns + def get_data(filters): - return get_stock_ledger_entries(filters, '<=', order="asc") or [] + return get_stock_ledger_entries(filters, "<=", order="asc") or [] diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 97a740e1844..1956238331e 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -12,7 +12,7 @@ from frappe.utils import cint, date_diff, flt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict -precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) + def execute(filters: Filters = None) -> Tuple: to_date = filters["to_date"] @@ -25,42 +25,52 @@ def execute(filters: Filters = None) -> Tuple: return columns, data, None, chart_data + def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]: "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] + precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) + for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 details = item_dict["details"] fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) - if not fifo_queue: continue + if not fifo_queue: + continue average_age = get_average_age(fifo_queue, to_date) earliest_age = date_diff(to_date, fifo_queue[0][1]) latest_age = date_diff(to_date, fifo_queue[-1][1]) range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict) - row = [details.name, details.item_name, details.description, - details.item_group, details.brand] + row = [details.name, details.item_name, details.description, details.item_group, details.brand] if filters.get("show_warehouse_wise_stock"): row.append(details.warehouse) - row.extend([ - flt(item_dict.get("total_qty"), precision), - average_age, - range1, range2, range3, above_range3, - earliest_age, latest_age, - details.stock_uom - ]) + row.extend( + [ + flt(item_dict.get("total_qty"), precision), + average_age, + range1, + range2, + range3, + above_range3, + earliest_age, + latest_age, + details.stock_uom, + ] + ) data.append(row) return data + def get_average_age(fifo_queue: List, to_date: str) -> float: batch_age = age_qty = total_qty = 0.0 for batch in fifo_queue: @@ -75,7 +85,11 @@ def get_average_age(fifo_queue: List, to_date: str) -> float: return flt(age_qty / total_qty, 2) if total_qty else 0.0 + def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: + + precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) + range1 = range2 = range3 = above_range3 = 0.0 for item in fifo_queue: @@ -93,6 +107,7 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D return range1, range2, range3, above_range3 + def get_columns(filters: Filters) -> List[Dict]: range_columns = [] setup_ageing_columns(filters, range_columns) @@ -102,82 +117,55 @@ def get_columns(filters: Filters) -> List[Dict]: "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 100 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 200 + "width": 100, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200}, { "label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 100, }, { "label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", - "width": 100 - }] + "width": 100, + }, + ] if filters.get("show_warehouse_wise_stock"): - columns +=[{ - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100 - }] + columns += [ + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + } + ] - columns.extend([ - { - "label": _("Available Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Average Age"), - "fieldname": "average_age", - "fieldtype": "Float", - "width": 100 - }]) + columns.extend( + [ + {"label": _("Available Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 100}, + {"label": _("Average Age"), "fieldname": "average_age", "fieldtype": "Float", "width": 100}, + ] + ) columns.extend(range_columns) - columns.extend([ - { - "label": _("Earliest"), - "fieldname": "earliest", - "fieldtype": "Int", - "width": 80 - }, - { - "label": _("Latest"), - "fieldname": "latest", - "fieldtype": "Int", - "width": 80 - }, - { - "label": _("UOM"), - "fieldname": "uom", - "fieldtype": "Link", - "options": "UOM", - "width": 100 - } - ]) + columns.extend( + [ + {"label": _("Earliest"), "fieldname": "earliest", "fieldtype": "Int", "width": 80}, + {"label": _("Latest"), "fieldname": "latest", "fieldtype": "Int", "width": 80}, + {"label": _("UOM"), "fieldname": "uom", "fieldtype": "Link", "options": "UOM", "width": 100}, + ] + ) return columns + def get_chart_data(data: List, filters: Filters) -> Dict: if not data: return [] @@ -187,7 +175,7 @@ def get_chart_data(data: List, filters: Filters) -> Dict: if filters.get("show_warehouse_wise_stock"): return {} - data.sort(key = lambda row: row[6], reverse=True) + data.sort(key=lambda row: row[6], reverse=True) if len(data) > 10: data = data[:10] @@ -197,42 +185,33 @@ def get_chart_data(data: List, filters: Filters) -> Dict: datapoints.append(row[6]) return { - "data" : { - "labels": labels, - "datasets": [ - { - "name": _("Average Age"), - "values": datapoints - } - ] - }, - "type" : "bar" + "data": {"labels": labels, "datasets": [{"name": _("Average Age"), "values": datapoints}]}, + "type": "bar", } + def setup_ageing_columns(filters: Filters, range_columns: List): ranges = [ f"0 - {filters['range1']}", f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}", f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}", - f"{cint(filters['range3']) + 1} - {_('Above')}" + f"{cint(filters['range3']) + 1} - {_('Above')}", ] for i, label in enumerate(ranges): - fieldname = 'range' + str(i+1) - add_column(range_columns, label=f"Age ({label})",fieldname=fieldname) + fieldname = "range" + str(i + 1) + add_column(range_columns, label=f"Age ({label})", fieldname=fieldname) -def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str = 'Float', width: int = 140): - range_columns.append(dict( - label=label, - fieldname=fieldname, - fieldtype=fieldtype, - width=width - )) + +def add_column( + range_columns: List, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140 +): + range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width)) class FIFOSlots: "Returns FIFO computed slots of inwarded stock as per date." - def __init__(self, filters: Dict = None , sle: List = None): + def __init__(self, filters: Dict = None, sle: List = None): self.item_details = {} self.transferred_item_details = {} self.serial_no_batch_purchase_details = {} @@ -241,13 +220,13 @@ class FIFOSlots: def generate(self) -> Dict: """ - Returns dict of the foll.g structure: - Key = Item A / (Item A, Warehouse A) - Key: { - 'details' -> Dict: ** item details **, - 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, - consumed/updated and maintained via FIFO. ** - } + Returns dict of the foll.g structure: + Key = Item A / (Item A, Warehouse A) + Key: { + 'details' -> Dict: ** item details **, + 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, + consumed/updated and maintained via FIFO. ** + } """ if self.sle is None: self.sle = self.__get_stock_ledger_entries() @@ -287,7 +266,9 @@ class FIFOSlots: return key, fifo_queue, transferred_item_key - def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): + def __compute_incoming_stock( + self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List + ): "Update FIFO Queue on inward stock." transfer_data = self.transferred_item_details.get(transfer_key) @@ -313,7 +294,9 @@ class FIFOSlots: self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date) fifo_queue.append([serial_no, row.posting_date]) - def __compute_outgoing_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): + def __compute_outgoing_stock( + self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List + ): "Update FIFO Queue on outward stock." if serial_nos: fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos] @@ -379,15 +362,13 @@ class FIFOSlots: def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict: "Aggregate Item-Wh wise data into single Item entry." item_aggregated_data = {} - for key,row in wh_wise_data.items(): + for key, row in wh_wise_data.items(): item = key[0] if not item_aggregated_data.get(item): - item_aggregated_data.setdefault(item, { - "details": frappe._dict(), - "fifo_queue": [], - "qty_after_transaction": 0.0, - "total_qty": 0.0 - }) + item_aggregated_data.setdefault( + item, + {"details": frappe._dict(), "fifo_queue": [], "qty_after_transaction": 0.0, "total_qty": 0.0}, + ) item_row = item_aggregated_data.get(item) item_row["details"].update(row["details"]) item_row["fifo_queue"].extend(row["fifo_queue"]) @@ -399,19 +380,29 @@ class FIFOSlots: def __get_stock_ledger_entries(self) -> List[Dict]: sle = frappe.qb.DocType("Stock Ledger Entry") - item = self.__get_item_query() # used as derived table in sle query + item = self.__get_item_query() # used as derived table in sle query sle_query = ( - frappe.qb.from_(sle).from_(item) + frappe.qb.from_(sle) + .from_(item) .select( - item.name, item.item_name, item.item_group, - item.brand, item.description, - item.stock_uom, item.has_serial_no, - sle.actual_qty, sle.posting_date, - sle.voucher_type, sle.voucher_no, - sle.serial_no, sle.batch_no, - sle.qty_after_transaction, sle.warehouse - ).where( + item.name, + item.item_name, + item.item_group, + item.brand, + item.description, + item.stock_uom, + item.has_serial_no, + sle.actual_qty, + sle.posting_date, + sle.voucher_type, + sle.voucher_no, + sle.serial_no, + sle.batch_no, + sle.qty_after_transaction, + sle.warehouse, + ) + .where( (sle.item_code == item.name) & (sle.company == self.filters.get("company")) & (sle.posting_date <= self.filters.get("to_date")) @@ -422,9 +413,7 @@ class FIFOSlots: if self.filters.get("warehouse"): sle_query = self.__get_warehouse_conditions(sle, sle_query) - sle_query = sle_query.orderby( - sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty - ) + sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty) return sle_query.run(as_dict=True) @@ -432,8 +421,7 @@ class FIFOSlots: item_table = frappe.qb.DocType("Item") item = frappe.qb.from_("Item").select( - "name", "item_name", "description", "stock_uom", - "brand", "item_group", "has_serial_no" + "name", "item_name", "description", "stock_uom", "brand", "item_group", "has_serial_no" ) if self.filters.get("item_code"): @@ -446,18 +434,13 @@ class FIFOSlots: def __get_warehouse_conditions(self, sle, sle_query) -> str: warehouse = frappe.qb.DocType("Warehouse") - lft, rgt = frappe.db.get_value( - "Warehouse", - self.filters.get("warehouse"), - ['lft', 'rgt'] - ) + lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ["lft", "rgt"]) warehouse_results = ( frappe.qb.from_(warehouse) - .select("name").where( - (warehouse.lft >= lft) - & (warehouse.rgt <= rgt) - ).run() + .select("name") + .where((warehouse.lft >= lft) & (warehouse.rgt <= rgt)) + .run() ) warehouse_results = [x[0] for x in warehouse_results] diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 3fc357e8d4f..fb363606233 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1,18 +1,16 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data -from erpnext.tests.utils import ERPNextTestCase -class TestStockAgeing(ERPNextTestCase): +class TestStockAgeing(FrappeTestCase): def setUp(self) -> None: self.filters = frappe._dict( - company="_Test Company", - to_date="2021-12-10", - range1=30, range2=60, range3=90 + company="_Test Company", to_date="2021-12-10", range1=30, range2=60, range3=90 ) def test_normal_inward_outward_queue(self): @@ -20,28 +18,37 @@ class TestStockAgeing(ERPNextTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=30, qty_after_transaction=30, + actual_qty=30, + qty_after_transaction=30, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=50, + actual_qty=20, + qty_after_transaction=50, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Entry", + posting_date="2021-12-02", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=40, + actual_qty=(-10), + qty_after_transaction=40, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -58,36 +65,48 @@ class TestStockAgeing(ERPNextTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=(-30), qty_after_transaction=(-30), + actual_qty=(-30), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=(-10), + actual_qty=20, + qty_after_transaction=(-10), warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Entry", + posting_date="2021-12-02", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=10, + actual_qty=20, + qty_after_transaction=10, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=10, qty_after_transaction=20, + actual_qty=10, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="004", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -107,28 +126,37 @@ class TestStockAgeing(ERPNextTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=30, qty_after_transaction=30, + actual_qty=30, + qty_after_transaction=30, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=50, + actual_qty=0, + qty_after_transaction=50, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=40, + actual_qty=(-10), + qty_after_transaction=40, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -150,28 +178,37 @@ class TestStockAgeing(ERPNextTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=1000, + actual_qty=0, + qty_after_transaction=1000, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Reconciliation", + posting_date="2021-12-01", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=400, + actual_qty=0, + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=390, + actual_qty=(-10), + qty_after_transaction=390, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -196,32 +233,41 @@ class TestStockAgeing(ERPNextTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=1000, + actual_qty=0, + qty_after_transaction=1000, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Reconciliation", + posting_date="2021-12-01", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=400, + actual_qty=0, + qty_after_transaction=400, warehouse="WH 2", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=990, + actual_qty=(-10), + qty_after_transaction=990, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="004", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] item_wise_slots, item_wh_wise_slots = generate_item_and_item_wh_wise_slots( - filters=self.filters,sle=sle + filters=self.filters, sle=sle ) # test without 'show_warehouse_wise_stock' @@ -234,7 +280,9 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(queue[1][0], 400.0) # test with 'show_warehouse_wise_stock' checked - item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots] + item_wh_balances = [ + item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots + ] self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"]) def test_repack_entry_same_item_split_rows(self): @@ -251,37 +299,49 @@ class TestStockAgeing(ERPNextTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=450, + actual_qty=(-50), + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=400, + actual_qty=(-50), + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=100, qty_after_transaction=500, + actual_qty=100, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -308,29 +368,38 @@ class TestStockAgeing(ERPNextTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-100), qty_after_transaction=400, + actual_qty=(-100), + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=450, + actual_qty=50, + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -355,37 +424,49 @@ class TestStockAgeing(ERPNextTestCase): Item 1 | 50 | 002 (repack) """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=20, qty_after_transaction=20, + actual_qty=20, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-30), + actual_qty=(-50), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-80), + actual_qty=(-50), + qty_after_transaction=(-80), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=(-30), + actual_qty=50, + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -397,7 +478,7 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(queue[0][0], -30.0) # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("002", "Flask Item", "WH 1")] self.assertEqual(transfer_bucket[0][0], 50) def test_repack_entry_same_item_overproduce(self): @@ -413,29 +494,38 @@ class TestStockAgeing(ERPNextTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=450, + actual_qty=(-50), + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=100, qty_after_transaction=550, + actual_qty=100, + qty_after_transaction=550, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -461,37 +551,49 @@ class TestStockAgeing(ERPNextTestCase): Item 1 | 50 | 002 (repack) """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=20, qty_after_transaction=20, + actual_qty=20, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-30), + actual_qty=(-50), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=20, + actual_qty=50, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=70, + actual_qty=50, + qty_after_transaction=70, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -504,7 +606,7 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(queue[1][0], 50.0) # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("002", "Flask Item", "WH 1")] self.assertFalse(transfer_bucket) def test_negative_stock_same_voucher(self): @@ -519,29 +621,38 @@ class TestStockAgeing(ERPNextTestCase): Item 1 | 80 | 001 """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-50), + actual_qty=(-50), + qty_after_transaction=(-50), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-100), + actual_qty=(-50), + qty_after_transaction=(-100), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=30, qty_after_transaction=(-70), + actual_qty=30, + qty_after_transaction=(-70), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -549,59 +660,71 @@ class TestStockAgeing(ERPNextTestCase): item_result = slots["Flask Item"] # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("001", "Flask Item", "WH 1")] self.assertEqual(transfer_bucket[0][0], 20) self.assertEqual(transfer_bucket[1][0], 50) self.assertEqual(item_result["fifo_queue"][0][0], -70.0) - sle.append(frappe._dict( - name="Flask Item", - actual_qty=80, qty_after_transaction=10, - warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", - voucher_no="001", - has_serial_no=False, serial_no=None - )) + sle.append( + frappe._dict( + name="Flask Item", + actual_qty=80, + qty_after_transaction=10, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + serial_no=None, + ) + ) fifo_slots = FIFOSlots(self.filters, sle) slots = fifo_slots.generate() item_result = slots["Flask Item"] - transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("001", "Flask Item", "WH 1")] self.assertFalse(transfer_bucket) self.assertEqual(item_result["fifo_queue"][0][0], 10.0) def test_precision(self): "Test if final balance qty is rounded off correctly." sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=0.3, qty_after_transaction=0.3, + actual_qty=0.3, + qty_after_transaction=0.3, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=0.6, qty_after_transaction=0.9, + actual_qty=0.6, + qty_after_transaction=0.9, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() report_data = format_report_data(self.filters, slots, self.filters["to_date"]) - row = report_data[0] # first row in report + row = report_data[0] # first row in report bal_qty = row[5] - range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance + range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance # check if value of Available Qty column matches with range bucket post format self.assertEqual(bal_qty, 0.9) self.assertEqual(bal_qty, range_qty_sum) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() @@ -610,4 +733,4 @@ def generate_item_and_item_wh_wise_slots(filters, sle): item_wh_wise_slots = FIFOSlots(filters, sle).generate() filters.show_warehouse_wise_stock = False - return item_wise_slots, item_wh_wise_slots \ No newline at end of file + return item_wise_slots, item_wh_wise_slots diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index ddc831037bf..da0776b9a84 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -25,84 +25,64 @@ def execute(filters=None): return columns, data, None, chart + def get_columns(filters): columns = [ - { - "label": _("Item"), - "options":"Item", - "fieldname": "name", - "fieldtype": "Link", - "width": 140 - }, + {"label": _("Item"), "options": "Item", "fieldname": "name", "fieldtype": "Link", "width": 140}, { "label": _("Item Name"), - "options":"Item", + "options": "Item", "fieldname": "item_name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Item Group"), - "options":"Item Group", + "options": "Item Group", "fieldname": "item_group", "fieldtype": "Link", - "width": 140 + "width": 140, }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("UOM"), - "fieldname": "uom", - "fieldtype": "Data", - "width": 120 - }] + {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Data", "width": 120}, + {"label": _("UOM"), "fieldname": "uom", "fieldtype": "Data", "width": 120}, + ] ranges = get_period_date_ranges(filters) for dummy, end_date in ranges: period = get_period(end_date, filters) - columns.append({ - "label": _(period), - "fieldname":scrub(period), - "fieldtype": "Float", - "width": 120 - }) + columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} + ) return columns + def get_period_date_ranges(filters): - from dateutil.relativedelta import relativedelta - from_date = round_down_to_nearest_frequency(filters.from_date, filters.range) - to_date = getdate(filters.to_date) + from dateutil.relativedelta import relativedelta - increment = { - "Monthly": 1, - "Quarterly": 3, - "Half-Yearly": 6, - "Yearly": 12 - }.get(filters.range,1) + from_date = round_down_to_nearest_frequency(filters.from_date, filters.range) + to_date = getdate(filters.to_date) - periodic_daterange = [] - for dummy in range(1, 53, increment): - if filters.range == "Weekly": - period_end_date = from_date + relativedelta(days=6) - else: - period_end_date = from_date + relativedelta(months=increment, days=-1) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(filters.range, 1) - if period_end_date > to_date: - period_end_date = to_date - periodic_daterange.append([from_date, period_end_date]) + periodic_daterange = [] + for dummy in range(1, 53, increment): + if filters.range == "Weekly": + period_end_date = from_date + relativedelta(days=6) + else: + period_end_date = from_date + relativedelta(months=increment, days=-1) - from_date = period_end_date + relativedelta(days=1) - if period_end_date == to_date: - break + if period_end_date > to_date: + period_end_date = to_date + periodic_daterange.append([from_date, period_end_date]) - return periodic_daterange + from_date = period_end_date + relativedelta(days=1) + if period_end_date == to_date: + break + + return periodic_daterange def round_down_to_nearest_frequency(date: str, frequency: str) -> datetime.datetime: @@ -132,12 +112,12 @@ def round_down_to_nearest_frequency(date: str, frequency: str) -> datetime.datet def get_period(posting_date, filters): months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - if filters.range == 'Weekly': + if filters.range == "Weekly": period = "Week " + str(posting_date.isocalendar()[1]) + " " + str(posting_date.year) - elif filters.range == 'Monthly': + elif filters.range == "Monthly": period = str(months[posting_date.month - 1]) + " " + str(posting_date.year) - elif filters.range == 'Quarterly': - period = "Quarter " + str(((posting_date.month-1)//3)+1) +" " + str(posting_date.year) + elif filters.range == "Quarterly": + period = "Quarter " + str(((posting_date.month - 1) // 3) + 1) + " " + str(posting_date.year) else: year = get_fiscal_year(posting_date, company=filters.company) period = str(year[2]) @@ -147,26 +127,26 @@ def get_period(posting_date, filters): def get_periodic_data(entry, filters): """Structured as: - Item 1 - - Balance (updated and carried forward): - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jun 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jul 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - Item 2 - - Balance (updated and carried forward): - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jun 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jul 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value + Item 1 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + Item 2 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value """ periodic_data = {} for d in entry: @@ -176,31 +156,36 @@ def get_periodic_data(entry, filters): # if period against item does not exist yet, instantiate it # insert existing balance dict against period, and add/subtract to it if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period): - previous_balance = periodic_data[d.item_code]['balance'].copy() + previous_balance = periodic_data[d.item_code]["balance"].copy() periodic_data[d.item_code][period] = previous_balance if d.voucher_type == "Stock Reconciliation": - if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get('balance').get(d.warehouse): - bal_qty = periodic_data[d.item_code]['balance'][d.warehouse] + if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get("balance").get( + d.warehouse + ): + bal_qty = periodic_data[d.item_code]["balance"][d.warehouse] qty_diff = d.qty_after_transaction - bal_qty else: qty_diff = d.actual_qty - if filters["value_quantity"] == 'Quantity': + if filters["value_quantity"] == "Quantity": value = qty_diff else: value = d.stock_value_difference # period-warehouse wise balance - periodic_data.setdefault(d.item_code, {}).setdefault('balance', {}).setdefault(d.warehouse, 0.0) + periodic_data.setdefault(d.item_code, {}).setdefault("balance", {}).setdefault(d.warehouse, 0.0) periodic_data.setdefault(d.item_code, {}).setdefault(period, {}).setdefault(d.warehouse, 0.0) - periodic_data[d.item_code]['balance'][d.warehouse] += value - periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]['balance'][d.warehouse] + periodic_data[d.item_code]["balance"][d.warehouse] += value + periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]["balance"][ + d.warehouse + ] return periodic_data + def get_data(filters): data = [] items = get_items(filters) @@ -229,14 +214,10 @@ def get_data(filters): return data + def get_chart_data(columns): labels = [d.get("label") for d in columns[5:]] - chart = { - "data": { - 'labels': labels, - 'datasets':[] - } - } + chart = {"data": {"labels": labels, "datasets": []}} chart["type"] = "line" return chart diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py index 32df5859375..f6c98f914d2 100644 --- a/erpnext/stock/report/stock_analytics/test_stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py @@ -1,14 +1,13 @@ import datetime -import unittest from frappe import _dict +from frappe.tests.utils import FrappeTestCase from erpnext.accounts.utils import get_fiscal_year from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges -from erpnext.tests.utils import ERPNextTestCase -class TestStockAnalyticsReport(ERPNextTestCase): +class TestStockAnalyticsReport(FrappeTestCase): def test_get_period_date_ranges(self): filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06") diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 6fd3fe7da48..99f820ecac6 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -12,21 +12,25 @@ from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_ac def execute(filters=None): if not erpnext.is_perpetual_inventory_enabled(filters.company): - frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") - .format(filters.company)) + frappe.throw( + _("Perpetual inventory required for the company {0} to view this report.").format( + filters.company + ) + ) data = get_data(filters) columns = get_columns(filters) return columns, data + def get_data(report_filters): data = [] filters = { "is_cancelled": 0, "company": report_filters.company, - "posting_date": ("<=", report_filters.as_on_date) + "posting_date": ("<=", report_filters.as_on_date), } currency_precision = get_currency_precision() or 2 @@ -43,18 +47,28 @@ def get_data(report_filters): return data + def get_stock_ledger_data(report_filters, filters): if report_filters.account: - warehouses = get_warehouses_based_on_account(report_filters.account, - report_filters.company) + warehouses = get_warehouses_based_on_account(report_filters.account, report_filters.company) filters["warehouse"] = ("in", warehouses) - return frappe.get_all("Stock Ledger Entry", filters=filters, - fields = ["name", "voucher_type", "voucher_no", - "sum(stock_value_difference) as stock_value", "posting_date", "posting_time"], - group_by = "voucher_type, voucher_no", - order_by = "posting_date ASC, posting_time ASC") + return frappe.get_all( + "Stock Ledger Entry", + filters=filters, + fields=[ + "name", + "voucher_type", + "voucher_no", + "sum(stock_value_difference) as stock_value", + "posting_date", + "posting_time", + ], + group_by="voucher_type, voucher_no", + order_by="posting_date ASC, posting_time ASC", + ) + def get_gl_data(report_filters, filters): if report_filters.account: @@ -62,17 +76,22 @@ def get_gl_data(report_filters, filters): else: stock_accounts = get_stock_accounts(report_filters.company) - filters.update({ - "account": ("in", stock_accounts) - }) + filters.update({"account": ("in", stock_accounts)}) if filters.get("warehouse"): del filters["warehouse"] - gl_entries = frappe.get_all("GL Entry", filters=filters, - fields = ["name", "voucher_type", "voucher_no", - "sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value"], - group_by = "voucher_type, voucher_no") + gl_entries = frappe.get_all( + "GL Entry", + filters=filters, + fields=[ + "name", + "voucher_type", + "voucher_no", + "sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value", + ], + group_by="voucher_type, voucher_no", + ) voucher_wise_gl_data = {} for d in gl_entries: @@ -81,6 +100,7 @@ def get_gl_data(report_filters, filters): return voucher_wise_gl_data + def get_columns(filters): return [ { @@ -88,46 +108,29 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Stock Ledger Entry", - "width": "80" - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date" - }, - { - "label": _("Posting Time"), - "fieldname": "posting_time", - "fieldtype": "Time" - }, - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": "110" + "width": "80", }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"}, + {"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"}, { "label": _("Voucher No"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": "110" - }, - { - "label": _("Stock Value"), - "fieldname": "stock_value", - "fieldtype": "Currency", - "width": "120" + "width": "110", }, + {"label": _("Stock Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": "120"}, { "label": _("Account Value"), "fieldname": "account_value", "fieldtype": "Currency", - "width": "120" + "width": "120", }, { "label": _("Difference Value"), "fieldname": "difference_value", "fieldtype": "Currency", - "width": "120" - } + "width": "120", + }, ] diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index a4608039310..261383d4a20 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -17,10 +17,11 @@ from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_va def execute(filters=None): is_reposting_item_valuation_in_progress() - if not filters: filters = {} + if not filters: + filters = {} - from_date = filters.get('from_date') - to_date = filters.get('to_date') + from_date = filters.get("from_date") + to_date = filters.get("to_date") if filters.get("company"): company_currency = erpnext.get_company_currency(filters.get("company")) @@ -32,8 +33,8 @@ def execute(filters=None): items = get_items(filters) sle = get_stock_ledger_entries(filters, items) - if filters.get('show_stock_ageing_data'): - filters['show_warehouse_wise_stock'] = True + if filters.get("show_stock_ageing_data"): + filters["show_warehouse_wise_stock"] = True item_wise_fifo_queue = FIFOSlots(filters, sle).generate() # if no stock ledger entry found return @@ -59,12 +60,12 @@ def execute(filters=None): item_reorder_qty = item_reorder_detail_map[item + warehouse]["warehouse_reorder_qty"] report_data = { - 'currency': company_currency, - 'item_code': item, - 'warehouse': warehouse, - 'company': company, - 'reorder_level': item_reorder_level, - 'reorder_qty': item_reorder_qty, + "currency": company_currency, + "item_code": item, + "warehouse": warehouse, + "company": company, + "reorder_level": item_reorder_level, + "reorder_qty": item_reorder_qty, } report_data.update(item_map[item]) report_data.update(qty_dict) @@ -72,21 +73,18 @@ def execute(filters=None): if include_uom: conversion_factors.setdefault(item, item_map[item].conversion_factor) - if filters.get('show_stock_ageing_data'): - fifo_queue = item_wise_fifo_queue[(item, warehouse)].get('fifo_queue') + if filters.get("show_stock_ageing_data"): + fifo_queue = item_wise_fifo_queue[(item, warehouse)].get("fifo_queue") - stock_ageing_data = { - 'average_age': 0, - 'earliest_age': 0, - 'latest_age': 0 - } + stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} if fifo_queue: fifo_queue = sorted(filter(_func, fifo_queue), key=_func) - if not fifo_queue: continue + if not fifo_queue: + continue - stock_ageing_data['average_age'] = get_average_age(fifo_queue, to_date) - stock_ageing_data['earliest_age'] = date_diff(to_date, fifo_queue[0][1]) - stock_ageing_data['latest_age'] = date_diff(to_date, fifo_queue[-1][1]) + stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date) + stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1]) + stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1]) report_data.update(stock_ageing_data) @@ -95,38 +93,130 @@ def execute(filters=None): add_additional_uom_columns(columns, data, include_uom, conversion_factors) return columns, data + def get_columns(filters): """return columns""" columns = [ - {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 100}, - {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90}, - {"label": _("Balance Qty"), "fieldname": "bal_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Balance Value"), "fieldname": "bal_val", "fieldtype": "Currency", "width": 100, "options": "currency"}, - {"label": _("Opening Qty"), "fieldname": "opening_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Opening Value"), "fieldname": "opening_val", "fieldtype": "Currency", "width": 110, "options": "currency"}, - {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("Balance Qty"), + "fieldname": "bal_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Balance Value"), + "fieldname": "bal_val", + "fieldtype": "Currency", + "width": 100, + "options": "currency", + }, + { + "label": _("Opening Qty"), + "fieldname": "opening_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Opening Value"), + "fieldname": "opening_val", + "fieldtype": "Currency", + "width": 110, + "options": "currency", + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80}, - {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80}, - {"label": _("Valuation Rate"), "fieldname": "val_rate", "fieldtype": "Currency", "width": 90, "convertible": "rate", "options": "currency"}, - {"label": _("Reorder Level"), "fieldname": "reorder_level", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Reorder Qty"), "fieldname": "reorder_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 100} + { + "label": _("Valuation Rate"), + "fieldname": "val_rate", + "fieldtype": "Currency", + "width": 90, + "convertible": "rate", + "options": "currency", + }, + { + "label": _("Reorder Level"), + "fieldname": "reorder_level", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Reorder Qty"), + "fieldname": "reorder_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, ] - if filters.get('show_stock_ageing_data'): - columns += [{'label': _('Average Age'), 'fieldname': 'average_age', 'width': 100}, - {'label': _('Earliest Age'), 'fieldname': 'earliest_age', 'width': 100}, - {'label': _('Latest Age'), 'fieldname': 'latest_age', 'width': 100}] + if filters.get("show_stock_ageing_data"): + columns += [ + {"label": _("Average Age"), "fieldname": "average_age", "width": 100}, + {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100}, + {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100}, + ] - if filters.get('show_variant_attributes'): - columns += [{'label': att_name, 'fieldname': att_name, 'width': 100} for att_name in get_variants_attributes()] + if filters.get("show_variant_attributes"): + columns += [ + {"label": att_name, "fieldname": att_name, "width": 100} + for att_name in get_variants_attributes() + ] return columns + def get_conditions(filters): conditions = "" if not filters.get("from_date"): @@ -141,28 +231,37 @@ def get_conditions(filters): conditions += " and sle.company = %s" % frappe.db.escape(filters.get("company")) if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", - filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) if filters.get("warehouse_type") and not filters.get("warehouse"): - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.warehouse_type = '%s' and sle.warehouse = wh.name)"%(filters.get("warehouse_type")) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.warehouse_type = '%s' and sle.warehouse = wh.name)" + % (filters.get("warehouse_type")) + ) return conditions + def get_stock_ledger_entries(filters, items): - item_conditions_sql = '' + item_conditions_sql = "" if items: - item_conditions_sql = ' and sle.item_code in ({})'\ - .format(', '.join(frappe.db.escape(i, percent=False) for i in items)) + item_conditions_sql = " and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i, percent=False) for i in items) + ) conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, @@ -171,8 +270,11 @@ def get_stock_ledger_entries(filters, items): `tabStock Ledger Entry` sle where sle.docstatus < 2 %s %s and is_cancelled = 0 - order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec - (item_conditions_sql, conditions), as_dict=1) + order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" + % (item_conditions_sql, conditions), # nosec + as_dict=1, + ) + def get_item_warehouse_map(filters, sle): iwb_map = {} @@ -184,13 +286,19 @@ def get_item_warehouse_map(filters, sle): for d in sle: key = (d.company, d.item_code, d.warehouse) if key not in iwb_map: - iwb_map[key] = frappe._dict({ - "opening_qty": 0.0, "opening_val": 0.0, - "in_qty": 0.0, "in_val": 0.0, - "out_qty": 0.0, "out_val": 0.0, - "bal_qty": 0.0, "bal_val": 0.0, - "val_rate": 0.0 - }) + iwb_map[key] = frappe._dict( + { + "opening_qty": 0.0, + "opening_val": 0.0, + "in_qty": 0.0, + "in_val": 0.0, + "out_qty": 0.0, + "out_val": 0.0, + "bal_qty": 0.0, + "bal_val": 0.0, + "val_rate": 0.0, + } + ) qty_dict = iwb_map[(d.company, d.item_code, d.warehouse)] @@ -201,9 +309,11 @@ def get_item_warehouse_map(filters, sle): value_diff = flt(d.stock_value_difference) - if d.posting_date < from_date or (d.posting_date == from_date - and d.voucher_type == "Stock Reconciliation" and - frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock"): + if d.posting_date < from_date or ( + d.posting_date == from_date + and d.voucher_type == "Stock Reconciliation" + and frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock" + ): qty_dict.opening_qty += qty_diff qty_dict.opening_val += value_diff @@ -223,6 +333,7 @@ def get_item_warehouse_map(filters, sle): return iwb_map + def filter_items_with_no_transactions(iwb_map, float_precision): for (company, item, warehouse) in sorted(iwb_map): qty_dict = iwb_map[(company, item, warehouse)] @@ -239,6 +350,7 @@ def filter_items_with_no_transactions(iwb_map, float_precision): return iwb_map + def get_items(filters): "Get items based on item code, item group or brand." conditions = [] @@ -247,15 +359,17 @@ def get_items(filters): else: if filters.get("item_group"): conditions.append(get_item_group_condition(filters.get("item_group"))) - if filters.get("brand"): # used in stock analytics report + if filters.get("brand"): # used in stock analytics report conditions.append("item.brand=%(brand)s") items = [] if conditions: - items = frappe.db.sql_list("""select name from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters) + items = frappe.db.sql_list( + """select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters + ) return items + def get_item_details(items, sle, filters): item_details = {} if not items: @@ -267,10 +381,13 @@ def get_item_details(items, sle, filters): cf_field = cf_join = "" if filters.get("include_uom"): cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" % frappe.db.escape(filters.get("include_uom")) + ) - res = frappe.db.sql(""" + res = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom %s from @@ -278,40 +395,57 @@ def get_item_details(items, sle, filters): %s where item.name in (%s) - """ % (cf_field, cf_join, ','.join(['%s'] *len(items))), items, as_dict=1) + """ + % (cf_field, cf_join, ",".join(["%s"] * len(items))), + items, + as_dict=1, + ) for item in res: item_details.setdefault(item.name, item) - if filters.get('show_variant_attributes', 0) == 1: + if filters.get("show_variant_attributes", 0) == 1: variant_values = get_variant_values_for(list(item_details)) item_details = {k: v.update(variant_values.get(k, {})) for k, v in iteritems(item_details)} return item_details + def get_item_reorder_details(items): item_reorder_details = frappe._dict() if items: - item_reorder_details = frappe.db.sql(""" + item_reorder_details = frappe.db.sql( + """ select parent, warehouse, warehouse_reorder_qty, warehouse_reorder_level from `tabItem Reorder` where parent in ({0}) - """.format(', '.join(frappe.db.escape(i, percent=False) for i in items)), as_dict=1) + """.format( + ", ".join(frappe.db.escape(i, percent=False) for i in items) + ), + as_dict=1, + ) return dict((d.parent + d.warehouse, d) for d in item_reorder_details) + def get_variants_attributes(): - '''Return all item variant attributes.''' - return [i.name for i in frappe.get_all('Item Attribute')] + """Return all item variant attributes.""" + return [i.name for i in frappe.get_all("Item Attribute")] + def get_variant_values_for(items): - '''Returns variant values for items.''' + """Returns variant values for items.""" attribute_map = {} - for attr in frappe.db.sql('''select parent, attribute, attribute_value + for attr in frappe.db.sql( + """select parent, attribute, attribute_value from `tabItem Variant Attribute` where parent in (%s) - ''' % ", ".join(["%s"] * len(items)), tuple(items), as_dict=1): - attribute_map.setdefault(attr['parent'], {}) - attribute_map[attr['parent']].update({attr['attribute']: attr['attribute_value']}) + """ + % ", ".join(["%s"] * len(items)), + tuple(items), + as_dict=1, + ): + attribute_map.setdefault(attr["parent"], {}) + attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]}) return attribute_map diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 81fa0458f29..b1fdaacac9d 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -41,19 +41,13 @@ def execute(filters=None): actual_qty += flt(sle.actual_qty, precision) stock_value += sle.stock_value_difference - if sle.voucher_type == 'Stock Reconciliation' and not sle.actual_qty: + if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: actual_qty = sle.qty_after_transaction stock_value = sle.stock_value - sle.update({ - "qty_after_transaction": actual_qty, - "stock_value": stock_value - }) + sle.update({"qty_after_transaction": actual_qty, "stock_value": stock_value}) - sle.update({ - "in_qty": max(sle.actual_qty, 0), - "out_qty": min(sle.actual_qty, 0) - }) + sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)}) if sle.serial_no: update_available_serial_nos(available_serial_nos, sle) @@ -66,6 +60,7 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data + def update_available_serial_nos(available_serial_nos, sle): serial_nos = get_serial_nos(sle.serial_no) key = (sle.item_code, sle.warehouse) @@ -85,45 +80,158 @@ def update_available_serial_nos(available_serial_nos, sle): else: existing_serial_no.append(sn) - sle.balance_serial_no = '\n'.join(existing_serial_no) + sle.balance_serial_no = "\n".join(existing_serial_no) + def get_columns(): columns = [ {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150}, - {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, - {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90}, - {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 150}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 150}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100}, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Balance Qty"), + "fieldname": "qty_after_transaction", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 150, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 150, + }, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Brand"), + "fieldname": "brand", + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, {"label": _("Description"), "fieldname": "description", "width": 200}, - {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, - {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, - {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, - {"label": _("Value Change"), "fieldname": "stock_value_difference", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, + { + "label": _("Incoming Rate"), + "fieldname": "incoming_rate", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Valuation Rate"), + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Balance Value"), + "fieldname": "stock_value", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + }, + { + "label": _("Value Change"), + "fieldname": "stock_value_difference", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + }, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, - {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, - {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, - {"label": _("Serial No"), "fieldname": "serial_no", "fieldtype": "Link", "options": "Serial No", "width": 100}, + { + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 100, + }, + { + "label": _("Batch"), + "fieldname": "batch_no", + "fieldtype": "Link", + "options": "Batch", + "width": 100, + }, + { + "label": _("Serial No"), + "fieldname": "serial_no", + "fieldtype": "Link", + "options": "Serial No", + "width": 100, + }, {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, - {"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, - {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110} + { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 100, + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 110, + }, ] return columns def get_stock_ledger_entries(filters, items): - item_conditions_sql = '' + item_conditions_sql = "" if items: - item_conditions_sql = 'and sle.item_code in ({})'\ - .format(', '.join(frappe.db.escape(i) for i in items)) + item_conditions_sql = "and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i) for i in items) + ) - sl_entries = frappe.db.sql(""" + sl_entries = frappe.db.sql( + """ SELECT concat_ws(" ", posting_date, posting_time) AS date, item_code, @@ -149,8 +257,12 @@ def get_stock_ledger_entries(filters, items): {item_conditions_sql} ORDER BY posting_date asc, posting_time asc, creation asc - """.format(sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql), - filters, as_dict=1) + """.format( + sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql + ), + filters, + as_dict=1, + ) return sl_entries @@ -167,8 +279,9 @@ def get_items(filters): items = [] if conditions: - items = frappe.db.sql_list("""select name from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters) + items = frappe.db.sql_list( + """select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters + ) return items @@ -183,10 +296,13 @@ def get_item_details(items, sl_entries, include_uom): cf_field = cf_join = "" if include_uom: cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" % frappe.db.escape(include_uom) + ) - res = frappe.db.sql(""" + res = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom {cf_field} from @@ -194,7 +310,12 @@ def get_item_details(items, sl_entries, include_uom): {cf_join} where item.name in ({item_codes}) - """.format(cf_field=cf_field, cf_join=cf_join, item_codes=','.join(['%s'] *len(items))), items, as_dict=1) + """.format( + cf_field=cf_field, cf_join=cf_join, item_codes=",".join(["%s"] * len(items)) + ), + items, + as_dict=1, + ) for item in res: item_details.setdefault(item.name, item) @@ -223,16 +344,20 @@ def get_opening_balance(filters, columns, sl_entries): return from erpnext.stock.stock_ledger import get_previous_sle - last_entry = get_previous_sle({ - "item_code": filters.item_code, - "warehouse_condition": get_warehouse_condition(filters.warehouse), - "posting_date": filters.from_date, - "posting_time": "00:00:00" - }) + + last_entry = get_previous_sle( + { + "item_code": filters.item_code, + "warehouse_condition": get_warehouse_condition(filters.warehouse), + "posting_date": filters.from_date, + "posting_time": "00:00:00", + } + ) # check if any SLEs are actually Opening Stock Reconciliation for sle in sl_entries: - if (sle.get("voucher_type") == "Stock Reconciliation" + if ( + sle.get("voucher_type") == "Stock Reconciliation" and sle.get("date").split()[0] == filters.from_date and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock" ): @@ -243,7 +368,7 @@ def get_opening_balance(filters, columns, sl_entries): "item_code": _("'Opening'"), "qty_after_transaction": last_entry.get("qty_after_transaction", 0), "valuation_rate": last_entry.get("valuation_rate", 0), - "stock_value": last_entry.get("stock_value", 0) + "stock_value": last_entry.get("stock_value", 0), } return row @@ -252,18 +377,22 @@ def get_opening_balance(filters, columns, sl_entries): def get_warehouse_condition(warehouse): warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) if warehouse_details: - return " exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt) + return ( + " exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) - return '' + return "" def get_item_group_condition(item_group): item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1) if item_group_details: - return "item.item_group in (select ig.name from `tabItem Group` ig \ - where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)"%(item_group_details.lft, - item_group_details.rgt) + return ( + "item.item_group in (select ig.name from `tabItem Group` ig \ + where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)" + % (item_group_details.lft, item_group_details.rgt) + ) - return '' + return "" diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 1b61863ce6a..98f0387a0fc 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -40,11 +40,7 @@ def get_stock_ledger_entries(filters): return frappe.get_all( "Stock Ledger Entry", fields=SLE_FIELDS, - filters={ - "item_code": filters.item_code, - "warehouse": filters.warehouse, - "is_cancelled": 0 - }, + filters={"item_code": filters.item_code, "warehouse": filters.warehouse, "is_cancelled": 0}, order_by="timestamp(posting_date, posting_time), creation", ) @@ -62,7 +58,7 @@ def add_invariant_check_fields(sles): fifo_value += qty * rate if sle.actual_qty < 0: - sle.consumption_rate = sle.stock_value_difference / sle.actual_qty + sle.consumption_rate = sle.stock_value_difference / sle.actual_qty balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference @@ -90,7 +86,7 @@ def add_invariant_check_fields(sles): sle.valuation_diff = ( sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None ) - sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value + sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value if idx > 0: sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value @@ -175,7 +171,6 @@ def get_columns(): "fieldtype": "Data", "label": "FIFO Queue", }, - { "fieldname": "fifo_queue_qty", "fieldtype": "Float", @@ -236,7 +231,6 @@ def get_columns(): "fieldtype": "Float", "label": "(I) Valuation Rate as per FIFO", }, - { "fieldname": "fifo_valuation_diff", "fieldtype": "Float", diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index a28b75250bf..49e797d6a30 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -32,8 +32,9 @@ def execute(filters=None): continue # item = item_map.setdefault(bin.item_code, get_item(bin.item_code)) - company = warehouse_company.setdefault(bin.warehouse, - frappe.db.get_value("Warehouse", bin.warehouse, "company")) + company = warehouse_company.setdefault( + bin.warehouse, frappe.db.get_value("Warehouse", bin.warehouse, "company") + ) if filters.brand and filters.brand != item.brand: continue @@ -59,10 +60,29 @@ def execute(filters=None): if reserved_qty_for_pos: bin.projected_qty -= reserved_qty_for_pos - data.append([item.name, item.item_name, item.description, item.item_group, item.brand, bin.warehouse, - item.stock_uom, bin.actual_qty, bin.planned_qty, bin.indented_qty, bin.ordered_qty, - bin.reserved_qty, bin.reserved_qty_for_production, bin.reserved_qty_for_sub_contract, reserved_qty_for_pos, - bin.projected_qty, re_order_level, re_order_qty, shortage_qty]) + data.append( + [ + item.name, + item.item_name, + item.description, + item.item_group, + item.brand, + bin.warehouse, + item.stock_uom, + bin.actual_qty, + bin.planned_qty, + bin.indented_qty, + bin.ordered_qty, + bin.reserved_qty, + bin.reserved_qty_for_production, + bin.reserved_qty_for_sub_contract, + reserved_qty_for_pos, + bin.projected_qty, + re_order_level, + re_order_qty, + shortage_qty, + ] + ) if include_uom: conversion_factors.append(item.conversion_factor) @@ -70,66 +90,180 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data + def get_columns(): return [ - {"label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 140}, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 140, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, {"label": _("Description"), "fieldname": "description", "width": 200}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 120}, - {"label": _("UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 100}, - {"label": _("Actual Qty"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Planned Qty"), "fieldname": "planned_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Requested Qty"), "fieldname": "indented_qty", "fieldtype": "Float", "width": 110, "convertible": "qty"}, - {"label": _("Ordered Qty"), "fieldname": "ordered_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reserved Qty"), "fieldname": "reserved_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reserved for Production"), "fieldname": "reserved_qty_for_production", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Reserved for Sub Contracting"), "fieldname": "reserved_qty_for_sub_contract", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Reserved for POS Transactions"), "fieldname": "reserved_qty_for_pos", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Projected Qty"), "fieldname": "projected_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reorder Level"), "fieldname": "re_order_level", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reorder Qty"), "fieldname": "re_order_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Shortage Qty"), "fieldname": "shortage_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"} + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Brand"), + "fieldname": "brand", + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, + }, + { + "label": _("Actual Qty"), + "fieldname": "actual_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Planned Qty"), + "fieldname": "planned_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Requested Qty"), + "fieldname": "indented_qty", + "fieldtype": "Float", + "width": 110, + "convertible": "qty", + }, + { + "label": _("Ordered Qty"), + "fieldname": "ordered_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved Qty"), + "fieldname": "reserved_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for Production"), + "fieldname": "reserved_qty_for_production", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for Sub Contracting"), + "fieldname": "reserved_qty_for_sub_contract", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for POS Transactions"), + "fieldname": "reserved_qty_for_pos", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Projected Qty"), + "fieldname": "projected_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reorder Level"), + "fieldname": "re_order_level", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reorder Qty"), + "fieldname": "re_order_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Shortage Qty"), + "fieldname": "shortage_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, ] + def get_bin_list(filters): conditions = [] if filters.item_code: - conditions.append("item_code = '%s' "%filters.item_code) + conditions.append("item_code = '%s' " % filters.item_code) if filters.warehouse: - warehouse_details = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions.append(" exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and bin.warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt)) + conditions.append( + " exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and bin.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) - bin_list = frappe.db.sql("""select item_code, warehouse, actual_qty, planned_qty, indented_qty, + bin_list = frappe.db.sql( + """select item_code, warehouse, actual_qty, planned_qty, indented_qty, ordered_qty, reserved_qty, reserved_qty_for_production, reserved_qty_for_sub_contract, projected_qty from tabBin bin {conditions} order by item_code, warehouse - """.format(conditions=" where " + " and ".join(conditions) if conditions else ""), as_dict=1) + """.format( + conditions=" where " + " and ".join(conditions) if conditions else "" + ), + as_dict=1, + ) return bin_list + def get_item_map(item_code, include_uom): """Optimization: get only the item doc and re_order_levels table""" condition = "" if item_code: - condition = 'and item_code = {0}'.format(frappe.db.escape(item_code, percent=False)) + condition = "and item_code = {0}".format(frappe.db.escape(item_code, percent=False)) cf_field = cf_join = "" if include_uom: cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%(include_uom)s" + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%(include_uom)s" + ) - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom{cf_field} from `tabItem` item {cf_join} @@ -137,16 +271,21 @@ def get_item_map(item_code, include_uom): and item.disabled=0 {condition} and (item.end_of_life > %(today)s or item.end_of_life is null or item.end_of_life='0000-00-00') - and exists (select name from `tabBin` bin where bin.item_code=item.name)"""\ - .format(cf_field=cf_field, cf_join=cf_join, condition=condition), - {"today": today(), "include_uom": include_uom}, as_dict=True) + and exists (select name from `tabBin` bin where bin.item_code=item.name)""".format( + cf_field=cf_field, cf_join=cf_join, condition=condition + ), + {"today": today(), "include_uom": include_uom}, + as_dict=True, + ) condition = "" if item_code: - condition = 'where parent={0}'.format(frappe.db.escape(item_code, percent=False)) + condition = "where parent={0}".format(frappe.db.escape(item_code, percent=False)) reorder_levels = frappe._dict() - for ir in frappe.db.sql("""select * from `tabItem Reorder` {condition}""".format(condition=condition), as_dict=1): + for ir in frappe.db.sql( + """select * from `tabItem Reorder` {condition}""".format(condition=condition), as_dict=1 + ): if ir.parent not in reorder_levels: reorder_levels[ir.parent] = [] diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py index a7b48356b8d..70f04da4753 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -12,12 +12,14 @@ def execute(filters=None): data = get_data(filters.warehouse) return columns, data + def validate_warehouse(filters): company = filters.company warehouse = filters.warehouse if not frappe.db.exists("Warehouse", {"name": warehouse, "company": company}): frappe.throw(_("Warehouse: {0} does not belong to {1}").format(warehouse, company)) + def get_columns(): columns = [ { @@ -25,49 +27,37 @@ def get_columns(): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 200 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Serial No Count"), - "fieldname": "total", - "fieldtype": "Float", - "width": 150 - }, - { - "label": _("Stock Qty"), - "fieldname": "stock_qty", - "fieldtype": "Float", - "width": 150 - }, - { - "label": _("Difference"), - "fieldname": "difference", - "fieldtype": "Float", - "width": 150 + "width": 200, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Serial No Count"), "fieldname": "total", "fieldtype": "Float", "width": 150}, + {"label": _("Stock Qty"), "fieldname": "stock_qty", "fieldtype": "Float", "width": 150}, + {"label": _("Difference"), "fieldname": "difference", "fieldtype": "Float", "width": 150}, ] return columns -def get_data(warehouse): - serial_item_list = frappe.get_all("Item", filters={ - 'has_serial_no': True, - }, fields=['item_code', 'item_name']) - status_list = ['Active', 'Expired'] +def get_data(warehouse): + serial_item_list = frappe.get_all( + "Item", + filters={ + "has_serial_no": True, + }, + fields=["item_code", "item_name"], + ) + + status_list = ["Active", "Expired"] data = [] for item in serial_item_list: - total_serial_no = frappe.db.count("Serial No", - filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}) + total_serial_no = frappe.db.count( + "Serial No", + filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}, + ) - actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], - filters={"warehouse": warehouse, "item_code": item.item_code}) + actual_qty = frappe.db.get_value( + "Bin", fieldname=["actual_qty"], filters={"warehouse": warehouse, "item_code": item.item_code} + ) # frappe.db.get_value returns null if no record exist. if not actual_qty: diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py index 11559aa2081..0a482fe90a9 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py @@ -21,11 +21,11 @@ def execute(filters=None): if consumed_details.get(item_code): for cd in consumed_details.get(item_code): - if (cd.voucher_no not in material_transfer_vouchers): + if cd.voucher_no not in material_transfer_vouchers: if cd.voucher_type in ["Delivery Note", "Sales Invoice"]: delivered_qty += abs(flt(cd.actual_qty)) delivered_amount += abs(flt(cd.stock_value_difference)) - elif cd.voucher_type!="Delivery Note": + elif cd.voucher_type != "Delivery Note": consumed_qty += abs(flt(cd.actual_qty)) consumed_amount += abs(flt(cd.stock_value_difference)) @@ -33,66 +33,98 @@ def execute(filters=None): total_qty += delivered_qty + consumed_qty total_amount += delivered_amount + consumed_amount - row = [cd.item_code, cd.item_name, cd.description, cd.stock_uom, \ - consumed_qty, consumed_amount, delivered_qty, delivered_amount, \ - total_qty, total_amount, ','.join(list(set(suppliers)))] + row = [ + cd.item_code, + cd.item_name, + cd.description, + cd.stock_uom, + consumed_qty, + consumed_amount, + delivered_qty, + delivered_amount, + total_qty, + total_amount, + ",".join(list(set(suppliers))), + ] data.append(row) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::100"] + \ - [_("Description") + "::150"] + [_("UOM") + ":Link/UOM:90"] + \ - [_("Consumed Qty") + ":Float:110"] + [_("Consumed Amount") + ":Currency:130"] + \ - [_("Delivered Qty") + ":Float:110"] + [_("Delivered Amount") + ":Currency:130"] + \ - [_("Total Qty") + ":Float:110"] + [_("Total Amount") + ":Currency:130"] + \ - [_("Supplier(s)") + "::250"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::100"] + + [_("Description") + "::150"] + + [_("UOM") + ":Link/UOM:90"] + + [_("Consumed Qty") + ":Float:110"] + + [_("Consumed Amount") + ":Currency:130"] + + [_("Delivered Qty") + ":Float:110"] + + [_("Delivered Amount") + ":Currency:130"] + + [_("Total Qty") + ":Float:110"] + + [_("Total Amount") + ":Currency:130"] + + [_("Supplier(s)") + "::250"] + ) return columns + def get_conditions(filters): conditions = "" values = [] - if filters.get('from_date') and filters.get('to_date'): + if filters.get("from_date") and filters.get("to_date"): conditions = "and sle.posting_date>=%s and sle.posting_date<=%s" - values = [filters.get('from_date'), filters.get('to_date')] + values = [filters.get("from_date"), filters.get("to_date")] return conditions, values + def get_consumed_details(filters): conditions, values = get_conditions(filters) consumed_details = {} - for d in frappe.db.sql("""select sle.item_code, i.item_name, i.description, + for d in frappe.db.sql( + """select sle.item_code, i.item_name, i.description, i.stock_uom, sle.actual_qty, sle.stock_value_difference, sle.voucher_no, sle.voucher_type from `tabStock Ledger Entry` sle, `tabItem` i - where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1): - consumed_details.setdefault(d.item_code, []).append(d) + where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" + % conditions, + values, + as_dict=1, + ): + consumed_details.setdefault(d.item_code, []).append(d) return consumed_details + def get_suppliers_details(filters): item_supplier_map = {} - supplier = filters.get('supplier') + supplier = filters.get("supplier") - for d in frappe.db.sql("""select pr.supplier, pri.item_code from + for d in frappe.db.sql( + """select pr.supplier, pri.item_code from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri where pr.name=pri.parent and pr.docstatus=1 and pri.item_code=(select name from `tabItem` where - is_stock_item=1 and name=pri.item_code)""", as_dict=1): - item_supplier_map.setdefault(d.item_code, []).append(d.supplier) + is_stock_item=1 and name=pri.item_code)""", + as_dict=1, + ): + item_supplier_map.setdefault(d.item_code, []).append(d.supplier) - for d in frappe.db.sql("""select pr.supplier, pri.item_code from + for d in frappe.db.sql( + """select pr.supplier, pri.item_code from `tabPurchase Invoice` pr, `tabPurchase Invoice Item` pri where pr.name=pri.parent and pr.docstatus=1 and ifnull(pr.update_stock, 0) = 1 and pri.item_code=(select name from `tabItem` - where is_stock_item=1 and name=pri.item_code)""", as_dict=1): - if d.item_code not in item_supplier_map: - item_supplier_map.setdefault(d.item_code, []).append(d.supplier) + where is_stock_item=1 and name=pri.item_code)""", + as_dict=1, + ): + if d.item_code not in item_supplier_map: + item_supplier_map.setdefault(d.item_code, []).append(d.supplier) if supplier: invalid_items = [] @@ -105,6 +137,9 @@ def get_suppliers_details(filters): return item_supplier_map + def get_material_transfer_vouchers(): - return frappe.db.sql_list("""select name from `tabStock Entry` where - purpose='Material Transfer' and docstatus=1""") + return frappe.db.sql_list( + """select name from `tabStock Entry` where + purpose='Material Transfer' and docstatus=1""" + ) diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index 1dcf863a9d0..4be492c0fd4 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -37,16 +37,21 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ }, ), ("Warehouse wise Item Balance Age and Value", {"_optional": True}), - ("Item Variant Details", {"item": "_Test Variant Item",}), - ("Total Stock Summary", {"group_by": "warehouse",}), + ( + "Item Variant Details", + { + "item": "_Test Variant Item", + }, + ), + ( + "Total Stock Summary", + { + "group_by": "warehouse", + }, + ), ("Batch Item Expiry Status", {}), ("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}), - ("Stock Ledger Invariant Check", - { - "warehouse": "_Test Warehouse - _TC", - "item": "_Test Item" - } - ), + ("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}), ] OPTIONAL_FILTERS = { diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.py b/erpnext/stock/report/total_stock_summary/total_stock_summary.py index 6f27558b887..21529da2a12 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py @@ -15,6 +15,7 @@ def execute(filters=None): return columns, stock + def get_columns(): columns = [ _("Company") + ":Link/Company:250", @@ -26,13 +27,16 @@ def get_columns(): return columns + def get_total_stock(filters): conditions = "" columns = "" if filters.get("group_by") == "Warehouse": if filters.get("company"): - conditions += " AND warehouse.company = %s" % frappe.db.escape(filters.get("company"), percent=False) + conditions += " AND warehouse.company = %s" % frappe.db.escape( + filters.get("company"), percent=False + ) conditions += " GROUP BY ledger.warehouse, item.item_code" columns += "'' as company, ledger.warehouse" @@ -40,7 +44,8 @@ def get_total_stock(filters): conditions += " GROUP BY warehouse.company, item.item_code" columns += " warehouse.company, '' as warehouse" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT %s, item.item_code, @@ -53,4 +58,6 @@ def get_total_stock(filters): INNER JOIN `tabWarehouse` warehouse ON warehouse.name = ledger.warehouse WHERE - ledger.actual_qty != 0 %s""" % (columns, conditions)) + ledger.actual_qty != 0 %s""" + % (columns, conditions) + ) diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index 294edb7378b..10d951b7bd4 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -22,7 +22,8 @@ from erpnext.stock.utils import is_reposting_item_valuation_in_progress def execute(filters=None): is_reposting_item_valuation_in_progress() - if not filters: filters = {} + if not filters: + filters = {} validate_filters(filters) @@ -40,7 +41,8 @@ def execute(filters=None): item_value = {} for (company, item, warehouse) in sorted(iwb_map): - if not item_map.get(item): continue + if not item_map.get(item): + continue row = [] qty_dict = iwb_map[(company, item, warehouse)] @@ -51,13 +53,13 @@ def execute(filters=None): total_stock_value += qty_dict.bal_val if wh.name == warehouse else 0.00 item_balance[(item, item_map[item]["item_group"])].append(row) - item_value.setdefault((item, item_map[item]["item_group"]),[]) + item_value.setdefault((item, item_map[item]["item_group"]), []) item_value[(item, item_map[item]["item_group"])].append(total_stock_value) - # sum bal_qty by item for (item, item_group), wh_balance in iteritems(item_balance): - if not item_ageing.get(item): continue + if not item_ageing.get(item): + continue total_stock_value = sum(item_value[(item, item_group)]) row = [item, item_group, total_stock_value] @@ -82,17 +84,19 @@ def execute(filters=None): add_warehouse_column(columns, warehouse_list) return columns, data + def get_columns(filters): """return columns""" columns = [ - _("Item")+":Link/Item:180", - _("Item Group")+"::100", - _("Value")+":Currency:100", - _("Age")+":Float:60", + _("Item") + ":Link/Item:180", + _("Item Group") + "::100", + _("Value") + ":Currency:100", + _("Age") + ":Float:60", ] return columns + def validate_filters(filters): if not (filters.get("item_code") or filters.get("warehouse")): sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) @@ -101,11 +105,12 @@ def validate_filters(filters): if not filters.get("company"): filters["company"] = frappe.defaults.get_user_default("Company") + def get_warehouse_list(filters): from frappe.core.doctype.user_permission.user_permission import get_permitted_documents - condition = '' - user_permitted_warehouse = get_permitted_documents('Warehouse') + condition = "" + user_permitted_warehouse = get_permitted_documents("Warehouse") value = () if user_permitted_warehouse: condition = "and name in %s" @@ -114,13 +119,20 @@ def get_warehouse_list(filters): condition = "and name = %s" value = filters.get("warehouse") - return frappe.db.sql("""select name + return frappe.db.sql( + """select name from `tabWarehouse` where is_group = 0 - {condition}""".format(condition=condition), value, as_dict=1) + {condition}""".format( + condition=condition + ), + value, + as_dict=1, + ) + def add_warehouse_column(columns, warehouse_list): if len(warehouse_list) > 1: - columns += [_("Total Qty")+":Int:50"] + columns += [_("Total Qty") + ":Int:50"] for wh in warehouse_list: - columns += [_(wh.name)+":Int:54"] + columns += [_(wh.name) + ":Int:54"] diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 35cad2ba305..9a7d8bbfe42 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -16,16 +16,20 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, frappe.db.auto_commit_on_many_writes = 1 if allow_negative_stock: - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + existing_allow_negative_stock = frappe.db.get_value( + "Stock Settings", None, "allow_negative_stock" + ) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - item_warehouses = frappe.db.sql(""" + item_warehouses = frappe.db.sql( + """ select distinct item_code, warehouse from (select item_code, warehouse from tabBin union select item_code, warehouse from `tabStock Ledger Entry`) a - """) + """ + ) for d in item_warehouses: try: repost_stock(d[0], d[1], allow_zero_rate, only_actual, only_bin, allow_negative_stock) @@ -34,11 +38,20 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, frappe.db.rollback() if allow_negative_stock: - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) + frappe.db.set_value( + "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock + ) frappe.db.auto_commit_on_many_writes = 0 -def repost_stock(item_code, warehouse, allow_zero_rate=False, - only_actual=False, only_bin=False, allow_negative_stock=False): + +def repost_stock( + item_code, + warehouse, + allow_zero_rate=False, + only_actual=False, + only_bin=False, + allow_negative_stock=False, +): if not only_bin: repost_actual_qty(item_code, warehouse, allow_zero_rate, allow_negative_stock) @@ -48,35 +61,42 @@ def repost_stock(item_code, warehouse, allow_zero_rate=False, "reserved_qty": get_reserved_qty(item_code, warehouse), "indented_qty": get_indented_qty(item_code, warehouse), "ordered_qty": get_ordered_qty(item_code, warehouse), - "planned_qty": get_planned_qty(item_code, warehouse) + "planned_qty": get_planned_qty(item_code, warehouse), } if only_bin: - qty_dict.update({ - "actual_qty": get_balance_qty_from_sle(item_code, warehouse) - }) + qty_dict.update({"actual_qty": get_balance_qty_from_sle(item_code, warehouse)}) update_bin_qty(item_code, warehouse, qty_dict) + def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False): - create_repost_item_valuation_entry({ - "item_code": item_code, - "warehouse": warehouse, - "posting_date": "1900-01-01", - "posting_time": "00:01", - "allow_negative_stock": allow_negative_stock, - "allow_zero_rate": allow_zero_rate - }) + create_repost_item_valuation_entry( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": "1900-01-01", + "posting_time": "00:01", + "allow_negative_stock": allow_negative_stock, + "allow_zero_rate": allow_zero_rate, + } + ) + def get_balance_qty_from_sle(item_code, warehouse): - balance_qty = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry` + balance_qty = frappe.db.sql( + """select qty_after_transaction from `tabStock Ledger Entry` where item_code=%s and warehouse=%s and is_cancelled=0 order by posting_date desc, posting_time desc, creation desc - limit 1""", (item_code, warehouse)) + limit 1""", + (item_code, warehouse), + ) return flt(balance_qty[0][0]) if balance_qty else 0.0 + def get_reserved_qty(item_code, warehouse): - reserved_qty = frappe.db.sql(""" + reserved_qty = frappe.db.sql( + """ select sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty) / so_item_qty)) from @@ -116,58 +136,76 @@ def get_reserved_qty(item_code, warehouse): ) tab where so_item_qty >= so_item_delivered_qty - """, (item_code, warehouse, item_code, warehouse)) + """, + (item_code, warehouse, item_code, warehouse), + ) return flt(reserved_qty[0][0]) if reserved_qty else 0 + def get_indented_qty(item_code, warehouse): # Ordered Qty is always maintained in stock UOM - inward_qty = frappe.db.sql(""" + inward_qty = frappe.db.sql( + """ select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer') and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr.status!='Stopped' and mr.docstatus=1 - """, (item_code, warehouse)) + """, + (item_code, warehouse), + ) inward_qty = flt(inward_qty[0][0]) if inward_qty else 0 - outward_qty = frappe.db.sql(""" + outward_qty = frappe.db.sql( + """ select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s and mr.material_request_type = 'Material Issue' and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr.status!='Stopped' and mr.docstatus=1 - """, (item_code, warehouse)) + """, + (item_code, warehouse), + ) outward_qty = flt(outward_qty[0][0]) if outward_qty else 0 requested_qty = inward_qty - outward_qty return requested_qty + def get_ordered_qty(item_code, warehouse): - ordered_qty = frappe.db.sql(""" + ordered_qty = frappe.db.sql( + """ select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor) from `tabPurchase Order Item` po_item, `tabPurchase Order` po where po_item.item_code=%s and po_item.warehouse=%s and po_item.qty > po_item.received_qty and po_item.parent=po.name and po.status not in ('Closed', 'Delivered') and po.docstatus=1 - and po_item.delivered_by_supplier = 0""", (item_code, warehouse)) + and po_item.delivered_by_supplier = 0""", + (item_code, warehouse), + ) return flt(ordered_qty[0][0]) if ordered_qty else 0 + def get_planned_qty(item_code, warehouse): - planned_qty = frappe.db.sql(""" + planned_qty = frappe.db.sql( + """ select sum(qty - produced_qty) from `tabWork Order` where production_item = %s and fg_warehouse = %s and status not in ("Stopped", "Completed", "Closed") - and docstatus=1 and qty > produced_qty""", (item_code, warehouse)) + and docstatus=1 and qty > produced_qty""", + (item_code, warehouse), + ) return flt(planned_qty[0][0]) if planned_qty else 0 def update_bin_qty(item_code, warehouse, qty_dict=None): from erpnext.stock.utils import get_bin + bin = get_bin(item_code, warehouse) mismatch = False for field, value in qty_dict.items(): @@ -181,41 +219,54 @@ def update_bin_qty(item_code, warehouse, qty_dict=None): bin.db_update() bin.clear_cache() -def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, posting_time=None, - fiscal_year=None): - if not posting_date: posting_date = nowdate() - if not posting_time: posting_time = nowtime() - condition = " and item.name='%s'" % item_code.replace("'", "\'") if item_code else "" +def set_stock_balance_as_per_serial_no( + item_code=None, posting_date=None, posting_time=None, fiscal_year=None +): + if not posting_date: + posting_date = nowdate() + if not posting_time: + posting_time = nowtime() - bin = frappe.db.sql("""select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom + condition = " and item.name='%s'" % item_code.replace("'", "'") if item_code else "" + + bin = frappe.db.sql( + """select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom from `tabBin` bin, tabItem item - where bin.item_code = item.name and item.has_serial_no = 1 %s""" % condition) + where bin.item_code = item.name and item.has_serial_no = 1 %s""" + % condition + ) for d in bin: - 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])) + 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]), + ) - sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry` + 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])) + order by posting_date desc limit 1""", + (d[0], d[1]), + ) sle_dict = { - 'doctype' : 'Stock Ledger Entry', - 'item_code' : d[0], - 'warehouse' : d[1], - 'transaction_date' : nowdate(), - 'posting_date' : posting_date, - 'posting_time' : posting_time, - 'voucher_type' : 'Stock Reconciliation (Manual)', - 'voucher_no' : '', - 'voucher_detail_no' : '', - 'actual_qty' : flt(serial_nos[0][0]) - flt(d[2]), - 'stock_uom' : d[3], - 'incoming_rate' : sle and flt(serial_nos[0][0]) > flt(d[2]) and flt(sle[0][0]) or 0, - 'company' : sle and cstr(sle[0][1]) or 0, - 'batch_no' : '', - 'serial_no' : '' + "doctype": "Stock Ledger Entry", + "item_code": d[0], + "warehouse": d[1], + "transaction_date": nowdate(), + "posting_date": posting_date, + "posting_time": posting_time, + "voucher_type": "Stock Reconciliation (Manual)", + "voucher_no": "", + "voucher_detail_no": "", + "actual_qty": flt(serial_nos[0][0]) - flt(d[2]), + "stock_uom": d[3], + "incoming_rate": sle and flt(serial_nos[0][0]) > flt(d[2]) and flt(sle[0][0]) or 0, + "company": sle and cstr(sle[0][1]) or 0, + "batch_no": "", + "serial_no": "", } sle_doc = frappe.get_doc(sle_dict) @@ -224,18 +275,19 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin sle_doc.insert() args = sle_dict.copy() - args.update({ - "sle_id": sle_doc.name - }) + args.update({"sle_id": sle_doc.name}) update_bin(args) - create_repost_item_valuation_entry({ - "item_code": d[0], - "warehouse": d[1], - "posting_date": posting_date, - "posting_time": posting_time - }) + create_repost_item_valuation_entry( + { + "item_code": d[0], + "warehouse": d[1], + "posting_date": posting_date, + "posting_time": posting_time, + } + ) + def reset_serial_no_status_and_warehouse(serial_nos=None): if not serial_nos: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 47a97c47fe5..d2c10018ba5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -3,6 +3,7 @@ import copy import json +from typing import Set, Tuple import frappe from frappe import _ @@ -19,28 +20,32 @@ from erpnext.stock.utils import ( ) -class NegativeStockError(frappe.ValidationError): pass +class NegativeStockError(frappe.ValidationError): + pass + + class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): - """ Create SL entries from SL entry dicts + """Create SL entries from SL entry dicts - args: - - allow_negative_stock: disable negative stock valiations if true - - via_landed_cost_voucher: landed cost voucher cancels and reposts - entries of purchase document. This flag is used to identify if - cancellation and repost is happening via landed cost voucher, in - such cases certain validations need to be ignored (like negative - stock) + args: + - allow_negative_stock: disable negative stock valiations if true + - via_landed_cost_voucher: landed cost voucher cancels and reposts + entries of purchase document. This flag is used to identify if + cancellation and repost is happening via landed cost voucher, in + such cases certain validations need to be ignored (like negative + stock) """ from erpnext.controllers.stock_controller import future_sle_exists + if sl_entries: cancel = sl_entries[0].get("is_cancelled") if cancel: validate_cancellation(sl_entries) - set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) + set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no")) args = get_args_for_future_sle(sl_entries[0]) future_sle_exists(args, sl_entries) @@ -50,19 +55,21 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc validate_serial_no(sle) if cancel: - sle['actual_qty'] = -flt(sle.get('actual_qty')) + sle["actual_qty"] = -flt(sle.get("actual_qty")) - if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): - sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, - sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) - sle['incoming_rate'] = 0.0 + if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"): + sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel( + sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no + ) + sle["incoming_rate"] = 0.0 - if sle['actual_qty'] > 0 and not sle.get('incoming_rate'): - sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, - sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) - sle['outgoing_rate'] = 0.0 + if sle["actual_qty"] > 0 and not sle.get("incoming_rate"): + sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel( + sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no + ) + sle["outgoing_rate"] = 0.0 - if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation": + if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation": sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() @@ -71,13 +78,16 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc # preserve previous_qty_after_transaction for qty reposting args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") - is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item") if is_stock_item: bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) update_bin_qty(bin_name, args) else: - frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + frappe.msgprint( + _("Item {0} ignored since it is not a stock item").format(args.get("item_code")) + ) + def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False): if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": @@ -89,28 +99,35 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou # Reposts only current voucher SL Entries # Updates valuation rate, stock value, stock queue for current transaction - update_entries_after({ - "item_code": args.get('item_code'), - "warehouse": args.get('warehouse'), - "posting_date": args.get("posting_date"), - "posting_time": args.get("posting_time"), - "voucher_type": args.get("voucher_type"), - "voucher_no": args.get("voucher_no"), - "sle_id": args.get('name'), - "creation": args.get('creation') - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + update_entries_after( + { + "item_code": args.get("item_code"), + "warehouse": args.get("warehouse"), + "posting_date": args.get("posting_date"), + "posting_time": args.get("posting_time"), + "voucher_type": args.get("voucher_type"), + "voucher_no": args.get("voucher_no"), + "sle_id": args.get("name"), + "creation": args.get("creation"), + }, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) # update qty in future sle and Validate negative qty update_qty_in_future_sle(args, allow_negative_stock) def get_args_for_future_sle(row): - return frappe._dict({ - 'voucher_type': row.get('voucher_type'), - 'voucher_no': row.get('voucher_no'), - 'posting_date': row.get('posting_date'), - 'posting_time': row.get('posting_time') - }) + return frappe._dict( + { + "voucher_type": row.get("voucher_type"), + "voucher_no": row.get("voucher_no"), + "posting_date": row.get("posting_date"), + "posting_time": row.get("posting_time"), + } + ) + def validate_serial_no(sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -118,79 +135,110 @@ def validate_serial_no(sle): for sn in get_serial_nos(sle.serial_no): args = copy.deepcopy(sle) args.serial_no = sn - args.warehouse = '' + args.warehouse = "" vouchers = [] - for row in get_stock_ledger_entries(args, '>'): + for row in get_stock_ledger_entries(args, ">"): voucher_type = frappe.bold(row.voucher_type) voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f'{voucher_type} {voucher_no}') + vouchers.append(f"{voucher_type} {voucher_no}") if vouchers: serial_no = frappe.bold(sn) - msg = (f'''The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.''' + '

    • ') + msg = ( + f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. + The list of the transactions are as below.""" + + "

      • " + ) - msg += '
      • '.join(vouchers) - msg += '
      ' + msg += "
    • ".join(vouchers) + msg += "
    " - title = 'Cannot Submit' if not sle.get('is_cancelled') else 'Cannot Cancel' + title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel" frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) + def validate_cancellation(args): if args[0].get("is_cancelled"): - repost_entry = frappe.db.get_value("Repost Item Valuation", { - 'voucher_type': args[0].voucher_type, - 'voucher_no': args[0].voucher_no, - 'docstatus': 1 - }, ['name', 'status'], as_dict=1) + repost_entry = frappe.db.get_value( + "Repost Item Valuation", + {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1}, + ["name", "status"], + as_dict=1, + ) if repost_entry: - if repost_entry.status == 'In Progress': - frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet.")) - if repost_entry.status == 'Queued': + if repost_entry.status == "In Progress": + frappe.throw( + _( + "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." + ) + ) + if repost_entry.status == "Queued": doc = frappe.get_doc("Repost Item Valuation", repost_entry.name) + doc.status = "Skipped" doc.flags.ignore_permissions = True doc.cancel() - doc.delete() + def set_as_cancel(voucher_type, voucher_no): - frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1, + frappe.db.sql( + """update `tabStock Ledger Entry` set is_cancelled=1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", - (now(), frappe.session.user, voucher_type, voucher_no)) + (now(), frappe.session.user, voucher_type, voucher_no), + ) + def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): args["doctype"] = "Stock Ledger Entry" sle = frappe.get_doc(args) sle.flags.ignore_permissions = 1 - sle.allow_negative_stock=allow_negative_stock + sle.allow_negative_stock = allow_negative_stock sle.via_landed_cost_voucher = via_landed_cost_voucher sle.submit() return sle -def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): + +def repost_future_sle( + args=None, + doc=None, + voucher_type=None, + voucher_no=None, + allow_negative_stock=None, + via_landed_cost_voucher=False, +): if not args and voucher_type and voucher_no: args = get_items_to_be_repost(voucher_type, voucher_no, doc) distinct_item_warehouses = get_distinct_item_warehouse(args, doc) + affected_transactions = get_affected_transactions(doc) i = get_current_index(doc) or 0 while i < len(args): - obj = update_entries_after({ - "item_code": args[i].get('item_code'), - "warehouse": args[i].get('warehouse'), - "posting_date": args[i].get('posting_date'), - "posting_time": args[i].get('posting_time'), - "creation": args[i].get("creation"), - "distinct_item_warehouses": distinct_item_warehouses - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + obj = update_entries_after( + { + "item_code": args[i].get("item_code"), + "warehouse": args[i].get("warehouse"), + "posting_date": args[i].get("posting_date"), + "posting_time": args[i].get("posting_time"), + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses, + }, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) + affected_transactions.update(obj.affected_transactions) - distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True + distinct_item_warehouses[ + (args[i].get("item_code"), args[i].get("warehouse")) + ].reposting_status = True if obj.new_items_found: for item_wh, data in iteritems(distinct_item_warehouses): - if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + if ("args_idx" not in data and not data.reposting_status) or ( + data.sle_changed and data.reposting_status + ): data.args_idx = len(args) args.append(data.sle) elif data.sle_changed and not data.reposting_status: @@ -200,82 +248,116 @@ def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, a i += 1 if doc and i % 2 == 0: - update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + update_args_in_repost_item_valuation( + doc, i, args, distinct_item_warehouses, affected_transactions + ) if doc and args: - update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + update_args_in_repost_item_valuation( + doc, i, args, distinct_item_warehouses, affected_transactions + ) -def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses): - frappe.db.set_value(doc.doctype, doc.name, { - 'items_to_be_repost': json.dumps(args, default=str), - 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str), - 'current_index': index - }) - frappe.db.commit() +def update_args_in_repost_item_valuation( + doc, index, args, distinct_item_warehouses, affected_transactions +): + doc.db_set( + { + "items_to_be_repost": json.dumps(args, default=str), + "distinct_item_and_warehouse": json.dumps( + {str(k): v for k, v in distinct_item_warehouses.items()}, default=str + ), + "current_index": index, + "affected_transactions": frappe.as_json(affected_transactions), + } + ) + + if not frappe.flags.in_test: + frappe.db.commit() + + frappe.publish_realtime( + "item_reposting_progress", + {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index}, + ) - frappe.publish_realtime('item_reposting_progress', { - 'name': doc.name, - 'items_to_be_repost': json.dumps(args, default=str), - 'current_index': index - }) def get_items_to_be_repost(voucher_type, voucher_no, doc=None): if doc and doc.items_to_be_repost: return json.loads(doc.items_to_be_repost) or [] - return frappe.db.get_all("Stock Ledger Entry", + return frappe.db.get_all( + "Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], order_by="creation asc", - group_by="item_code, warehouse" + group_by="item_code, warehouse", ) + def get_distinct_item_warehouse(args=None, doc=None): distinct_item_warehouses = {} if doc and doc.distinct_item_and_warehouse: distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse) - distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()} + distinct_item_warehouses = { + frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items() + } else: for i, d in enumerate(args): - distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ - "reposting_status": False, - "sle": d, - "args_idx": i - })) + distinct_item_warehouses.setdefault( + (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i}) + ) return distinct_item_warehouses + +def get_affected_transactions(doc) -> Set[Tuple[str, str]]: + if not doc.affected_transactions: + return set() + + transactions = frappe.parse_json(doc.affected_transactions) + return {tuple(transaction) for transaction in transactions} + + def get_current_index(doc=None): if doc and doc.current_index: return doc.current_index + class update_entries_after(object): """ - update valution rate and qty after transaction - from the current time-bucket onwards + update valution rate and qty after transaction + from the current time-bucket onwards - :param args: args as dict + :param args: args as dict - args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00" - } + args = { + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00" + } """ - def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1): + + def __init__( + self, + args, + allow_zero_rate=False, + allow_negative_stock=None, + via_landed_cost_voucher=False, + verbose=1, + ): self.exceptions = {} self.verbose = verbose self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher - self.allow_negative_stock = allow_negative_stock \ - or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + self.allow_negative_stock = allow_negative_stock or cint( + frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + ) self.args = frappe._dict(args) self.item_code = args.get("item_code") if self.args.sle_id: - self.args['name'] = self.args.sle_id + self.args["name"] = self.args.sle_id self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() @@ -283,34 +365,36 @@ class update_entries_after(object): self.new_items_found = False self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) + self.affected_transactions: Set[Tuple[str, str]] = set() self.data = frappe._dict() self.initialize_previous_data(self.args) self.build() def get_precision(self): - company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency") - self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), - currency=company_base_currency) + company_base_currency = frappe.get_cached_value("Company", self.company, "default_currency") + self.precision = get_field_precision( + frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), currency=company_base_currency + ) def initialize_previous_data(self, args): """ - Get previous sl entries for current item for each related warehouse - and assigns into self.data dict + Get previous sl entries for current item for each related warehouse + and assigns into self.data dict - :Data Structure: + :Data Structure: - self.data = { - warehouse1: { - 'previus_sle': {}, - 'qty_after_transaction': 10, - 'valuation_rate': 100, - 'stock_value': 1000, - 'prev_stock_value': 1000, - 'stock_queue': '[[10, 100]]', - 'stock_value_difference': 1000 - } - } + self.data = { + warehouse1: { + 'previus_sle': {}, + 'qty_after_transaction': 10, + 'valuation_rate': 100, + 'stock_value': 1000, + 'prev_stock_value': 1000, + 'stock_queue': '[[10, 100]]', + 'stock_value_difference': 1000 + } + } """ self.data.setdefault(args.warehouse, frappe._dict()) @@ -321,11 +405,13 @@ class update_entries_after(object): for key in ("qty_after_transaction", "valuation_rate", "stock_value"): setattr(warehouse_dict, key, flt(previous_sle.get(key))) - warehouse_dict.update({ - "prev_stock_value": previous_sle.stock_value or 0.0, - "stock_queue": json.loads(previous_sle.stock_queue or "[]"), - "stock_value_difference": 0.0 - }) + warehouse_dict.update( + { + "prev_stock_value": previous_sle.stock_value or 0.0, + "stock_queue": json.loads(previous_sle.stock_queue or "[]"), + "stock_value_difference": 0.0, + } + ) def build(self): from erpnext.controllers.stock_controller import future_sle_exists @@ -358,9 +444,10 @@ class update_entries_after(object): self.process_sle(sle) def get_sle_against_current_voucher(self): - self.args['time_format'] = '%H:%i:%s' + self.args["time_format"] = "%H:%i:%s" - return frappe.db.sql(""" + return frappe.db.sql( + """ select *, timestamp(posting_date, posting_time) as "timestamp" from @@ -374,22 +461,29 @@ class update_entries_after(object): order by creation ASC for update - """, self.args, as_dict=1) + """, + self.args, + as_dict=1, + ) def get_future_entries_to_fix(self): # includes current entry! - args = self.data[self.args.warehouse].previous_sle \ - or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse}) + args = self.data[self.args.warehouse].previous_sle or frappe._dict( + {"item_code": self.item_code, "warehouse": self.args.warehouse} + ) return list(self.get_sle_after_datetime(args)) def get_dependent_entries_to_fix(self, entries_to_fix, sle): - dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no, - excluded_sle=sle.name) + dependant_sle = get_sle_by_voucher_detail_no( + sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name + ) if not dependant_sle: return entries_to_fix - elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: + elif ( + dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse + ): return entries_to_fix elif dependant_sle.item_code != self.item_code: self.update_distinct_item_warehouses(dependant_sle) @@ -401,14 +495,14 @@ class update_entries_after(object): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({ - "sle": dependant_sle - }) + val = frappe._dict({"sle": dependant_sle}) if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val self.new_items_found = True else: - existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + existing_sle_posting_date = ( + self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + ) if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): val.sle_changed = True self.distinct_item_warehouses[key] = val @@ -417,18 +511,20 @@ class update_entries_after(object): def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) - args = self.data[dependant_sle.warehouse].previous_sle \ - or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse}) + args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict( + {"item_code": self.item_code, "warehouse": dependant_sle.warehouse} + ) future_sle_for_dependant = list(self.get_sle_after_datetime(args)) entries_to_fix.extend(future_sle_for_dependant) - return sorted(entries_to_fix, key=lambda k: k['timestamp']) + return sorted(entries_to_fix, key=lambda k: k["timestamp"]) def process_sle(self, sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos # previous sle data for this warehouse self.wh_data = self.data[sle.warehouse] + self.affected_transactions.add((sle.voucher_type, sle.voucher_no)) if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock): # validate negative stock for serialized items, fifo valuation @@ -447,24 +543,32 @@ class update_entries_after(object): if sle.voucher_type == "Stock Reconciliation": self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) else: - if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) if self.valuation_method != "Moving Average": self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] else: if self.valuation_method == "Moving Average": self.get_moving_average_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) else: self.get_fifo_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) - self.wh_data.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)) + self.wh_data.stock_value = sum( + (flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue) + ) # rounding as per precision self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision) @@ -479,7 +583,7 @@ class update_entries_after(object): sle.stock_value = self.wh_data.stock_value sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_value_difference = stock_value_difference - sle.doctype="Stock Ledger Entry" + sle.doctype = "Stock Ledger Entry" frappe.get_doc(sle).db_update() if not self.args.get("sle_id"): @@ -487,8 +591,8 @@ class update_entries_after(object): def validate_negative_stock(self, sle): """ - validate negative stock for entries current datetime onwards - will not consider cancelled entries + validate negative stock for entries current datetime onwards + will not consider cancelled entries """ diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) @@ -517,13 +621,24 @@ class update_entries_after(object): self.recalculate_amounts_in_stock_entry(sle.voucher_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return - elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): + elif sle.voucher_type in ( + "Purchase Receipt", + "Purchase Invoice", + "Delivery Note", + "Sales Invoice", + ): if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"): from erpnext.controllers.sales_and_purchase_return import ( get_rate_for_return, # don't move this import to top ) - rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, - voucher_detail_no=sle.voucher_detail_no, sle = sle) + + rate = get_rate_for_return( + sle.voucher_type, + sle.voucher_no, + sle.item_code, + voucher_detail_no=sle.voucher_detail_no, + sle=sle, + ) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): rate_field = "valuation_rate" @@ -531,8 +646,9 @@ class update_entries_after(object): rate_field = "incoming_rate" # check in item table - item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item", - sle.voucher_detail_no, ["item_code", rate_field]) + item_code, incoming_rate = frappe.db.get_value( + sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field] + ) if item_code == sle.item_code: rate = incoming_rate @@ -542,15 +658,18 @@ class update_entries_after(object): else: ref_doctype = "Purchase Receipt Item Supplied" - rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no, - "item_code": sle.item_code}, rate_field) + rate = frappe.db.get_value( + ref_doctype, + {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, + rate_field, + ) return rate def update_outgoing_rate_on_transaction(self, sle): """ - Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return - In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount + Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return + In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount """ if sle.actual_qty and sle.voucher_detail_no: outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty) @@ -580,24 +699,33 @@ class update_entries_after(object): # Update item's incoming rate on transaction item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code") if item_code == sle.item_code: - frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate) + frappe.db.set_value( + sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate + ) else: # packed item - frappe.db.set_value("Packed Item", + frappe.db.set_value( + "Packed Item", {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, - "incoming_rate", outgoing_rate) + "incoming_rate", + outgoing_rate, + ) def update_rate_on_purchase_receipt(self, sle, outgoing_rate): if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): - frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate) + frappe.db.set_value( + sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate + ) else: - frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate) + frappe.db.set_value( + "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate + ) # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice - if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes': + if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == "Yes": doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) - for d in (doc.items + doc.supplied_items): + for d in doc.items + doc.supplied_items: d.db_update() def get_serialized_values(self, sle): @@ -624,29 +752,34 @@ class update_entries_after(object): new_stock_qty = self.wh_data.qty_after_transaction + actual_qty if new_stock_qty > 0: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change + new_stock_value = ( + self.wh_data.qty_after_transaction * self.wh_data.valuation_rate + ) + stock_value_change if new_stock_value >= 0: # calculate new valuation rate only if stock value is positive # else it remains the same as that of previous entry self.wh_data.valuation_rate = new_stock_value / new_stock_qty if not self.wh_data.valuation_rate and sle.voucher_detail_no: - allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) + allow_zero_rate = self.check_if_allow_zero_valuation_rate( + sle.voucher_type, sle.voucher_detail_no + ) if not allow_zero_rate: self.wh_data.valuation_rate = self.get_fallback_rate(sle) def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company - all_serial_nos = frappe.get_all("Serial No", - fields=["purchase_rate", "name", "company"], - filters = {'name': ('in', serial_nos)}) + all_serial_nos = frappe.get_all( + "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)} + ) - incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company) + incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company == sle.company) # Get rate for serial nos which has been transferred to other company - invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company] + invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company] for serial_no in invalid_serial_nos: - incoming_rate = frappe.db.sql(""" + incoming_rate = frappe.db.sql( + """ select incoming_rate from `tabStock Ledger Entry` where @@ -660,7 +793,9 @@ class update_entries_after(object): ) order by posting_date desc limit 1 - """, (sle.company, serial_no, serial_no+'\n%', '%\n'+serial_no, '%\n'+serial_no+'\n%')) + """, + (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"), + ) incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0 @@ -674,15 +809,17 @@ class update_entries_after(object): if flt(self.wh_data.qty_after_transaction) <= 0: self.wh_data.valuation_rate = sle.incoming_rate else: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \ - (actual_qty * sle.incoming_rate) + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + ( + actual_qty * sle.incoming_rate + ) self.wh_data.valuation_rate = new_stock_value / new_stock_qty elif sle.outgoing_rate: if new_stock_qty: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \ - (actual_qty * sle.outgoing_rate) + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + ( + actual_qty * sle.outgoing_rate + ) self.wh_data.valuation_rate = new_stock_value / new_stock_qty else: @@ -697,7 +834,9 @@ class update_entries_after(object): # Get valuation rate from previous SLE or Item master, if item does not have the # allow zero valuration rate flag set if not self.wh_data.valuation_rate and sle.voucher_detail_no: - allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) + allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate( + sle.voucher_type, sle.voucher_detail_no + ) if not allow_zero_valuation_rate: self.wh_data.valuation_rate = self.get_fallback_rate(sle) @@ -711,24 +850,26 @@ class update_entries_after(object): self.wh_data.stock_queue.append([0, 0]) # last row has the same rate, just updated the qty - if self.wh_data.stock_queue[-1][1]==incoming_rate: + if self.wh_data.stock_queue[-1][1] == incoming_rate: self.wh_data.stock_queue[-1][0] += actual_qty else: # Item has a positive balance qty, add new entry if self.wh_data.stock_queue[-1][0] > 0: self.wh_data.stock_queue.append([actual_qty, incoming_rate]) - else: # negative balance qty + else: # negative balance qty qty = self.wh_data.stock_queue[-1][0] + actual_qty - if qty > 0: # new balance qty is positive + if qty > 0: # new balance qty is positive self.wh_data.stock_queue[-1] = [qty, incoming_rate] - else: # new balance qty is still negative, maintain same rate + else: # new balance qty is still negative, maintain same rate self.wh_data.stock_queue[-1][0] = qty else: qty_to_pop = abs(actual_qty) while qty_to_pop: if not self.wh_data.stock_queue: # Get valuation rate from last sle if exists or from valuation rate field in item master - allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) + allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate( + sle.voucher_type, sle.voucher_detail_no + ) if not allow_zero_valuation_rate: _rate = self.get_fallback_rate(sle) else: @@ -744,12 +885,9 @@ class update_entries_after(object): index = i break - # If no entry found with outgoing rate, collapse stack + # If no entry found with outgoing rate, consume as per FIFO if index is None: # nosemgrep - new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate - new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop - self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]] - break + index = 0 else: index = 0 @@ -771,14 +909,18 @@ class update_entries_after(object): batch[0] = batch[0] - qty_to_pop qty_to_pop = 0 - stock_value = _round_off_if_near_zero(sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))) + stock_value = _round_off_if_near_zero( + sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)) + ) stock_qty = _round_off_if_near_zero(sum((flt(batch[0]) for batch in self.wh_data.stock_queue))) if stock_qty: self.wh_data.valuation_rate = stock_value / flt(stock_qty) if not self.wh_data.stock_queue: - self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]) + self.wh_data.stock_queue.append( + [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate] + ) def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no): ref_item_dt = "" @@ -795,10 +937,16 @@ class update_entries_after(object): def get_fallback_rate(self, sle) -> float: """When exact incoming rate isn't available use any of other "average" rates as fallback. - This should only get used for negative stock.""" - return get_valuation_rate(sle.item_code, sle.warehouse, - sle.voucher_type, sle.voucher_no, self.allow_zero_rate, - currency=erpnext.get_company_currency(sle.company), company=sle.company) + This should only get used for negative stock.""" + return get_valuation_rate( + sle.item_code, + sle.warehouse, + sle.voucher_type, + sle.voucher_no, + self.allow_zero_rate, + currency=erpnext.get_company_currency(sle.company), + company=sle.company, + ) def get_sle_before_datetime(self, args): """get previous stock ledger entry before current time-bucket""" @@ -815,18 +963,27 @@ class update_entries_after(object): for warehouse, exceptions in iteritems(self.exceptions): deficiency = min(e["diff"] for e in exceptions) - if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in - frappe.local.flags.currently_saving): + if ( + exceptions[0]["voucher_type"], + exceptions[0]["voucher_no"], + ) in frappe.local.flags.currently_saving: msg = _("{0} units of {1} needed in {2} to complete this transaction.").format( - abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]), - frappe.get_desk_link('Warehouse', warehouse)) + abs(deficiency), + frappe.get_desk_link("Item", exceptions[0]["item_code"]), + frappe.get_desk_link("Warehouse", warehouse), + ) else: - msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( - abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]), - frappe.get_desk_link('Warehouse', warehouse), - exceptions[0]["posting_date"], exceptions[0]["posting_time"], - frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"])) + msg = _( + "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." + ).format( + abs(deficiency), + frappe.get_desk_link("Item", exceptions[0]["item_code"]), + frappe.get_desk_link("Warehouse", warehouse), + exceptions[0]["posting_date"], + exceptions[0]["posting_time"], + frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]), + ) if msg: msg_list.append(msg) @@ -834,7 +991,7 @@ class update_entries_after(object): if msg_list: message = "\n\n".join(msg_list) if self.verbose: - frappe.throw(message, NegativeStockError, title='Insufficient Stock') + frappe.throw(message, NegativeStockError, title=_("Insufficient Stock")) else: raise NegativeStockError(message) @@ -843,17 +1000,16 @@ class update_entries_after(object): for warehouse, data in self.data.items(): bin_name = get_or_make_bin(self.item_code, warehouse) - frappe.db.set_value('Bin', bin_name, { - "valuation_rate": data.valuation_rate, - "actual_qty": data.qty_after_transaction, - "stock_value": data.stock_value - }) + updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} + if data.valuation_rate is not None: + updated_values["valuation_rate"] = data.valuation_rate + frappe.db.set_value("Bin", bin_name, updated_values) def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" - args['time_format'] = '%H:%i:%s' + args["time_format"] = "%H:%i:%s" if not args.get("posting_date"): args["posting_date"] = "1900-01-01" if not args.get("posting_time"): @@ -864,7 +1020,8 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): voucher_no = args.get("voucher_no") voucher_condition = f"and voucher_no != '{voucher_no}'" - sle = frappe.db.sql(""" + sle = frappe.db.sql( + """ select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` where item_code = %(item_code)s @@ -874,32 +1031,48 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) order by timestamp(posting_date, posting_time) desc, creation desc limit 1 - for update""".format(voucher_condition=voucher_condition), args, as_dict=1) + for update""".format( + voucher_condition=voucher_condition + ), + args, + as_dict=1, + ) return sle[0] if sle else frappe._dict() + def get_previous_sle(args, for_update=False): """ - get the last sle on or before the current time-bucket, - to get actual qty before transaction, this function - is called from various transaction like stock entry, reco etc + get the last sle on or before the current time-bucket, + to get actual qty before transaction, this function + is called from various transaction like stock entry, reco etc - args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00", - "sle": "name of reference Stock Ledger Entry" - } + args = { + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00", + "sle": "name of reference Stock Ledger Entry" + } """ args["name"] = args.get("sle", None) or "" sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) return sle and sle[0] or {} -def get_stock_ledger_entries(previous_sle, operator=None, - order="desc", limit=None, for_update=False, debug=False, check_serial_no=True): + +def get_stock_ledger_entries( + previous_sle, + operator=None, + order="desc", + limit=None, + for_update=False, + debug=False, + check_serial_no=True, +): """get stock ledger entries filtered by specific posting datetime conditions""" - conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator) + conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( + operator + ) if previous_sle.get("warehouse"): conditions += " and warehouse = %(warehouse)s" elif previous_sle.get("warehouse_condition"): @@ -908,15 +1081,21 @@ def get_stock_ledger_entries(previous_sle, operator=None, if check_serial_no and previous_sle.get("serial_no"): # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no")))) serial_no = previous_sle.get("serial_no") - conditions += (""" and + conditions += ( + """ and ( serial_no = {0} or serial_no like {1} or serial_no like {2} or serial_no like {3} ) - """).format(frappe.db.escape(serial_no), frappe.db.escape('{}\n%'.format(serial_no)), - frappe.db.escape('%\n{}'.format(serial_no)), frappe.db.escape('%\n{}\n%'.format(serial_no))) + """ + ).format( + frappe.db.escape(serial_no), + frappe.db.escape("{}\n%".format(serial_no)), + frappe.db.escape("%\n{}".format(serial_no)), + frappe.db.escape("%\n{}\n%".format(serial_no)), + ) if not previous_sle.get("posting_date"): previous_sle["posting_date"] = "1900-01-01" @@ -926,34 +1105,59 @@ def get_stock_ledger_entries(previous_sle, operator=None, if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" - return frappe.db.sql(""" + return frappe.db.sql( + """ select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` where item_code = %%(item_code)s and is_cancelled = 0 %(conditions)s order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s - %(limit)s %(for_update)s""" % { + %(limit)s %(for_update)s""" + % { "conditions": conditions, "limit": limit or "", "for_update": for_update and "for update" or "", - "order": order - }, previous_sle, as_dict=1, debug=debug) + "order": order, + }, + previous_sle, + as_dict=1, + debug=debug, + ) + def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): - return frappe.db.get_value('Stock Ledger Entry', - {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]}, - ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'], - as_dict=1) + return frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]}, + [ + "item_code", + "warehouse", + "posting_date", + "posting_time", + "timestamp(posting_date, posting_time) as timestamp", + ], + as_dict=1, + ) -def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, - allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True): + +def get_valuation_rate( + item_code, + warehouse, + voucher_type, + voucher_no, + allow_zero_rate=False, + currency=None, + company=None, + raise_error_if_no_rate=True, +): if not company: - company = frappe.get_cached_value("Warehouse", warehouse, "company") + company = frappe.get_cached_value("Warehouse", warehouse, "company") # Get valuation rate from last sle for the same item and warehouse - last_valuation_rate = frappe.db.sql("""select valuation_rate + last_valuation_rate = frappe.db.sql( + """select valuation_rate from `tabStock Ledger Entry` force index (item_warehouse) where item_code = %s @@ -961,18 +1165,23 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, AND valuation_rate >= 0 AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type)) + order by posting_date desc, posting_time desc, name desc limit 1""", + (item_code, warehouse, voucher_no, voucher_type), + ) if not last_valuation_rate: # Get valuation rate from last sle for the item against any warehouse - last_valuation_rate = frappe.db.sql("""select valuation_rate + last_valuation_rate = frappe.db.sql( + """select valuation_rate from `tabStock Ledger Entry` force index (item_code) where item_code = %s AND valuation_rate > 0 AND is_cancelled = 0 AND NOT(voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type)) + order by posting_date desc, posting_time desc, name desc limit 1""", + (item_code, voucher_no, voucher_type), + ) if last_valuation_rate: return flt(last_valuation_rate[0][0]) @@ -987,19 +1196,37 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, if not valuation_rate: # try in price list - valuation_rate = frappe.db.get_value('Item Price', - dict(item_code=item_code, buying=1, currency=currency), - 'price_list_rate') + valuation_rate = frappe.db.get_value( + "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate" + ) - if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \ - and cint(erpnext.is_perpetual_inventory_enabled(company)): + if ( + not allow_zero_rate + and not valuation_rate + and raise_error_if_no_rate + and cint(erpnext.is_perpetual_inventory_enabled(company)) + ): frappe.local.message_log = [] form_link = get_link_to_form("Item", item_code) - message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no) + message = _( + "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." + ).format(form_link, voucher_type, voucher_no) message += "

    " + _("Here are the options to proceed:") - solutions = "
  • " + _("If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.").format(voucher_type) + "
  • " - solutions += "
  • " + _("If not, you can Cancel / Submit this entry") + " {0} ".format(frappe.bold("after")) + _("performing either one below:") + "
  • " + solutions = ( + "
  • " + + _( + "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table." + ).format(voucher_type) + + "
  • " + ) + solutions += ( + "
  • " + + _("If not, you can Cancel / Submit this entry") + + " {0} ".format(frappe.bold("after")) + + _("performing either one below:") + + "
  • " + ) sub_solutions = "
    • " + _("Create an incoming stock transaction for the Item.") + "
    • " sub_solutions += "
    • " + _("Mention Valuation Rate in the Item master.") + "
    " msg = message + solutions + sub_solutions + "" @@ -1008,6 +1235,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate + def update_qty_in_future_sle(args, allow_negative_stock=False): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" datetime_limit_condition = "" @@ -1024,7 +1252,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabStock Ledger Entry` set qty_after_transaction = qty_after_transaction + {qty_shift} where @@ -1039,10 +1268,15 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): ) ) {datetime_limit_condition} - """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args) + """.format( + qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition + ), + args, + ) validate_negative_qty_in_future_sle(args, allow_negative_stock) + def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): @@ -1054,8 +1288,9 @@ def get_stock_reco_qty_shift(args): stock_reco_qty_shift = flt(args.actual_qty) else: # reco is being submitted - last_balance = get_previous_sle_of_current_voucher(args, - exclude_current_voucher=True).get("qty_after_transaction") + last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get( + "qty_after_transaction" + ) if last_balance is not None: stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) @@ -1064,10 +1299,12 @@ def get_stock_reco_qty_shift(args): return stock_reco_qty_shift + def get_next_stock_reco(args): """Returns next nearest stock reconciliaton's details.""" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, posting_date, posting_time, creation, voucher_no from @@ -1085,7 +1322,11 @@ def get_next_stock_reco(args): ) ) limit 1 - """, args, as_dict=1) + """, + args, + as_dict=1, + ) + def get_datetime_limit_condition(detail): return f""" @@ -1097,9 +1338,11 @@ def get_datetime_limit_condition(detail): ) )""" + def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): - allow_negative_stock = cint(allow_negative_stock) \ - or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + allow_negative_stock = cint(allow_negative_stock) or cint( + frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + ) if allow_negative_stock: return @@ -1108,32 +1351,40 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): neg_sle = get_future_sle_with_negative_qty(args) if neg_sle: - message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + message = _( + "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." + ).format( abs(neg_sle[0]["qty_after_transaction"]), - frappe.get_desk_link('Item', args.item_code), - frappe.get_desk_link('Warehouse', args.warehouse), - neg_sle[0]["posting_date"], neg_sle[0]["posting_time"], - frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"])) - - frappe.throw(message, NegativeStockError, title='Insufficient Stock') + frappe.get_desk_link("Item", args.item_code), + frappe.get_desk_link("Warehouse", args.warehouse), + neg_sle[0]["posting_date"], + neg_sle[0]["posting_time"], + frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]), + ) + frappe.throw(message, NegativeStockError, title=_("Insufficient Stock")) if not args.batch_no: return neg_batch_sle = get_future_sle_with_negative_batch_qty(args) if neg_batch_sle: - message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + message = _( + "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." + ).format( abs(neg_batch_sle[0]["cumulative_total"]), - frappe.get_desk_link('Batch', args.batch_no), - frappe.get_desk_link('Warehouse', args.warehouse), - neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"], - frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"])) - frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch") + frappe.get_desk_link("Batch", args.batch_no), + frappe.get_desk_link("Warehouse", args.warehouse), + neg_batch_sle[0]["posting_date"], + neg_batch_sle[0]["posting_time"], + frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]), + ) + frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch")) def get_future_sle_with_negative_qty(args): - return frappe.db.sql(""" + return frappe.db.sql( + """ select qty_after_transaction, posting_date, posting_time, voucher_type, voucher_no @@ -1147,11 +1398,15 @@ def get_future_sle_with_negative_qty(args): and qty_after_transaction < 0 order by timestamp(posting_date, posting_time) asc limit 1 - """, args, as_dict=1) + """, + args, + as_dict=1, + ) def get_future_sle_with_negative_batch_qty(args): - batch_ledger = frappe.db.sql(""" + batch_ledger = frappe.db.sql( + """ select posting_date, posting_time, voucher_type, voucher_no, actual_qty from `tabStock Ledger Entry` @@ -1161,7 +1416,10 @@ def get_future_sle_with_negative_batch_qty(args): and batch_no=%(batch_no)s and is_cancelled = 0 order by timestamp(posting_date, posting_time), creation - """, args, as_dict=1) + """, + args, + as_dict=1, + ) cumulative_total = 0.0 current_posting_datetime = get_datetime(str(args.posting_date) + " " + str(args.posting_time)) @@ -1170,16 +1428,18 @@ def get_future_sle_with_negative_batch_qty(args): if cumulative_total > -1e-6: continue - if (get_datetime(str(entry.posting_date) + " " + str(entry.posting_time)) - >= current_posting_datetime): + if ( + get_datetime(str(entry.posting_date) + " " + str(entry.posting_time)) + >= current_posting_datetime + ): entry.cumulative_total = cumulative_total return [entry] def _round_off_if_near_zero(number: float, precision: int = 6) -> float: - """ Rounds off the number to zero only if number is close to zero for decimal - specified in precision. Precision defaults to 6. + """Rounds off the number to zero only if number is close to zero for decimal + specified in precision. Precision defaults to 6. """ if abs(0.0 - flt(number)) < (1.0 / (10**precision)): return 0.0 diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index b8bdf39301e..98eaecf7907 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -12,8 +12,13 @@ from six import string_types import erpnext -class InvalidWarehouseCompany(frappe.ValidationError): pass -class PendingRepostingError(frappe.ValidationError): pass +class InvalidWarehouseCompany(frappe.ValidationError): + pass + + +class PendingRepostingError(frappe.ValidationError): + pass + def get_stock_value_from_bin(warehouse=None, item_code=None): values = {} @@ -26,22 +31,27 @@ def get_stock_value_from_bin(warehouse=None, item_code=None): and w2.lft between w1.lft and w1.rgt ) """ - values['warehouse'] = warehouse + values["warehouse"] = warehouse if item_code: conditions += " and `tabBin`.item_code = %(item_code)s" - values['item_code'] = item_code + values["item_code"] = item_code - query = """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1 - and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s""" % conditions + query = ( + """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1 + and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s""" + % conditions + ) stock_value = frappe.db.sql(query, values) return stock_value + def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): - if not posting_date: posting_date = nowdate() + if not posting_date: + posting_date = nowdate() values, condition = [posting_date], "" @@ -63,13 +73,19 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): values.append(item_code) condition += " AND item_code = %s" - stock_ledger_entries = frappe.db.sql(""" + stock_ledger_entries = frappe.db.sql( + """ SELECT item_code, stock_value, name, warehouse FROM `tabStock Ledger Entry` sle WHERE posting_date <= %s {0} and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC - """.format(condition), values, as_dict=1) + """.format( + condition + ), + values, + as_dict=1, + ) sle_map = {} for sle in stock_ledger_entries: @@ -78,23 +94,32 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): return sum(sle_map.values()) + @frappe.whitelist() -def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None, - with_valuation_rate=False, with_serial_no=False): +def get_stock_balance( + item_code, + warehouse, + posting_date=None, + posting_time=None, + with_valuation_rate=False, + with_serial_no=False, +): """Returns stock balance quantity at given warehouse on given posting date or current date. If `with_valuation_rate` is True, will return tuple (qty, rate)""" from erpnext.stock.stock_ledger import get_previous_sle - if posting_date is None: posting_date = nowdate() - if posting_time is None: posting_time = nowtime() + if posting_date is None: + posting_date = nowdate() + if posting_time is None: + posting_time = nowtime() args = { "item_code": item_code, - "warehouse":warehouse, + "warehouse": warehouse, "posting_date": posting_date, - "posting_time": posting_time + "posting_time": posting_time, } last_entry = get_previous_sle(args) @@ -103,33 +128,41 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None if with_serial_no: serial_nos = get_serial_nos_data_after_transactions(args) - return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) - if last_entry else (0.0, 0.0, None)) + return ( + (last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) + if last_entry + else (0.0, 0.0, None) + ) else: - return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) + return ( + (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) + ) else: return last_entry.qty_after_transaction if last_entry else 0.0 + def get_serial_nos_data_after_transactions(args): from pypika import CustomFunction serial_nos = set() args = frappe._dict(args) - sle = frappe.qb.DocType('Stock Ledger Entry') - Timestamp = CustomFunction('timestamp', ['date', 'time']) + sle = frappe.qb.DocType("Stock Ledger Entry") + Timestamp = CustomFunction("timestamp", ["date", "time"]) - stock_ledger_entries = frappe.qb.from_( - sle - ).select( - 'serial_no','actual_qty' - ).where( - (sle.item_code == args.item_code) - & (sle.warehouse == args.warehouse) - & (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time)) - & (sle.is_cancelled == 0) - ).orderby( - sle.posting_date, sle.posting_time, sle.creation - ).run(as_dict=1) + stock_ledger_entries = ( + frappe.qb.from_(sle) + .select("serial_no", "actual_qty") + .where( + (sle.item_code == args.item_code) + & (sle.warehouse == args.warehouse) + & ( + Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time) + ) + & (sle.is_cancelled == 0) + ) + .orderby(sle.posting_date, sle.posting_time, sle.creation) + .run(as_dict=1) + ) for stock_ledger_entry in stock_ledger_entries: changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no) @@ -138,12 +171,15 @@ def get_serial_nos_data_after_transactions(args): else: serial_nos.difference_update(changed_serial_no) - return '\n'.join(serial_nos) + return "\n".join(serial_nos) + def get_serial_nos_data(serial_nos): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + return get_serial_nos(serial_nos) + @frappe.whitelist() def get_latest_stock_qty(item_code, warehouse=None): values, condition = [item_code], "" @@ -160,37 +196,48 @@ def get_latest_stock_qty(item_code, warehouse=None): values.append(warehouse) condition += " AND warehouse = %s" - actual_qty = frappe.db.sql("""select sum(actual_qty) from tabBin - where item_code=%s {0}""".format(condition), values)[0][0] + actual_qty = frappe.db.sql( + """select sum(actual_qty) from tabBin + where item_code=%s {0}""".format( + condition + ), + values, + )[0][0] return actual_qty def get_latest_stock_balance(): bin_map = {} - for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value - FROM tabBin""", as_dict=1): - bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value)) + for d in frappe.db.sql( + """SELECT item_code, warehouse, stock_value as stock_value + FROM tabBin""", + as_dict=1, + ): + bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value)) return bin_map + def get_bin(item_code, warehouse): bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin: bin_obj = _create_bin(item_code, warehouse) else: - bin_obj = frappe.get_doc('Bin', bin, for_update=True) + bin_obj = frappe.get_doc("Bin", bin, for_update=True) bin_obj.flags.ignore_permissions = True return bin_obj -def get_or_make_bin(item_code: str , warehouse: str) -> str: - bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) + +def get_or_make_bin(item_code: str, warehouse: str) -> str: + bin_record = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin_record: bin_obj = _create_bin(item_code, warehouse) bin_record = bin_obj.name return bin_record + def _create_bin(item_code, warehouse): """Create a bin and take care of concurrent inserts.""" @@ -206,20 +253,24 @@ def _create_bin(item_code, warehouse): return bin_obj + def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): """WARNING: This function is deprecated. Inline this function instead of using it.""" from erpnext.stock.doctype.bin.bin import update_stock - is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + + is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item") if is_stock_item: bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher) else: frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + @frappe.whitelist() def get_incoming_rate(args, raise_error_if_no_rate=True): """Get Incoming Rate based on valuation method""" from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate + if isinstance(args, string_types): args = json.loads(args) @@ -229,37 +280,53 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): else: valuation_method = get_valuation_method(args.get("item_code")) previous_sle = get_previous_sle(args) - if valuation_method == 'FIFO': + if valuation_method == "FIFO": if previous_sle: - previous_stock_queue = json.loads(previous_sle.get('stock_queue', '[]') or '[]') - in_rate = get_fifo_rate(previous_stock_queue, args.get("qty") or 0) if previous_stock_queue else 0 - elif valuation_method == 'Moving Average': - in_rate = previous_sle.get('valuation_rate') or 0 + previous_stock_queue = json.loads(previous_sle.get("stock_queue", "[]") or "[]") + in_rate = ( + get_fifo_rate(previous_stock_queue, args.get("qty") or 0) if previous_stock_queue else 0 + ) + elif valuation_method == "Moving Average": + in_rate = previous_sle.get("valuation_rate") or 0 if not in_rate: - voucher_no = args.get('voucher_no') or args.get('name') - in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), - args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), - currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), - raise_error_if_no_rate=raise_error_if_no_rate) + voucher_no = args.get("voucher_no") or args.get("name") + in_rate = get_valuation_rate( + args.get("item_code"), + args.get("warehouse"), + args.get("voucher_type"), + voucher_no, + args.get("allow_zero_valuation"), + currency=erpnext.get_company_currency(args.get("company")), + company=args.get("company"), + raise_error_if_no_rate=raise_error_if_no_rate, + ) return flt(in_rate) + def get_avg_purchase_rate(serial_nos): """get average value of serial numbers""" serial_nos = get_valid_serial_nos(serial_nos) - return flt(frappe.db.sql("""select avg(purchase_rate) from `tabSerial No` - where name in (%s)""" % ", ".join(["%s"] * len(serial_nos)), - tuple(serial_nos))[0][0]) + return flt( + frappe.db.sql( + """select avg(purchase_rate) from `tabSerial No` + where name in (%s)""" + % ", ".join(["%s"] * len(serial_nos)), + tuple(serial_nos), + )[0][0] + ) + def get_valuation_method(item_code): """get valuation method from item or default""" - val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True) + val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True) if not val_method: val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO" return val_method + def get_fifo_rate(previous_stock_queue, qty): """get FIFO (average) Rate from Queue""" if flt(qty) >= 0: @@ -286,10 +353,11 @@ def get_fifo_rate(previous_stock_queue, qty): return outgoing_cost / available_qty_for_outgoing -def get_valid_serial_nos(sr_nos, qty=0, item_code=''): + +def get_valid_serial_nos(sr_nos, qty=0, item_code=""): """split serial nos, validate and return list of valid serial nos""" # TODO: remove duplicates in client side - serial_nos = cstr(sr_nos).strip().replace(',', '\n').split('\n') + serial_nos = cstr(sr_nos).strip().replace(",", "\n").split("\n") valid_serial_nos = [] for val in serial_nos: @@ -305,19 +373,29 @@ def get_valid_serial_nos(sr_nos, qty=0, item_code=''): return valid_serial_nos + def validate_warehouse_company(warehouse, company): warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company", cache=True) if warehouse_company and warehouse_company != company: - frappe.throw(_("Warehouse {0} does not belong to company {1}").format(warehouse, company), - InvalidWarehouseCompany) + frappe.throw( + _("Warehouse {0} does not belong to company {1}").format(warehouse, company), + InvalidWarehouseCompany, + ) + def is_group_warehouse(warehouse): if frappe.db.get_value("Warehouse", warehouse, "is_group", cache=True): frappe.throw(_("Group node warehouse is not allowed to select for transactions")) + def validate_disabled_warehouse(warehouse): if frappe.db.get_value("Warehouse", warehouse, "disabled", cache=True): - frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse))) + frappe.throw( + _("Disabled Warehouse {0} cannot be used for this transaction.").format( + get_link_to_form("Warehouse", warehouse) + ) + ) + def update_included_uom_in_report(columns, result, include_uom, conversion_factors): if not include_uom or not conversion_factors: @@ -335,11 +413,14 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto convertible_columns.setdefault(key, d.get("convertible")) # Add new column to show qty/rate as per the selected UOM - columns.insert(idx+1, { - 'label': "{0} (per {1})".format(d.get("label"), include_uom), - 'fieldname': "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)), - 'fieldtype': 'Currency' if d.get("convertible") == 'rate' else 'Float' - }) + columns.insert( + idx + 1, + { + "label": "{0} (per {1})".format(d.get("label"), include_uom), + "fieldname": "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)), + "fieldtype": "Currency" if d.get("convertible") == "rate" else "Float", + }, + ) update_dict_values = [] for row_idx, row in enumerate(result): @@ -351,13 +432,13 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto if not conversion_factors[row_idx]: conversion_factors[row_idx] = 1 - if convertible_columns.get(key) == 'rate': + if convertible_columns.get(key) == "rate": new_value = flt(value) * conversion_factors[row_idx] else: new_value = flt(value) / conversion_factors[row_idx] if not is_dict_obj: - row.insert(key+1, new_value) + row.insert(key + 1, new_value) else: new_key = "{0}_{1}".format(key, frappe.scrub(include_uom)) update_dict_values.append([row, new_key, new_value]) @@ -366,11 +447,17 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto row, key, value = data row[key] = value + def get_available_serial_nos(args): - return frappe.db.sql(""" SELECT name from `tabSerial No` + return frappe.db.sql( + """ SELECT name from `tabSerial No` WHERE item_code = %(item_code)s and warehouse = %(warehouse)s and timestamp(purchase_date, purchase_time) <= timestamp(%(posting_date)s, %(posting_time)s) - """, args, as_dict=1) + """, + args, + as_dict=1, + ) + def add_additional_uom_columns(columns, result, include_uom, conversion_factors): if not include_uom or not conversion_factors: @@ -379,60 +466,54 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors) convertible_column_map = {} for col_idx in list(reversed(range(0, len(columns)))): col = columns[col_idx] - if isinstance(col, dict) and col.get('convertible') in ['rate', 'qty']: + if isinstance(col, dict) and col.get("convertible") in ["rate", "qty"]: next_col = col_idx + 1 columns.insert(next_col, col.copy()) - columns[next_col]['fieldname'] += '_alt' - convertible_column_map[col.get('fieldname')] = frappe._dict({ - 'converted_col': columns[next_col]['fieldname'], - 'for_type': col.get('convertible') - }) - if col.get('convertible') == 'rate': - columns[next_col]['label'] += ' (per {})'.format(include_uom) + columns[next_col]["fieldname"] += "_alt" + convertible_column_map[col.get("fieldname")] = frappe._dict( + {"converted_col": columns[next_col]["fieldname"], "for_type": col.get("convertible")} + ) + if col.get("convertible") == "rate": + columns[next_col]["label"] += " (per {})".format(include_uom) else: - columns[next_col]['label'] += ' ({})'.format(include_uom) + columns[next_col]["label"] += " ({})".format(include_uom) for row_idx, row in enumerate(result): for convertible_col, data in convertible_column_map.items(): - conversion_factor = conversion_factors[row.get('item_code')] or 1 + conversion_factor = conversion_factors[row.get("item_code")] or 1 for_type = data.for_type value_before_conversion = row.get(convertible_col) - if for_type == 'rate': + if for_type == "rate": row[data.converted_col] = flt(value_before_conversion) * conversion_factor else: row[data.converted_col] = flt(value_before_conversion) / conversion_factor result[row_idx] = row + def get_incoming_outgoing_rate_for_cancel(item_code, voucher_type, voucher_no, voucher_detail_no): - outgoing_rate = frappe.db.sql("""SELECT abs(stock_value_difference / actual_qty) + outgoing_rate = frappe.db.sql( + """SELECT abs(stock_value_difference / actual_qty) FROM `tabStock Ledger Entry` WHERE voucher_type = %s and voucher_no = %s and item_code = %s and voucher_detail_no = %s ORDER BY CREATION DESC limit 1""", - (voucher_type, voucher_no, item_code, voucher_detail_no)) + (voucher_type, voucher_no, item_code, voucher_detail_no), + ) outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0 return outgoing_rate + def is_reposting_item_valuation_in_progress(): - reposting_in_progress = frappe.db.exists("Repost Item Valuation", - {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) + reposting_in_progress = frappe.db.exists( + "Repost Item Valuation", {"docstatus": 1, "status": ["in", ["Queued", "In Progress"]]} + ) if reposting_in_progress: - frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) - - -def calculate_mapped_packed_items_return(return_doc): - parent_items = set([item.parent_item for item in return_doc.packed_items]) - against_doc = frappe.get_doc(return_doc.doctype, return_doc.return_against) - - for original_bundle, returned_bundle in zip(against_doc.items, return_doc.items): - if original_bundle.item_code in parent_items: - for returned_packed_item, original_packed_item in zip(return_doc.packed_items, against_doc.packed_items): - if returned_packed_item.parent_item == original_bundle.item_code: - returned_packed_item.parent_detail_docname = returned_bundle.name - returned_packed_item.qty = (original_packed_item.qty / original_bundle.qty) * returned_bundle.qty + frappe.msgprint( + _("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1 + ) def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool: @@ -440,22 +521,25 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool filters = { "docstatus": 1, - "status": ["in", ["Queued","In Progress", "Failed"]], + "status": ["in", ["Queued", "In Progress"]], "posting_date": ["<=", posting_date], } - reposting_pending = frappe.db.exists("Repost Item Valuation", filters) + reposting_pending = frappe.db.exists("Repost Item Valuation", filters) if reposting_pending and throw_error: - msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") - frappe.msgprint(msg, - raise_exception=PendingRepostingError, - title="Stock Reposting Ongoing", - indicator="red", - primary_action={ - "label": _("Show pending entries"), - "client_action": "erpnext.route_to_pending_reposts", - "args": filters, - } - ) + msg = _( + "Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later." + ) + frappe.msgprint( + msg, + raise_exception=PendingRepostingError, + title="Stock Reposting Ongoing", + indicator="red", + primary_action={ + "label": _("Show pending entries"), + "client_action": "erpnext.route_to_pending_reposts", + "args": filters, + }, + ) return bool(reposting_pending) diff --git a/erpnext/support/__init__.py b/erpnext/support/__init__.py index b9a7c1e8cee..7b6845d2fd1 100644 --- a/erpnext/support/__init__.py +++ b/erpnext/support/__init__.py @@ -1,6 +1,5 @@ - install_docs = [ - {'doctype':'Role', 'role_name':'Support Team', 'name':'Support Team'}, - {'doctype':'Role', 'role_name':'Maintenance User', 'name':'Maintenance User'}, - {'doctype':'Role', 'role_name':'Maintenance Manager', 'name':'Maintenance Manager'} + {"doctype": "Role", "role_name": "Support Team", "name": "Support Team"}, + {"doctype": "Role", "role_name": "Maintenance User", "name": "Maintenance User"}, + {"doctype": "Role", "role_name": "Maintenance Manager", "name": "Maintenance Manager"}, ] diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 744b2989ff2..62e8d342f94 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -67,8 +67,9 @@ class Issue(Document): self.customer = contact.get_link_for("Customer") if not self.company: - self.company = frappe.db.get_value("Lead", self.lead, "company") or \ - frappe.db.get_default("Company") + self.company = frappe.db.get_value("Lead", self.lead, "company") or frappe.db.get_default( + "Company" + ) def reset_sla_fields(self): self.agreement_status = "" @@ -103,19 +104,20 @@ class Issue(Document): def handle_hold_time(self, status): if self.service_level_agreement: # set response and resolution variance as None as the issue is on Hold - pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"], - filters={"parent": self.service_level_agreement}) + pause_sla_on = frappe.db.get_all( + "Pause SLA On Status", fields=["status"], filters={"parent": self.service_level_agreement} + ) hold_statuses = [entry.status for entry in pause_sla_on] update_values = {} if hold_statuses: if self.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() + update_values["on_hold_since"] = frappe.flags.current_time or now_datetime() if not self.first_responded_on: - update_values['response_by'] = None - update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 + update_values["response_by"] = None + update_values["response_by_variance"] = 0 + update_values["resolution_by"] = None + update_values["resolution_by_variance"] = 0 # calculate hold time when status is changed from any hold status to any non-hold status if self.status not in hold_statuses and status in hold_statuses: @@ -125,7 +127,7 @@ class Issue(Document): if self.on_hold_since: # last_hold_time will be added to the sla variables last_hold_time = time_diff_in_seconds(now_time, self.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time + update_values["total_hold_time"] = hold_time + last_hold_time # re-calculate SLA variables after issue changes from any hold status to any non-hold status # add hold time to SLA variables @@ -134,25 +136,31 @@ class Issue(Document): now_time = frappe.flags.current_time or now_datetime() if not self.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + response_by = get_expected_time_for( + parameter="response", service_level=priority, start_date_time=start_date_time + ) response_by = add_to_date(response_by, seconds=round(last_hold_time)) response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time + update_values["response_by"] = response_by + update_values["response_by_variance"] = response_by_variance + last_hold_time - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + resolution_by = get_expected_time_for( + parameter="resolution", service_level=priority, start_date_time=start_date_time + ) resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) - update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - update_values['on_hold_since'] = None + update_values["resolution_by"] = resolution_by + update_values["resolution_by_variance"] = resolution_by_variance + last_hold_time + update_values["on_hold_since"] = None self.db_set(update_values) def update_agreement_status(self): if self.service_level_agreement and self.agreement_status == "Ongoing": - if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \ - cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0: + if ( + cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 + or cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0 + ): self.agreement_status = "Failed" else: @@ -160,30 +168,34 @@ class Issue(Document): def update_agreement_status_on_custom_status(self): """ - Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status + Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status """ - if not self.first_responded_on: # first_responded_on set when first reply is sent to customer + if not self.first_responded_on: # first_responded_on set when first reply is sent to customer self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2) - if not self.resolution_date: # resolution_date set when issue has been closed + if not self.resolution_date: # resolution_date set when issue has been closed self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2) - self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" + self.agreement_status = ( + "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" + ) def create_communication(self): communication = frappe.new_doc("Communication") - communication.update({ - "communication_type": "Communication", - "communication_medium": "Email", - "sent_or_received": "Received", - "email_status": "Open", - "subject": self.subject, - "sender": self.raised_by, - "content": self.description, - "status": "Linked", - "reference_doctype": "Issue", - "reference_name": self.name - }) + communication.update( + { + "communication_type": "Communication", + "communication_medium": "Email", + "sent_or_received": "Received", + "email_status": "Open", + "subject": self.subject, + "sender": self.raised_by, + "content": self.description, + "status": "Linked", + "reference_doctype": "Issue", + "reference_name": self.name, + } + ) communication.ignore_permissions = True communication.ignore_mandatory = True communication.save() @@ -216,23 +228,31 @@ class Issue(Document): # Replicate linked Communications # TODO: get all communications in timeline before this, and modify them to append them to new doc comm_to_split_from = frappe.get_doc("Communication", communication_id) - communications = frappe.get_all("Communication", - filters={"reference_doctype": "Issue", + communications = frappe.get_all( + "Communication", + filters={ + "reference_doctype": "Issue", "reference_name": comm_to_split_from.reference_name, - "creation": (">=", comm_to_split_from.creation)}) + "creation": (">=", comm_to_split_from.creation), + }, + ) for communication in communications: doc = frappe.get_doc("Communication", communication.name) doc.reference_name = replicated_issue.name doc.save(ignore_permissions=True) - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Info", - "reference_doctype": "Issue", - "reference_name": replicated_issue.name, - "content": " - Split the Issue from {1}".format(self.name, frappe.bold(self.name)), - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Info", + "reference_doctype": "Issue", + "reference_name": replicated_issue.name, + "content": " - Split the Issue from {1}".format( + self.name, frappe.bold(self.name) + ), + } + ).insert(ignore_permissions=True) return replicated_issue.name @@ -243,7 +263,9 @@ class Issue(Document): def before_insert(self): if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): if frappe.flags.in_test: - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) else: self.set_response_and_resolution_time() @@ -252,11 +274,19 @@ class Issue(Document): if not service_level_agreement: if frappe.db.get_value("Issue", self.name, "service_level_agreement"): - frappe.throw(_("Couldn't Set Service Level Agreement {0}.").format(self.service_level_agreement)) + frappe.throw( + _("Couldn't Set Service Level Agreement {0}.").format(self.service_level_agreement) + ) return - if (service_level_agreement.customer and self.customer) and not (service_level_agreement.customer == self.customer): - frappe.throw(_("This Service Level Agreement is specific to Customer {0}").format(service_level_agreement.customer)) + if (service_level_agreement.customer and self.customer) and not ( + service_level_agreement.customer == self.customer + ): + frappe.throw( + _("This Service Level Agreement is specific to Customer {0}").format( + service_level_agreement.customer + ) + ) self.service_level_agreement = service_level_agreement.name if not self.priority: @@ -269,40 +299,59 @@ class Issue(Document): self.service_level_agreement_creation = now_datetime() start_date_time = get_datetime(self.service_level_agreement_creation) - self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + self.response_by = get_expected_time_for( + parameter="response", service_level=priority, start_date_time=start_date_time + ) + self.resolution_by = get_expected_time_for( + parameter="resolution", service_level=priority, start_date_time=start_date_time + ) self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime())) self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime())) def change_service_level_agreement_and_priority(self): - if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ - frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): + if ( + self.service_level_agreement + and frappe.db.exists("Issue", self.name) + and frappe.db.get_single_value("Support Settings", "track_service_level_agreement") + ): if not self.priority == frappe.db.get_value("Issue", self.name, "priority"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) frappe.msgprint(_("Priority has been changed to {0}.").format(self.priority)) - if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) + if not self.service_level_agreement == frappe.db.get_value( + "Issue", self.name, "service_level_agreement" + ): + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) + frappe.msgprint( + _("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement) + ) @frappe.whitelist() def reset_service_level_agreement(self, reason, user): if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Info", - "reference_doctype": self.doctype, - "reference_name": self.name, - "comment_email": user, - "content": " resetted Service Level Agreement - {0}".format(_(reason)), - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Info", + "reference_doctype": self.doctype, + "reference_name": self.name, + "comment_email": user, + "content": " resetted Service Level Agreement - {0}".format(_(reason)), + } + ).insert(ignore_permissions=True) self.service_level_agreement_creation = now_datetime() - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) self.agreement_status = "Ongoing" self.save() @@ -310,10 +359,12 @@ class Issue(Document): def get_priority(issue): service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement) priority = service_level_agreement.get_service_level_agreement_priority(issue.priority) - priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list - }) + priority.update( + { + "support_and_resolution": service_level_agreement.support_and_resolution, + "holiday_list": service_level_agreement.holiday_list, + } + ) return priority @@ -334,10 +385,12 @@ def get_expected_time_for(parameter, service_level, start_date_time): support_days = {} for service in service_level.get("support_and_resolution"): - support_days[service.workday] = frappe._dict({ - "start_time": service.start_time, - "end_time": service.end_time, - }) + support_days[service.workday] = frappe._dict( + { + "start_time": service.start_time, + "end_time": service.end_time, + } + ) holidays = get_holidays(service_level.get("holiday_list")) weekdays = get_weekdays() @@ -346,14 +399,19 @@ def get_expected_time_for(parameter, service_level, start_date_time): current_weekday = weekdays[current_date_time.weekday()] if not is_holiday(current_date_time, holidays) and current_weekday in support_days: - start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) \ - if getdate(current_date_time) == getdate(start_date_time) and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time \ + start_time = ( + current_date_time + - datetime(current_date_time.year, current_date_time.month, current_date_time.day) + if getdate(current_date_time) == getdate(start_date_time) + and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time else support_days[current_weekday].start_time + ) end_time = support_days[current_weekday].end_time time_left_today = time_diff_in_seconds(end_time, start_time) # no time left for support today - if time_left_today <= 0: pass + if time_left_today <= 0: + pass elif allotted_seconds: if time_left_today >= allotted_seconds: expected_time = datetime.combine(getdate(current_date_time), get_time(start_time)) @@ -372,6 +430,7 @@ def get_expected_time_for(parameter, service_level, start_date_time): return current_date_time + def set_service_level_agreement_variance(issue=None): current_time = frappe.flags.current_time or now_datetime() @@ -382,17 +441,25 @@ def set_service_level_agreement_variance(issue=None): for issue in frappe.get_list("Issue", filters=filters): doc = frappe.get_doc("Issue", issue.name) - if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer + if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer variance = round(time_diff_in_seconds(doc.response_by, current_time), 2) - frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False) + frappe.db.set_value( + dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False + ) if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) + frappe.db.set_value( + dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False + ) - if not doc.resolution_date: # resolution_date set when issue has been closed + if not doc.resolution_date: # resolution_date set when issue has been closed variance = round(time_diff_in_seconds(doc.resolution_by, current_time), 2) - frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False) + frappe.db.set_value( + dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False + ) if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) + frappe.db.set_value( + dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False + ) def set_resolution_time(issue): @@ -403,18 +470,20 @@ def set_resolution_time(issue): def set_user_resolution_time(issue): # total time taken by a user to close the issue apart from wait_time - communications = frappe.get_list("Communication", filters={ - "reference_doctype": issue.doctype, - "reference_name": issue.name - }, + communications = frappe.get_list( + "Communication", + filters={"reference_doctype": issue.doctype, "reference_name": issue.name}, fields=["sent_or_received", "name", "creation"], - order_by="creation" + order_by="creation", ) pending_time = [] for i in range(len(communications)): - if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent": - wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) + if ( + communications[i].sent_or_received == "Received" + and communications[i - 1].sent_or_received == "Sent" + ): + wait_time = time_diff_in_seconds(communications[i].creation, communications[i - 1].creation) if wait_time > 0: pending_time.append(wait_time) @@ -431,7 +500,7 @@ def get_list_context(context=None): "row_template": "templates/includes/issue_row.html", "show_sidebar": True, "show_search": True, - "no_breadcrumbs": True + "no_breadcrumbs": True, } @@ -448,7 +517,8 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord ignore_permissions = False if is_website_user(): - if not filters: filters = {} + if not filters: + filters = {} if customer: filters["customer"] = customer @@ -457,7 +527,9 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord ignore_permissions = True - return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) + return get_list( + doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions + ) @frappe.whitelist() @@ -466,18 +538,26 @@ def set_multiple_status(names, status): for name in names: set_status(name, status) + @frappe.whitelist() def set_status(name, status): st = frappe.get_doc("Issue", name) st.status = status st.save() + def auto_close_tickets(): """Auto-close replied support tickets after 7 days""" - auto_close_after_days = frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 + auto_close_after_days = ( + frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 + ) - issues = frappe.db.sql(""" select name from tabIssue where status='Replied' and - modified resolution: - frappe.throw(_("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) + frappe.throw( + _("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format( + priority.priority, priority.idx + ) + ) # Check if repeated priority if not len(set(priorities)) == len(priorities): @@ -59,15 +66,27 @@ class ServiceLevelAgreement(Document): for support_and_resolution in self.support_and_resolution: # Check if start and end time is set for every support day if not (support_and_resolution.start_time or support_and_resolution.end_time): - frappe.throw(_("Set Start Time and End Time for \ - Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx))) + frappe.throw( + _( + "Set Start Time and End Time for \ + Support Day {0} at index {1}.".format( + support_and_resolution.workday, support_and_resolution.idx + ) + ) + ) support_days.append(support_and_resolution.workday) support_and_resolution.idx = week.index(support_and_resolution.workday) + 1 if support_and_resolution.start_time >= support_and_resolution.end_time: - frappe.throw(_("Start Time can't be greater than or equal to End Time \ - for {0}.".format(support_and_resolution.workday))) + frappe.throw( + _( + "Start Time can't be greater than or equal to End Time \ + for {0}.".format( + support_and_resolution.workday + ) + ) + ) # Check for repeated workday if not len(set(support_days)) == len(support_days): @@ -75,12 +94,21 @@ class ServiceLevelAgreement(Document): frappe.throw(_("Workday {0} has been repeated.").format(repeated_days)) def validate_doc(self): - if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement") and self.enable: - frappe.throw(_("{0} is not enabled in {1}").format(frappe.bold("Track Service Level Agreement"), - get_link_to_form("Support Settings", "Support Settings"))) + if ( + not frappe.db.get_single_value("Support Settings", "track_service_level_agreement") + and self.enable + ): + frappe.throw( + _("{0} is not enabled in {1}").format( + frappe.bold("Track Service Level Agreement"), + get_link_to_form("Support Settings", "Support Settings"), + ) + ) if self.default_service_level_agreement: - if frappe.db.exists("Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]}): + if frappe.db.exists( + "Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]} + ): frappe.throw(_("A Default Service Level Agreement already exists.")) else: if self.start_date and self.end_date: @@ -91,11 +119,18 @@ class ServiceLevelAgreement(Document): frappe.throw(_("End Date of Agreement can't be less than today.")) if self.entity_type and self.entity: - if frappe.db.exists("Service Level Agreement", {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}): - frappe.throw(_("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format(self.entity_type, self.entity)) + if frappe.db.exists( + "Service Level Agreement", + {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}, + ): + frappe.throw( + _("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format( + self.entity_type, self.entity + ) + ) def validate_condition(self): - temp_doc = frappe.new_doc('Issue') + temp_doc = frappe.new_doc("Issue") if self.condition: try: frappe.safe_eval(self.condition, None, get_context(temp_doc)) @@ -105,58 +140,77 @@ class ServiceLevelAgreement(Document): def get_service_level_agreement_priority(self, priority): priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name}) - return frappe._dict({ - "priority": priority.priority, - "response_time": priority.response_time, - "resolution_time": priority.resolution_time - }) + return frappe._dict( + { + "priority": priority.priority, + "response_time": priority.response_time, + "resolution_time": priority.resolution_time, + } + ) + def check_agreement_status(): - service_level_agreements = frappe.get_list("Service Level Agreement", filters=[ - {"active": 1}, - {"default_service_level_agreement": 0} - ], fields=["name"]) + service_level_agreements = frappe.get_list( + "Service Level Agreement", + filters=[{"active": 1}, {"default_service_level_agreement": 0}], + fields=["name"], + ) for service_level_agreement in service_level_agreements: doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name) if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()): frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0) + def get_active_service_level_agreement_for(doc): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): return filters = [ ["Service Level Agreement", "active", "=", 1], - ["Service Level Agreement", "enable", "=", 1] + ["Service Level Agreement", "enable", "=", 1], ] - if doc.get('priority'): - filters.append(["Service Level Priority", "priority", "=", doc.get('priority')]) + if doc.get("priority"): + filters.append(["Service Level Priority", "priority", "=", doc.get("priority")]) - customer = doc.get('customer') + customer = doc.get("customer") or_filters = [ - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] + [ + "Service Level Agreement", + "entity", + "in", + [customer, get_customer_group(customer), get_customer_territory(customer)], + ] ] - service_level_agreement = doc.get('service_level_agreement') + service_level_agreement = doc.get("service_level_agreement") if service_level_agreement: or_filters = [ - ["Service Level Agreement", "name", "=", doc.get('service_level_agreement')], + ["Service Level Agreement", "name", "=", doc.get("service_level_agreement")], ] - default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] - default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter, - fields=["name", "default_priority", "condition"]) + default_sla_filter = filters + [ + ["Service Level Agreement", "default_service_level_agreement", "=", 1] + ] + default_sla = frappe.get_all( + "Service Level Agreement", + filters=default_sla_filter, + fields=["name", "default_priority", "condition"], + ) filters += [["Service Level Agreement", "default_service_level_agreement", "=", 0]] - agreements = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters, - fields=["name", "default_priority", "condition"]) + agreements = frappe.get_all( + "Service Level Agreement", + filters=filters, + or_filters=or_filters, + fields=["name", "default_priority", "condition"], + ) # check if the current document on which SLA is to be applied fulfills all the conditions filtered_agreements = [] for agreement in agreements: - condition = agreement.get('condition') + condition = agreement.get("condition") if not condition or (condition and frappe.safe_eval(condition, None, get_context(doc))): filtered_agreements.append(agreement) @@ -165,17 +219,25 @@ def get_active_service_level_agreement_for(doc): return filtered_agreements[0] if filtered_agreements else None + def get_context(doc): - return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))} + return { + "doc": doc.as_dict(), + "nowdate": nowdate, + "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils")), + } + def get_customer_group(customer): if customer: return frappe.db.get_value("Customer", customer, "customer_group") + def get_customer_territory(customer): if customer: return frappe.db.get_value("Customer", customer, "territory") + @frappe.whitelist() def get_service_level_agreement_filters(name, customer=None): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): @@ -183,25 +245,37 @@ def get_service_level_agreement_filters(name, customer=None): filters = [ ["Service Level Agreement", "active", "=", 1], - ["Service Level Agreement", "enable", "=", 1] + ["Service Level Agreement", "enable", "=", 1], ] if not customer: - or_filters = [ - ["Service Level Agreement", "default_service_level_agreement", "=", 1] - ] + or_filters = [["Service Level Agreement", "default_service_level_agreement", "=", 1]] else: # Include SLA with No Entity and Entity Type or_filters = [ - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]], - ["Service Level Agreement", "default_service_level_agreement", "=", 1] + [ + "Service Level Agreement", + "entity", + "in", + [customer, get_customer_group(customer), get_customer_territory(customer), ""], + ], + ["Service Level Agreement", "default_service_level_agreement", "=", 1], ] return { - "priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])], - "service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)] + "priority": [ + priority.priority + for priority in frappe.get_list( + "Service Level Priority", filters={"parent": name}, fields=["priority"] + ) + ], + "service_level_agreements": [ + d.name + for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters) + ], } + def get_repeated(values): unique_list = [] diff = [] diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py index 22e2c374e12..8fd3025c30a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py @@ -3,11 +3,6 @@ from frappe import _ def get_data(): return { - 'fieldname': 'service_level_agreement', - 'transactions': [ - { - 'label': _('Issue'), - 'items': ['Issue'] - } - ] + "fieldname": "service_level_agreement", + "transactions": [{"label": _("Issue"), "items": ["Issue"]}], } diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 4c4a684333f..0137be08f8d 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -16,55 +16,131 @@ class TestServiceLevelAgreement(unittest.TestCase): def test_service_level_agreement(self): # Default Service Level Agreement - create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type=None, entity=None, response_time=14400, resolution_time=21600) + create_default_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, + ) - get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1) + get_default_service_level_agreement = get_service_level_agreement( + default_service_level_agreement=1 + ) - self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name) - self.assertEqual(create_default_service_level_agreement.entity_type, get_default_service_level_agreement.entity_type) - self.assertEqual(create_default_service_level_agreement.entity, get_default_service_level_agreement.entity) - self.assertEqual(create_default_service_level_agreement.default_service_level_agreement, get_default_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_default_service_level_agreement.name, get_default_service_level_agreement.name + ) + self.assertEqual( + create_default_service_level_agreement.entity_type, + get_default_service_level_agreement.entity_type, + ) + self.assertEqual( + create_default_service_level_agreement.entity, get_default_service_level_agreement.entity + ) + self.assertEqual( + create_default_service_level_agreement.default_service_level_agreement, + get_default_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Customer customer = create_customer() - create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity=customer, response_time=7200, resolution_time=10800) - get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer) + create_customer_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Customer", + entity=customer, + response_time=7200, + resolution_time=10800, + ) + get_customer_service_level_agreement = get_service_level_agreement( + entity_type="Customer", entity=customer + ) - self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name) - self.assertEqual(create_customer_service_level_agreement.entity_type, get_customer_service_level_agreement.entity_type) - self.assertEqual(create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity) - self.assertEqual(create_customer_service_level_agreement.default_service_level_agreement, get_customer_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_customer_service_level_agreement.name, get_customer_service_level_agreement.name + ) + self.assertEqual( + create_customer_service_level_agreement.entity_type, + get_customer_service_level_agreement.entity_type, + ) + self.assertEqual( + create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity + ) + self.assertEqual( + create_customer_service_level_agreement.default_service_level_agreement, + get_customer_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Customer Group customer_group = create_customer_group() - create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity=customer_group, response_time=7200, resolution_time=10800) - get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group) + create_customer_group_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Customer Group", + entity=customer_group, + response_time=7200, + resolution_time=10800, + ) + get_customer_group_service_level_agreement = get_service_level_agreement( + entity_type="Customer Group", entity=customer_group + ) - self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name) - self.assertEqual(create_customer_group_service_level_agreement.entity_type, get_customer_group_service_level_agreement.entity_type) - self.assertEqual(create_customer_group_service_level_agreement.entity, get_customer_group_service_level_agreement.entity) - self.assertEqual(create_customer_group_service_level_agreement.default_service_level_agreement, get_customer_group_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_customer_group_service_level_agreement.name, + get_customer_group_service_level_agreement.name, + ) + self.assertEqual( + create_customer_group_service_level_agreement.entity_type, + get_customer_group_service_level_agreement.entity_type, + ) + self.assertEqual( + create_customer_group_service_level_agreement.entity, + get_customer_group_service_level_agreement.entity, + ) + self.assertEqual( + create_customer_group_service_level_agreement.default_service_level_agreement, + get_customer_group_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Territory territory = create_territory() - create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity=territory, response_time=7200, resolution_time=10800) - get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory) + create_territory_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Territory", + entity=territory, + response_time=7200, + resolution_time=10800, + ) + get_territory_service_level_agreement = get_service_level_agreement( + entity_type="Territory", entity=territory + ) - self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name) - self.assertEqual(create_territory_service_level_agreement.entity_type, get_territory_service_level_agreement.entity_type) - self.assertEqual(create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity) - self.assertEqual(create_territory_service_level_agreement.default_service_level_agreement, get_territory_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_territory_service_level_agreement.name, get_territory_service_level_agreement.name + ) + self.assertEqual( + create_territory_service_level_agreement.entity_type, + get_territory_service_level_agreement.entity_type, + ) + self.assertEqual( + create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity + ) + self.assertEqual( + create_territory_service_level_agreement.default_service_level_agreement, + get_territory_service_level_agreement.default_service_level_agreement, + ) -def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None): +def get_service_level_agreement( + default_service_level_agreement=None, entity_type=None, entity=None +): if default_service_level_agreement: filters = {"default_service_level_agreement": default_service_level_agreement} else: @@ -73,93 +149,96 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ service_level_agreement = frappe.get_doc("Service Level Agreement", filters) return service_level_agreement -def create_service_level_agreement(default_service_level_agreement, holiday_list, employee_group, - response_time, entity_type, entity, resolution_time): + +def create_service_level_agreement( + default_service_level_agreement, + holiday_list, + employee_group, + response_time, + entity_type, + entity, + resolution_time, +): employee_group = make_employee_group() make_holiday_list() make_priorities() - service_level_agreement = frappe.get_doc({ - "doctype": "Service Level Agreement", - "enable": 1, - "service_level": "__Test Service Level", - "default_service_level_agreement": default_service_level_agreement, - "default_priority": "Medium", - "holiday_list": holiday_list, - "employee_group": employee_group, - "entity_type": entity_type, - "entity": entity, - "start_date": frappe.utils.getdate(), - "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), - "priorities": [ - { - "priority": "Low", - "response_time": response_time, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - }, - { - "priority": "Medium", - "response_time": response_time, - "default_priority": 1, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - }, - { - "priority": "High", - "response_time": response_time, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - } - ], - "pause_sla_on": [ - { - "status": "Replied" - } - ], - "support_and_resolution": [ - { - "workday": "Monday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Tuesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Wednesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Thursday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Friday", - "start_time": "10:00:00", - "end_time": "18:00:00", - } - ] - }) + service_level_agreement = frappe.get_doc( + { + "doctype": "Service Level Agreement", + "enable": 1, + "service_level": "__Test Service Level", + "default_service_level_agreement": default_service_level_agreement, + "default_priority": "Medium", + "holiday_list": holiday_list, + "employee_group": employee_group, + "entity_type": entity_type, + "entity": entity, + "start_date": frappe.utils.getdate(), + "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), + "priorities": [ + { + "priority": "Low", + "response_time": response_time, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", + }, + { + "priority": "Medium", + "response_time": response_time, + "default_priority": 1, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", + }, + { + "priority": "High", + "response_time": response_time, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", + }, + ], + "pause_sla_on": [{"status": "Replied"}], + "support_and_resolution": [ + { + "workday": "Monday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + { + "workday": "Tuesday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + { + "workday": "Wednesday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + { + "workday": "Thursday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + { + "workday": "Friday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + ], + } + ) filters = { "default_service_level_agreement": service_level_agreement.default_service_level_agreement, - "service_level": service_level_agreement.service_level + "service_level": service_level_agreement.service_level, } if not default_service_level_agreement: - filters.update({ - "entity_type": entity_type, - "entity": entity - }) + filters.update({"entity_type": entity_type, "entity": entity}) service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters) @@ -171,24 +250,26 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list def create_customer(): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test Customer", - "customer_group": "Commercial", - "customer_type": "Individual", - "territory": "Rest Of The World" - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test Customer", + "customer_group": "Commercial", + "customer_type": "Individual", + "territory": "Rest Of The World", + } + ) if not frappe.db.exists("Customer", "_Test Customer"): customer.insert(ignore_permissions=True) return customer.name else: return frappe.db.exists("Customer", "_Test Customer") + def create_customer_group(): - customer_group = frappe.get_doc({ - "doctype": "Customer Group", - "customer_group_name": "_Test SLA Customer Group" - }) + customer_group = frappe.get_doc( + {"doctype": "Customer Group", "customer_group_name": "_Test SLA Customer Group"} + ) if not frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}): customer_group.insert() @@ -196,11 +277,14 @@ def create_customer_group(): else: return frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}) + def create_territory(): - territory = frappe.get_doc({ - "doctype": "Territory", - "territory_name": "_Test SLA Territory", - }) + territory = frappe.get_doc( + { + "doctype": "Territory", + "territory_name": "_Test SLA Territory", + } + ) if not frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}): territory.insert() @@ -208,42 +292,65 @@ def create_territory(): else: return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}) + def create_service_level_agreements_for_issues(): - create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type=None, entity=None, response_time=14400, resolution_time=21600) + create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, + ) create_customer() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type="Customer", entity="_Test Customer", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Customer", + entity="_Test Customer", + response_time=7200, + resolution_time=10800, + ) create_customer_group() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Customer Group", + entity="_Test SLA Customer Group", + response_time=7200, + resolution_time=10800, + ) create_territory() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Territory", + entity="_Test SLA Territory", + response_time=7200, + resolution_time=10800, + ) + def make_holiday_list(): holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") if not holiday_list: - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "__Test Holiday List", - "from_date": "2019-01-01", - "to_date": "2019-12-31", - "holidays": [ - { - "description": "Test Holiday 1", - "holiday_date": "2019-03-05" - }, - { - "description": "Test Holiday 2", - "holiday_date": "2019-03-07" - }, - { - "description": "Test Holiday 3", - "holiday_date": "2019-02-11" - }, - ] - }).insert() + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": "__Test Holiday List", + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "holidays": [ + {"description": "Test Holiday 1", "holiday_date": "2019-03-05"}, + {"description": "Test Holiday 2", "holiday_date": "2019-03-07"}, + {"description": "Test Holiday 3", "holiday_date": "2019-02-11"}, + ], + } + ).insert() diff --git a/erpnext/support/doctype/warranty_claim/test_warranty_claim.py b/erpnext/support/doctype/warranty_claim/test_warranty_claim.py index f022d55a4b1..19e23493fe5 100644 --- a/erpnext/support/doctype/warranty_claim/test_warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/test_warranty_claim.py @@ -5,7 +5,8 @@ import unittest import frappe -test_records = frappe.get_test_records('Warranty Claim') +test_records = frappe.get_test_records("Warranty Claim") + class TestWarrantyClaim(unittest.TestCase): pass diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py index 87e95410264..5e2ea067a86 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import frappe from frappe import _, session from frappe.utils import now_datetime @@ -15,27 +14,33 @@ class WarrantyClaim(TransactionBase): return _("{0}: From {1}").format(self.status, self.customer_name) def validate(self): - if session['user'] != 'Guest' and not self.customer: + if session["user"] != "Guest" and not self.customer: frappe.throw(_("Customer is required")) - if self.status=="Closed" and not self.resolution_date and \ - frappe.db.get_value("Warranty Claim", self.name, "status")!="Closed": + if ( + self.status == "Closed" + and not self.resolution_date + and frappe.db.get_value("Warranty Claim", self.name, "status") != "Closed" + ): self.resolution_date = now_datetime() def on_cancel(self): - lst = frappe.db.sql("""select t1.name + lst = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t2.prevdoc_docname = %s and t1.docstatus!=2""", - (self.name)) + (self.name), + ) if lst: - lst1 = ','.join(x[0] for x in lst) + lst1 = ",".join(x[0] for x in lst) frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1)) else: - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") def on_update(self): pass + @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc, map_child_doc @@ -44,25 +49,25 @@ def make_maintenance_visit(source_name, target_doc=None): target_doc.prevdoc_doctype = source_parent.doctype target_doc.prevdoc_docname = source_parent.name - visit = frappe.db.sql("""select t1.name + visit = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent=t1.name and t2.prevdoc_docname=%s - and t1.docstatus=1 and t1.completion_status='Fully Completed'""", source_name) + and t1.docstatus=1 and t1.completion_status='Fully Completed'""", + source_name, + ) if not visit: - target_doc = get_mapped_doc("Warranty Claim", source_name, { - "Warranty Claim": { - "doctype": "Maintenance Visit", - "field_map": {} - } - }, target_doc) + target_doc = get_mapped_doc( + "Warranty Claim", + source_name, + {"Warranty Claim": {"doctype": "Maintenance Visit", "field_map": {}}}, + target_doc, + ) source_doc = frappe.get_doc("Warranty Claim", source_name) if source_doc.get("item_code"): - table_map = { - "doctype": "Maintenance Visit Purpose", - "postprocess": _update_links - } + table_map = {"doctype": "Maintenance Visit Purpose", "postprocess": _update_links} map_child_doc(source_doc, target_doc, table_map, source_doc) return target_doc diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py index 2ab0fb88a7f..5b51ef81c7b 100644 --- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py @@ -7,21 +7,17 @@ import frappe def execute(filters=None): columns = [ + {"fieldname": "creation_date", "label": "Date", "fieldtype": "Date", "width": 300}, { - 'fieldname': 'creation_date', - 'label': 'Date', - 'fieldtype': 'Date', - 'width': 300 - }, - { - 'fieldname': 'first_response_time', - 'fieldtype': 'Duration', - 'label': 'First Response Time', - 'width': 300 + "fieldname": "first_response_time", + "fieldtype": "Duration", + "label": "First Response Time", + "width": 300, }, ] - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT date(creation) as creation_date, avg(first_response_time) as avg_response_time @@ -31,6 +27,8 @@ def execute(filters=None): and first_response_time > 0 GROUP BY creation_date ORDER BY creation_date desc - ''', (filters.from_date, filters.to_date)) + """, + (filters.from_date, filters.to_date), + ) return columns, data diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py index 543a34cb304..d8a200ad686 100644 --- a/erpnext/support/report/issue_analytics/issue_analytics.py +++ b/erpnext/support/report/issue_analytics/issue_analytics.py @@ -15,6 +15,7 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): return IssueAnalytics(filters).run() + class IssueAnalytics(object): def __init__(self, filters=None): """Issue Analytics Report""" @@ -31,101 +32,98 @@ class IssueAnalytics(object): def get_columns(self): self.columns = [] - if self.filters.based_on == 'Customer': - self.columns.append({ - 'label': _('Customer'), - 'options': 'Customer', - 'fieldname': 'customer', - 'fieldtype': 'Link', - 'width': 200 - }) + if self.filters.based_on == "Customer": + self.columns.append( + { + "label": _("Customer"), + "options": "Customer", + "fieldname": "customer", + "fieldtype": "Link", + "width": 200, + } + ) - elif self.filters.based_on == 'Assigned To': - self.columns.append({ - 'label': _('User'), - 'fieldname': 'user', - 'fieldtype': 'Link', - 'options': 'User', - 'width': 200 - }) + elif self.filters.based_on == "Assigned To": + self.columns.append( + {"label": _("User"), "fieldname": "user", "fieldtype": "Link", "options": "User", "width": 200} + ) - elif self.filters.based_on == 'Issue Type': - self.columns.append({ - 'label': _('Issue Type'), - 'fieldname': 'issue_type', - 'fieldtype': 'Link', - 'options': 'Issue Type', - 'width': 200 - }) + elif self.filters.based_on == "Issue Type": + self.columns.append( + { + "label": _("Issue Type"), + "fieldname": "issue_type", + "fieldtype": "Link", + "options": "Issue Type", + "width": 200, + } + ) - elif self.filters.based_on == 'Issue Priority': - self.columns.append({ - 'label': _('Issue Priority'), - 'fieldname': 'priority', - 'fieldtype': 'Link', - 'options': 'Issue Priority', - 'width': 200 - }) + elif self.filters.based_on == "Issue Priority": + self.columns.append( + { + "label": _("Issue Priority"), + "fieldname": "priority", + "fieldtype": "Link", + "options": "Issue Priority", + "width": 200, + } + ) for end_date in self.periodic_daterange: period = self.get_period(end_date) - self.columns.append({ - 'label': _(period), - 'fieldname': scrub(period), - 'fieldtype': 'Int', - 'width': 120 - }) + self.columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Int", "width": 120} + ) - self.columns.append({ - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Int', - 'width': 120 - }) + self.columns.append( + {"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "width": 120} + ) def get_data(self): self.get_issues() self.get_rows() def get_period(self, date): - months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - if self.filters.range == 'Weekly': - period = 'Week ' + str(date.isocalendar()[1]) - elif self.filters.range == 'Monthly': + if self.filters.range == "Weekly": + period = "Week " + str(date.isocalendar()[1]) + elif self.filters.range == "Monthly": period = str(months[date.month - 1]) - elif self.filters.range == 'Quarterly': - period = 'Quarter ' + str(((date.month - 1) // 3) + 1) + elif self.filters.range == "Quarterly": + period = "Quarter " + str(((date.month - 1) // 3) + 1) else: year = get_fiscal_year(date, self.filters.company) period = str(year[0]) - if getdate(self.filters.from_date).year != getdate(self.filters.to_date).year and self.filters.range != 'Yearly': - period += ' ' + str(date.year) + if ( + getdate(self.filters.from_date).year != getdate(self.filters.to_date).year + and self.filters.range != "Yearly" + ): + period += " " + str(date.year) return period def get_period_date_ranges(self): from dateutil.relativedelta import MO, relativedelta + from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) - increment = { - 'Monthly': 1, - 'Quarterly': 3, - 'Half-Yearly': 6, - 'Yearly': 12 - }.get(self.filters.range, 1) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( + self.filters.range, 1 + ) - if self.filters.range in ['Monthly', 'Quarterly']: + if self.filters.range in ["Monthly", "Quarterly"]: from_date = from_date.replace(day=1) - elif self.filters.range == 'Yearly': + elif self.filters.range == "Yearly": from_date = get_fiscal_year(from_date)[1] else: from_date = from_date + relativedelta(from_date, weekday=MO(-1)) self.periodic_daterange = [] for dummy in range(1, 53): - if self.filters.range == 'Weekly': + if self.filters.range == "Weekly": period_end_date = add_days(from_date, 6) else: period_end_date = add_to_date(from_date, months=increment, days=-1) @@ -142,25 +140,26 @@ class IssueAnalytics(object): def get_issues(self): filters = self.get_common_filters() self.field_map = { - 'Customer': 'customer', - 'Issue Type': 'issue_type', - 'Issue Priority': 'priority', - 'Assigned To': '_assign' + "Customer": "customer", + "Issue Type": "issue_type", + "Issue Priority": "priority", + "Assigned To": "_assign", } - self.entries = frappe.db.get_all('Issue', - fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'], - filters=filters + self.entries = frappe.db.get_all( + "Issue", + fields=[self.field_map.get(self.filters.based_on), "name", "opening_date"], + filters=filters, ) def get_common_filters(self): filters = {} - filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date]) + filters["opening_date"] = ("between", [self.filters.from_date, self.filters.to_date]) - if self.filters.get('assigned_to'): - filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%') + if self.filters.get("assigned_to"): + filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%") - for entry in ['company', 'status', 'priority', 'customer', 'project']: + for entry in ["company", "status", "priority", "customer", "project"]: if self.filters.get(entry): filters[entry] = self.filters.get(entry) @@ -171,14 +170,14 @@ class IssueAnalytics(object): self.get_periodic_data() for entity, period_data in iteritems(self.issue_periodic_data): - if self.filters.based_on == 'Customer': - row = {'customer': entity} - elif self.filters.based_on == 'Assigned To': - row = {'user': entity} - elif self.filters.based_on == 'Issue Type': - row = {'issue_type': entity} - elif self.filters.based_on == 'Issue Priority': - row = {'priority': entity} + if self.filters.based_on == "Customer": + row = {"customer": entity} + elif self.filters.based_on == "Assigned To": + row = {"user": entity} + elif self.filters.based_on == "Issue Type": + row = {"issue_type": entity} + elif self.filters.based_on == "Issue Priority": + row = {"priority": entity} total = 0 for end_date in self.periodic_daterange: @@ -187,7 +186,7 @@ class IssueAnalytics(object): row[scrub(period)] = amount total += amount - row['total'] = total + row["total"] = total self.data.append(row) @@ -195,9 +194,9 @@ class IssueAnalytics(object): self.issue_periodic_data = frappe._dict() for d in self.entries: - period = self.get_period(d.get('opening_date')) + period = self.get_period(d.get("opening_date")) - if self.filters.based_on == 'Assigned To': + if self.filters.based_on == "Assigned To": if d._assign: for entry in json.loads(d._assign): self.issue_periodic_data.setdefault(entry, frappe._dict()).setdefault(period, 0.0) @@ -207,18 +206,12 @@ class IssueAnalytics(object): field = self.field_map.get(self.filters.based_on) value = d.get(field) if not value: - value = _('Not Specified') + value = _("Not Specified") self.issue_periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0.0) self.issue_periodic_data[value][period] += 1 def get_chart_data(self): length = len(self.columns) - labels = [d.get('label') for d in self.columns[1:length-1]] - self.chart = { - 'data': { - 'labels': labels, - 'datasets': [] - }, - 'type': 'line' - } + labels = [d.get("label") for d in self.columns[1 : length - 1]] + self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py index b27dc46ad28..169392e5e92 100644 --- a/erpnext/support/report/issue_analytics/test_issue_analytics.py +++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -11,7 +10,8 @@ from erpnext.support.doctype.service_level_agreement.test_service_level_agreemen ) from erpnext.support.report.issue_analytics.issue_analytics import execute -months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + class TestIssueAnalytics(unittest.TestCase): @classmethod @@ -24,8 +24,8 @@ class TestIssueAnalytics(unittest.TestCase): self.current_month = str(months[current_month_date.month - 1]).lower() self.last_month = str(months[last_month_date.month - 1]).lower() if current_month_date.year != last_month_date.year: - self.current_month += '_' + str(current_month_date.year) - self.last_month += '_' + str(last_month_date.year) + self.current_month += "_" + str(current_month_date.year) + self.last_month += "_" + str(last_month_date.year) def test_issue_analytics(self): create_service_level_agreements_for_issues() @@ -39,146 +39,88 @@ class TestIssueAnalytics(unittest.TestCase): def compare_result_for_customer(self): filters = { - 'company': '_Test Company', - 'based_on': 'Customer', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "company": "_Test Company", + "based_on": "Customer", + "from_date": add_months(getdate(), -1), + "to_date": getdate(), + "range": "Monthly", } report = execute(filters) expected_data = [ - { - 'customer': '__Test Customer 2', - self.last_month: 1.0, - self.current_month: 0.0, - 'total': 1.0 - }, - { - 'customer': '__Test Customer 1', - self.last_month: 0.0, - self.current_month: 1.0, - 'total': 1.0 - }, - { - 'customer': '__Test Customer', - self.last_month: 1.0, - self.current_month: 1.0, - 'total': 2.0 - } + {"customer": "__Test Customer 2", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0}, + {"customer": "__Test Customer 1", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0}, + {"customer": "__Test Customer", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0}, ] - self.assertEqual(expected_data, report[1]) # rows - self.assertEqual(len(report[0]), 4) # cols + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols def compare_result_for_issue_type(self): filters = { - 'company': '_Test Company', - 'based_on': 'Issue Type', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "company": "_Test Company", + "based_on": "Issue Type", + "from_date": add_months(getdate(), -1), + "to_date": getdate(), + "range": "Monthly", } report = execute(filters) expected_data = [ - { - 'issue_type': 'Discomfort', - self.last_month: 1.0, - self.current_month: 0.0, - 'total': 1.0 - }, - { - 'issue_type': 'Service Request', - self.last_month: 0.0, - self.current_month: 1.0, - 'total': 1.0 - }, - { - 'issue_type': 'Bug', - self.last_month: 1.0, - self.current_month: 1.0, - 'total': 2.0 - } + {"issue_type": "Discomfort", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0}, + {"issue_type": "Service Request", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0}, + {"issue_type": "Bug", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0}, ] - self.assertEqual(expected_data, report[1]) # rows - self.assertEqual(len(report[0]), 4) # cols + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols def compare_result_for_issue_priority(self): filters = { - 'company': '_Test Company', - 'based_on': 'Issue Priority', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "company": "_Test Company", + "based_on": "Issue Priority", + "from_date": add_months(getdate(), -1), + "to_date": getdate(), + "range": "Monthly", } report = execute(filters) expected_data = [ - { - 'priority': 'Medium', - self.last_month: 1.0, - self.current_month: 1.0, - 'total': 2.0 - }, - { - 'priority': 'Low', - self.last_month: 1.0, - self.current_month: 0.0, - 'total': 1.0 - }, - { - 'priority': 'High', - self.last_month: 0.0, - self.current_month: 1.0, - 'total': 1.0 - } + {"priority": "Medium", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0}, + {"priority": "Low", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0}, + {"priority": "High", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0}, ] - self.assertEqual(expected_data, report[1]) # rows - self.assertEqual(len(report[0]), 4) # cols + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols def compare_result_for_assignment(self): filters = { - 'company': '_Test Company', - 'based_on': 'Assigned To', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "company": "_Test Company", + "based_on": "Assigned To", + "from_date": add_months(getdate(), -1), + "to_date": getdate(), + "range": "Monthly", } report = execute(filters) expected_data = [ - { - 'user': 'test@example.com', - self.last_month: 1.0, - self.current_month: 1.0, - 'total': 2.0 - }, - { - 'user': 'test1@example.com', - self.last_month: 2.0, - self.current_month: 1.0, - 'total': 3.0 - } + {"user": "test@example.com", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0}, + {"user": "test1@example.com", self.last_month: 2.0, self.current_month: 1.0, "total": 3.0}, ] - self.assertEqual(expected_data, report[1]) # rows - self.assertEqual(len(report[0]), 4) # cols + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols def create_issue_types(): - for entry in ['Bug', 'Service Request', 'Discomfort']: - if not frappe.db.exists('Issue Type', entry): - frappe.get_doc({ - 'doctype': 'Issue Type', - '__newname': entry - }).insert() + for entry in ["Bug", "Service Request", "Discomfort"]: + if not frappe.db.exists("Issue Type", entry): + frappe.get_doc({"doctype": "Issue Type", "__newname": entry}).insert() def create_records(): @@ -190,29 +132,15 @@ def create_records(): last_month_date = add_months(current_month_date, -1) issue = make_issue(current_month_date, "__Test Customer", 2, "High", "Bug") - add_assignment({ - "assign_to": ["test@example.com"], - "doctype": "Issue", - "name": issue.name - }) + add_assignment({"assign_to": ["test@example.com"], "doctype": "Issue", "name": issue.name}) issue = make_issue(last_month_date, "__Test Customer", 2, "Low", "Bug") - add_assignment({ - "assign_to": ["test1@example.com"], - "doctype": "Issue", - "name": issue.name - }) + add_assignment({"assign_to": ["test1@example.com"], "doctype": "Issue", "name": issue.name}) issue = make_issue(current_month_date, "__Test Customer 1", 2, "Medium", "Service Request") - add_assignment({ - "assign_to": ["test1@example.com"], - "doctype": "Issue", - "name": issue.name - }) + add_assignment({"assign_to": ["test1@example.com"], "doctype": "Issue", "name": issue.name}) issue = make_issue(last_month_date, "__Test Customer 2", 2, "Medium", "Discomfort") - add_assignment({ - "assign_to": ["test@example.com", "test1@example.com"], - "doctype": "Issue", - "name": issue.name - }) + add_assignment( + {"assign_to": ["test@example.com", "test1@example.com"], "doctype": "Issue", "name": issue.name} + ) diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index b5d52278b89..a81bcc7f32a 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -13,6 +13,7 @@ from six import iteritems def execute(filters=None): return IssueSummary(filters).run() + class IssueSummary(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -28,82 +29,77 @@ class IssueSummary(object): def get_columns(self): self.columns = [] - if self.filters.based_on == 'Customer': - self.columns.append({ - 'label': _('Customer'), - 'options': 'Customer', - 'fieldname': 'customer', - 'fieldtype': 'Link', - 'width': 200 - }) + if self.filters.based_on == "Customer": + self.columns.append( + { + "label": _("Customer"), + "options": "Customer", + "fieldname": "customer", + "fieldtype": "Link", + "width": 200, + } + ) - elif self.filters.based_on == 'Assigned To': - self.columns.append({ - 'label': _('User'), - 'fieldname': 'user', - 'fieldtype': 'Link', - 'options': 'User', - 'width': 200 - }) + elif self.filters.based_on == "Assigned To": + self.columns.append( + {"label": _("User"), "fieldname": "user", "fieldtype": "Link", "options": "User", "width": 200} + ) - elif self.filters.based_on == 'Issue Type': - self.columns.append({ - 'label': _('Issue Type'), - 'fieldname': 'issue_type', - 'fieldtype': 'Link', - 'options': 'Issue Type', - 'width': 200 - }) + elif self.filters.based_on == "Issue Type": + self.columns.append( + { + "label": _("Issue Type"), + "fieldname": "issue_type", + "fieldtype": "Link", + "options": "Issue Type", + "width": 200, + } + ) - elif self.filters.based_on == 'Issue Priority': - self.columns.append({ - 'label': _('Issue Priority'), - 'fieldname': 'priority', - 'fieldtype': 'Link', - 'options': 'Issue Priority', - 'width': 200 - }) + elif self.filters.based_on == "Issue Priority": + self.columns.append( + { + "label": _("Issue Priority"), + "fieldname": "priority", + "fieldtype": "Link", + "options": "Issue Priority", + "width": 200, + } + ) - self.statuses = ['Open', 'Replied', 'On Hold', 'Resolved', 'Closed'] + self.statuses = ["Open", "Replied", "On Hold", "Resolved", "Closed"] for status in self.statuses: - self.columns.append({ - 'label': _(status), - 'fieldname': scrub(status), - 'fieldtype': 'Int', - 'width': 80 - }) + self.columns.append( + {"label": _(status), "fieldname": scrub(status), "fieldtype": "Int", "width": 80} + ) - self.columns.append({ - 'label': _('Total Issues'), - 'fieldname': 'total_issues', - 'fieldtype': 'Int', - 'width': 100 - }) + self.columns.append( + {"label": _("Total Issues"), "fieldname": "total_issues", "fieldtype": "Int", "width": 100} + ) self.sla_status_map = { - 'SLA Failed': 'failed', - 'SLA Fulfilled': 'fulfilled', - 'SLA Ongoing': 'ongoing' + "SLA Failed": "failed", + "SLA Fulfilled": "fulfilled", + "SLA Ongoing": "ongoing", } for label, fieldname in self.sla_status_map.items(): - self.columns.append({ - 'label': _(label), - 'fieldname': fieldname, - 'fieldtype': 'Int', - 'width': 100 - }) + self.columns.append( + {"label": _(label), "fieldname": fieldname, "fieldtype": "Int", "width": 100} + ) - self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time', - 'Avg Resolution Time', 'Avg User Resolution Time'] + self.metrics = [ + "Avg First Response Time", + "Avg Response Time", + "Avg Hold Time", + "Avg Resolution Time", + "Avg User Resolution Time", + ] for metric in self.metrics: - self.columns.append({ - 'label': _(metric), - 'fieldname': scrub(metric), - 'fieldtype': 'Duration', - 'width': 170 - }) + self.columns.append( + {"label": _(metric), "fieldname": scrub(metric), "fieldtype": "Duration", "width": 170} + ) def get_data(self): self.get_issues() @@ -112,26 +108,37 @@ class IssueSummary(object): def get_issues(self): filters = self.get_common_filters() self.field_map = { - 'Customer': 'customer', - 'Issue Type': 'issue_type', - 'Issue Priority': 'priority', - 'Assigned To': '_assign' + "Customer": "customer", + "Issue Type": "issue_type", + "Issue Priority": "priority", + "Assigned To": "_assign", } - self.entries = frappe.db.get_all('Issue', - fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date', 'status', 'avg_response_time', - 'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'], - filters=filters + self.entries = frappe.db.get_all( + "Issue", + fields=[ + self.field_map.get(self.filters.based_on), + "name", + "opening_date", + "status", + "avg_response_time", + "first_response_time", + "total_hold_time", + "user_resolution_time", + "resolution_time", + "agreement_status", + ], + filters=filters, ) def get_common_filters(self): filters = {} - filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date]) + filters["opening_date"] = ("between", [self.filters.from_date, self.filters.to_date]) - if self.filters.get('assigned_to'): - filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%') + if self.filters.get("assigned_to"): + filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%") - for entry in ['company', 'status', 'priority', 'customer', 'project']: + for entry in ["company", "status", "priority", "customer", "project"]: if self.filters.get(entry): filters[entry] = self.filters.get(entry) @@ -142,20 +149,20 @@ class IssueSummary(object): self.get_summary_data() for entity, data in iteritems(self.issue_summary_data): - if self.filters.based_on == 'Customer': - row = {'customer': entity} - elif self.filters.based_on == 'Assigned To': - row = {'user': entity} - elif self.filters.based_on == 'Issue Type': - row = {'issue_type': entity} - elif self.filters.based_on == 'Issue Priority': - row = {'priority': entity} + if self.filters.based_on == "Customer": + row = {"customer": entity} + elif self.filters.based_on == "Assigned To": + row = {"user": entity} + elif self.filters.based_on == "Issue Type": + row = {"issue_type": entity} + elif self.filters.based_on == "Issue Priority": + row = {"priority": entity} for status in self.statuses: count = flt(data.get(status, 0.0)) row[scrub(status)] = count - row['total_issues'] = data.get('total_issues', 0.0) + row["total_issues"] = data.get("total_issues", 0.0) for sla_status in self.sla_status_map.values(): value = flt(data.get(sla_status), 0.0) @@ -174,36 +181,41 @@ class IssueSummary(object): status = d.status agreement_status = scrub(d.agreement_status) - if self.filters.based_on == 'Assigned To': + if self.filters.based_on == "Assigned To": if d._assign: for entry in json.loads(d._assign): self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0) self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0) - self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault("total_issues", 0.0) self.issue_summary_data[entry][status] += 1 self.issue_summary_data[entry][agreement_status] += 1 - self.issue_summary_data[entry]['total_issues'] += 1 + self.issue_summary_data[entry]["total_issues"] += 1 else: field = self.field_map.get(self.filters.based_on) value = d.get(field) if not value: - value = _('Not Specified') + value = _("Not Specified") self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0) self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0) - self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0) + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault("total_issues", 0.0) self.issue_summary_data[value][status] += 1 self.issue_summary_data[value][agreement_status] += 1 - self.issue_summary_data[value]['total_issues'] += 1 + self.issue_summary_data[value]["total_issues"] += 1 self.get_metrics_data() def get_metrics_data(self): issues = [] - metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time', - 'avg_resolution_time', 'avg_user_resolution_time'] + metrics_list = [ + "avg_response_time", + "avg_first_response_time", + "avg_hold_time", + "avg_resolution_time", + "avg_user_resolution_time", + ] for entry in self.entries: issues.append(entry.name) @@ -211,7 +223,7 @@ class IssueSummary(object): field = self.field_map.get(self.filters.based_on) if issues: - if self.filters.based_on == 'Assigned To': + if self.filters.based_on == "Assigned To": assignment_map = frappe._dict() for d in self.entries: if d._assign: @@ -219,11 +231,15 @@ class IssueSummary(object): for metric in metrics_list: self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0) - self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0 - self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0 - self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0 - self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0 - self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0 + self.issue_summary_data[entry]["avg_response_time"] += d.get("avg_response_time") or 0.0 + self.issue_summary_data[entry]["avg_first_response_time"] += ( + d.get("first_response_time") or 0.0 + ) + self.issue_summary_data[entry]["avg_hold_time"] += d.get("total_hold_time") or 0.0 + self.issue_summary_data[entry]["avg_resolution_time"] += d.get("resolution_time") or 0.0 + self.issue_summary_data[entry]["avg_user_resolution_time"] += ( + d.get("user_resolution_time") or 0.0 + ) if not assignment_map.get(entry): assignment_map[entry] = 0 @@ -234,7 +250,8 @@ class IssueSummary(object): self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry)) else: - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT {0}, AVG(first_response_time) as avg_frt, AVG(avg_response_time) as avg_resp_time, @@ -245,21 +262,30 @@ class IssueSummary(object): WHERE name IN %(issues)s GROUP BY {0} - """.format(field), {'issues': issues}, as_dict=1) + """.format( + field + ), + {"issues": issues}, + as_dict=1, + ) for entry in data: value = entry.get(field) if not value: - value = _('Not Specified') + value = _("Not Specified") for metric in metrics_list: self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0) - self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0 - self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0 - self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0 - self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0 - self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0 + self.issue_summary_data[value]["avg_response_time"] = entry.get("avg_resp_time") or 0.0 + self.issue_summary_data[value]["avg_first_response_time"] = entry.get("avg_frt") or 0.0 + self.issue_summary_data[value]["avg_hold_time"] = entry.get("avg_hold_time") or 0.0 + self.issue_summary_data[value]["avg_resolution_time"] = ( + entry.get("avg_resolution_time") or 0.0 + ) + self.issue_summary_data[value]["avg_user_resolution_time"] = ( + entry.get("avg_user_resolution_time") or 0.0 + ) def get_chart_data(self): self.chart = [] @@ -273,47 +299,30 @@ class IssueSummary(object): entity = self.filters.based_on entity_field = self.field_map.get(entity) - if entity == 'Assigned To': - entity_field = 'user' + if entity == "Assigned To": + entity_field = "user" for entry in self.data: 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')) + 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")) self.chart = { - 'data': { - 'labels': labels[:30], - 'datasets': [ - { - 'name': 'Open', - 'values': open_issues[:30] - }, - { - 'name': 'Replied', - 'values': replied_issues[:30] - }, - { - 'name': 'On Hold', - 'values': on_hold_issues[:30] - }, - { - 'name': 'Resolved', - 'values': resolved_issues[:30] - }, - { - 'name': 'Closed', - 'values': closed_issues[:30] - } - ] + "data": { + "labels": labels[:30], + "datasets": [ + {"name": "Open", "values": open_issues[:30]}, + {"name": "Replied", "values": replied_issues[:30]}, + {"name": "On Hold", "values": on_hold_issues[:30]}, + {"name": "Resolved", "values": resolved_issues[:30]}, + {"name": "Closed", "values": closed_issues[:30]}, + ], }, - 'type': 'bar', - 'barOptions': { - 'stacked': True - } + "type": "bar", + "barOptions": {"stacked": True}, } def get_report_summary(self): @@ -326,41 +335,41 @@ class IssueSummary(object): 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') + open_issues += entry.get("open") + replied += entry.get("replied") + on_hold += entry.get("on_hold") + resolved += entry.get("resolved") + closed += entry.get("closed") self.report_summary = [ { - 'value': open_issues, - 'indicator': 'Red', - 'label': _('Open'), - 'datatype': 'Int', + "value": open_issues, + "indicator": "Red", + "label": _("Open"), + "datatype": "Int", }, { - 'value': replied, - 'indicator': 'Grey', - 'label': _('Replied'), - 'datatype': 'Int', + "value": replied, + "indicator": "Grey", + "label": _("Replied"), + "datatype": "Int", }, { - 'value': on_hold, - 'indicator': 'Grey', - 'label': _('On Hold'), - 'datatype': 'Int', + "value": on_hold, + "indicator": "Grey", + "label": _("On Hold"), + "datatype": "Int", }, { - 'value': resolved, - 'indicator': 'Green', - 'label': _('Resolved'), - 'datatype': 'Int', + "value": resolved, + "indicator": "Green", + "label": _("Resolved"), + "datatype": "Int", }, { - 'value': closed, - 'indicator': 'Green', - 'label': _('Closed'), - 'datatype': 'Int', - } + "value": closed, + "indicator": "Green", + "label": _("Closed"), + "datatype": "Int", + }, ] diff --git a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py index e3a7e5f54b1..b4bd460a67f 100644 --- a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py +++ b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py @@ -8,34 +8,36 @@ from frappe.utils import add_to_date, get_datetime, getdate from six import iteritems time_slots = { - '12AM - 3AM': '00:00:00-03:00:00', - '3AM - 6AM': '03:00:00-06:00:00', - '6AM - 9AM': '06:00:00-09:00:00', - '9AM - 12PM': '09:00:00-12:00:00', - '12PM - 3PM': '12:00:00-15:00:00', - '3PM - 6PM': '15:00:00-18:00:00', - '6PM - 9PM': '18:00:00-21:00:00', - '9PM - 12AM': '21:00:00-23:00:00' + "12AM - 3AM": "00:00:00-03:00:00", + "3AM - 6AM": "03:00:00-06:00:00", + "6AM - 9AM": "06:00:00-09:00:00", + "9AM - 12PM": "09:00:00-12:00:00", + "12PM - 3PM": "12:00:00-15:00:00", + "3PM - 6PM": "15:00:00-18:00:00", + "6PM - 9PM": "18:00:00-21:00:00", + "9PM - 12AM": "21:00:00-23:00:00", } + def execute(filters=None): columns, data = [], [] - if not filters.get('periodicity'): - filters['periodicity'] = 'Daily' + if not filters.get("periodicity"): + filters["periodicity"] = "Daily" columns = get_columns() data, timeslot_wise_count = get_data(filters) chart = get_chart_data(timeslot_wise_count) return columns, data, None, chart + def get_data(filters): start_date = getdate(filters.from_date) data = [] time_slot_wise_total_count = {} - while(start_date <= getdate(filters.to_date)): - hours_count = {'date': start_date} + while start_date <= getdate(filters.to_date): + hours_count = {"date": start_date} for key, value in iteritems(time_slots): - start_time, end_time = value.split('-') + start_time, end_time = value.split("-") start_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), start_time)) end_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), end_time)) hours_count[key] = get_hours_count(start_time, end_time) @@ -48,49 +50,57 @@ def get_data(filters): return data, time_slot_wise_total_count + def get_hours_count(start_time, end_time): - data = frappe.db.sql(""" select count(*) from `tabIssue` where creation - between %(start_time)s and %(end_time)s""", { - 'start_time': start_time, - 'end_time': end_time - }, as_list=1) or [] + data = ( + frappe.db.sql( + """ select count(*) from `tabIssue` where creation + between %(start_time)s and %(end_time)s""", + {"start_time": start_time, "end_time": end_time}, + as_list=1, + ) + or [] + ) return data[0][0] if len(data) > 0 else 0 -def get_columns(): - columns = [{ - "fieldname": "date", - "label": _("Date"), - "fieldtype": "Date", - "width": 100 - }] - for label in ['12AM - 3AM', '3AM - 6AM', '6AM - 9AM', - '9AM - 12PM', '12PM - 3PM', '3PM - 6PM', '6PM - 9PM', '9PM - 12AM']: - columns.append({ - "fieldname": label, - "label": _(label), - "fieldtype": "Data", - "width": 120 - }) +def get_columns(): + columns = [{"fieldname": "date", "label": _("Date"), "fieldtype": "Date", "width": 100}] + + for label in [ + "12AM - 3AM", + "3AM - 6AM", + "6AM - 9AM", + "9AM - 12PM", + "12PM - 3PM", + "3PM - 6PM", + "6PM - 9PM", + "9PM - 12AM", + ]: + columns.append({"fieldname": label, "label": _(label), "fieldtype": "Data", "width": 120}) return columns + def get_chart_data(timeslot_wise_count): total_count = [] - timeslots = ['12AM - 3AM', '3AM - 6AM', '6AM - 9AM', - '9AM - 12PM', '12PM - 3PM', '3PM - 6PM', '6PM - 9PM', '9PM - 12AM'] + timeslots = [ + "12AM - 3AM", + "3AM - 6AM", + "6AM - 9AM", + "9AM - 12PM", + "12PM - 3PM", + "3PM - 6PM", + "6PM - 9PM", + "9PM - 12AM", + ] datasets = [] for data in timeslots: total_count.append(timeslot_wise_count.get(data, 0)) - datasets.append({'values': total_count}) + datasets.append({"values": total_count}) - chart = { - "data": { - 'labels': timeslots, - 'datasets': datasets - } - } + chart = {"data": {"labels": timeslots, "datasets": datasets}} chart["type"] = "line" return chart diff --git a/erpnext/support/web_form/issues/issues.py b/erpnext/support/web_form/issues/issues.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/support/web_form/issues/issues.py +++ b/erpnext/support/web_form/issues/issues.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 0c24484bdfb..1c88883abce 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -11,8 +11,8 @@ from frappe.model.document import Document from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number -END_CALL_STATUSES = ['No Answer', 'Completed', 'Busy', 'Failed'] -ONGOING_CALL_STATUSES = ['Ringing', 'In Progress'] +END_CALL_STATUSES = ["No Answer", "Completed", "Busy", "Failed"] +ONGOING_CALL_STATUSES = ["Ringing", "In Progress"] class CallLog(Document): @@ -20,18 +20,17 @@ class CallLog(Document): deduplicate_dynamic_links(self) def before_insert(self): - """Add lead(third party person) links to the document. - """ - lead_number = self.get('from') if self.is_incoming_call() else self.get('to') + """Add lead(third party person) links to the document.""" + lead_number = self.get("from") if self.is_incoming_call() else self.get("to") lead_number = strip_number(lead_number) contact = get_contact_with_phone_number(strip_number(lead_number)) if contact: - self.add_link(link_type='Contact', link_name=contact) + self.add_link(link_type="Contact", link_name=contact) lead = get_lead_with_phone_number(lead_number) if lead: - self.add_link(link_type='Lead', link_name=lead) + self.add_link(link_type="Lead", link_name=lead) def after_insert(self): self.trigger_call_popup() @@ -39,29 +38,29 @@ class CallLog(Document): def on_update(self): def _is_call_missed(doc_before_save, doc_after_save): # FIXME: This works for Exotel but not for all telepony providers - return doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES + return ( + doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES + ) def _is_call_ended(doc_before_save, doc_after_save): return doc_before_save.status not in END_CALL_STATUSES and self.status in END_CALL_STATUSES doc_before_save = self.get_doc_before_save() - if not doc_before_save: return + if not doc_before_save: + return if _is_call_missed(doc_before_save, self): - frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self) + frappe.publish_realtime("call_{id}_missed".format(id=self.id), self) self.trigger_call_popup() if _is_call_ended(doc_before_save, self): - frappe.publish_realtime('call_{id}_ended'.format(id=self.id), self) + frappe.publish_realtime("call_{id}_ended".format(id=self.id), self) def is_incoming_call(self): - return self.type == 'Incoming' + return self.type == "Incoming" def add_link(self, link_type, link_name): - self.append('links', { - 'link_doctype': link_type, - 'link_name': link_name - }) + self.append("links", {"link_doctype": link_type, "link_name": link_name}) def trigger_call_popup(self): if self.is_incoming_call(): @@ -72,53 +71,63 @@ class CallLog(Document): emails = set(scheduled_employees).intersection(employee_emails) if frappe.conf.developer_mode: - self.add_comment(text=f""" + self.add_comment( + text=f""" Scheduled Employees: {scheduled_employees} Matching Employee: {employee_emails} Show Popup To: {emails} - """) + """ + ) if employee_emails and not emails: self.add_comment(text=_("No employee was scheduled for call popup")) for email in emails: - frappe.publish_realtime('show_call_popup', self, user=email) + frappe.publish_realtime("show_call_popup", self, user=email) @frappe.whitelist() def add_call_summary(call_log, summary): - doc = frappe.get_doc('Call Log', call_log) - doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '

    ' + summary) + doc = frappe.get_doc("Call Log", call_log) + doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "

    " + summary) + def get_employees_with_number(number): number = strip_number(number) - if not number: return [] + if not number: + return [] - employee_emails = frappe.cache().hget('employees_with_number', number) - if employee_emails: return employee_emails + employee_emails = frappe.cache().hget("employees_with_number", number) + if employee_emails: + return employee_emails - employees = frappe.get_all('Employee', filters={ - 'cell_number': ['like', '%{}%'.format(number)], - 'user_id': ['!=', ''] - }, fields=['user_id']) + employees = frappe.get_all( + "Employee", + filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]}, + fields=["user_id"], + ) employee_emails = [employee.user_id for employee in employees] - frappe.cache().hset('employees_with_number', number, employee_emails) + frappe.cache().hset("employees_with_number", number, employee_emails) return employee_emails + def link_existing_conversations(doc, state): """ Called from hooks on creation of Contact or Lead to link all the existing conversations. """ - if doc.doctype != 'Contact': return + if doc.doctype != "Contact": + return try: numbers = [d.phone for d in doc.phone_nos] for number in numbers: number = strip_number(number) - if not number: continue - logs = frappe.db.sql_list(""" + if not number: + continue + logs = frappe.db.sql_list( + """ SELECT cl.name FROM `tabCall Log` cl LEFT JOIN `tabDynamic Link` dl ON cl.name = dl.parent @@ -131,44 +140,42 @@ def link_existing_conversations(doc, state): ELSE 0 END )=0 - """, dict( - phone_number='%{}'.format(number), - docname=doc.name, - doctype = doc.doctype - ) + """, + dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype), ) for log in logs: - call_log = frappe.get_doc('Call Log', log) + call_log = frappe.get_doc("Call Log", log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) call_log.save(ignore_permissions=True) frappe.db.commit() except Exception: - frappe.log_error(title=_('Error during caller information update')) + frappe.log_error(title=_("Error during caller information update")) + def get_linked_call_logs(doctype, docname): # content will be shown in timeline - logs = frappe.get_all('Dynamic Link', fields=['parent'], filters={ - 'parenttype': 'Call Log', - 'link_doctype': doctype, - 'link_name': docname - }) + logs = frappe.get_all( + "Dynamic Link", + fields=["parent"], + filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname}, + ) logs = set([log.parent for log in logs]) - logs = frappe.get_all('Call Log', fields=['*'], filters={ - 'name': ['in', logs] - }) + logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]}) timeline_contents = [] for log in logs: log.show_call_button = 0 - timeline_contents.append({ - 'icon': 'call', - 'is_card': True, - 'creation': log.creation, - 'template': 'call_link', - 'template_data': log - }) + timeline_contents.append( + { + "icon": "call", + "is_card": True, + "creation": log.creation, + "template": "call_link", + "template_data": log, + } + ) return timeline_contents diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py index 08e244d8897..5edf81df736 100644 --- a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py @@ -20,35 +20,38 @@ class IncomingCallSettings(Document): self.validate_call_schedule_overlaps(self.call_handling_schedule) def validate_call_schedule_timeslot(self, schedule: list): - """ Make sure that to time slot is ahead of from time slot. - """ + """Make sure that to time slot is ahead of from time slot.""" errors = [] for record in schedule: from_time = self.time_to_seconds(record.from_time) to_time = self.time_to_seconds(record.to_time) if from_time >= to_time: errors.append( - _('Call Schedule Row {0}: To time slot should always be ahead of From time slot.').format(record.idx) + _("Call Schedule Row {0}: To time slot should always be ahead of From time slot.").format( + record.idx + ) ) if errors: - frappe.throw('
    '.join(errors)) + frappe.throw("
    ".join(errors)) def validate_call_schedule_overlaps(self, schedule: list): - """Check if any time slots are overlapped in a day schedule. - """ + """Check if any time slots are overlapped in a day schedule.""" week_days = set([each.day_of_week for each in schedule]) for day in week_days: - timeslots = [(record.from_time, record.to_time) for record in schedule if record.day_of_week==day] + timeslots = [ + (record.from_time, record.to_time) for record in schedule if record.day_of_week == day + ] # convert time in timeslot into an integer represents number of seconds timeslots = sorted(map(lambda seq: tuple(map(self.time_to_seconds, seq)), timeslots)) - if len(timeslots) < 2: continue + if len(timeslots) < 2: + continue for i in range(1, len(timeslots)): - if self.check_timeslots_overlap(timeslots[i-1], timeslots[i]): - frappe.throw(_('Please fix overlapping time slots for {0}.').format(day)) + if self.check_timeslots_overlap(timeslots[i - 1], timeslots[i]): + frappe.throw(_("Please fix overlapping time slots for {0}.").format(day)) @staticmethod def check_timeslots_overlap(ts1: Tuple[int, int], ts2: Tuple[int, int]) -> bool: @@ -58,7 +61,6 @@ class IncomingCallSettings(Document): @staticmethod def time_to_seconds(time: str) -> int: - """Convert time string of format HH:MM:SS into seconds - """ + """Convert time string of format HH:MM:SS into seconds""" date_time = datetime.strptime(time, "%H:%M:%S") return date_time - datetime(1900, 1, 1) diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 218481581c5..3926f77b1fa 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -52,24 +52,6 @@
    - diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 892d62513e2..fb4cecf8266 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -300,13 +300,13 @@ {% if values | len > 20 %} - + {% endif %} {% if values %}
    {% for value in values %} -
    +